DonutworkHelp Center
Donutwork documentation

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.

  1. Discover providers and create a generation connection.
  2. Create and publish a prompt.
  3. Optionally create a lexical knowledge base, or configure Qdrant plus an embedding connection before creating a vector knowledge base.
  4. Add approved text documents.
  5. Create an assistant that links the generation connection, prompt, and permitted knowledge bases.
  6. For schema-constrained multi-turn state, create and activate a structured response contract.
  7. Test with the native assistant endpoint, Chat Completions, or the stored Conversations API.
  8. 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

GET
/2026-02-01/ai-gateway/providers.json
Required permissionai_connections:read

Path, 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

GET
/2026-02-01/ai-gateway/connections.json
Required permissionai_connections:read

Path, 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.

POST
/2026-02-01/ai-gateway/connections.json
Required permissionai_connections:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "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
}
namestringRequired
Human-readable connection name, up to 120 characters.
providerstringRequired
Provider key returned by the provider discovery endpoint.
purposestring
generation (default) or embedding.
modelstringRequired
Exact provider model identifier, up to 160 characters.
openrouter_require_parametersboolean
OpenRouter only. Require routed endpoints to support every request parameter. Default: false.
base_urlstring
Required HTTPS endpoint for Ollama and OpenAI-compatible providers.
credentialstring
Write-only provider credential. Required when the selected provider requires authentication.
input_cost_per_millionnumber
Optional estimated USD cost per one million input tokens.
output_cost_per_millionnumber
Optional estimated USD cost per one million output tokens.

Responses

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.

PUT
/2026-02-01/ai-gateway/connections/{connectionId}.json
Required permissionai_connections:write

Path, query, and header parameters

connectionIdstring· pathRequired
Connection identifier.

Request Body

JSON
{
  "name": "Production answers",
  "status": "active",
  "input_cost_per_million": 0.15,
  "output_cost_per_million": 0.6
}
namestring
Updated display name.
modelstring
Updated provider model identifier, when the connection is not locked by a vector knowledge base.
base_urlstring
Updated HTTPS endpoint for providers that support a custom URL.
openrouter_require_parametersboolean
OpenRouter only. Enable or disable parameter-compatible endpoint routing.
input_cost_per_millionnumber
Estimated USD input price.
output_cost_per_millionnumber
Estimated USD output price.
statusstring
active or disabled.

Responses

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

POST
/2026-02-01/ai-gateway/connections/{connectionId}/rotate.json
Required permissionai_connections:write

Path, query, and header parameters

connectionIdstring· pathRequired
Connection identifier.

Request Body

JSON
{
  "credential": "YOUR_NEW_PROVIDER_API_KEY"
}
credentialstringRequired
New write-only provider credential.

Responses

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.

POST
/2026-02-01/ai-gateway/connections/{connectionId}/test.json
Required permissionai_connections:write

Path, query, and header parameters

connectionIdstring· pathRequired
Connection identifier.

Responses

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.

DELETE
/2026-02-01/ai-gateway/connections/{connectionId}.json
Required permissionai_connections:write

Path, query, and header parameters

connectionIdstring· pathRequired
Connection identifier.

Responses

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.

POST
/2026-02-01/ai-gateway/embeddings
Required permissionai_embeddings:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "connection_id": "connection_embeddings_123",
  "model": "voyage-4",
  "input": [
    "Return policy",
    "Shipping policy"
  ],
  "input_type": "document",
  "dimensions": 1024
}
connection_idstringRequired
Active embedding connection in the current Donutwork project.
modelstring
Optional safety assertion. When supplied it must match the configured connection model.
inputstring | string[]Required
One to 64 non-empty passages; maximum 128 KB each and 1 MB per request.
input_typestring
Provider-specific semantic role, currently query or document for Voyage AI.
dimensionsinteger
Requested vector size from 1 to 65,536 when supported by the adapter.

Responses

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.

POST
/2026-02-01/ai-gateway/embeddings.json
Required permissionai_embeddings:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "connection_id": "connection_embeddings_123",
  "model": "voyage-4",
  "input": [
    "Return policy"
  ],
  "input_type": "document",
  "dimensions": 1024
}
connection_idstringRequired
Active embedding connection in the current Donutwork project.
modelstring
Optional safety assertion matching the configured connection model.
inputstring | string[]Required
One to 64 non-empty passages within the documented byte limits.
input_typestring
Provider-specific semantic role when supported.
dimensionsinteger
Requested vector size when supported by the adapter.

