AI Agents
Create, publish and operate persistent AI collaborators with bounded permissions, schedules, approvals and auditable runs.
AI Agents API
Use the AI Agents API to define a collaborator, assign one or more Tasks, publish an immutable configuration and start or monitor its work. Every resource is isolated to the organization associated with the bearer token.
Call these endpoints only from a trusted backend. Agent input, results, memory and action arguments can contain business data. Grant API keys only the scopes and Agent tools required by the integration.
All examples use API version 2026-02-01. Send JSON with Content-Type: application/json and authenticate with Authorization: Bearer YOUR_DONUTWORK_TOKEN.
Lifecycle at a glance
- Create an Agent in draft state.
- Add at least one Task and select the tools it can use.
- Run the safety check, then publish the Agent.
- Start runs manually through the API, or let published schedule and event triggers start them.
- Decide requested approvals and reconcile actions whose external outcome could not be confirmed.
Publishing creates an immutable release. Updating an Agent or Task changes only the draft until a new release is published. Use an Idempotency-Key header when creating a run; reusing the same key with a different payload returns 409 Conflict.
Agents
List Agents
ai_agents:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Agents visible to the authenticated organization.
{
"elements": [
{
"id": "agent_123",
"name": "Subscription Success",
"goal": "Review subscription risks every morning",
"status": "active",
"approval_policy": "supervised",
"current_release_version": 2
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/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-agents/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.
Create an Agent
The Agent starts as a draft. If principal_id is omitted, Donutwork creates a company-scoped permission profile for it.
ai_agents:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Subscription Success",
"goal": "Review subscription risks every morning and publish priorities",
"approval_policy": "supervised",
"inference_profile": "donut_managed_credits",
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"max_steps": 8,
"max_tool_calls": 6,
"max_runs_per_day": 24
}nameRequiredgoalRequiredinstructionsapproval_policyinference_profileassistant_idprincipal_idtool_namesmax_stepsmax_tool_callsmax_runs_per_dayResponses
Draft Agent created.
{
"id": "agent_123",
"name": "Subscription Success",
"status": "draft",
"approval_policy": "supervised"
}A required field, profile, policy or limit is invalid.
{
"error": "Invalid Agent configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Subscription Success",
"goal": "Review subscription risks every morning and publish priorities",
"approval_policy": "supervised",
"inference_profile": "donut_managed_credits",
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"max_steps": 8,
"max_tool_calls": 6,
"max_runs_per_day": 24
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Subscription Success",
"goal": "Review subscription risks every morning and publish priorities",
"approval_policy": "supervised",
"inference_profile": "donut_managed_credits",
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"max_steps": 8,
"max_tool_calls": 6,
"max_runs_per_day": 24
}),
});
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-agents/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 => '{
"name": "Subscription Success",
"goal": "Review subscription risks every morning and publish priorities",
"approval_policy": "supervised",
"inference_profile": "donut_managed_credits",
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"max_steps": 8,
"max_tool_calls": 6,
"max_runs_per_day": 24
}',
]);
$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.
Get an Agent
The default API response includes Tasks, releases and memory metadata but excludes decrypted memory content.
ai_agents:readPath, query, and header parameters
agentIdRequiredResponses
Agent configuration and related resources.
{
"id": "agent_123",
"name": "Subscription Success",
"status": "active",
"tasks": [
{
"id": "task_123",
"name": "Morning review"
}
],
"releases": [
{
"id": "release_2",
"version": 2,
"status": "active"
}
],
"memory_usage": {
"items": 3,
"used_bytes": 640,
"capacity_bytes": 24000
}
}Agent not found in the authenticated organization.
{
"error": "Agent not found"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/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-agents/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.
Update an Agent draft
Only supplied fields are changed. Publish again before saved draft changes affect new runs.
ai_agents:writePath, query, and header parameters
agentIdRequiredRequest Body
{
"goal": "Review subscription risks and publish the top five priorities",
"approval_policy": "delegated",
"max_runs_per_day": 12
}namegoalinstructionsapproval_policyinference_profileassistant_idprincipal_idtool_namesmax_stepsmax_tool_callsmax_runs_per_dayResponses
Agent draft updated.
{
"id": "agent_123",
"name": "Subscription Success",
"approval_policy": "delegated",
"draft_revision": 4
}Agent not found.
{
"error": "Agent not found"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"goal": "Review subscription risks and publish the top five priorities",
"approval_policy": "delegated",
"max_runs_per_day": 12
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"goal": "Review subscription risks and publish the top five priorities",
"approval_policy": "delegated",
"max_runs_per_day": 12
}),
});
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-agents/agents/AGENTID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"goal": "Review subscription risks and publish the top five priorities",
"approval_policy": "delegated",
"max_runs_per_day": 12
}',
]);
$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.
Delete an Agent
Deletion stops triggers and pending work while retaining published releases and run history according to the configured retention policy. Running or unresolved runs must be completed first.
ai_agents:writePath, query, and header parameters
agentIdRequiredResponses
Agent and its current Tasks removed from the workspace.
{
"deleted": true,
"agent_id": "agent_123"
}The Agent still has running or unresolved work.
{
"error": "Complete active Agent runs before deleting it"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID.json', {
method: 'DELETE',
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-agents/agents/AGENTID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
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.
Pause or reactivate an Agent
Pausing prevents new work and cancels queued runs and pending approvals. Reactivation requires a compatible published release and an active permission profile.
ai_agents:writePath, query, and header parameters
agentIdRequiredRequest Body
{
"status": "paused"
}statusRequiredResponses
Agent status changed.
{
"id": "agent_123",
"status": "paused"
}The Agent cannot be activated with its current release or permission profile.
{
"error": "Publish the Agent before activating it"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/status.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"status": "paused"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/status.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"status": "paused"
}),
});
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-agents/agents/AGENTID/status.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": "paused"
}',
]);
$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.
Tasks
Create a Task
A Task defines one expected outcome and one start condition. For schedule, include a schedule object. For event, include event_name.
ai_agents:writePath, query, and header parameters
agentIdRequiredRequest Body
{
"name": "Morning review",
"objective": "Publish the five subscriptions that need attention",
"trigger_type": "schedule",
"schedule": {
"frequency": "daily",
"interval": 1,
"time": "08:00",
"timezone": "Europe/Rome"
},
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"status": "active",
"overlap_policy": "skip",
"on_error": "stop",
"max_runs_per_day": 1
}nameRequiredobjectiveRequiredinstructionstrigger_typescheduleevent_nametool_namesemail_deliverydepends_ondependency_policyoverlap_policyon_errormax_runs_per_dayResponses
Task saved in the Agent draft.
{
"id": "task_123",
"agent_id": "agent_123",
"name": "Morning review",
"status": "active",
"trigger_type": "schedule"
}Task fields, schedule or dependency graph are invalid.
{
"error": "Invalid Task configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Morning review",
"objective": "Publish the five subscriptions that need attention",
"trigger_type": "schedule",
"schedule": {
"frequency": "daily",
"interval": 1,
"time": "08:00",
"timezone": "Europe/Rome"
},
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"status": "active",
"overlap_policy": "skip",
"on_error": "stop",
"max_runs_per_day": 1
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Morning review",
"objective": "Publish the five subscriptions that need attention",
"trigger_type": "schedule",
"schedule": {
"frequency": "daily",
"interval": 1,
"time": "08:00",
"timezone": "Europe/Rome"
},
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"status": "active",
"overlap_policy": "skip",
"on_error": "stop",
"max_runs_per_day": 1
}),
});
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-agents/agents/AGENTID/tasks.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": "Morning review",
"objective": "Publish the five subscriptions that need attention",
"trigger_type": "schedule",
"schedule": {
"frequency": "daily",
"interval": 1,
"time": "08:00",
"timezone": "Europe/Rome"
},
"tool_names": [
"search_billing_records",
"publish_artifact"
],
"status": "active",
"overlap_policy": "skip",
"on_error": "stop",
"max_runs_per_day": 1
}',
]);
$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.
Update a Task
The request replaces the saved Task configuration. Publish the Agent again before changes affect new runs.
ai_agents:writePath, query, and header parameters
agentIdRequiredtaskIdRequiredRequest Body
{
"name": "Morning review",
"objective": "Publish the top five priorities",
"trigger_type": "manual",
"status": "active",
"tool_names": [
"search_billing_records",
"publish_artifact"
]
}Responses
Task draft updated.
{
"id": "task_123",
"agent_id": "agent_123",
"trigger_type": "manual"
}Agent or Task not found.
{
"error": "Task not found"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks/TASKID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Morning review",
"objective": "Publish the top five priorities",
"trigger_type": "manual",
"status": "active",
"tool_names": [
"search_billing_records",
"publish_artifact"
]
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks/TASKID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Morning review",
"objective": "Publish the top five priorities",
"trigger_type": "manual",
"status": "active",
"tool_names": [
"search_billing_records",
"publish_artifact"
]
}),
});
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-agents/agents/AGENTID/tasks/TASKID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"name": "Morning review",
"objective": "Publish the top five priorities",
"trigger_type": "manual",
"status": "active",
"tool_names": [
"search_billing_records",
"publish_artifact"
]
}',
]);
$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.
Delete a Task
ai_agents:writePath, query, and header parameters
agentIdRequiredtaskIdRequiredResponses
Task removed and references from current draft dependencies cleaned up.
{
"deleted": true,
"agent_id": "agent_123",
"task_id": "task_123"
}The Task still has running or unresolved work.
{
"error": "Complete active Task runs before deleting it"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks/TASKID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/tasks/TASKID.json', {
method: 'DELETE',
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-agents/agents/AGENTID/tasks/TASKID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
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.
Releases
Check an Agent configuration
Run this check immediately before publishing. Errors block publication; warnings describe choices that deserve review.
ai_agents:writePath, query, and header parameters
agentIdRequiredResponses
Configuration check completed.
{
"passed": true,
"errors": [],
"warnings": [
{
"code": "delegated_actions",
"message": "Delegated actions can proceed without human approval"
}
]
}Agent not found.
{
"error": "Agent not found"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/safety-check.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/safety-check.json', {
method: 'POST',
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-agents/agents/AGENTID/safety-check.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
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.
Publish an Agent
Publication snapshots the Agent, its current Tasks, permissions, selected inference profile and exposed tool contracts. A successful publication activates the Agent and its published triggers.
ai_agents:writePath, query, and header parameters
agentIdRequiredResponses
New immutable release published and activated.
{
"id": "release_2",
"agent_id": "agent_123",
"version": 2,
"status": "active"
}The safety check found blocking configuration errors.
{
"error": "The Agent safety check failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases.json', {
method: 'POST',
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-agents/agents/AGENTID/releases.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
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.
Restore a published release
Restoring changes which release starts future runs. Existing runs remain bound to their original release.
ai_agents:writePath, query, and header parameters
agentIdRequiredreleaseIdRequiredResponses
Published release activated.
{
"id": "release_1",
"agent_id": "agent_123",
"version": 1,
"status": "active"
}The release is incompatible with the current tools or references a deleted Task.
{
"error": "Release cannot be restored"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases/RELEASEID/activate.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases/RELEASEID/activate.json', {
method: 'POST',
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-agents/agents/AGENTID/releases/RELEASEID/activate.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
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.
Delete a published release
Deleting a non-current version removes it from the versions available for restoration and prevents it from starting new activities. Existing run history remains bound to its immutable snapshot. Restore or publish another version before deleting the current one.
ai_agents:writePath, query, and header parameters
agentIdRequiredreleaseIdRequiredResponses
Published version removed from the available release history.
{
"deleted": true,
"agent_id": "agent_123",
"release_id": "release_1",
"version": 1
}Agent or published version not found.
{
"error": "Agent release not found"
}The selected version is current or changed concurrently.
{
"error": "The current published version cannot be deleted"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases/RELEASEID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/releases/RELEASEID.json', {
method: 'DELETE',
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-agents/agents/AGENTID/releases/RELEASEID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
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.
Runs
List runs
Use the optional agentId query parameter to restrict results to one Agent.
ai_agent_runs:readPath, query, and header parameters
agentIdResponses
Recent runs in the authenticated organization.
{
"elements": [
{
"id": "run_123",
"agent_id": "agent_123",
"task_id": "task_123",
"status": "completed",
"source": "api",
"created_at": "2026-09-11T08:00:00+00:00"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/runs.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/runs.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-agents/runs.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.
Get a run
This detailed response can include decrypted run input, working steps, output and artifacts. Store and display it according to your own data-access policy.
ai_agent_runs:readPath, query, and header parameters
runIdRequiredResponses
Detailed run state and results.
{
"id": "run_123",
"status": "completed",
"input": {
"report_date": "2026-09-11"
},
"output": {
"answer": "Five subscriptions need attention."
},
"steps": [
{
"sequence": 1,
"status": "completed",
"summary": "Reviewed current subscription records"
}
],
"artifacts": []
}Run not found.
{
"error": "Run not found"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID.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-agents/runs/RUNID.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 a run
If task_id is omitted, the Agent must have exactly one active Task in its published release. input must be a JSON object smaller than 128 KB.
ai_agent_runs:writePath, query, and header parameters
agentIdRequiredRequest Body
{
"task_id": "task_123",
"input": {
"report_date": "2026-09-11"
},
"idempotency_key": "subscription-review-2026-09-11"
}task_idinputidempotency_keyResponses
Run created and queued.
{
"id": "run_123",
"agent_id": "agent_123",
"task_id": "task_123",
"status": "queued",
"source": "api"
}The Agent is not active, its release must be updated, a limit was reached, or the idempotency key conflicts.
{
"error": "Unable to create the Agent run"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/runs.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"task_id": "task_123",
"input": {
"report_date": "2026-09-11"
},
"idempotency_key": "subscription-review-2026-09-11"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/runs.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"task_id": "task_123",
"input": {
"report_date": "2026-09-11"
},
"idempotency_key": "subscription-review-2026-09-11"
}),
});
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-agents/agents/AGENTID/runs.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 => '{
"task_id": "task_123",
"input": {
"report_date": "2026-09-11"
},
"idempotency_key": "subscription-review-2026-09-11"
}',
]);
$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.
Cancel a run
Queued, running and approval-waiting runs can be cancelled. A completed or otherwise terminal run cannot be changed.
ai_agent_runs:writePath, query, and header parameters
runIdRequiredResponses
Run cancelled.
{
"id": "run_123",
"status": "cancelled"
}Run is already terminal.
{
"error": "The Agent run cannot be cancelled"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID/cancel.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID/cancel.json', {
method: 'POST',
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-agents/runs/RUNID/cancel.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
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.
Reconcile an action outcome
Use this endpoint only when a run is in reconciliation_required. First verify the real outcome in the destination system. Donutwork does not execute the action again.
ai_agent_runs:writePath, query, and header parameters
runIdRequiredstepIdRequiredRequest Body
{
"outcome": "succeeded",
"result": {
"external_reference": "crm_456"
},
"note": "Verified in the destination audit log"
}outcomeRequiredresultnoteRequiredResponses
Verified outcome recorded.
{
"id": "run_123",
"status": "queued"
}The step or run no longer requires reconciliation.
{
"error": "This Agent step no longer requires reconciliation"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID/steps/STEPID/reconcile.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"outcome": "succeeded",
"result": {
"external_reference": "crm_456"
},
"note": "Verified in the destination audit log"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/runs/RUNID/steps/STEPID/reconcile.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"outcome": "succeeded",
"result": {
"external_reference": "crm_456"
},
"note": "Verified in the destination audit log"
}),
});
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-agents/runs/RUNID/steps/STEPID/reconcile.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 => '{
"outcome": "succeeded",
"result": {
"external_reference": "crm_456"
},
"note": "Verified in the destination audit log"
}',
]);
$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.
Approvals
List approvals
The default status is pending. Use all to retrieve pending and decided approvals.
ai_agent_approvals:readPath, query, and header parameters
statusResponses
Approval requests matching the selected status.
{
"elements": [
{
"id": "approval_123",
"agent_id": "agent_123",
"run_id": "run_123",
"status": "pending",
"summary": "Start the customer follow-up workflow",
"arguments": {
"workflow_id": "workflow_123"
}
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/approvals.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/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-agents/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.
Decide an approval
An approval is one-shot and applies only to the exact action and arguments in the request. Later sensitive actions require a separate decision.
ai_agent_approvals:writePath, query, and header parameters
approvalIdRequiredRequest Body
{
"decision": "approved",
"note": "Reviewed against the customer follow-up policy"
}decisionRequirednoteResponses
Decision recorded and the run made eligible to continue when approved.
{
"id": "approval_123",
"status": "approved",
"run_id": "run_123"
}Approval is no longer pending or the run is no longer waiting for it.
{
"error": "The approval is no longer pending"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/approvals/APPROVALID/decision.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"decision": "approved",
"note": "Reviewed against the customer follow-up policy"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/approvals/APPROVALID/decision.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"decision": "approved",
"note": "Reviewed against the customer follow-up policy"
}),
});
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-agents/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",
"note": "Reviewed against the customer follow-up policy"
}',
]);
$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.
Permission profiles
An Agent permission profile is company-scoped and determines which Donutwork business permissions the Agent may exercise. A Task can further restrict tools but cannot expand this profile.
List permission profiles
ai_agents:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Agent permission profiles in the authenticated organization.
{
"elements": [
{
"id": "principal_123",
"name": "Subscription Success access",
"status": "active",
"grants": [
"recurring_payments.view"
]
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/principals.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/principals.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-agents/principals.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 permission profile
ai_agents:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Subscription Success access",
"grants": [
"recurring_payments.view",
"email_gateway.manage"
]
}nameRequiredsluggrantsResponses
Active permission profile created.
{
"id": "principal_123",
"name": "Subscription Success access",
"status": "active",
"grants": [
"recurring_payments.view",
"email_gateway.manage"
]
}A profile with the same name already exists.
{
"error": "A service principal with this name already exists"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/principals.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Subscription Success access",
"grants": [
"recurring_payments.view",
"email_gateway.manage"
]
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/principals.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Subscription Success access",
"grants": [
"recurring_payments.view",
"email_gateway.manage"
]
}),
});
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-agents/principals.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": "Subscription Success access",
"grants": [
"recurring_payments.view",
"email_gateway.manage"
]
}',
]);
$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.
Update or revoke a permission profile
Revoking a profile pauses Agents that use it. Reactivation requires a new active profile and a valid published configuration.
ai_agents:writePath, query, and header parameters
principalIdRequiredRequest Body
{
"name": "Subscription Success access",
"grants": [
"recurring_payments.view"
],
"status": "active"
}namegrantsstatusResponses
Permission profile updated.
{
"id": "principal_123",
"status": "active",
"grants": [
"recurring_payments.view"
]
}Permission profile not found.
{
"error": "Service principal not found"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-agents/principals/PRINCIPALID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Subscription Success access",
"grants": [
"recurring_payments.view"
],
"status": "active"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/principals/PRINCIPALID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Subscription Success access",
"grants": [
"recurring_payments.view"
],
"status": "active"
}),
});
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-agents/principals/PRINCIPALID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"name": "Subscription Success access",
"grants": [
"recurring_payments.view"
],
"status": "active"
}',
]);
$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.
Company settings and tools
Get Agent settings
ai_agents:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Company Agent profiles, retention and volume limits.
{
"enabled_profiles": [
"ai_gateway",
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 30,
"max_concurrent_runs": 3,
"monthly_run_limit": 1000
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/settings.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-agents/settings.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.
Update Agent settings
At least one environment-enabled inference profile must remain enabled for the company. Agents never switch profiles automatically during a run.
ai_agents:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"enabled_profiles": [
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 14,
"max_concurrent_runs": 5,
"monthly_run_limit": 2500
}enabled_profilesRequireddefault_profileRequiredretention_daysmax_concurrent_runsmonthly_run_limitResponses
Company Agent settings updated.
{
"enabled_profiles": [
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 14,
"max_concurrent_runs": 5,
"monthly_run_limit": 2500
}No profile is enabled or the default profile is not in the enabled list.
{
"error": "The default profile must be enabled for the company"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-agents/settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"enabled_profiles": [
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 14,
"max_concurrent_runs": 5,
"monthly_run_limit": 2500
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/settings.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"enabled_profiles": [
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 14,
"max_concurrent_runs": 5,
"monthly_run_limit": 2500
}),
});
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-agents/settings.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"enabled_profiles": [
"donut_managed_credits"
],
"default_profile": "donut_managed_credits",
"retention_days": 14,
"max_concurrent_runs": 5,
"monthly_run_limit": 2500
}',
]);
$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.
List Agent tools
Tool definitions include their JSON input schema, required business permissions, risk classification and whether the action can be delegated. Treat the returned catalog as authoritative for the current environment.
ai_agents:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Current Agent tool catalog.
{
"elements": [
{
"name": "publish_artifact",
"description": "Publish a named result for this run",
"parameters": {
"type": "object"
},
"permissions": [],
"risk": "R0",
"read_only": false,
"delegatable": true
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-agents/tools.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/tools.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-agents/tools.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.
Pin Agent memory
Pinned memory is managed by your organization and is made available to every Task before each decision. Pinning the same key replaces its content. Content is encrypted at rest and must fit within the Agent memory capacity.
ai_agents:writePath, query, and header parameters
agentIdRequiredRequest Body
{
"key": "brand_tone",
"content": "Use concise, direct language in every customer-facing draft."
}keyRequiredcontentRequiredResponses
Pinned memory saved or replaced.
{
"id": "memory_123",
"agent_id": "agent_123",
"key": "brand_tone",
"kind": "pinned"
}The key, content or resulting total memory size is invalid.
{
"error": "Agent memory capacity exceeded"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/memory.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"key": "brand_tone",
"content": "Use concise, direct language in every customer-facing draft."
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/memory.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"key": "brand_tone",
"content": "Use concise, direct language in every customer-facing draft."
}),
});
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-agents/agents/AGENTID/memory.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 => '{
"key": "brand_tone",
"content": "Use concise, direct language in every customer-facing draft."
}',
]);
$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.
Delete Agent memory
Both pinned information and temporary information learned by the Agent can be removed. Once deleted, the item is no longer supplied to any Task in future activities.
ai_agents:writePath, query, and header parameters
agentIdRequiredmemoryIdRequiredResponses
Memory item deleted.
{
"deleted": true,
"agent_id": "agent_123",
"memory_id": "memory_123"
}Agent or memory item not found.
{
"error": "Memory not found"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/memory/MEMORYID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-agents/agents/AGENTID/memory/MEMORYID.json', {
method: 'DELETE',
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-agents/agents/AGENTID/memory/MEMORYID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'DELETE',
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.