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
- Read
/copilot/me.jsonand/copilot/bootstrap.json. Useconfig.can_writeand available suggestions to build the interface. - Create a conversation and save
conversation.id. Do not repeat creation after a timeout without checking the conversation list. - The REST default for message and confirm is
response_mode=mercure. POST/copilot/events.json, open SSE atstream.hub_urlwith query parametertopic=stream.topicandAuthorization: Bearer <stream.token>, and sendstream_id=stream.idin the message/confirm body. Use the stream JWT only on the hub. - An asynchronous request returns
202withstatus=accepted. Handleready,progress,delta,message,action,completed, anderrorevents. Read conversation state after a disconnect. For a simple server-side integration, explicitly chooseresponse_mode=buffered, as in the examples below, without a stream ticket. - Keep
client_message_idstable 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. - 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_idwith 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
copilot:readPath, 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
copilot:readPath, 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
copilot:writePath, 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
copilot:readPath, 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
copilot:writePath, 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
copilot:readPath, query, and header parameters
conversationIdRequiredResponses
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
copilot:writePath, query, and header parameters
conversationIdRequiredResponses
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
copilot:writePath, query, and header parameters
conversationIdRequiredRequest Body
{
"message": "Summarize my available billing information.",
"client_message_id": "example-msg-001",
"response_mode": "buffered"
}messageRequiredclient_message_idRequiredresponse_modestream_idpage_contextinput_answerrevision_action_idcontinuation_action_idRequest 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
copilot:readPath, query, and header parameters
conversationIdRequiredclientMessageIdRequiredRequest 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
copilot:writePath, query, and header parameters
conversationIdRequiredRequest Body
{
"message_id": "MESSAGE_ID",
"rating": "positive",
"reason_code": "helpful"
}message_idRequiredratingRequiredreason_codeResponses
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
copilot:writePath, query, and header parameters
conversationIdRequiredactionIdRequiredRequest Body
{
"response_mode": "buffered"
}response_modestream_idRequest 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
copilot:writePath, query, and header parameters
conversationIdRequiredactionIdRequiredRequest 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.