Responses

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

GET
/2026-02-01/ai-gateway/vector-store-providers.json
Required permissionai_vector_stores:read

Path, 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

GET
/2026-02-01/ai-gateway/vector-stores.json
Required permissionai_vector_stores:read

Path, 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

POST
/2026-02-01/ai-gateway/vector-stores.json
Required permissionai_vector_stores:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "name": "Knowledge search",
  "provider": "qdrant",
  "endpoint": "https://qdrant.example.com:6333",
  "api_key": "YOUR_QDRANT_API_KEY",
  "collection_prefix": "product_docs"
}
namestringRequired
Human-readable configuration name.
providerstringRequired
Vector store provider key. Use qdrant.
endpointstringRequired
HTTPS endpoint containing scheme, host, and optional port.
api_keystringRequired
Write-only Qdrant API key.
ca_certificatestring
Optional PEM CA certificate for a privately issued TLS certificate. This value is write-only.
collection_prefixstring
Lowercase public naming prefix used for managed knowledge indexes; defaults to donutwork.

Responses

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

PUT
/2026-02-01/ai-gateway/vector-stores/{vectorStoreId}.json
Required permissionai_vector_stores:write

Path, query, and header parameters

vectorStoreIdstring· pathRequired
Vector store identifier.

Request Body

JSON
{
  "name": "Knowledge search",
  "status": "active"
}
namestring
Updated display name.
endpointstring
Updated HTTPS endpoint.
collection_prefixstring
Updated naming prefix for new managed indexes.
statusstring
active or disabled.

Responses

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.

POST
/2026-02-01/ai-gateway/vector-stores/{vectorStoreId}/rotate.json
Required permissionai_vector_stores:write

Path, query, and header parameters

vectorStoreIdstring· pathRequired
Vector store identifier.

Request Body

JSON
{
  "api_key": "YOUR_NEW_QDRANT_API_KEY"
}
api_keystringRequired
New write-only Qdrant API key.
ca_certificatestring
Optional replacement PEM CA certificate. This value is write-only.

Responses

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

POST
/2026-02-01/ai-gateway/vector-stores/{vectorStoreId}/test.json
Required permissionai_vector_stores:write

Path, query, and header parameters

vectorStoreIdstring· pathRequired
Vector store identifier.

Responses

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

DELETE
/2026-02-01/ai-gateway/vector-stores/{vectorStoreId}.json
Required permissionai_vector_stores:write

Path, query, and header parameters

vectorStoreIdstring· pathRequired
Vector store identifier.

Responses

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

GET
/2026-02-01/ai-gateway/prompts.json
Required permissionai_prompts:read

Path, 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

POST
/2026-02-01/ai-gateway/prompts.json
Required permissionai_prompts:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "name": "Product assistant",
  "content": "Answer accurately using approved information. If the answer is unavailable, say so clearly."
}
namestringRequired
Prompt name, up to 120 characters.
contentstringRequired
Reusable assistant instructions, up to 32,000 characters.

Responses

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

GET
/2026-02-01/ai-gateway/knowledge-bases.json
Required permissionai_knowledge_bases:read

Path, 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.

POST
/2026-02-01/ai-gateway/knowledge-bases.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "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"
}
namestringRequired
Knowledge base name, up to 120 characters.
descriptionstring
Optional description, up to 500 characters.
retrieval_modestring
lexical (default) or vector.
content_storage_modestring
donutwork (default), or vector_store_payload for thin storage in an external vector store. The latter requires vector retrieval.
vector_store_idstring
Required for vector mode. Identifier of an active Qdrant configuration.
embedding_connection_idstring
Required for vector mode. Identifier of an active embedding connection.

Responses

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.

POST
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/documents.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.

Request Body

JSON
{
  "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"
    ]
  }
}
namestringRequired
Source name displayed in citations, up to 180 characters.
contentstringRequired
Approved plain-text source content.
metadataobject
Up to 20 fields and 8 KB. Reserved fields are page, section, language, and acl. An empty acl makes the document public.

