AI Gateway
Configure AI providers, assistants, knowledge retrieval, structured state, stored conversations, usage, and budgets through one public API.
AI Gateway API
The AI Gateway API lets your server configure provider connections, prompts, optional knowledge sources, assistants, and conversational inference. Donutwork provides a stable assistant identifier while the provider and model can be changed independently in your configuration.
For complete requests that build on each other, follow the AI Gateway guided examples, also available in Italian.
Call these endpoints from a trusted backend. Never embed a Donutwork bearer token, provider credential, vector store API key, or private CA certificate in browser or mobile application code. Credentials are write-only and are never returned by the API.
Recommended setup
- Discover providers and create a
generationconnection. - Create and publish a prompt.
- Optionally create a lexical knowledge base, or configure Qdrant plus an
embeddingconnection before creating a vector knowledge base. - Add approved text documents.
- Create an assistant that links the generation connection, prompt, and permitted knowledge bases.
- For schema-constrained multi-turn state, create and activate a structured response contract.
- Test with the native assistant endpoint, Chat Completions, or the stored Conversations API.
- Monitor usage and configure a monthly budget.
All examples use API version 2026-02-01. Send JSON with Content-Type: application/json and authenticate with Authorization: Bearer YOUR_DONUTWORK_TOKEN.
Retrieval modes
Inference endpoints accept an optional retrieval object:
assistant: use the knowledge bases saved on the assistant;lexical: use only linked lexical knowledge bases;vector: use only linked vector knowledge bases;none: answer without document retrieval.
knowledge_base_ids may only contain knowledge bases already linked to the assistant. limit accepts values from 1 to 10. Responses include retrieval metadata and citations when relevant source passages were used.
Providers and connections
List AI providers
ai_connections:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Available provider adapters and their public capabilities.
{
"elements": [
{
"key": "openai",
"name": "OpenAI",
"capabilities": [
"chat",
"streaming",
"embeddings",
"tools",
"web_search",
"structured_output"
]
},
{
"key": "openrouter",
"name": "OpenRouter",
"capabilities": [
"chat",
"streaming",
"embeddings"
]
},
{
"key": "azure_openai",
"name": "Azure OpenAI · Microsoft Foundry",
"capabilities": [
"chat",
"streaming",
"embeddings",
"structured_output"
]
},
{
"key": "anthropic",
"name": "Anthropic",
"capabilities": [
"chat",
"streaming",
"tools",
"web_search"
]
},
{
"key": "openai_compatible",
"name": "OpenAI-compatible",
"capabilities": [
"chat",
"streaming"
]
},
{
"key": "openai_embeddings_compatible",
"name": "OpenAI-compatible embeddings",
"capabilities": [
"embeddings"
]
},
{
"key": "voyage",
"name": "Voyage AI",
"capabilities": [
"embeddings"
]
},
{
"key": "ollama",
"name": "Ollama",
"capabilities": [
"chat",
"streaming",
"embeddings"
]
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/providers.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/providers.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-gateway/providers.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.
List connections
ai_connections:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Provider connections visible to the authenticated organization.
{
"elements": [
{
"id": "connection_123",
"name": "Production answers",
"provider": "openrouter",
"purpose": "generation",
"model": "google/gemini-3.5-flash-lite",
"status": "active",
"credential_configured": true,
"openrouter_require_parameters": true,
"last_test_status": "succeeded",
"structured_output_status": "supported",
"structured_output_model": "google/gemini-3.5-flash-lite",
"structured_output_checked_at": "2026-09-17T10:00:00+00:00"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections.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-gateway/connections.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 connection
Create a generation connection for assistants or an embedding connection for vector knowledge bases. Use the exact model identifier enabled by your provider account. OpenRouter connections can opt into parameter-compatible routing; when enabled, Donutwork sends provider.require_parameters=true so OpenRouter only selects endpoints that accept every parameter in the request.
ai_connections:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Production answers",
"provider": "openai",
"purpose": "generation",
"model": "MODEL_ID",
"credential": "YOUR_PROVIDER_API_KEY",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}nameRequiredproviderRequiredpurposemodelRequiredopenrouter_require_parametersbase_urlcredentialinput_cost_per_millionoutput_cost_per_millionResponses
Connection created. The credential is not included in the response.
{
"id": "connection_123",
"name": "Production answers",
"provider": "openai",
"purpose": "generation",
"model": "MODEL_ID",
"status": "active",
"credential_configured": true
}Unsupported provider, purpose, model configuration, URL, or credential.
{
"error": "Invalid connection configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Production answers",
"provider": "openai",
"purpose": "generation",
"model": "MODEL_ID",
"credential": "YOUR_PROVIDER_API_KEY",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Production answers",
"provider": "openai",
"purpose": "generation",
"model": "MODEL_ID",
"credential": "YOUR_PROVIDER_API_KEY",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}),
});
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-gateway/connections.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": "Production answers",
"provider": "openai",
"purpose": "generation",
"model": "MODEL_ID",
"credential": "YOUR_PROVIDER_API_KEY",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}',
]);
$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 connection
Provider and purpose cannot be changed. For an embedding connection already used by a vector knowledge base, create a new connection instead of changing the model.
ai_connections:writePath, query, and header parameters
connectionIdRequiredRequest Body
{
"name": "Production answers",
"status": "active",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}namemodelbase_urlopenrouter_require_parametersinput_cost_per_millionoutput_cost_per_millionstatusResponses
Connection updated.
{
"id": "connection_123",
"name": "Production answers",
"status": "active"
}Connection not found.
{
"error": "AI connection not found"
}The requested model change would invalidate a linked vector knowledge base.
{
"error": "Create a new embedding connection and reindex instead"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Production answers",
"status": "active",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Production answers",
"status": "active",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}),
});
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-gateway/connections/CONNECTIONID.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": "Production answers",
"status": "active",
"input_cost_per_million": 0.15,
"output_cost_per_million": 0.6
}',
]);
$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.
Rotate a connection credential
ai_connections:writePath, query, and header parameters
connectionIdRequiredRequest Body
{
"credential": "YOUR_NEW_PROVIDER_API_KEY"
}credentialRequiredResponses
Credential rotated. Secret material is never returned.
{
"id": "connection_123",
"credential_configured": true
}Connection not found.
{
"error": "AI connection not found"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID/rotate.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"credential": "YOUR_NEW_PROVIDER_API_KEY"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID/rotate.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"credential": "YOUR_NEW_PROVIDER_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-gateway/connections/CONNECTIONID/rotate.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 => '{
"credential": "YOUR_NEW_PROVIDER_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.
Test a connection
This performs a real provider request and may consume tokens or incur provider cost. For a generation connection backed by OpenRouter, a successful test also checks the configured model's current supported_parameters metadata and persists structured_output_status as supported, unsupported, or unknown. Changing the model invalidates the previous capability result.
ai_connections:writePath, query, and header parameters
connectionIdRequiredResponses
Provider request succeeded. structured_output_status is returned for OpenRouter generation connections.
{
"status": "succeeded",
"connection_id": "connection_123",
"duration_ms": 420,
"usage": {
"input_tokens": 8,
"output_tokens": 1,
"total_tokens": 9
},
"estimated_cost_usd": 0.000002,
"request_id": "request_123",
"structured_output_status": "supported"
}Connection is disabled.
{
"error": "The AI connection is disabled"
}Provider rejected or could not complete the test.
{
"error": "Provider request failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID/test.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID/test.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-gateway/connections/CONNECTIONID/test.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 connection
Deletion is blocked while an assistant or vector knowledge base depends on the connection.
ai_connections:writePath, query, and header parameters
connectionIdRequiredResponses
Connection deleted.
{
"deleted": true,
"connection_id": "connection_123"
}Connection is still used by another resource.
{
"error": "Remove linked resources before deleting this connection"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/connections/CONNECTIONID.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-gateway/connections/CONNECTIONID.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.
Create embeddings
This endpoint uses an active embedding connection owned by the authenticated organization. It returns vectors but never exposes the provider credential or base URL. The required scope is ai_embeddings:write.
input_type is available for providers that distinguish query and document embeddings, such as Voyage AI. dimensions is accepted only when the selected adapter supports an explicit output dimension.
ai_embeddings:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy",
"Shipping policy"
],
"input_type": "document",
"dimensions": 1024
}connection_idRequiredmodelinputRequiredinput_typedimensionsResponses
Embedding vectors plus normalized usage and Donutwork ledger metadata.
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
0.012,
-0.034
]
}
],
"model": "voyage-4",
"usage": {
"prompt_tokens": 8,
"total_tokens": 8
},
"donutwork": {
"request_id": "request_123",
"run_id": "run_123",
"connection_id": "connection_embeddings_123",
"provider": "voyage",
"input_items": 2,
"dimensions": 1024,
"estimated_cost_usd": 0.000001
}
}The configured hard budget would be exceeded.
{
"error": "AI Gateway monthly budget would be exceeded"
}Input, connection purpose, input type, or dimension is invalid.
{
"error": "Invalid embedding request"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/embeddings' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy",
"Shipping policy"
],
"input_type": "document",
"dimensions": 1024
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/embeddings', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy",
"Shipping policy"
],
"input_type": "document",
"dimensions": 1024
}),
});
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-gateway/embeddings');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy",
"Shipping policy"
],
"input_type": "document",
"dimensions": 1024
}',
]);
$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.
Embeddings JSON alias
This alias has the same connection, limits, provider options, ledger behavior, and ai_embeddings:write scope as /ai-gateway/embeddings.
ai_embeddings:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy"
],
"input_type": "document",
"dimensions": 1024
}connection_idRequiredmodelinputRequiredinput_typedimensionsResponses
Embedding vectors with normalized usage and ledger metadata.
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [
0.012,
-0.034
]
}
],
"model": "voyage-4",
"usage": {
"prompt_tokens": 4,
"total_tokens": 4
},
"donutwork": {
"run_id": "run_123",
"connection_id": "connection_embeddings_123",
"provider": "voyage",
"input_items": 1,
"dimensions": 1024
}
}Input or embedding connection contract is invalid.
{
"error": "Invalid embedding request"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/embeddings.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy"
],
"input_type": "document",
"dimensions": 1024
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/embeddings.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy"
],
"input_type": "document",
"dimensions": 1024
}),
});
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-gateway/embeddings.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 => '{
"connection_id": "connection_embeddings_123",
"model": "voyage-4",
"input": [
"Return policy"
],
"input_type": "document",
"dimensions": 1024
}',
]);
$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.
Vector stores
Vector stores are optional. They are needed only by knowledge bases configured with retrieval_mode: vector. The public API currently supports Qdrant over HTTPS.
List vector store providers
ai_vector_stores:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Supported vector store providers.
{
"elements": [
{
"key": "qdrant",
"name": "Qdrant",
"capabilities": [
"dense_vectors",
"metadata_filters"
]
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-store-providers.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-store-providers.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-gateway/vector-store-providers.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.
List vector stores
ai_vector_stores:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Configured vector stores without secret material.
{
"elements": [
{
"id": "vector_store_123",
"name": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"status": "active",
"credential_configured": true,
"tls_ca_configured": false,
"last_test_status": "succeeded"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores.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-gateway/vector-stores.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 vector store
ai_vector_stores:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"api_key": "YOUR_QDRANT_API_KEY",
"collection_prefix": "product_docs"
}nameRequiredproviderRequiredendpointRequiredapi_keyRequiredca_certificatecollection_prefixResponses
Vector store configuration created without returning its API key or certificate.
{
"id": "vector_store_123",
"name": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"status": "active",
"credential_configured": true
}Endpoint, provider, key, certificate, or prefix is invalid.
{
"error": "Invalid vector store configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"api_key": "YOUR_QDRANT_API_KEY",
"collection_prefix": "product_docs"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"api_key": "YOUR_QDRANT_API_KEY",
"collection_prefix": "product_docs"
}),
});
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-gateway/vector-stores.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": "Knowledge search",
"provider": "qdrant",
"endpoint": "https://qdrant.example.com:6333",
"api_key": "YOUR_QDRANT_API_KEY",
"collection_prefix": "product_docs"
}',
]);
$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 vector store
ai_vector_stores:writePath, query, and header parameters
vectorStoreIdRequiredRequest Body
{
"name": "Knowledge search",
"status": "active"
}nameendpointcollection_prefixstatusResponses
Vector store updated.
{
"id": "vector_store_123",
"name": "Knowledge search",
"status": "active"
}Vector store not found.
{
"error": "Vector store not found"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Knowledge search",
"status": "active"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Knowledge search",
"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-gateway/vector-stores/VECTORSTOREID.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": "Knowledge search",
"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.
Rotate vector store access
Send the CA certificate again when the endpoint still requires it. Omitting it switches the configuration to the public system trust bundle.
ai_vector_stores:writePath, query, and header parameters
vectorStoreIdRequiredRequest Body
{
"api_key": "YOUR_NEW_QDRANT_API_KEY"
}api_keyRequiredca_certificateResponses
Access material rotated without returning it.
{
"id": "vector_store_123",
"credential_configured": true
}Vector store not found.
{
"error": "Vector store not found"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID/rotate.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"api_key": "YOUR_NEW_QDRANT_API_KEY"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID/rotate.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"api_key": "YOUR_NEW_QDRANT_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-gateway/vector-stores/VECTORSTOREID/rotate.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 => '{
"api_key": "YOUR_NEW_QDRANT_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.
Test a vector store
ai_vector_stores:writePath, query, and header parameters
vectorStoreIdRequiredResponses
Connectivity and authentication succeeded.
{
"status": "succeeded",
"vector_store_id": "vector_store_123",
"duration_ms": 86,
"collections_count": 4,
"request_id": "request_123"
}Vector store is disabled.
{
"error": "The vector store is disabled"
}The remote service could not be reached or rejected the request.
{
"error": "Vector store request failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID/test.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID/test.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-gateway/vector-stores/VECTORSTOREID/test.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 vector store
ai_vector_stores:writePath, query, and header parameters
vectorStoreIdRequiredResponses
Vector store configuration deleted.
{
"deleted": true,
"vector_store_id": "vector_store_123"
}A knowledge base still uses this vector store.
{
"error": "Remove linked knowledge bases before deletion"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/vector-stores/VECTORSTOREID.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-gateway/vector-stores/VECTORSTOREID.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.
Prompts
A prompt contains reusable system instructions. Do not place credentials or unneeded personal data in prompt content.
List prompts
ai_prompts:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Published prompt configurations.
{
"elements": [
{
"id": "prompt_123",
"name": "Product assistant",
"content": "Answer accurately and state when information is unavailable.",
"version": 1,
"status": "published"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/prompts.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/prompts.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-gateway/prompts.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 prompt
ai_prompts:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Product assistant",
"content": "Answer accurately using approved information. If the answer is unavailable, say so clearly."
}nameRequiredcontentRequiredResponses
Prompt version 1 created and published.
{
"id": "prompt_123",
"name": "Product assistant",
"version": 1,
"status": "published"
}Name or content is missing or too large.
{
"error": "Invalid prompt"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/prompts.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Product assistant",
"content": "Answer accurately using approved information. If the answer is unavailable, say so clearly."
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/prompts.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Product assistant",
"content": "Answer accurately using approved information. If the answer is unavailable, say so clearly."
}),
});
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-gateway/prompts.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": "Product assistant",
"content": "Answer accurately using approved information. If the answer is unavailable, say so clearly."
}',
]);
$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.
Knowledge bases and documents
A lexical knowledge base searches matching words and needs no vector store. A vector knowledge base searches by meaning and requires an active Qdrant configuration plus an active embedding connection. Documents are split into searchable passages automatically. Each passage can carry page, section, language, access principals, and a chunking strategy version so retrieval remains traceable across reindexing changes.
List knowledge bases
ai_knowledge_bases:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Knowledge bases and readiness metadata.
{
"elements": [
{
"id": "kb_123",
"name": "Product policies",
"description": "Approved customer-facing policies",
"retrieval_mode": "vector",
"index_status": "ready",
"status": "ready",
"documents_count": 3,
"chunks_count": 24
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases.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-gateway/knowledge-bases.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 knowledge base
For retrieval_mode: vector, both vector_store_id and embedding_connection_id are required. The embedding connection must use purpose: embedding.
ai_knowledge_bases:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Product policies",
"description": "Approved customer-facing policies",
"retrieval_mode": "vector",
"content_storage_mode": "vector_store_payload",
"vector_store_id": "vector_store_123",
"embedding_connection_id": "connection_embeddings_123"
}nameRequireddescriptionretrieval_modecontent_storage_modevector_store_idembedding_connection_idResponses
Knowledge base created and ready for documents.
{
"id": "kb_123",
"name": "Product policies",
"retrieval_mode": "vector",
"index_status": "ready",
"status": "empty",
"documents_count": 0,
"chunks_count": 0
}Retrieval mode or required linked resources are invalid.
{
"error": "Invalid knowledge base configuration"
}A required external embedding or vector service did not complete setup.
{
"error": "Knowledge base setup failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Product policies",
"description": "Approved customer-facing policies",
"retrieval_mode": "vector",
"content_storage_mode": "vector_store_payload",
"vector_store_id": "vector_store_123",
"embedding_connection_id": "connection_embeddings_123"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Product policies",
"description": "Approved customer-facing policies",
"retrieval_mode": "vector",
"content_storage_mode": "vector_store_payload",
"vector_store_id": "vector_store_123",
"embedding_connection_id": "connection_embeddings_123"
}),
});
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-gateway/knowledge-bases.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": "Product policies",
"description": "Approved customer-facing policies",
"retrieval_mode": "vector",
"content_storage_mode": "vector_store_payload",
"vector_store_id": "vector_store_123",
"embedding_connection_id": "connection_embeddings_123"
}',
]);
$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.
Add a text document
Vector knowledge bases create embeddings and index the resulting passages during this request. Use only current, approved source text.
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequiredRequest Body
{
"name": "Returns policy",
"content": "Unused products may be returned within 30 days of delivery when they meet the published return conditions.",
"metadata": {
"page": 4,
"section": "Returns",
"language": "en",
"acl": [
"group:customer-care"
]
}
}nameRequiredcontentRequiredmetadataResponses
Document stored, split into passages, and indexed when vector retrieval is enabled.
{
"document_id": "document_123",
"chunks_count": 4,
"retrieval_mode": "vector",
"index_status": "ready",
"indexed_in_vector_store": true
}Knowledge base not found.
{
"error": "Knowledge base not found"
}The document was accepted but vector indexing could not be completed. Resolve the external connection issue before retrying.
{
"error": "Vector indexing failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Returns policy",
"content": "Unused products may be returned within 30 days of delivery when they meet the published return conditions.",
"metadata": {
"page": 4,
"section": "Returns",
"language": "en",
"acl": [
"group:customer-care"
]
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Returns policy",
"content": "Unused products may be returned within 30 days of delivery when they meet the published return conditions.",
"metadata": {
"page": 4,
"section": "Returns",
"language": "en",
"acl": [
"group:customer-care"
]
}
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/documents.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": "Returns policy",
"content": "Unused products may be returned within 30 days of delivery when they meet the published return conditions.",
"metadata": {
"page": 4,
"section": "Returns",
"language": "en",
"acl": [
"group:customer-care"
]
}
}',
]);
$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.
Enqueue a remote document
For a vector knowledge base configured with content_storage_mode: vector_store_payload, this endpoint creates a durable ingestion manifest and returns immediately. Donutwork stores the source reference encrypted; the worker fetches and parses the source, then stores searchable text with its vectors in the configured Qdrant payload instead of duplicating chunks in Donutwork content storage.
Sources may be an HTTPS URL or an object in Google Drive, SharePoint, or S3 linked through a managed source connector. The manifest stores only the protected source reference and lifecycle state. Redirects to unsafe destinations, embedded URL credentials, content over 10 MB, and normalized text over 5 MB are rejected.
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequiredRequest Body
{
"name": "Returns policy",
"source": {
"type": "remote_url",
"url": "https://storage.example/policies/returns.md",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
},
"metadata": {
"department": "legal",
"locale": "en"
}
}nameRequiredsourceRequiredmetadataResponses
Durable manifest queued for ingestion.
{
"id": "document_123",
"knowledge_base_id": "kb_123",
"name": "Returns policy",
"source_type": "remote_url",
"content_storage_mode": "vector_store_payload",
"status": "queued",
"index_status": "queued",
"ingestion_attempts": 0
}Source, content type, checksum, metadata, or knowledge base storage mode is invalid.
{
"error": "Invalid ingestion manifest"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/ingestions.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Returns policy",
"source": {
"type": "remote_url",
"url": "https://storage.example/policies/returns.md",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
},
"metadata": {
"department": "legal",
"locale": "en"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/ingestions.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Returns policy",
"source": {
"type": "remote_url",
"url": "https://storage.example/policies/returns.md",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
},
"metadata": {
"department": "legal",
"locale": "en"
}
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/ingestions.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": "Returns policy",
"source": {
"type": "remote_url",
"url": "https://storage.example/policies/returns.md",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
},
"metadata": {
"department": "legal",
"locale": "en"
}
}',
]);
$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 document ingestion status
ai_knowledge_bases:readPath, query, and header parameters
knowledgeBaseIdRequireddocumentIdRequiredResponses
Sanitized document and ingestion state. The encrypted source reference is never returned.
{
"id": "document_123",
"knowledge_base_id": "kb_123",
"status": "ready",
"index_status": "ready",
"bytes": 4200,
"chunks_count": 8,
"content_type": "text/markdown",
"ingestion_attempts": 1
}The document is not in this organization and knowledge base.
{
"error": "Document not found"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.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-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.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 a text document
Replace the content or metadata of a text-backed document. Use the /source.json operation below for remote ingestion manifests instead.
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequireddocumentIdRequiredRequest Body
{
"name": "Returns policy",
"content": "Updated policy text...",
"metadata": {
"department": "legal",
"locale": "en"
}
}namecontentmetadataResponses
Text document updated and indexing state returned.
{
"id": "document_123",
"knowledge_base_id": "kb_123",
"name": "Returns policy",
"status": "ready",
"index_status": "ready"
}Document is not in this company and knowledge base.
{
"error": "Document not found"
}Document storage mode or replacement data is invalid.
{
"error": "Invalid document payload"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Returns policy",
"content": "Updated policy text...",
"metadata": {
"department": "legal",
"locale": "en"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Returns policy",
"content": "Updated policy text...",
"metadata": {
"department": "legal",
"locale": "en"
}
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID.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": "Returns policy",
"content": "Updated policy text...",
"metadata": {
"department": "legal",
"locale": "en"
}
}',
]);
$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.
Replace an expiring remote source
Use this endpoint to rotate a signed URL or replace the source manifest. The new descriptor is encrypted, ingestion attempts are reset, and the document is queued again without exposing either the previous or replacement URL.
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequireddocumentIdRequiredRequest Body
{
"source": {
"type": "remote_url",
"url": "https://storage.example/new-signed-url",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
}
}Responses
Source version replaced and document queued.
{
"id": "document_123",
"source_version": 2,
"status": "queued",
"index_status": "queued",
"ingestion_attempts": 0
}The document is not a remote ingestion or the new source is invalid.
{
"error": "Invalid remote source"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID/source.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"source": {
"type": "remote_url",
"url": "https://storage.example/new-signed-url",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID/source.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"source": {
"type": "remote_url",
"url": "https://storage.example/new-signed-url",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
}
}),
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/documents/DOCUMENTID/source.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 => '{
"source": {
"type": "remote_url",
"url": "https://storage.example/new-signed-url",
"content_type": "text/markdown",
"sha256": "OPTIONAL_LOWERCASE_SHA256"
}
}',
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Search a knowledge base
The response contains text passages that your application may use directly or provide to another search workflow. Vector queries may consume embedding tokens.
ai_knowledge_bases:readPath, query, and header parameters
knowledgeBaseIdRequiredRequest Body
{
"query": "How long do I have to return a product?",
"limit": 5,
"principals": [
"group:customer-care"
]
}queryRequiredlimitprincipalsResponses
Matching authorized passages returned.
{
"knowledge_base": {
"id": "kb_123",
"name": "Product policies",
"retrieval_mode": "vector",
"index_status": "ready"
},
"count": 1,
"results": [
{
"chunk_id": "chunk_123",
"document_id": "document_123",
"document_name": "Returns policy",
"position": 0,
"score": 0.91,
"content": "Unused products may be returned within 30 days of delivery.",
"page": 4,
"section": "Returns",
"language": "en",
"chunking_strategy_version": "words-v1"
}
]
}The knowledge base or one of its required connections is not ready.
{
"error": "Knowledge base is not ready"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/search.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"query": "How long do I have to return a product?",
"limit": 5,
"principals": [
"group:customer-care"
]
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/search.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"query": "How long do I have to return a product?",
"limit": 5,
"principals": [
"group:customer-care"
]
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/search.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 => '{
"query": "How long do I have to return a product?",
"limit": 5,
"principals": [
"group:customer-care"
]
}',
]);
$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.
Retrieval evaluation
Use a reference dataset before adding hybrid search or reranking. Each case supplies a query, expected document or passage identifiers, and optional access principals. Donutwork reports hit rate, recall, precision, mean reciprocal rank, and normalized discounted cumulative gain at the selected cutoff. A recommendation is produced only for datasets with at least ten cases.
List retrieval evaluations
ai_knowledge_bases:readPath, query, and header parameters
knowledgeBaseIdRequiredlimitResponses
Recent sanitized evaluation summaries.
{
"elements": [
{
"id": "evaluation_123",
"knowledge_base_id": "kb_123",
"name": "Returns baseline",
"cases_count": 20,
"limit": 5,
"metrics": {
"recall_at_5": 0.94,
"mrr_at_5": 0.88,
"ndcg_at_5": 0.9
},
"decision": {
"status": "sufficient",
"recommendation": "keep_current_retrieval"
}
}
]
}Knowledge base not found.
{
"error": "Knowledge base not found"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Run a retrieval evaluation
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequiredRequest Body
{
"name": "Returns baseline",
"limit": 5,
"cases": [
{
"query": "return period",
"relevant_document_ids": [
"document_123"
],
"principals": [
"group:customer-care"
]
}
]
}nameRequiredlimitcasesRequiredResponses
Evaluation completed and retained as a baseline.
{
"id": "evaluation_123",
"status": "completed",
"cases_count": 20,
"limit": 5,
"metrics": {
"hit_rate_at_5": 1,
"recall_at_5": 0.94,
"precision_at_5": 0.31,
"mrr_at_5": 0.88,
"ndcg_at_5": 0.9
},
"decision": {
"status": "sufficient",
"recommendation": "keep_current_retrieval"
}
}Dataset, expected identifiers, principals, or cutoff is invalid.
{
"error": "Invalid evaluation dataset"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Returns baseline",
"limit": 5,
"cases": [
{
"query": "return period",
"relevant_document_ids": [
"document_123"
],
"principals": [
"group:customer-care"
]
}
]
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Returns baseline",
"limit": 5,
"cases": [
{
"query": "return period",
"relevant_document_ids": [
"document_123"
],
"principals": [
"group:customer-care"
]
}
]
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"name": "Returns baseline",
"limit": 5,
"cases": [
{
"query": "return period",
"relevant_document_ids": [
"document_123"
],
"principals": [
"group:customer-care"
]
}
]
}',
]);
$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.
Rerun a retrieval baseline
ai_knowledge_bases:writePath, query, and header parameters
knowledgeBaseIdRequiredevaluationIdRequiredRequest Body
{
"name": "Returns after reindex",
"limit": 5
}Responses
The protected baseline dataset was executed again.
{
"id": "evaluation_456",
"baseline_evaluation_id": "evaluation_123",
"status": "completed",
"cases_count": 20,
"metrics": {
"recall_at_5": 0.97,
"mrr_at_5": 0.91
}
}Knowledge base or baseline evaluation not found.
{
"error": "Retrieval evaluation not found"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations/EVALUATIONID/rerun.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Returns after reindex",
"limit": 5
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations/EVALUATIONID/rerun.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Returns after reindex",
"limit": 5
}),
});
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-gateway/knowledge-bases/KNOWLEDGEBASEID/evaluations/EVALUATIONID/rerun.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": "Returns after reindex",
"limit": 5
}',
]);
$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.
Managed source connectors
Source connectors keep reusable authentication separate from document manifests. Configuration is write-only. A document ingestion references a connector and a remote object identifier, while searchable text can remain in the external vector payload.
List source connector types
ai_knowledge_bases:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Supported connector and authentication contracts.
{
"elements": [
{
"type": "google_drive",
"label": "Google Drive",
"authentication": "oauth_refresh_token",
"object_fields": [
"file_id"
]
},
{
"type": "sharepoint",
"label": "SharePoint",
"authentication": "client_credentials",
"object_fields": [
"drive_id",
"item_id"
]
},
{
"type": "s3",
"label": "Amazon S3 compatible",
"authentication": "aws_signature_v4",
"object_fields": [
"key",
"version_id"
]
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connector-types.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connector-types.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-gateway/source-connector-types.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.
List source connectors
ai_knowledge_bases:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Sanitized source connector configurations.
{
"elements": [
{
"id": "connector_123",
"name": "Corporate archive",
"type": "s3",
"status": "active",
"config_version": 1
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors.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-gateway/source-connectors.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 source connector
ai_knowledge_bases:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Corporate archive",
"type": "s3",
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"secret_access_key": "YOUR_SECRET_ACCESS_KEY"
}
}nameRequiredtypeRequiredconfigRequiredResponses
Active connector created without returning its protected configuration.
{
"id": "connector_123",
"name": "Corporate archive",
"type": "s3",
"status": "active",
"config_version": 1
}Connector type or configuration is invalid.
{
"error": "Invalid source connector configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Corporate archive",
"type": "s3",
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"secret_access_key": "YOUR_SECRET_ACCESS_KEY"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Corporate archive",
"type": "s3",
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"secret_access_key": "YOUR_SECRET_ACCESS_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-gateway/source-connectors.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": "Corporate archive",
"type": "s3",
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_ACCESS_KEY_ID",
"secret_access_key": "YOUR_SECRET_ACCESS_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.
Rotate source connector authentication
ai_knowledge_bases:writePath, query, and header parameters
connectorIdRequiredRequest Body
{
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_NEW_ACCESS_KEY_ID",
"secret_access_key": "YOUR_NEW_SECRET_ACCESS_KEY"
}
}Responses
Authentication rotated and configuration version incremented.
{
"id": "connector_123",
"name": "Corporate archive",
"type": "s3",
"status": "active",
"config_version": 2
}Source connector not found.
{
"error": "Source connector not found"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors/CONNECTORID/rotate.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_NEW_ACCESS_KEY_ID",
"secret_access_key": "YOUR_NEW_SECRET_ACCESS_KEY"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors/CONNECTORID/rotate.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_NEW_ACCESS_KEY_ID",
"secret_access_key": "YOUR_NEW_SECRET_ACCESS_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-gateway/source-connectors/CONNECTORID/rotate.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 => '{
"config": {
"region": "eu-west-1",
"bucket": "approved-documents",
"access_key_id": "YOUR_NEW_ACCESS_KEY_ID",
"secret_access_key": "YOUR_NEW_SECRET_ACCESS_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 source connector
ai_knowledge_bases:writePath, query, and header parameters
connectorIdRequiredResponses
Unused connector deleted.
{
"deleted": true,
"source_connector_id": "connector_123"
}One or more document manifests still reference the connector.
{
"error": "Connector is still in use"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors/CONNECTORID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/source-connectors/CONNECTORID.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-gateway/source-connectors/CONNECTORID.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.
Assistants and inference
An assistant links one generation or decision connection. Generation assistants require a published prompt and may use knowledge bases, answer limits, and a native web-search policy. Decision assistants require decision_questions and do not use prompts, knowledge bases, or web search. The assistant ID or slug is the stable identifier used by calling applications. Web search is supported for compatible native OpenAI and Anthropic models; Vertex Gemini, OpenAI-compatible, and Ollama connections are rejected with 422 when web search is enabled.
Decision assistants use an active connection with purpose decision and keep their decision_questions on the assistant. Invoke them through the decision endpoint below; the chat endpoint is for generation assistants.
web_search.enabled defaults to false. max_uses defaults to 3 and accepts 1–10. allowed_domains accepts at most 64 ASCII hostnames, each at most 253 bytes, without schemes, ports, credentials, paths, query strings, fragments, IP addresses, or wildcards. Hostnames are lowercased, trailing dots and duplicates are removed, and an allowed hostname includes its subdomains. An absent or empty list permits the public web.
List assistants
ai_assistants:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Configured assistants.
{
"elements": [
{
"id": "assistant_123",
"name": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"temperature": 0.2,
"max_tokens": 800,
"retrieval_limit": 5,
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com"
]
},
"status": "active"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants.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-gateway/assistants.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 assistant
ai_assistants:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"temperature": 0.2,
"max_tokens": 800,
"retrieval_limit": 5,
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com",
"docs.example.org"
]
}
}nameRequiredslugconnection_idRequiredprompt_iddecision_questionsknowledge_base_idstemperaturemax_tokensretrieval_limitweb_searchResponses
Active assistant created.
{
"id": "assistant_123",
"name": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com",
"docs.example.org"
]
},
"status": "active"
}A linked connection, prompt, or knowledge base was not found.
{
"error": "Linked resource not found"
}The connection purpose, slug, limits, web-search policy/provider/model, or linked resources are invalid.
{
"error": "Invalid assistant configuration"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"temperature": 0.2,
"max_tokens": 800,
"retrieval_limit": 5,
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com",
"docs.example.org"
]
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"temperature": 0.2,
"max_tokens": 800,
"retrieval_limit": 5,
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com",
"docs.example.org"
]
}
}),
});
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-gateway/assistants.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": "Product helper",
"slug": "product-helper",
"connection_id": "connection_123",
"prompt_id": "prompt_123",
"knowledge_base_ids": [
"kb_123"
],
"temperature": 0.2,
"max_tokens": 800,
"retrieval_limit": 5,
"web_search": {
"enabled": true,
"max_uses": 3,
"allowed_domains": [
"example.com",
"docs.example.org"
]
}
}',
]);
$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.
Invoke an assistant
Use either a single message or a messages array. The final message must have role user. Supplying an Idempotency-Key header is recommended for safe retries. Callers must not send web_search, tools, tool_choice, allowed_domains, or max_uses; the effective policy is loaded only from the persisted assistant.
ai_inference:writePath, query, and header parameters
assistantIdRequiredIdempotency-KeyRequest Body
{
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"variables": {
"locale": "en"
},
"retrieval": {
"mode": "assistant",
"limit": 5
}
}messagemessagesvariablesretrievalidempotency_keyResponses
Assistant response with typed citations, web-search metadata when enabled, and usage.
{
"id": "run_123",
"request_id": "request_123",
"assistant_id": "assistant_123",
"answer": "The return period is 30 days.",
"citations": [
{
"type": "knowledge_base",
"document_id": "document_123",
"document_name": "Returns policy",
"source": 1
},
{
"type": "web",
"title": "Returns guidance",
"url": "https://example.com/returns",
"provider": "openai",
"start_index": 10,
"end_index": 24
}
],
"web_search": {
"enabled": true,
"used": true,
"requests_count": 1,
"queries": [
"return period"
],
"sources": [
{
"title": "Returns guidance",
"url": "https://example.com/returns",
"provider": "openai"
}
]
},
"usage": {
"input_tokens": 120,
"output_tokens": 18,
"total_tokens": 138
},
"retrieval": {
"mode": "assistant",
"knowledge_base_ids": [
"kb_123"
],
"results_count": 1
}
}A configured hard monthly budget would be exceeded.
{
"error": "AI Gateway monthly budget would be exceeded"
}A dependency is unavailable or the idempotency key cannot be replayed.
{
"error": "Assistant dependency unavailable"
}The selected AI provider failed the request.
{
"error": "Provider request failed"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants/ASSISTANTID/chat.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"variables": {
"locale": "en"
},
"retrieval": {
"mode": "assistant",
"limit": 5
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants/ASSISTANTID/chat.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"variables": {
"locale": "en"
},
"retrieval": {
"mode": "assistant",
"limit": 5
}
}),
});
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-gateway/assistants/ASSISTANTID/chat.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 => '{
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"variables": {
"locale": "en"
},
"retrieval": {
"mode": "assistant",
"limit": 5
}
}',
]);
$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 with a decision assistant
The assistant must have been created with a decision connection and configured decision_questions. Send only the current state; request-level questions are rejected. The answer keys correspond to the assistant's configured question keys. Fields inside each answer depend on the decision provider. Do not include secrets or personal data in the state.
ai_inference:writePath, query, and header parameters
assistantIdRequiredIdempotency-KeyRequest Body
{
"state": {
"ticket_type": "billing",
"priority": "high"
}
}stateRequiredidempotency_keyResponses
Typed answers and usage for the configured questions.
{
"id": "RUN_ID",
"request_id": "REQUEST_ID",
"assistant_id": "ASSISTANT_ID",
"connection_id": "CONNECTION_ID",
"answers": {
"routing": {
"type": "choice",
"value": "billing"
}
},
"routing": {},
"usage": {
"input_tokens": 80,
"output_tokens": 12,
"total_tokens": 92
}
}Decision assistant not found in this company.
{
"error": "Decision assistant not found."
}Invalid state or request-level questions supplied.
{
"error": "questions are configured on the Decision assistant and cannot be overridden per request."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants/ASSISTANTID/decisions.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"state": {
"ticket_type": "billing",
"priority": "high"
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/assistants/ASSISTANTID/decisions.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"state": {
"ticket_type": "billing",
"priority": "high"
}
}),
});
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-gateway/assistants/ASSISTANTID/decisions.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 => '{
"state": {
"ticket_type": "billing",
"priority": "high"
}
}',
]);
$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 with a connection
Use a decision connection directly when each request supplies its own named questions. A choice question needs a non-empty object of named criteria; score and noul are also supported. Keep the same idempotency key for an identical retry only.
ai_inference:writePath, query, and header parameters
Idempotency-KeyRequest Body
{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}connection_idRequiredstateRequiredquestionsRequiredidempotency_keyResponses
Typed answers, routing, and usage.
{
"id": "RUN_ID",
"request_id": "REQUEST_ID",
"assistant_id": null,
"connection_id": "CONNECTION_ID",
"answers": {
"routing": {
"type": "choice",
"value": "billing"
}
},
"routing": {},
"usage": {
"input_tokens": 80,
"output_tokens": 12,
"total_tokens": 92
}
}Connection not found in this company.
{
"error": "Decision connection not found."
}Invalid state, question structure, or connection purpose.
{
"error": "A Decision connection is required."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/decisions' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/decisions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}),
});
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-gateway/decisions');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}',
]);
$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.
Decisions JSON alias
This route has the same scope, request, idempotency behavior, and response as /ai-gateway/decisions.
ai_inference:writePath, query, and header parameters
Idempotency-KeyRequest Body
{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}connection_idRequiredstateRequiredquestionsRequiredidempotency_keyResponses
Same decision response as the extensionless route.
{
"id": "RUN_ID",
"request_id": "REQUEST_ID",
"assistant_id": null,
"connection_id": "CONNECTION_ID",
"answers": {
"routing": {
"type": "choice",
"value": "billing"
}
},
"routing": {},
"usage": {
"input_tokens": 80,
"output_tokens": 12,
"total_tokens": 92
}
}Invalid decision request.
{
"error": "Decision questions must be a non-empty JSON object."
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/decisions.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/decisions.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}),
});
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-gateway/decisions.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 => '{
"connection_id": "CONNECTION_ID",
"state": {
"ticket_type": "billing"
},
"questions": {
"routing": {
"type": "choice",
"instructions": "Select the team that should handle the ticket.",
"criteria": {
"billing": "Payments and invoices",
"support": "Other product questions"
}
}
}
}',
]);
$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.
Chat Completions
This OpenAI-compatible endpoint is stateless: send the conversation messages required for every request. Set model to assistant/{id-or-slug}. With stream: true, the response uses Server-Sent Events and ends with data: [DONE]. Set stream_options.include_usage to receive a final usage chunk. Normal text remains in ordinary chat.completion.chunk choices; provider tool lifecycle and final web metadata use additive donutwork objects on chunks with an empty choices array.
ai_inference:writePath, query, and header parameters
Idempotency-KeyRequest Body
{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"retrieval": {
"mode": "assistant",
"limit": 5
},
"stream": false
}modelRequiredmessagesRequiredvariablesretrievalstreamstream_optionsidempotency_keyResponses
Buffered Chat Completions response when stream is false.
{
"id": "chatcmpl-run_123",
"object": "chat.completion",
"model": "assistant/product-helper",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The return period is 30 days."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 120,
"completion_tokens": 18,
"total_tokens": 138
},
"donutwork": {
"request_id": "request_123",
"assistant_id": "assistant_123",
"citations": [
{
"document_id": "document_123",
"document_name": "Returns policy",
"source": 1
}
],
"retrieval": {
"mode": "assistant",
"results_count": 1
},
"idempotent_replay": false
}
}SSE chunks when stream is true. Each data event contains a chat.completion.chunk object and the stream ends with the documented terminal marker.
Server-Sent Events containing chat.completion.chunk objects, followed by the terminal marker.Model, messages, retrieval, streaming, or idempotency settings are invalid.
{
"error": "Invalid completion request"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/chat/completions' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"retrieval": {
"mode": "assistant",
"limit": 5
},
"stream": false
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"retrieval": {
"mode": "assistant",
"limit": 5
},
"stream": false
}),
});
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-gateway/chat/completions');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"retrieval": {
"mode": "assistant",
"limit": 5
},
"stream": false
}',
]);
$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.
Chat Completions JSON alias
This alias has the same request, response, streaming behavior, and scope as /ai-gateway/chat/completions.
ai_inference:writePath, query, and header parameters
Idempotency-KeyRequest Body
{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"stream": false
}modelRequiredmessagesRequiredvariablesretrievalstreamstream_optionsidempotency_keyResponses
OpenAI-compatible completion response.
{
"id": "chatcmpl-run_123",
"object": "chat.completion",
"model": "assistant/product-helper",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "The return period is 30 days."
},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 120,
"completion_tokens": 18,
"total_tokens": 138
}
}Invalid completion request.
{
"error": "Invalid completion request"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/chat/completions.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"stream": false
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/chat/completions.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"stream": false
}),
});
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-gateway/chat/completions.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 => '{
"model": "assistant/product-helper",
"messages": [
{
"role": "user",
"content": "What is the return period?"
}
],
"stream": false
}',
]);
$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.
Structured response contracts
Structured response contracts define the durable JSON state built by a stored conversation. A contract contains a strict state schema, a valid initial state, domain-specific extraction instructions, and a maximum output-token limit. New contracts start as draft and must be active before a structured conversation can select them.
The schema root must be an object. Supported JSON Schema keywords are type, properties, required, additionalProperties, items, enum, anyOf, and description. Every object property must appear in required, and additionalProperties must be false; represent an optional value with anyOf, including a null branch. Donutwork validates schemas and states locally before a provider request.
Each structured conversation snapshots the selected contract version. Later contract updates affect only new conversations.
List structured response contracts
ai_assistants:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Structured response contracts in the current Donutwork project.
{
"elements": [
{
"id": "contract_123",
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192,
"version": 1,
"status": "active"
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts.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-gateway/structured-contracts.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 structured response contract
ai_assistants:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent. Use only canonical filter names and values supplied by the caller.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "Concise cumulative full-text search terms."
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192
}nameRequiredsluginstructionsstate_schemaRequiredinitial_stateRequiredmax_output_tokensResponses
Draft contract version 1 created.
{
"id": "contract_123",
"name": "E-commerce search state",
"slug": "ecommerce-search",
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192,
"version": 1,
"status": "draft"
}Name, schema, initial state, or output limit is invalid.
{
"error": "state_schema root type must be object"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent. Use only canonical filter names and values supplied by the caller.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "Concise cumulative full-text search terms."
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent. Use only canonical filter names and values supplied by the caller.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "Concise cumulative full-text search terms."
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192
}),
});
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-gateway/structured-contracts.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": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent. Use only canonical filter names and values supplied by the caller.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string",
"description": "Concise cumulative full-text search terms."
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192
}',
]);
$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 structured response contract
ai_assistants:readPath, query, and header parameters
contractIdRequiredResponses
Contract configuration and current version.
{
"id": "contract_123",
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 192,
"version": 1,
"status": "active"
}Contract not found in the current organization and project.
{
"error": "Structured contract not found"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID.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-gateway/structured-contracts/CONTRACTID.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 a structured response contract
Updating a contract replaces its editable definition and increments version. Existing structured conversations continue using their encrypted snapshot of the version selected at creation. Archived contracts cannot be edited.
ai_assistants:writePath, query, and header parameters
contractIdRequiredRequest Body
{
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only exact canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 160
}nameRequiredsluginstructionsstate_schemaRequiredinitial_stateRequiredmax_output_tokensResponses
Contract updated and version incremented.
{
"id": "contract_123",
"name": "E-commerce search state",
"version": 2,
"status": "active"
}Contract not found.
{
"error": "Structured contract not found"
}Archived contracts cannot be edited.
{
"error": "An archived structured contract cannot be edited"
}Replacement schema or initial state is invalid.
{
"error": "Invalid structured contract"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only exact canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 160
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"name": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only exact canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 160
}),
});
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-gateway/structured-contracts/CONTRACTID.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": "E-commerce search state",
"slug": "ecommerce-search",
"instructions": "Preserve existing intent and apply only exact canonical filters.",
"state_schema": {
"type": "object",
"properties": {
"keywords": {
"type": "string"
},
"filters": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"name",
"value"
],
"additionalProperties": false
}
}
},
"required": [
"keywords",
"filters"
],
"additionalProperties": false
},
"initial_state": {
"keywords": "",
"filters": []
},
"max_output_tokens": 160
}',
]);
$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.
Change structured response contract status
Use active to make a contract available to new conversations. Use archived to prevent new selection and further edits. Status changes do not alter conversations that already hold a contract snapshot.
ai_assistants:writePath, query, and header parameters
contractIdRequiredRequest Body
{
"status": "active"
}statusRequiredResponses
Contract status updated.
{
"id": "contract_123",
"name": "E-commerce search state",
"version": 1,
"status": "active"
}Contract not found.
{
"error": "Structured contract not found"
}Unsupported status.
{
"error": "Structured contract status is invalid"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID/status.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"status": "active"
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID/status.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"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-gateway/structured-contracts/CONTRACTID/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": "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.
Validate structured state
This validation is local to Donutwork and does not call the AI provider or consume model tokens.
ai_assistants:readPath, query, and header parameters
contractIdRequiredRequest Body
{
"state": {
"keywords": "cotton bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
}
}stateRequiredResponses
State conforms to the contract.
{
"valid": true,
"state": {
"keywords": "cotton bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"contract_version": 1
}Contract not found.
{
"error": "Structured contract not found"
}State does not conform to the contract schema.
{
"error": "$.filters: invalid value type"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID/validate.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"state": {
"keywords": "cotton bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
}
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/structured-contracts/CONTRACTID/validate.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"state": {
"keywords": "cotton bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
}
}),
});
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-gateway/structured-contracts/CONTRACTID/validate.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 => '{
"state": {
"keywords": "cotton bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
}
}',
]);
$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.
Stored conversations
The Conversations API supports two modes:
textis the default. It stores message history and sends recent context with each turn. Text conversations can use retrieval, assistant-managed web search, streaming, and protected rolling summaries.structuredmaintains a schema-constrained JSON state across turns. Each provider call receives only the current state and the new message. Structured turns do not stream, use retrieval modenone, and disable web search.
Use Chat Completions instead when your application manages text history itself. Web citations and normalized search metadata are persisted with text assistant messages. Anthropic continuation blocks required for later text turns are encrypted at rest and never returned by the public API. Automatic summaries never use web search.
A structured conversation pins the active contract version selected at creation. Send the state_version returned by the previous response as expected_state_version on every turn. This prevents concurrent callers from silently overwriting newer state.
Create a conversation
ai_conversations:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"assistant_id": "search-parser",
"mode": "structured",
"structured_contract_id": "ecommerce-search",
"external_user_id": "customer-42",
"retention_days": 1
}assistant_idRequiredmodestructured_contract_idinitial_stateexternal_user_idvariablesretention_daysResponses
Empty active structured conversation created with state version 0.
{
"id": "conversation_123",
"assistant_id": "assistant_123",
"status": "active",
"mode": "structured",
"structured_contract": {
"id": "contract_123",
"version": 1
},
"state_version": 0,
"structured_state": {
"keywords": "",
"filters": []
},
"messages_count": 0,
"external_user_id_hash": "opaque_user_reference",
"retention_days": 1,
"created_at": "2026-09-17T10:00:00+00:00",
"expires_at": "2026-09-18T10:00:00+00:00",
"messages": []
}The assistant connection is unavailable or OpenRouter structured-output capability has not been verified for the configured model.
{
"error": "Verify the OpenRouter connection to detect structured output support for its configured model"
}Assistant, mode, contract, initial state, variables, external user reference, or retention is invalid.
{
"error": "An active structured contract is required"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"assistant_id": "search-parser",
"mode": "structured",
"structured_contract_id": "ecommerce-search",
"external_user_id": "customer-42",
"retention_days": 1
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations.json', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"assistant_id": "search-parser",
"mode": "structured",
"structured_contract_id": "ecommerce-search",
"external_user_id": "customer-42",
"retention_days": 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-gateway/conversations.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 => '{
"assistant_id": "search-parser",
"mode": "structured",
"structured_contract_id": "ecommerce-search",
"external_user_id": "customer-42",
"retention_days": 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.
Omit mode, structured_contract_id, and initial_state to create a backward-compatible text conversation.
Get a conversation
ai_conversations:readPath, query, and header parameters
conversationIdRequiredResponses
Conversation metadata, ordered message history, and current structured state when applicable.
{
"id": "conversation_123",
"assistant_id": "assistant_123",
"status": "active",
"mode": "structured",
"structured_contract": {
"id": "contract_123",
"version": 1
},
"state_version": 1,
"structured_state": {
"keywords": "bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"messages_count": 2,
"retention_days": 1,
"expires_at": "2026-09-18T10:00:00+00:00",
"messages": [
{
"id": "message_user_1",
"role": "user",
"content": "For a double bed",
"status": "succeeded",
"sequence": 1
},
{
"id": "message_assistant_1",
"role": "assistant",
"status": "succeeded",
"sequence": 2,
"structured": {
"action": "update",
"changes": {
"/keywords": "bedspread",
"/filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"remove": [],
"clarification": null,
"state": {
"keywords": "bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"state_version": 1,
"contract_id": "contract_123",
"contract_version": 1
}
}
]
}Conversation not found.
{
"error": "Conversation not found"
}Conversation retention period has ended.
{
"error": "The conversation has expired"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/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/ai-gateway/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.
Add a conversation turn
Every turn requires a new Idempotency-Key. A successful retry with the same key and exact logical input returns the saved turn without a second provider request. Do not reuse a key for different input or policy state.
For a structured conversation, expected_state_version is required and must match the latest version returned by create, get, or the previous turn. Send only the new user input; Donutwork provides the current state to the model. If your application has dynamic reference data such as an e-commerce filter catalog, serialize that compact data into message for the current turn.
const message = JSON.stringify({
query: 'For a double bed',
available_filters: {
bed_size: ['Single', 'Double', 'King'],
},
});ai_conversations:writePath, query, and header parameters
conversationIdRequiredIdempotency-KeyRequiredRequest Body
{
"message": "For a double bed",
"expected_state_version": 0
}messageRequiredexpected_state_versionretrievalstreamidempotency_keyResponses
Structured state advanced atomically. The same payload is also available under assistant_message.structured.
{
"conversation": {
"id": "conversation_123",
"assistant_id": "assistant_123",
"status": "active",
"mode": "structured",
"structured_contract": {
"id": "contract_123",
"version": 1
},
"state_version": 1,
"structured_state": {
"keywords": "bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"messages_count": 2
},
"user_message": {
"id": "message_user_1",
"role": "user",
"status": "succeeded",
"sequence": 1
},
"assistant_message": {
"id": "message_assistant_1",
"role": "assistant",
"status": "succeeded",
"sequence": 2,
"structured": {
"action": "update",
"changes": {
"/keywords": "bedspread",
"/filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"remove": [],
"clarification": null,
"state": {
"keywords": "bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"state_version": 1,
"contract_id": "contract_123",
"contract_version": 1
}
},
"structured": {
"action": "update",
"changes": {
"/keywords": "bedspread",
"/filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"remove": [],
"clarification": null,
"state": {
"keywords": "bedspread",
"filters": [
{
"name": "bed_size",
"value": "Double"
}
]
},
"state_version": 1,
"contract_id": "contract_123",
"contract_version": 1
},
"idempotent_replay": false
}Text conversations only: SSE start, delta, completion, and terminal events when stream is true.
Server-Sent Events for the durable text conversation turn.The expected state version is stale, another turn is processing, the key was reused incorrectly, a previous attempt failed, or the conversation reached its message limit.
{
"error": "The structured state changed. Reload the conversation and retry with the current version"
}Conversation retention period has ended.
{
"error": "The conversation has expired"
}Message, expected state version, retrieval, streaming, or Idempotency-Key is missing or invalid.
{
"error": "expected_state_version is required for structured conversations"
}The provider response did not conform to the contract. The durable state remains unchanged.
{
"error": "The AI provider returned invalid structured output"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Idempotency-Key: example-conversation-turn-0001' \
--header 'Content-Type: application/json' \
--data '{
"message": "For a double bed",
"expected_state_version": 0
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
"Idempotency-Key": "example-conversation-turn-0001",
'Content-Type': 'application/json',
},
body: JSON.stringify({
"message": "For a double bed",
"expected_state_version": 0
}),
});
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-gateway/conversations/CONVERSATIONID.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
'Idempotency-Key: example-conversation-turn-0001',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => '{
"message": "For a double bed",
"expected_state_version": 0
}',
]);
$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.
conversation.structured_state is the authoritative complete state after the turn. The additive structured object contains the same state plus an observable JSON Pointer diff: changes contains new or replaced values and remove contains removed paths. action is update, reset, or clarify; when it is clarify, clarification contains the question for the caller to present to the user.
If a structured turn returns 409 because the version is stale, fetch the conversation, replace the local state and version with the returned values, and retry the same logical input at most once. Keep the same Idempotency-Key for transport retries of an identical turn; use a new key for each new user message.
For text conversations, stream: true returns normalized conversation.started, conversation.tool.started, conversation.tool.completed, conversation.delta, conversation.completed, or conversation.error events, followed by [DONE], while preserving the same durable user and assistant messages. Tool events identify web_search, a call ID, the bounded query when available, and validated sources on completion. Long active text conversations can be summarized automatically according to organization settings; the rolling memory is used as context and is not returned as a separate message.
Close a conversation
Closing is idempotent and prevents new turns while retaining the history until its expiry date.
ai_conversations:writePath, query, and header parameters
conversationIdRequiredResponses
Conversation closed.
{
"id": "conversation_123",
"assistant_id": "assistant_123",
"status": "closed",
"messages_count": 4,
"retention_days": 30
}A turn is still being processed.
{
"error": "Conversation cannot be closed during an active turn"
}Request examples
curl --request POST 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID/close.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID/close.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-gateway/conversations/CONVERSATIONID/close.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.
Update conversation retention
The new expiry is calculated from the update time and is applied to the conversation and all its messages. It cannot exceed the organization maximum.
ai_conversations:writePath, query, and header parameters
conversationIdRequiredRequest Body
{
"retention_days": 45
}retention_daysRequiredResponses
Conversation and message expiry updated.
{
"id": "conversation_123",
"status": "active",
"retention_days": 45,
"expires_at": "2026-10-04T10:00:00+00:00"
}Retention is missing or exceeds the allowed maximum.
{
"error": "Invalid retention period"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID/retention.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"retention_days": 45
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID/retention.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"retention_days": 45
}),
});
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-gateway/conversations/CONVERSATIONID/retention.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 => '{
"retention_days": 45
}',
]);
$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 conversation
Deletion removes the conversation and its stored messages. It is rejected while a turn is being processed.
ai_conversations:writePath, query, and header parameters
conversationIdRequiredResponses
Conversation and messages deleted.
{
"deleted": true,
"conversation_id": "conversation_123"
}A turn is still being processed.
{
"error": "Conversation cannot be deleted during an active turn"
}Request examples
curl --request DELETE 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversations/CONVERSATIONID.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/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/ai-gateway/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.
Get conversation settings
ai_conversations:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Organization retention and rolling-summary policy.
{
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 12
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversation-settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversation-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-gateway/conversation-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 conversation settings
ai_conversations:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 12
}default_retention_daysRequiredmax_retention_daysRequiredsummary_enabledRequiredsummary_trigger_messagesRequiredsummary_keep_recent_messagesRequiredResponses
Organization conversation policy updated.
{
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 12
}Retention or summary thresholds are invalid.
{
"error": "Invalid conversation settings"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversation-settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 12
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/conversation-settings.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 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-gateway/conversation-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 => '{
"default_retention_days": 30,
"max_retention_days": 90,
"summary_enabled": true,
"summary_trigger_messages": 40,
"summary_keep_recent_messages": 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.
Usage and budgets
Usage responses contain operational metadata, token counts, web-search enabled/used state and request count, latency, separate token/tool cost, total estimated cost, the pricing snapshot, and sanitized error information. Tool estimates default to USD 10 per 1,000 searches for OpenAI and Anthropic and may be overridden by the deployment; verify them against the current provider price list. Usage responses do not return search queries, snippets, page content, request messages, assistant answers, provider credentials, or retrieved passage content. Search queries and sources needed for audit remain only inside the encrypted audit payload.
Get usage
ai_usage:readPath, query, and header parameters
limitResponses
Usage summary, budget status, and recent run metadata.
{
"summary": {
"requests": 12,
"succeeded": 11,
"failed": 1,
"input_tokens": 4200,
"output_tokens": 900,
"total_tokens": 5100,
"estimated_cost_usd": 0.021,
"costed_requests": 12
},
"budget": {
"monthly_budget_usd": 50,
"hard_limit": true,
"spent_usd": 0.021,
"remaining_usd": 49.979,
"period": "2026-08"
},
"runs": [
{
"id": "run_123",
"assistant_id": "assistant_123",
"provider": "openai",
"model": "MODEL_ID",
"request_id": "request_123",
"status": "succeeded",
"duration_ms": 680,
"usage": {
"input_tokens": 120,
"output_tokens": 18,
"total_tokens": 138
},
"estimated_cost_usd": 0.00003
}
]
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/usage.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/usage.json', {
method: 'GET',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
},
});
const requestId = response.headers.get('x-request-id');
if (!response.ok) {
const error = await response.text();
throw new Error(`${response.status} ${error} [${requestId}]`);
}
const contentType = response.headers.get('content-type') ?? '';
const data = response.status === 204 ? null
: contentType.includes('application/json') ? await response.json()
: await response.text();$request = curl_init('https://api.hub.donutwork.com/2026-02-01/ai-gateway/usage.json');
curl_setopt_array($request, [
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('DONUTWORK_API_KEY'),
],
]);
$response = curl_exec($request);
if ($response === false) {
$error = curl_error($request);
curl_close($request);
throw new RuntimeException($error);
}
$status = curl_getinfo($request, CURLINFO_RESPONSE_CODE);
$contentType = (string) curl_getinfo($request, CURLINFO_CONTENT_TYPE);
curl_close($request);
if ($status >= 400) { throw new RuntimeException($response); }
$data = $status === 204 ? null
: (str_contains($contentType, 'application/json')
? json_decode($response, true, flags: JSON_THROW_ON_ERROR) : $response);Replace uppercase placeholders and sample values with your own data and company resource IDs. Keep the API key on a trusted server. Reuse an idempotency key only for the same logical request and payload.
Get budget settings
ai_usage:readPath, query, and header parameters
No path, query, or additional header parameters.
Responses
Current monthly budget status.
{
"monthly_budget_usd": 50,
"hard_limit": true,
"spent_usd": 0.021,
"remaining_usd": 49.979,
"period": "2026-08"
}Request examples
curl --request GET 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/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-gateway/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 budget settings
A hard limit requires a positive monthly budget. Costs are estimates based on prices configured on the provider connections, not provider invoices.
ai_usage:writePath, query, and header parameters
No path, query, or additional header parameters.
Request Body
{
"monthly_budget_usd": 50,
"hard_limit": true
}monthly_budget_usdhard_limitResponses
Budget settings updated.
{
"monthly_budget_usd": 50,
"hard_limit": true,
"spent_usd": 0.021,
"remaining_usd": 49.979,
"period": "2026-08"
}Budget value or hard-limit combination is invalid.
{
"error": "Invalid budget configuration"
}Request examples
curl --request PUT 'https://api.hub.donutwork.com/2026-02-01/ai-gateway/settings.json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"monthly_budget_usd": 50,
"hard_limit": true
}'const response = await fetch('https://api.hub.donutwork.com/2026-02-01/ai-gateway/settings.json', {
method: 'PUT',
headers: {
Authorization: 'Bearer ' + process.env.DONUTWORK_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"monthly_budget_usd": 50,
"hard_limit": true
}),
});
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-gateway/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 => '{
"monthly_budget_usd": 50,
"hard_limit": true
}',
]);
$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.
Common errors
| Status | Meaning | Recommended action |
|---|---|---|
400 | The request body is not a JSON object. | Send valid JSON and Content-Type: application/json. |
401 | Bearer token is missing, invalid, or revoked. | Replace the token on your backend. |
403 | The token does not have the required scope. | Grant the scope shown on the endpoint. |
402 | A hard AI budget would be exceeded. | Review usage, pricing, and budget settings. |
404 | The resource does not exist or is not available to this organization. | Verify the public resource identifier. |
409 | A dependency, lifecycle rule, or idempotency claim prevents the operation. | Resolve the dependency or retry with the correct key. |
410 | A stored conversation expired. | Create a new conversation. |
413 | Message or conversation input is too large. | Reduce message count or content size. |
422 | Payload fields or linked resources are invalid. | Correct the request using the endpoint field descriptions. |
502 | An external AI or vector service failed. | Test the related connection and retry with a new idempotency key when appropriate. |
Error responses include a request identifier when available. Provide that identifier to support; never send bearer tokens or provider credentials in support messages.