DonutworkHelp Center
Documentazione Donutwork

Copilot API

Integrate user-bound Copilot conversations, live progress, feedback, and explicitly confirmed actions.

Copilot API

Copilot runs on behalf of an authenticated user in one company. It requires a user-bound mobile API key or a personal application OAuth grant with copilot:read and/or copilot:write. A generic company API key is insufficient. The user must have current membership, backoffice.access, copilot.use, and full company access; business actions also use current user permissions. Company and user IDs in a request body cannot set identity.

Personal OAuth

Authorize through the registered application's /i3/oauth/authorize on the Donutwork application host, with response_type=code, registered client_id and redirect_uri, requested scopes, random state (16–512 characters), and PKCE S256 (code_challenge, code_challenge_method=S256). Optional companyId limits the company selection. Verify returned state. Exchange the single-use code within 10 minutes through /i3/oauth/token on your trusted backend with client_id, client_secret, redirect_uri, code, and code_verifier (43–128 characters). Never ship the secret in a native or browser client. The response's access_token is the Bearer token; company_id, user_id, and copilot_url describe the selected identity. No refresh-token flow is introduced. Mixed business scopes additionally require permission to manage company integrations.

Conversation and streaming flow

  1. Read /copilot/me.json and /copilot/bootstrap.json. Use config.can_write and available suggestions to build the interface.
  2. Create a conversation and save conversation.id. Do not repeat creation after a timeout without checking the conversation list.
  3. The REST default for message and confirm is response_mode=mercure. POST /copilot/events.json, open SSE at stream.hub_url with query parameter topic=stream.topic and Authorization: Bearer <stream.token>, and send stream_id=stream.id in the message/confirm body. Use the stream JWT only on the hub.
  4. An asynchronous request returns 202 with status=accepted. Handle ready, progress, delta, message, action, completed, and error events. Read conversation state after a disconnect. For a simple server-side integration, explicitly choose response_mode=buffered, as in the examples below, without a stream ticket.
  5. Keep client_message_id stable for the same message and payload. It must contain 8–128 characters: first alphanumeric, then alphanumeric, dot, underscore, colon, or hyphen. Messages are at most 4,000 characters. Read progress with that same ID.
  6. Display any action proposal and obtain explicit human approval before calling confirm. After an ambiguous confirmation response, read the conversation to recover the outcome instead of repeating confirm. After success, a new turn can use continuation_action_id with the confirmed action ID.

Message, feedback, and confirmation requests use JSON, at most 32 KiB. Responses are private and must not be cached. presentation.version=1 covers action cards and stream_version=2 is advertised by bootstrap. Read/list access follows user ownership; administrators can read company conversations, while mutations and progress remain owner-scoped.

Endpoint reference

Response examples are partial illustrations of the envelope; conversation, action, credit, and presentation data depend on the current user's permissions and state. Replace all uppercase placeholders with returned IDs. Examples use buffered mode explicitly where a streaming ticket would otherwise be required.

Read authenticated identity

GET
/2026-02-01/copilot/me.json
Required permissioncopilot:read

Path, query, and header parameters

No path, query, or additional header parameters.

Responses

Partial successful response example.

{
  "status": "completed",
  "user": {
    "id": "USER_ID",
    "name": "Alex Example",
    "language": "en"
  },
  "company": {
    "id": "COMPANY_ID",
    "name": "Example Studio",
    "timezone": "Europe/Rome"
  },
  "is_administrator": false,
  "request_id": "REQUEST_ID"
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/copilot/me.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/me.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/copilot/me.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.

Read bootstrap

GET
/2026-02-01/copilot/bootstrap.json
Required permissioncopilot:read

Path, query, and header parameters

No path, query, or additional header parameters.

Responses

Partial successful response example.

{
  "status": "completed",
  "config": {
    "version": 1,
    "can_write": true,
    "max_message_length": 4000,
    "response_modes": [
      "mercure",
      "buffered"
    ],
    "default_response_mode": "mercure",
    "presentation_version": 1,
    "stream_version": 2
  }
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/copilot/bootstrap.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/bootstrap.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/copilot/bootstrap.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 stream ticket

POST
/2026-02-01/copilot/events.json
Required permissioncopilot:write

Path, query, and header parameters

No path, query, or additional header parameters.

Responses

Partial successful response example.

{
  "status": "completed",
  "stream": {
    "id": "STREAM_ID",
    "hub_url": "https://stream.example.com/.well-known/mercure",
    "topic": "PRIVATE_TOPIC",
    "token": "STREAM_JWT",
    "expires_at": 1792760400
  }
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/copilot/events.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/events.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/copilot/events.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.

List conversations

GET
/2026-02-01/copilot/conversations.json
Required permissioncopilot:read

Path, query, and header parameters

No path, query, or additional header parameters.

Responses

Partial successful response example.

{
  "status": "completed",
  "conversations": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations.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/copilot/conversations.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 conversation

POST
/2026-02-01/copilot/conversations.json
Required permissioncopilot:write

Path, query, and header parameters

No path, query, or additional header parameters.

Responses

Partial successful response example.

{
  "status": "completed",
  "conversation": {
    "id": "CONVERSATION_ID"
  }
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations.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/copilot/conversations.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.

Read conversation

GET
/2026-02-01/copilot/conversations/{conversationId}.json
Required permissioncopilot:read

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.

Responses

Partial successful response example.

{
  "status": "completed",
  "conversation": {
    "id": "CONVERSATION_ID"
  },
  "messages": [],
  "actions": []
}

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID.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/copilot/conversations/CONVERSATIONID.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.

Delete own conversation

DELETE
/2026-02-01/copilot/conversations/{conversationId}.json
Required permissioncopilot:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.

Responses

Partial successful response example.

{
  "status": "completed",
  "deleted": true,
  "request_id": "REQUEST_ID"
}

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID.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/copilot/conversations/CONVERSATIONID.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.

Send a message

POST
/2026-02-01/copilot/conversations/{conversationId}/messages.json
Required permissioncopilot:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.

Request Body

JSON
{
  "message": "Summarize my available billing information.",
  "client_message_id": "example-msg-001",
  "response_mode": "buffered"
}
messagestringRequired
User message, at most 4,000 characters.
client_message_idstringRequired
Stable 8–128-character message identifier.
response_modestring
buffered for this example; REST default is mercure.
stream_idstring
Required for mercure: stream.id from the ticket.
page_contextobject
Optional resource reference; resolved and authorized by the server.
input_answerobject
Structured answer to a clarification; cannot be combined with revision_action_id.
revision_action_idstring
Proposal to revise; use separately from input_answer.
continuation_action_idstring
Confirmed action ID for a subsequent turn.

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/messages.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "message": "Summarize my available billing information.",
  "client_message_id": "example-msg-001",
  "response_mode": "buffered"
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/messages.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "message": "Summarize my available billing information.",
    "client_message_id": "example-msg-001",
    "response_mode": "buffered"
  }),
});
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/copilot/conversations/CONVERSATIONID/messages.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 => '{
  "message": "Summarize my available billing information.",
  "client_message_id": "example-msg-001",
  "response_mode": "buffered"
}',
]);
$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 own message progress