Responses

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.

POST
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/ingestions.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Vector knowledge base using vector_store_payload.

Request Body

JSON
{
  "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"
  }
}
namestringRequired
Document name used in citations, up to 180 characters.
sourceobjectRequired
Remote URL source descriptor. URL and any signed query are encrypted at rest.
metadataobject
Up to 20 scalar fields and 8 KB, copied to the external vector payload.

Responses

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

GET
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/documents/{documentId}.json
Required permissionai_knowledge_bases:read

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.
documentIdstring· pathRequired
Document manifest identifier.

Responses

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.

PUT
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/documents/{documentId}.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Company-owned knowledge base identifier.
documentIdstring· pathRequired
Text document identifier in that knowledge base.

Request Body

JSON
{
  "name": "Returns policy",
  "content": "Updated policy text...",
  "metadata": {
    "department": "legal",
    "locale": "en"
  }
}
namestring
Replacement document name.
contentstring
Replacement text content to re-index.
metadataobject
Complete sanitized metadata supplied to the vector payload.

Responses

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.

PUT
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/documents/{documentId}/source.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.
documentIdstring· pathRequired
Remote document manifest identifier.

Request Body

JSON
{
  "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.

POST
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/search.json
Required permissionai_knowledge_bases:read

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.

Request Body

JSON
{
  "query": "How long do I have to return a product?",
  "limit": 5,
  "principals": [
    "group:customer-care"
  ]
}
querystringRequired
Search text, up to 32,000 characters.
limitinteger
Maximum passages to return, from 1 to 10. Default: 5.
principalsarray
Caller principals used to prefilter restricted passages. Public passages remain visible.

Responses

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

GET
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/evaluations.json
Required permissionai_knowledge_bases:read

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.
limitinteger· query
Maximum recent evaluations. Default: 20.

Responses

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

POST
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/evaluations.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.

Request Body

JSON
{
  "name": "Returns baseline",
  "limit": 5,
  "cases": [
    {
      "query": "return period",
      "relevant_document_ids": [
        "document_123"
      ],
      "principals": [
        "group:customer-care"
      ]
    }
  ]
}
namestringRequired
Dataset run name, up to 120 characters.
limitinteger
Ranking cutoff from 1 to 10. Default: 5.
casesarrayRequired
Between 1 and 100 cases. Every case needs a query plus relevant_document_ids and/or relevant_chunk_ids.

Responses

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

POST
/2026-02-01/ai-gateway/knowledge-bases/{knowledgeBaseId}/evaluations/{evaluationId}/rerun.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

knowledgeBaseIdstring· pathRequired
Knowledge base identifier.
evaluationIdstring· pathRequired
Existing baseline evaluation identifier.

Request Body

JSON
{
  "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

GET
/2026-02-01/ai-gateway/source-connector-types.json
Required permissionai_knowledge_bases:read

Path, 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

GET
/2026-02-01/ai-gateway/source-connectors.json
Required permissionai_knowledge_bases:read

Path, 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

POST
/2026-02-01/ai-gateway/source-connectors.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "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"
  }
}
namestringRequired
Connector name, up to 120 characters.
typestringRequired
google_drive, sharepoint, or s3.
configobjectRequired
Write-only authentication and source configuration for the selected type.

Responses

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

POST
/2026-02-01/ai-gateway/source-connectors/{connectorId}/rotate.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

connectorIdstring· pathRequired
Source connector identifier.

Request Body

JSON
{
  "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

DELETE
/2026-02-01/ai-gateway/source-connectors/{connectorId}.json
Required permissionai_knowledge_bases:write

Path, query, and header parameters

connectorIdstring· pathRequired
Source connector identifier.

Responses

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

GET
/2026-02-01/ai-gateway/assistants.json
Required permissionai_assistants:read

Path, 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

POST
/2026-02-01/ai-gateway/assistants.json
Required permissionai_assistants:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "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"
    ]
  }
}
namestringRequired
Assistant name, up to 120 characters.
slugstring
Stable lowercase API name. Generated from name when omitted.
connection_idstringRequired
Company-scoped generation or decision connection identifier.
prompt_idstring
Published prompt identifier, required for a generation assistant.
decision_questionsobject
Required for a decision assistant: one to 64 named choice, score, or noul questions.
knowledge_base_idsarray
Up to 10 knowledge base identifiers this assistant may consult.
temperaturenumber
Creativity value from 0 to 2. Default: 0.2.
max_tokensinteger
Maximum generated tokens from 1 to 8192. Default: 800.
retrieval_limitinteger
Maximum retrieved passages from 1 to 10. Default: 5.
web_searchobject
Assistant-only native web-search policy: enabled boolean, max_uses 1–10, and optional allowed_domains hostnames.

