AI Runtime
Govern AI systems and agents, record executions, authorize tool actions, resolve approvals, and export technical evidence.
AI Runtime API
AI Runtime is a vendor-neutral governance layer for observing AI executions, authorizing agent actions, requesting human oversight, and exporting technical evidence. It does not proxy model traffic or execute tools. Your application performs the model or tool call after applying the returned decision.
Availability
AI Runtime is a non-production surface. In a production deployment its REST routes may be unavailable and resolve as not found. Confirm enablement with Donutwork before building a dependency on it.
Lifecycle and safety model
- Create an AI system and register a versioned agent identity.
- Start an execution and retain its ID.
- Record LLM traces without raw prompt or response content.
- Call
authorizebefore a governed tool action. - Continue only on
ALLOW, or after aREQUIRE_APPROVALapproval becomesapproved; never continue onDENY. - Record the tool outcome and deterministic evaluations.
- Finish the execution and export evidence.
Authorization is fail-closed: no matching policy and evaluator failures both return DENY. Executions transition only from running to completed, failed, or cancelled. Use Idempotency-Key on execution, trace, authorization, and evaluation writes; the same key must never be reused with a different payload.
AI systems
List systems
ai_runtime:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Systems in the authenticated company.
{
"elements": [
{
"id": "SYSTEM_ID",
"slug": "finance-agent",
"name": "Finance agent",
"purpose": "Review payment proposals",
"environment": "staging",
"status": "active"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Create a system
ai_runtime:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Finance agent",
"slug": "finance-agent",
"purpose": "Review payment proposals before execution",
"environment": "staging",
"owner_id": "team-finance",
"risk_metadata": {
"impact": "medium"
}
}nameRequiredpurposeRequiredslugenvironmentstatusowner_idrisk_metadatametadataResponses
System registered.
{
"id": "SYSTEM_ID",
"slug": "finance-agent",
"status": "active"
}The slug already belongs to another system.
{
"error": "An AI System with this slug already exists."
}A required field or enum is invalid.
{
"error": "purpose is required and must be at most 1000 characters."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Finance agent",
"slug": "finance-agent",
"purpose": "Review payment proposals before execution",
"environment": "staging",
"owner_id": "team-finance",
"risk_metadata": {
"impact": "medium"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Finance agent",
"slug": "finance-agent",
"purpose": "Review payment proposals before execution",
"environment": "staging",
"owner_id": "team-finance",
"risk_metadata": {
"impact": "medium"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"name": "Finance agent",
"slug": "finance-agent",
"purpose": "Review payment proposals before execution",
"environment": "staging",
"owner_id": "team-finance",
"risk_metadata": {
"impact": "medium"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve a system
ai_runtime:readPath, query, and header parameters
systemIdRequiredResponses
System record.
{
"id": "SYSTEM_ID",
"name": "Finance agent",
"status": "active"
}No system is visible in this company.
{
"error": "AI System not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems/SYSTEMID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems/SYSTEMID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/systems/SYSTEMID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Agent identities
Agent identities are versioned records, not credentials. A revoked identity is terminal; register a new version instead of reactivating it.
List agent identities
ai_runtime:readPath, query, and header parameters
systemIdResponses
Agent identities, newest versions first per name.
{
"elements": [
{
"id": "AGENT_ID",
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"status": "active"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Register an agent identity
ai_runtime:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"environment": "staging",
"capabilities": [
"payment.review",
"payment.propose"
]
}system_idRequirednameRequiredversionRequiredenvironmentcapabilitiesstatusmetadataResponses
Agent version registered.
{
"id": "AGENT_ID",
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"status": "active"
}The system is inactive or this name/version already exists.
{
"error": "This Agent Identity version is already registered."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"environment": "staging",
"capabilities": [
"payment.review",
"payment.propose"
]
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"environment": "staging",
"capabilities": [
"payment.review",
"payment.propose"
]
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"system_id": "SYSTEM_ID",
"name": "finance-agent",
"version": "1.0.0",
"environment": "staging",
"capabilities": [
"payment.review",
"payment.propose"
]
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve an agent identity
ai_runtime:readPath, query, and header parameters
agentIdRequiredResponses
Agent identity record.
{
"id": "AGENT_ID",
"system_id": "SYSTEM_ID",
"status": "active"
}Agent identity not found in this company.
{
"error": "Agent Identity not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents/AGENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents/AGENTID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/agents/AGENTID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Executions and traces
List executions
ai_runtime:readPath, query, and header parameters
systemIdResponses
Executions ordered newest first.
{
"elements": [
{
"id": "EXECUTION_ID",
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"status": "running"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Start an execution
ai_runtime_executions:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"metadata": {
"correlation_id": "order-8472"
}
}system_idRequiredagent_idactor_typeactor_idmetadataResponses
Execution opened in running state.
{
"id": "EXECUTION_ID",
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"status": "running",
"started_at": "2026-09-18T10:00:00+00:00"
}System or agent is inactive, or an idempotency key was reused with different content.
{
"error": "The Agent Identity is not active for this AI System."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"metadata": {
"correlation_id": "order-8472"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"metadata": {
"correlation_id": "order-8472"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"system_id": "SYSTEM_ID",
"agent_id": "AGENT_ID",
"metadata": {
"correlation_id": "order-8472"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve an execution
ai_runtime:readPath, query, and header parameters
executionIdRequiredResponses
Execution state and sanitized metadata.
{
"id": "EXECUTION_ID",
"status": "running",
"started_at": "2026-09-18T10:00:00+00:00"
}Execution not found in this company.
{
"error": "AI Execution not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Finish an execution
ai_runtime_executions:writePath, query, and header parameters
executionIdRequiredRequest Body
{
"status": "completed",
"metadata": {
"outcome": "proposal_created"
}
}statuserror_codemetadataResponses
Execution closed.
{
"id": "EXECUTION_ID",
"status": "completed",
"completed_at": "2026-09-18T10:00:08+00:00"
}The execution has already left running state.
{
"error": "Execution cannot transition from completed."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/finish.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"status": "completed",
"metadata": {
"outcome": "proposal_created"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/finish.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"status": "completed",
"metadata": {
"outcome": "proposal_created"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/finish.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"status": "completed",
"metadata": {
"outcome": "proposal_created"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Record an LLM trace
ai_runtime_executions:writePath, query, and header parameters
executionIdRequiredRequest Body
{
"provider": "openai",
"model": "gpt-example",
"model_version": "2026-09",
"model_type": "generation",
"status": "completed",
"latency_ms": 842,
"usage": {
"input_tokens": 320,
"output_tokens": 84
},
"request_hash": "LOWERCASE_SHA256",
"response_hash": "LOWERCASE_SHA256"
}providerRequiredmodelRequiredmodel_versionmodel_typestatusrequested_at / responded_atlatency_msusagerequest_hash / response_hasherror_codeResponses
Trace stored and model inventory updated.
{
"id": "TRACE_ID",
"kind": "llm",
"model_id": "MODEL_ID",
"status": "completed",
"audit_sequence": 2
}Execution is closed or idempotency conflicts.
{
"error": "The AI Execution is already completed."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/llm-traces.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"provider": "openai",
"model": "gpt-example",
"model_version": "2026-09",
"model_type": "generation",
"status": "completed",
"latency_ms": 842,
"usage": {
"input_tokens": 320,
"output_tokens": 84
},
"request_hash": "LOWERCASE_SHA256",
"response_hash": "LOWERCASE_SHA256"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/llm-traces.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"provider": "openai",
"model": "gpt-example",
"model_version": "2026-09",
"model_type": "generation",
"status": "completed",
"latency_ms": 842,
"usage": {
"input_tokens": 320,
"output_tokens": 84
},
"request_hash": "LOWERCASE_SHA256",
"response_hash": "LOWERCASE_SHA256"
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/llm-traces.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"provider": "openai",
"model": "gpt-example",
"model_version": "2026-09",
"model_type": "generation",
"status": "completed",
"latency_ms": 842,
"usage": {
"input_tokens": 320,
"output_tokens": 84
},
"request_hash": "LOWERCASE_SHA256",
"response_hash": "LOWERCASE_SHA256"
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Raw prompt and response values, when supplied, are used only to calculate hashes and are not persisted. Prefer calculating hashes in your own trust boundary.
Record a tool trace
ai_runtime_executions:writePath, query, and header parameters
executionIdRequiredRequest Body
{
"phase": "completed",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"decision_id": "DECISION_ID"
}phaseactionRequiredtoolmcp_serverresourcedecision_idRequirederror_codemetadataResponses
Tool outcome recorded.
{
"id": "TOOL_TRACE_ID",
"kind": "tool",
"phase": "completed",
"status": "completed",
"audit_sequence": 6
}Decision mismatch, denied action, pending approval, or closed execution.
{
"error": "Tool action is still awaiting approval."
}A completed action omitted its authorization decision.
{
"error": "Completed tool traces require an authorization decision."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/tool-traces.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"phase": "completed",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"decision_id": "DECISION_ID"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/tool-traces.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"phase": "completed",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"decision_id": "DECISION_ID"
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/tool-traces.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"phase": "completed",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"decision_id": "DECISION_ID"
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Read the audit trace
ai_runtime:readPath, query, and header parameters
executionIdRequiredResponses
Up to 500 tamper-evident audit events ordered by sequence.
{
"elements": [
{
"sequence": 1,
"event_type": "ai.execution.started",
"previous_hash": null,
"current_hash": "SHA256"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/trace.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/trace.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/trace.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Model inventory
ai_runtime:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Models observed in company LLM traces.
{
"elements": [
{
"id": "MODEL_ID",
"provider": "openai",
"name": "gpt-example",
"type": "generation"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/models.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/models.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/models.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
ai_runtime:readPath, query, and header parameters
modelIdRequiredResponses
Model, distinct execution IDs, and associated traces.
{
"model": {
"id": "MODEL_ID",
"provider": "openai",
"name": "gpt-example"
},
"executions": [
"EXECUTION_ID"
],
"traces": []
}Model not found in this company.
{
"error": "AI Model not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/models/MODELID/usage.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/models/MODELID/usage.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/models/MODELID/usage.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Policies and authorization
Policies are evaluated by descending priority with deterministic tie-breaking. Selectors accept exact values, *, and trailing-prefix wildcards. Conditions accept one of eq, gt, gte, lt, lte, or in per field.
List policies
ai_runtime:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Policy versions ordered by name and descending version.
{
"elements": [
{
"id": "POLICY_ID",
"name": "High-value payment review",
"version": 1,
"status": "active",
"effect": "REQUIRE_APPROVAL"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Create a policy
ai_runtime:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "High-value payment review",
"version": 1,
"status": "active",
"priority": 100,
"systems": [
"SYSTEM_ID"
],
"agents": [
"AGENT_ID"
],
"actions": [
"payment.execute"
],
"tools": [
"payments-mcp.*"
],
"conditions": {
"amount": {
"gt": 500
}
},
"effect": "REQUIRE_APPROVAL",
"rule": "high_value_payment",
"reason": "Payments above 500 require review."
}nameRequiredversionstatusprioritysystems / agentsactionsRequiredtoolsconditionseffectRequiredrule / reasonResponses
Policy version created, and activated when requested.
{
"id": "POLICY_ID",
"name": "High-value payment review",
"version": 1,
"status": "active",
"effect": "REQUIRE_APPROVAL"
}This policy name/version already exists.
{
"error": "This policy version already exists."
}Selector, condition, or effect is invalid.
{
"error": "Policy effect must be ALLOW, DENY, or REQUIRE_APPROVAL."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "High-value payment review",
"version": 1,
"status": "active",
"priority": 100,
"systems": [
"SYSTEM_ID"
],
"agents": [
"AGENT_ID"
],
"actions": [
"payment.execute"
],
"tools": [
"payments-mcp.*"
],
"conditions": {
"amount": {
"gt": 500
}
},
"effect": "REQUIRE_APPROVAL",
"rule": "high_value_payment",
"reason": "Payments above 500 require review."
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "High-value payment review",
"version": 1,
"status": "active",
"priority": 100,
"systems": [
"SYSTEM_ID"
],
"agents": [
"AGENT_ID"
],
"actions": [
"payment.execute"
],
"tools": [
"payments-mcp.*"
],
"conditions": {
"amount": {
"gt": 500
}
},
"effect": "REQUIRE_APPROVAL",
"rule": "high_value_payment",
"reason": "Payments above 500 require review."
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"name": "High-value payment review",
"version": 1,
"status": "active",
"priority": 100,
"systems": [
"SYSTEM_ID"
],
"agents": [
"AGENT_ID"
],
"actions": [
"payment.execute"
],
"tools": [
"payments-mcp.*"
],
"conditions": {
"amount": {
"gt": 500
}
},
"effect": "REQUIRE_APPROVAL",
"rule": "high_value_payment",
"reason": "Payments above 500 require review."
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve a policy
ai_runtime:readPath, query, and header parameters
policyIdRequiredResponses
Policy version.
{
"id": "POLICY_ID",
"name": "High-value payment review",
"version": 1,
"status": "active"
}Policy not found in this company.
{
"error": "AI Policy not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies/POLICYID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies/POLICYID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/policies/POLICYID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Authorize a tool action
ai_runtime_authorization:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"system_id": "SYSTEM_ID",
"execution_id": "EXECUTION_ID",
"agent_id": "AGENT_ID",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"context": {
"amount": 750,
"currency": "EUR"
}
}system_idRequiredexecution_idRequiredagent_idRequiredactionRequiredtoolmcp_serverresourcecontextResponses
Fail-closed policy decision. REQUIRE_APPROVAL also contains a pending approval.
{
"decision": "REQUIRE_APPROVAL",
"decision_id": "DECISION_ID",
"policy_id": "POLICY_ID",
"policy_version": 1,
"rule": "high_value_payment",
"reason": "Payments above 500 require review.",
"approval_id": "APPROVAL_ID",
"approval": {
"status": "pending"
}
}Resource relationships, execution state, or idempotency conflict.
{
"error": "Execution does not belong to the requested Agent Identity."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/authorize.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"system_id": "SYSTEM_ID",
"execution_id": "EXECUTION_ID",
"agent_id": "AGENT_ID",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"context": {
"amount": 750,
"currency": "EUR"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/authorize.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"system_id": "SYSTEM_ID",
"execution_id": "EXECUTION_ID",
"agent_id": "AGENT_ID",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"context": {
"amount": 750,
"currency": "EUR"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/authorize.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"system_id": "SYSTEM_ID",
"execution_id": "EXECUTION_ID",
"agent_id": "AGENT_ID",
"action": "payment.execute",
"tool": "payments-mcp.execute",
"mcp_server": "payments-mcp",
"resource": "payment:proposal-123",
"context": {
"amount": 750,
"currency": "EUR"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
A decision is not tool execution
Donutwork records and governs intent; it never performs the requested tool action. Your application must enforce the decision and record the eventual outcome.
Human approvals
List approvals
ai_runtime_approvals:readPath, query, and header parameters
statusResponses
Approvals ordered newest first.
{
"elements": [
{
"id": "APPROVAL_ID",
"execution_id": "EXECUTION_ID",
"action": "payment.execute",
"status": "pending"
}
]
}Unsupported status filter.
{
"error": "Approval status filter is invalid."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Retrieve an approval
ai_runtime_approvals:readPath, query, and header parameters
approvalIdRequiredResponses
Approval and decision linkage.
{
"id": "APPROVAL_ID",
"decision_id": "DECISION_ID",
"status": "pending",
"requested_at": "2026-09-18T10:00:04+00:00"
}Approval not found in this company.
{
"error": "AI Approval not found."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Decide an approval
ai_runtime_approvals:writePath, query, and header parameters
approvalIdRequiredRequest Body
{
"decision": "approved",
"reason": "Reviewed against purchase order PO-8472"
}decisionRequiredreasonResponses
One-shot decision recorded; decided_by is derived from the authenticated API key.
{
"id": "APPROVAL_ID",
"status": "approved",
"decided_by": "API_KEY_ID",
"reason": "Reviewed against purchase order PO-8472"
}Approval already resolved or execution closed before approval.
{
"error": "Approval is already resolved as approved."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID/decision.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"decision": "approved",
"reason": "Reviewed against purchase order PO-8472"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID/decision.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"decision": "approved",
"reason": "Reviewed against purchase order PO-8472"
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/approvals/APPROVALID/decision.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"decision": "approved",
"reason": "Reviewed against purchase order PO-8472"
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Evaluations
ai_runtime:readPath, query, and header parameters
executionIdResponses
Evaluations ordered newest first.
{
"elements": [
{
"id": "EVALUATION_ID",
"name": "policy-conformance",
"score": 0.98,
"threshold": 0.95,
"passed": true
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
ai_runtime_executions:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"execution_id": "EXECUTION_ID",
"trace_id": "TRACE_ID",
"name": "policy-conformance",
"type": "threshold",
"score": 0.98,
"threshold": 0.95,
"severity": "high",
"target_type": "llm_trace"
}execution_idRequiredtrace_idmodel_idnameRequiredtypescore / thresholdRequiredseveritytarget_typemetadataResponses
Deterministic result recorded.
{
"id": "EVALUATION_ID",
"evaluator": "deterministic.threshold",
"evaluator_version": "1",
"score": 0.98,
"threshold": 0.95,
"passed": true,
"severity": "none"
}Unsupported evaluator or out-of-range values.
{
"error": "Evaluation score and threshold must be between 0 and 1."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"execution_id": "EXECUTION_ID",
"trace_id": "TRACE_ID",
"name": "policy-conformance",
"type": "threshold",
"score": 0.98,
"threshold": 0.95,
"severity": "high",
"target_type": "llm_trace"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"execution_id": "EXECUTION_ID",
"trace_id": "TRACE_ID",
"name": "policy-conformance",
"type": "threshold",
"score": 0.98,
"threshold": 0.95,
"severity": "high",
"target_type": "llm_trace"
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evaluations.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"execution_id": "EXECUTION_ID",
"trace_id": "TRACE_ID",
"name": "policy-conformance",
"type": "threshold",
"score": 0.98,
"threshold": 0.95,
"severity": "high",
"target_type": "llm_trace"
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Controls, compliance mappings, and evidence
These endpoints report technical evidence. They are not legal advice, do not decide whether a regulation applies, and do not certify compliance.
ai_runtime:readPath, query, and header parameters
executionIdRequiredResponses
Evidence availability for logging, identity, models, policy, oversight, evaluation, audit, and tools.
{
"elements": [
{
"id": "audit_integrity",
"status": "evidence_available"
},
{
"id": "human_oversight",
"status": "evidence_missing"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/controls.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/controls.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/controls.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
ai_runtime:readPath, query, and header parameters
executionIdRequiredpackSlugRequiredResponses
Requirements mapped to available and missing evidence.
{
"slug": "eu-ai-act",
"execution_id": "EXECUTION_ID",
"requirements": [
{
"technical_status": "partial",
"evidence_available": [
"audit_integrity"
],
"evidence_missing": [
"human_oversight"
]
}
],
"disclaimer": "Technical mappings only; not legal advice and not a certification of compliance."
}Unknown compliance pack.
{
"error": "Unsupported compliance pack."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/compliance/PACKSLUG.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/compliance/PACKSLUG.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/executions/EXECUTIONID/compliance/PACKSLUG.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Export execution evidence
ai_runtime:readPath, query, and header parameters
executionIdRequiredResponses
Evidence schema v1 with runtime facts, hashes, verification, controls, and mappings.
{
"schema": "donutwork.ai-runtime.evidence",
"schema_version": "1.0",
"tenant": {
"company_id": "COMPANY_ID"
},
"execution": {
"id": "EXECUTION_ID"
},
"audit_verification": {
"valid": true
},
"consistency": {
"mode": "audit_head_snapshot",
"status": "verified"
}
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/executions/EXECUTIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/executions/EXECUTIONID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/executions/EXECUTIONID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Export system evidence
ai_runtime:readPath, query, and header parameters
systemIdRequiredfromtoResponses
Up to 100 matching execution evidence documents.
{
"schema": "donutwork.ai-runtime.system-evidence",
"schema_version": "1.0",
"filter": {
"from": null,
"to": null
},
"system": {
"id": "SYSTEM_ID"
},
"executions": []
}from or to is not a valid timestamp.
{
"error": "Evidence time filters must be valid timestamps."
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/systems/SYSTEMID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/systems/SYSTEMID.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-runtime/evidence/systems/SYSTEMID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Evidence excludes raw prompts, responses, documents, tool payloads, headers, credentials, and idempotency keys. consistency.status=verified means the exported audit events matched the chain head read for that export; retry an incomplete_or_concurrent_change export before archiving it.