GET
/2026-02-01/copilot/conversations/{conversationId}/progress/{clientMessageId}.json
Required permissioncopilot:read

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.
clientMessageIdstring· pathRequired
Same stable client_message_id sent with the message.

Request examples

curl --request GET 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/progress/CLIENTMESSAGEID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/progress/CLIENTMESSAGEID.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/copilot/conversations/CONVERSATIONID/progress/CLIENTMESSAGEID.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.

Save feedback

POST
/2026-02-01/copilot/conversations/{conversationId}/feedback.json
Required permissioncopilot:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.

Request Body

JSON
{
  "message_id": "MESSAGE_ID",
  "rating": "positive",
  "reason_code": "helpful"
}
message_idstringRequired
Message ID in the owned conversation.
ratingstringRequired
positive or negative.
reason_codestring
Empty, accurate, helpful, incorrect, incomplete, unclear, or permission_issue.

Responses

Partial successful response example.

{
  "status": "completed",
  "saved": true,
  "request_id": "REQUEST_ID"
}

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/feedback.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "message_id": "MESSAGE_ID",
  "rating": "positive",
  "reason_code": "helpful"
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/feedback.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "message_id": "MESSAGE_ID",
    "rating": "positive",
    "reason_code": "helpful"
  }),
});
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/copilot/conversations/CONVERSATIONID/feedback.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 => '{
  "message_id": "MESSAGE_ID",
  "rating": "positive",
  "reason_code": "helpful"
}',
]);
$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.

Confirm a proposal after human approval

POST
/2026-02-01/copilot/conversations/{conversationId}/actions/{actionId}/confirm.json
Required permissioncopilot:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.
actionIdstring· pathRequired
24-character hexadecimal proposal ID from the conversation.

Request Body

JSON
{
  "response_mode": "buffered"
}
response_modestring
buffered for this example; REST default is mercure.
stream_idstring
Required for mercure: stream.id from the ticket.

Request examples

curl --request POST 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/actions/ACTIONID/confirm.json' \
  --header 'Authorization: Bearer YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
  "response_mode": "buffered"
}'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/actions/ACTIONID/confirm.json', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "response_mode": "buffered"
  }),
});
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/copilot/conversations/CONVERSATIONID/actions/ACTIONID/confirm.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 => '{
  "response_mode": "buffered"
}',
]);
$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 proposal

DELETE
/2026-02-01/copilot/conversations/{conversationId}/actions/{actionId}.json
Required permissioncopilot:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation ID returned by create/list; mutations require ownership.
actionIdstring· pathRequired
24-character hexadecimal proposal ID from the conversation.

Request examples

curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/actions/ACTIONID.json' \
  --header 'Authorization: Bearer YOUR_API_KEY'
const response = await fetch('https://api.hub.donutwork.com/2026-02-01/copilot/conversations/CONVERSATIONID/actions/ACTIONID.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/copilot/conversations/CONVERSATIONID/actions/ACTIONID.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.

Errors and recovery

Handler errors use { "status": "error", "error": { "code": "...", "message": "..." }, "http_status": 422, "request_id": "..." }. Router authentication/scope failures retain the common API error envelope.

  • 401/403: invalid key, insufficient scopes, user identity, membership, or current permissions.
  • 404: conversation or proposal missing or unavailable to the current user.
  • 413/415/422: body limit, content type, invalid message, stream, or action input.
  • 429: API allowance or Copilot turn/credit limits; respect the indicated limits.

Retry a message only with the original client ID and payload to recover its persisted turn. Confirmation has separate recovery semantics: GET the result instead of automatically resending. Never log bearer or stream tokens, conversation content, or sensitive user data.

On this page