Responses

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.

POST
/2026-02-01/ai-gateway/assistants/{assistantId}/chat.json
Required permissionai_inference:write

Path, query, and header parameters

assistantIdstring· pathRequired
Assistant ID or slug.
Idempotency-Keystring· header
Unique key from 8 to 128 safe characters. Reuse only for the exact same request.

Request Body

JSON
{
  "messages": [
    {
      "role": "user",
      "content": "What is the return period?"
    }
  ],
  "variables": {
    "locale": "en"
  },
  "retrieval": {
    "mode": "assistant",
    "limit": 5
  }
}
messagestring
Convenience single user message. Use this or messages.
messagesarray
Up to 21 user/assistant messages; the final message must be user.
variablesobject
Values referenced by the selected prompt.
retrievalobject
Optional mode, knowledge_base_ids, and limit override.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/assistants/{assistantId}/decisions.json
Required permissionai_inference:write

Path, query, and header parameters

assistantIdstring· pathRequired
Company-scoped decision assistant ID or slug.
Idempotency-Keystring· header
Retry key for the same state and configured questions.

Request Body

JSON
{
  "state": {
    "ticket_type": "billing",
    "priority": "high"
  }
}
statestring|object|arrayRequired
Non-empty decision input, up to 100,000 encoded bytes.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/decisions
Required permissionai_inference:write

Path, query, and header parameters

Idempotency-Keystring· header
Retry key for the same connection, state, and questions.

Request Body

JSON
{
  "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_idstringRequired
Active company-scoped connection with purpose decision.
statestring|object|arrayRequired
Non-empty decision input, up to 100,000 encoded bytes.
questionsobjectRequired
One to 64 named choice, score, or noul questions; each has type and instructions.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/decisions.json
Required permissionai_inference:write

Path, query, and header parameters

Idempotency-Keystring· header
Retry key for the same connection, state, and questions.

Request Body

JSON
{
  "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_idstringRequired
Active company-scoped decision connection.
statestring|object|arrayRequired
Non-empty decision input, up to 100,000 encoded bytes.
questionsobjectRequired
One to 64 named decision questions.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/chat/completions
Required permissionai_inference:write

Path, query, and header parameters

Idempotency-Keystring· header
Unique retry key for this exact completion request.

Request Body

JSON
{
  "model": "assistant/product-helper",
  "messages": [
    {
      "role": "user",
      "content": "What is the return period?"
    }
  ],
  "retrieval": {
    "mode": "assistant",
    "limit": 5
  },
  "stream": false
}
modelstringRequired
assistant/{id-or-slug}.
messagesarrayRequired
Conversation messages. The final message must have role user.
variablesobject
Values referenced by the assistant prompt.
retrievalobject
Optional mode, knowledge_base_ids, and limit override.
streamboolean
Return Server-Sent Events when true. Default: false.
stream_optionsobject
Set include_usage to true for a final usage event.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/chat/completions.json
Required permissionai_inference:write

Path, query, and header parameters

Idempotency-Keystring· header
Unique retry key for this exact completion request.

Request Body

JSON
{
  "model": "assistant/product-helper",
  "messages": [
    {
      "role": "user",
      "content": "What is the return period?"
    }
  ],
  "stream": false
}
modelstringRequired
assistant/{id-or-slug}.
messagesarrayRequired
Conversation messages ending with a user message.
variablesobject
Values referenced by the assistant prompt.
retrievalobject
Optional retrieval override.
streamboolean
Return Server-Sent Events when true.
stream_optionsobject
Streaming options, including include_usage.
idempotency_keystring
Body alternative to the Idempotency-Key header.

Responses

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

GET
/2026-02-01/ai-gateway/structured-contracts.json
Required permissionai_assistants:read

Path, 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

POST
/2026-02-01/ai-gateway/structured-contracts.json
Required permissionai_assistants:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "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
}
namestringRequired
Human-readable contract name, up to 120 characters.
slugstring
Stable project-scoped slug. When omitted, it is derived from the name.
instructionsstring
Domain semantics appended to structured turns, up to 4,000 characters. Put types and allowed shapes in the schema.
state_schemaobjectRequired
Strict JSON Schema for the complete durable state.
initial_stateobjectRequired
Initial state that already conforms to state_schema.
max_output_tokensinteger
Provider output limit from 32 to 256 tokens. Default: 128.

Responses

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

GET
/2026-02-01/ai-gateway/structured-contracts/{contractId}.json
Required permissionai_assistants:read

Path, query, and header parameters

contractIdstring· pathRequired
Structured response contract identifier.

Responses

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.

PUT
/2026-02-01/ai-gateway/structured-contracts/{contractId}.json
Required permissionai_assistants:write

Path, query, and header parameters

contractIdstring· pathRequired
Structured response contract identifier.

Request Body

JSON
{
  "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
}
namestringRequired
Replacement display name.
slugstring
Replacement stable slug.
instructionsstring
Replacement domain instructions.
state_schemaobjectRequired
Replacement strict state schema.
initial_stateobjectRequired
Replacement initial state conforming to the schema.
max_output_tokensinteger
Replacement limit from 32 to 256.

Responses

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.

POST
/2026-02-01/ai-gateway/structured-contracts/{contractId}/status.json
Required permissionai_assistants:write

Path, query, and header parameters

contractIdstring· pathRequired
Structured response contract identifier.

Request Body

JSON
{
  "status": "active"
}
statusstringRequired
draft, active, or archived.

Responses

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.

POST
/2026-02-01/ai-gateway/structured-contracts/{contractId}/validate.json
Required permissionai_assistants:read

Path, query, and header parameters

contractIdstring· pathRequired
Structured response contract identifier.

Request Body

JSON
{
  "state": {
    "keywords": "cotton bedspread",
    "filters": [
      {
        "name": "bed_size",
        "value": "Double"
      }
    ]
  }
}
stateobjectRequired
Complete state to validate against the contract's current schema.

Responses

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:

  • text is 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.
  • structured maintains 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 mode none, 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

POST
/2026-02-01/ai-gateway/conversations.json
Required permissionai_conversations:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "assistant_id": "search-parser",
  "mode": "structured",
  "structured_contract_id": "ecommerce-search",
  "external_user_id": "customer-42",
  "retention_days": 1
}
assistant_idstringRequired
Active assistant ID or slug.
modestring
text (default) or structured.
structured_contract_idstring
Required when mode is structured. Accepts the ID or slug of an active contract in the same project.
initial_stateobject
Structured mode only. Optional state override that must conform to the selected contract schema.
external_user_idstring
Your own stable user reference. The original value is not returned.
variablesobject
Up to 25 scalar prompt variables for all turns in this conversation.
retention_daysinteger
Conversation lifetime from 1 to 365 days. Default: 30.

Responses

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

GET
/2026-02-01/ai-gateway/conversations/{conversationId}.json
Required permissionai_conversations:read

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation identifier.

Responses

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'],
  },
});
PUT
/2026-02-01/ai-gateway/conversations/{conversationId}.json
Required permissionai_conversations:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation identifier.
Idempotency-Keystring· headerRequired
Unique key from 8 to 128 safe characters for this exact turn.

Request Body

JSON
{
  "message": "For a double bed",
  "expected_state_version": 0
}
messagestringRequired
New user message, up to 32,000 characters.
expected_state_versioninteger
Required for structured conversations. Use the state_version from the previous response.
retrievalobject
Text mode: optional retrieval override. Structured mode accepts only mode none.
streamboolean
Text mode only. Return Server-Sent Events while persisting the turn. Default: false.
idempotency_keystring
Body alternative to the required Idempotency-Key header.

Responses

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.

POST
/2026-02-01/ai-gateway/conversations/{conversationId}/close.json
Required permissionai_conversations:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation identifier.

Responses

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.

PUT
/2026-02-01/ai-gateway/conversations/{conversationId}/retention.json
Required permissionai_conversations:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation identifier.

Request Body

JSON
{
  "retention_days": 45
}
retention_daysintegerRequired
New lifetime in days, from 1 to the configured organization maximum.

Responses

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.

DELETE
/2026-02-01/ai-gateway/conversations/{conversationId}.json
Required permissionai_conversations:write

Path, query, and header parameters

conversationIdstring· pathRequired
Conversation identifier.

Responses

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

GET
/2026-02-01/ai-gateway/conversation-settings.json
Required permissionai_conversations:read

Path, 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

PUT
/2026-02-01/ai-gateway/conversation-settings.json
Required permissionai_conversations:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "default_retention_days": 30,
  "max_retention_days": 90,
  "summary_enabled": true,
  "summary_trigger_messages": 40,
  "summary_keep_recent_messages": 12
}
default_retention_daysintegerRequired
Default lifetime for new conversations, from 1 to 365 days.
max_retention_daysintegerRequired
Maximum lifetime callers may request, from 1 to 365 days.
summary_enabledbooleanRequired
Enable protected rolling memory for long conversations.
summary_trigger_messagesintegerRequired
Unsummarized message count that triggers an update, from 20 to 200.
summary_keep_recent_messagesintegerRequired
Recent messages kept verbatim in context, from 4 to 50 and lower than the trigger.

Responses

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

GET
/2026-02-01/ai-gateway/usage.json
Required permissionai_usage:read

Path, query, and header parameters

limitinteger· query
Maximum recent usage records. Default: 50.

Responses

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

GET
/2026-02-01/ai-gateway/settings.json
Required permissionai_usage:read

Path, 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.

PUT
/2026-02-01/ai-gateway/settings.json
Required permissionai_usage:write

Path, query, and header parameters

No path, query, or additional header parameters.

Request Body

JSON
{
  "monthly_budget_usd": 50,
  "hard_limit": true
}
monthly_budget_usdnumber
Positive monthly budget in USD, or null to remove it when hard_limit is false.
hard_limitboolean
Reject projected inference requests that would exceed the configured budget.

Responses

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

StatusMeaningRecommended action
400The request body is not a JSON object.Send valid JSON and Content-Type: application/json.
401Bearer token is missing, invalid, or revoked.Replace the token on your backend.
403The token does not have the required scope.Grant the scope shown on the endpoint.
402A hard AI budget would be exceeded.Review usage, pricing, and budget settings.
404The resource does not exist or is not available to this organization.Verify the public resource identifier.
409A dependency, lifecycle rule, or idempotency claim prevents the operation.Resolve the dependency or retry with the correct key.
410A stored conversation expired.Create a new conversation.
413Message or conversation input is too large.Reduce message count or content size.
422Payload fields or linked resources are invalid.Correct the request using the endpoint field descriptions.
502An 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.

On this page

AI Gateway APIRecommended setupRetrieval modesProviders and connectionsList AI providersList connectionsCreate a connectionUpdate a connectionRotate a connection credentialTest a connectionDelete a connectionCreate embeddingsEmbeddings JSON aliasVector storesList vector store providersList vector storesCreate a vector storeUpdate a vector storeRotate vector store accessTest a vector storeDelete a vector storePromptsList promptsCreate a promptKnowledge bases and documentsList knowledge basesCreate a knowledge baseAdd a text documentEnqueue a remote documentGet document ingestion statusUpdate a text documentReplace an expiring remote sourceSearch a knowledge baseRetrieval evaluationList retrieval evaluationsRun a retrieval evaluationRerun a retrieval baselineManaged source connectorsList source connector typesList source connectorsCreate a source connectorRotate source connector authenticationDelete a source connectorAssistants and inferenceList assistantsCreate an assistantInvoke an assistantDecide with a decision assistantDecide with a connectionDecisions JSON aliasChat CompletionsChat Completions JSON aliasStructured response contractsList structured response contractsCreate a structured response contractGet a structured response contractUpdate a structured response contractChange structured response contract statusValidate structured stateStored conversationsCreate a conversationGet a conversationAdd a conversation turnClose a conversationUpdate conversation retentionDelete a conversationGet conversation settingsUpdate conversation settingsUsage and budgetsGet usageGet budget settingsUpdate budget settingsCommon errors