API Reference
Reference for the implemented public REST API, hosted widget stream transport, and the dashboard routes that often look
API Reference
Reference for the implemented public REST API, hosted widget stream transport, and the dashboard routes that often look similar but are not the same API surface.
Start with the public API domain
Use https://api.kravos.ai/v1 for server-to-server REST integrations. Routes on your Kravos.ai web app
domain are either hosted widget transports or dashboard session routes unless this page says otherwise.
Quickstart
- Create an agent-scoped API key from API Keys.
- Give the key the smallest permission set for the workflow:
chat,retrieve,ingest, ortrace_read. - Call the public API base URL with either
Authorization: Bearer ...orx-api-key.
curl -X POST https://api.kravos.ai/v1/chat \
-H "Authorization: Bearer spk_..." \
-H "Content-Type: application/json" \
-d '{"message":"What are your opening hours?"}'
Successful JSON responses use this shape:
{
"data": {},
"meta": {
"requestId": "req_abc123"
}
}
Base URLs
| Surface | Base URL | Use for |
|---|---|---|
| Public REST API | https://api.kravos.ai/v1 | Server-to-server chat, retrieval, sources, conversations |
| Hosted web app / widget | https://your-domain.com/api | Widget SDK routes, dashboard routes, hosted stream transport |
| Deprecated dashboard shim | https://your-domain.com/api/v1 | Only POST /chat and POST /retrieve shims that return 410 |
Dashboard Agent-Duplication Routes
Agent duplication is an authenticated dashboard operation, not a public /v1 API. It requires an active dashboard
session for an ADMIN or SUPERADMIN; agent API keys and organization API keys cannot call these routes. The trusted
session supplies the tenant. A source agent or duplication operation outside that tenant, or an unrelated agent path,
returns 404 rather than disclosing that resource.
| Endpoint | Purpose |
|---|---|
POST /api/agents/:agentId/duplicate | Create or resume a same-tenant duplication operation |
GET /api/agents/:agentId/duplicate/:duplicationId | Read its current copy status and value-free warnings |
POST /api/agents/:agentId/duplicate/:duplicationId/cancel | Request cancellation and return the durable post-cancel status |
Create or resume a duplicate
Send a JSON request body. Every include field defaults to true; send false only for surfaces you intentionally
want to exclude. name is optional, trimmed, and limited to 120 characters.
Voice preferences, including native voice settings, are copied. Legacy provider-owned agent/tool identifiers, synchronization timestamps, fingerprints, and runtime-tool attachment state are reset. Review native voice defaults and deployment readiness before starting a new Voice Lab session; no remote provider sync action is required.
POST /api/agents/agent_123/duplicate
Idempotency-Key: duplicate-support-agent-2026-09-09
Content-Type: application/json
{
"name": "Support Agent — EMEA",
"include": {
"knowledgeBase": true,
"skills": true,
"tools": true,
"mcpServers": true,
"interceptors": true,
"workspaceArtifacts": true
}
}
You may instead send idempotencyKey in the body. If both locations are used, their values must match. Reuse the same
key only to safely retry the same source agent and request intent. A matching retry returns 202 and the durable
operation's current state; a different name or include selection returns 409 CONFLICT and creates nothing.
{
"data": {
"agentId": "agent_copy_456",
"duplicationId": "dup_789",
"status": "PENDING"
}
}
status can be PENDING, COPYING, COMPLETED, FAILED, or CANCELLED. A replay can therefore return a later
state than PENDING.
Status and cancellation
Use either the source or destination agent ID in the status and cancellation paths. Status responses use
Cache-Control: private, no-store and include operation counters, source-stage states, stable failure codes, and
value-free warnings. Required nullable failureCode describes an operation-level failure. Required nullable
workspaceStage is either null (no workspace stage) or { "state": "...", "failureCode": null }, with a stable
failure code instead of null when available. It has no separate counters. This reports workspace-only failures even
when stages and warnings are empty. Warnings identify a surface and remediation action, not configuration values
or credentials.
Cancellation is idempotent. Repeating it after COMPLETED, FAILED, or CANCELLED returns the existing durable
status; it does not alter the source agent or other work.
Errors and retry behavior
| Status | Code / condition | What to do |
|---|---|---|
400 | VALIDATION_ERROR | Correct the request body or make the header/body idempotency key match. |
401 / 403 | missing session or insufficient dashboard role | Sign in with an organization administrator account. |
404 | source, operation, or path is outside the tenant/path scope | Use an agent and operation from the active organization. |
409 | CONFLICT | Inspect error.details.reason: IDEMPOTENCY_KEY_REUSED means the key has a different fingerprint, CONCURRENT_REQUEST means a serializable transaction conflicted, and CONFIGURATION_REVIEW_REQUIRED means source configuration failed duplication policy inspection. Review unknown secret-shaped fields, invalid MCP connection configuration, or unknown external identifiers only for the configuration case. Retry the same intent with the same key only for the concurrent-request case. |
429 | LIMIT_EXCEEDED | The tenant cannot create another agent under its current entitlement. Free capacity or change the entitlement, then retry. |
500 | INTERNAL_ERROR | Retry only after checking the operation status; do not generate a new key for an uncertain request. |
There is no separate per-route duplication throttle. The 429 above is the existing tenant agent-capacity entitlement,
not a new copy cap. Duplication copies already-ready material and never silently queues re-ingestion; an operator must
explicitly re-ingest any blocked source after the operation completes. If a source changes after the manifest is
captured, its stage becomes terminally BLOCKED with MANIFEST_FENCE_CHANGED; it is not retried against that
snapshot. Review the report and start a new duplication with a new idempotency key once the source has settled.
Knowledge-base snapshots include only the captured published document generation, not an in-progress recrawl's staged documents. If the published generation changes before copying, the source is blocked as a changed manifest. Workspace artifact selection includes generated outputs and user uploads; knowledge-source uploads are handled only by the knowledge-base option. Recovery resumes the same durable artifact destination rather than creating another billed copy.
AGENT_DUPLICATION_COPY_OUTCOME_UNKNOWN means the storage-provider write outcome requires administrator
reconciliation. Reserved storage remains held until an administrator confirms whether the write completed; do not
retry the copy, release the reservation, or delete the provider object manually. These dashboard routes do not provide
a self-service reconciliation endpoint.
Authentication
Public API requests authenticate with either header format:
Authorization: Bearer spk_...
or:
x-api-key: spk_...
Agent keys created in the dashboard are scoped to one agent and currently use the spk_ raw-key prefix. Org-scoped keys
exist for organization automation and MCP-style platform access, but /v1 and /api/sdk routes reject org keys because
those routes must resolve a single agent.
| Permission | Used for |
|---|---|
chat | Chat submit, durable turn reattach/cancel, conversation reads |
retrieve | POST /v1/retrieve |
ingest | Source reads/writes, sync jobs, ingestion-job reads |
ingest.read | Fine-grained route check; base ingest keys satisfy it |
ingest.write | Fine-grained route check; base ingest keys satisfy it |
usage.read | Fine-grained route check; base public keys satisfy it |
trace_read | GET /v1/agent-runs/:runId |
Permission compatibility
Permissions are checked with directional compatibility so keys created through either vocabulary keep working:
- Alias groups (bidirectional): holding any member of a group satisfies a requirement for any other member.
chat↔chat:write;retrieve↔sources:read↔retrieval:read;ingest↔ingest.read↔ingest.write↔sources:write;skill_fetch↔skills:read;usage.read↔usage:read. - Base permission fallback: a key holding
ingestsatisfies fine-grainedingest.read/ingest.writechecks. - One-directional legacy grant: keys created with
chat,retrieve, oringesthistorically receivedusage.read; that grant is preserved, so those keys satisfyGET /v1/usage. The reverse never holds —usage.readdoes not grant chat access. - A literal
*permission satisfies every check.
Allowed origins
The API service accepts CORS requests, then the key allowlist decides whether the request is allowed:
- empty
allowedOriginsmeans any origin is accepted - a non-empty list requires an
Originheader - the
Originvalue must exactly match one configured URL - blocked origins return
403; revoked, inactive, or expired keys return401
This lets browser-based integrations use public endpoints without turning allowed origins into a hard product cap.
Request IDs
Send x-request-id when you have one. If you omit it, the API creates one. JSON responses include meta.requestId, and
the x-request-id response header is exposed for browser clients.
Errors
Public /v1 JSON errors use this envelope:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired API key",
"details": {}
},
"meta": {
"requestId": "req_abc123"
}
}
details is present only for validation failures or other structured errors.
Runtime failures follow a safe rule: 4xx and 504 responses from the agent runtime are propagated with their
machine-readable codes, while any other runtime failure returns 500 TURN_FAILED without internal details. The
Platform MCP chat tools follow the same rule and surface runtime 5xx failures as a generic tool error that references
the request ID instead of leaking internals.
| Code | Typical status | Meaning |
|---|---|---|
UNAUTHORIZED | 401 | Missing, invalid, expired, inactive, or revoked API key |
FORBIDDEN | 403 | Origin blocked, missing permission, or route needs agent key |
PLAN_RESTRICTED | 403 | Current plan does not include public API access |
VALIDATION_ERROR | 400 | Request body or query string did not match the route schema |
NOT_FOUND | 404 | Resource does not exist for the key's agent |
LIMIT_EXCEEDED | 429 | Source or monthly ingestion-job entitlement limit reached |
TURN_LIMIT_EXCEEDED | 429 | Chat turn quota reached |
CONVERSATION_NOT_FOUND | 404 | conversationId does not exist for the key agent |
CONVERSATION_NOT_ACTIVE | 409 | Conversation exists but is not in AI_ACTIVE state |
IDEMPOTENCY_CONFLICT | 409 | Durable turn idempotency key was reused with a different request hash |
CONVERSATION_RECONCILING | 409 | Conversation queue is blocked while an earlier turn is reconciled |
END_USER_NOT_FOUND | 404 | endUserId does not exist in the tenant |
TURN_NOT_FOUND | 404 | Durable turn does not exist within the authenticated tenant, agent, and end-user scope |
RUNTIME_DATA_UNAVAILABLE | 503 | Optional runtime-data forwarding is temporarily unavailable |
TURN_FAILED | 500 | Chat runtime failed |
RUNTIME_TIMEOUT | 504 | Runtime timed out |
GONE | 410 | Dashboard-domain shim moved to api.kravos.ai |
UNSUPPORTED_ATTACHMENT_TYPE | 400 | Hosted stream attachment not supported by selected model |
UNSUPPORTED_TYPE | 400 | Hosted stream file MIME type is not allowed |
FILE_TOO_LARGE | 400 | Hosted stream file exceeds the implemented per-type limit |
INTERNAL_ERROR | 500 | Unexpected server failure |
Pagination And Filtering
List endpoints use cursor pagination on the public API.
| Endpoint | Query params | Notes |
|---|---|---|
GET /v1/sources | limit, cursor, status, type | limit defaults to 20 and is capped at 100 |
GET /v1/conversations | limit, cursor, endUserId | limit defaults to 20 and is capped at 100 |
GET /v1/conversations/:id/messages | limit, cursor | limit defaults to 50; there is no history cap |
GET /v1/agent-runs/:id | limit, cursor | Sequence cursor; each page defaults to 20 and is capped at 100 |
nextCursor is null when there are no more results. Use enum values for status and type; invalid filters are not
applied by the current implementation. Agent-run cursors are the last event sequence. Continue requesting pages until
hasMore is false; there is no total trace retrieval cap.
Choose The Right Endpoint
- use
POST /v1/chatwhen you want one complete reply - use
POST /v1/chat/streamwhen the client should render tokens as they arrive - use
GET /v1/chat/turns/:turnId/streamto reattach to an accepted durable turn without generating again - use
POST /v1/chat/turns/:turnId/cancelto explicitly stop a durable turn - use
POST /v1/retrievewhen you only want source chunks, not AI generation - use
/v1/sources*when you are managing ingestion programmatically - use
/v1/conversations*when you need API-created transcript history - use
GET /v1/agent-runs/:runIdwhen you need a redacted execution trace for one completed or running turn - use
GET /v1/usagewhen you need current-period usage counters
Usage Semantics
GET /v1/usage is scoped to the key agent: data.turns.used counts platform turns recorded for that agent,
data.ingestionJobs.used counts jobs created on that agent's sources, data.sources.active counts that agent's
sources, and data.tokens reports the additive { input, output } token aggregate from AI messages of that agent's
conversations — all within the current period. Period bounds come from the tenant subscription, falling back to the
current calendar month. Plan limit values are tenant entitlements and are reported as limits, never as used amounts.
The route only admits agent-scoped keys and fails closed with 500 INTERNAL_ERROR if agent context is missing. This is
the REST plan-counter surface; tenant cost aggregates are available through the Platform MCP get_usage tool instead.
Chat Turn Request And Response
Both POST /v1/chat and POST /v1/chat/stream share one request contract.
Durable execution cohort
For agents enabled for durable execution, both chat endpoints admit turns to the same per-conversation queue.
Turns on one conversation execute in submission order; different conversations can run independently. The synchronous
endpoint still returns the same JSON envelope and reply fields, not a new job response. Both submit endpoints return
the accepted identity in the x-turn-id response header, which is CORS-exposed so browser clients can read it with
response.headers.get('x-turn-id'). Keep this ID separately from conversationId and x-request-id: it identifies one
accepted turn and is used for reattach and cancellation.
Send an optional Idempotency-Key header on either submit endpoint when you need safe retries. The key is scoped to the
tenant. Reusing it with the same request hash attaches to the existing turn instead of generating another reply; reusing
it with a different request hash returns 409 IDEMPOTENCY_CONFLICT. Reuse both the key and the original request when
retrying an uncertain submission. Without the header, the server assigns a fresh identity to each submission, so a lost
first response cannot be deduplicated by resubmitting the body alone. Idempotency does not bypass authentication,
authorization, or request rate limits.
POST /v1/chat/stream
Authorization: Bearer spk_...
Content-Type: application/json
Idempotency-Key: invoice-summary-request-123
If an earlier turn on a conversation has an unknown provider or tool outcome, new submissions return
409 CONVERSATION_RECONCILING rather than queueing indefinitely. That conversation requires operator reconciliation;
repeated submissions or new idempotency keys do not release it.
Disconnecting, aborting the HTTP request, or closing the stream only detaches from an accepted durable turn. Execution continues independently. To stop execution, use the explicit cancel endpoint below; disconnect is not cancellation.
Reattach to a durable turn
GET /v1/chat/turns/:turnId/stream?cursor=0 replays stored SSE events and then tails the same turn's live output.
It does not submit another message or regenerate the reply. The response remains text/event-stream with the same
AI SDK UI-message payloads as the original stream; durable events carry SSE id: cursors. Retain the last fully
processed event ID together with the corresponding local text and restart/reset state, then reconnect after that
cursor. cursor is a nonnegative safe integer, defaults to 0 (replay from the beginning), and takes precedence over
the supported Last-Event-ID request header when both are supplied.
curl --no-buffer --include --fail-with-body \
"https://api.kravos.ai/v1/chat/turns/${TURN_ID}/stream?cursor=12" \
-H "Authorization: Bearer ${KRAVOS_API_KEY}"
Alternatively, omit cursor and send Last-Event-ID: 12. Reattach uses the same agent-scoped API-key authentication
(Authorization: Bearer ... or x-api-key) and chat permission as submission. The server scopes the turn to the
authenticated tenant and agent; a turn ID alone grants no access. A publishable key must also carry a verified
x-sdk-session-id and can access only turns belonging to that SDK session's end user. These rules also apply to cancel.
Restarted execution and partial text
A recovered turn can emit data-turn-restart when execution resumes from its durable checkpoint. Its payload is
{"type":"data-turn-restart","data":{"attempt":2},"transient":true}; data.attempt identifies the new execution attempt.
Despite transient: true, the event is retained in the durable SSE replay log.
On each data-turn-restart, discard all text accumulated for this turn since the previous restart, or since the
turn began if this is its first restart. Then render subsequent text-delta parts as replacement text, not as a
continuation of the abandoned partial reply. Do not remove earlier conversation turns.
For example, a live or replayed stream can contain:
id: 13
data: {"type":"text-delta","id":"old_text","delta":"Abandoned partial reply"}
id: 14
data: {"type":"data-turn-restart","data":{"attempt":2},"transient":true}
id: 15
data: {"type":"text-delta","id":"new_text","delta":"Replacement reply"}
After event 15, the rendered text is Replacement reply, not the concatenation of both replies. Reattaching does
not itself restart execution: it can replay old-attempt text, a restart, and then replacement text in sequence.
A reconnect cursor can fall on either side of a restart. Before it, process the replayed restart and reset the text.
At or after it, the restart will not be replayed, so preserve the already-applied reset and the replacement text
corresponding to that cursor. Apply each event, including its reset, before advancing the saved cursor. If that local
text and restart/reset state is missing or cannot be matched to the cursor, clear this turn's local output and reattach
with cursor=0 to reconstruct it; a cursor alone is not enough to resume rendering safely.
Explicitly cancel a durable turn
POST /v1/chat/turns/:turnId/cancel requests cancellation using the same API-key authentication, chat permission,
tenant/agent scope, and publishable-key end-user restriction as reattach. No request body is required.
curl --fail-with-body -X POST \
"https://api.kravos.ai/v1/chat/turns/${TURN_ID}/cancel" \
-H "Authorization: Bearer ${KRAVOS_API_KEY}"
The successful 200 response reports the turn's current state:
{
"data": {
"turnId": "turn_123",
"state": "CANCELLING"
},
"meta": {
"requestId": "req_abc123"
}
}
Cancellation is an intent, not a promise that all in-flight effects are already stopped. Continue observing the turn for its terminal outcome. Known provider spend remains charged and unused holds are released; cancellation does not refund completed work. An uncertain external outcome requires reconciliation rather than blindly repeating it.
These durable behaviors apply only to the enabled cohort. Existing non-cohort chat requests retain their direct execution path and do not provide a durable turn ID or durable reattach/cancel lifecycle.
Identity: at most one of three
Provide at most one identity target. Supplying more than one returns 400 VALIDATION_ERROR; all three may be omitted.
| Field | Meaning |
|---|---|
conversationId | Continue an existing conversation. 404 CONVERSATION_NOT_FOUND if it does not exist for the key agent; 409 CONVERSATION_NOT_ACTIVE if it is not in AI_ACTIVE state. |
endUserId | Internal end user ID for a new conversation. 404 END_USER_NOT_FOUND if the user does not exist in the tenant. |
externalEndUserId | Stable caller-defined identity. Resolved idempotently to the same internal end user; concurrent first requests never create duplicates. Trimmed before validation and bounded to 1–200 characters on both the public REST routes and the MCP create_end_user tool; whitespace-only values are rejected. |
Omit all three to let the API create a fresh anonymous identity for the turn.
Persistence
persist defaults to true. Set it to false for a turn that must not append messages or an execution trace:
- with
conversationId, the turn can read that conversation's existing context but does not append to it - with
endUserIdorexternalEndUserId, the identity is resolved for correlation but the turn creates no conversation - with no identity target, the turn is fully anonymous and creates neither an end user nor a conversation
Request-scoped runtime data
Both POST /v1/chat and POST /v1/chat/stream accept the same three optional runtime fields. They are additive and do
not change identity validation, JSON response shapes, or realtime-token behavior.
| Field | Model | Model provider | Configured Kravos tools | Durable runtime-input storage |
|---|---|---|---|---|
context | Yes | Yes | Not automatically | Durable cohort request snapshot |
toolContext | No | No | Explicit selected bindings | Durable cohort request snapshot |
toolCredentials | Never | Never | Explicit selected auth slots | Never in plaintext; encrypted per-turn envelope in durable cohort |
These public endpoints run text turns; the provider column states the data-visibility contract when
the same context field is used by widget voice. Never put secrets in context. Use toolContext for hidden non-secret
binding values and toolCredentials for short-lived credentials selected by configured tools.
context and toolContext must be JSON objects. Their values may recursively contain strings, finite numbers,
booleans, null, arrays, and objects. toolCredentials must be an object mapping slot names to strings.
Synchronous example: explain the selected invoice
Run this from your backend with an agent-scoped chat key supplied through your established secret store as
KRAVOS_API_KEY. Derive the customer identity and permitted context from the authenticated application session;
do not trust arbitrary browser-submitted customer IDs. All customer values below are synthetic.
curl --fail-with-body -X POST https://api.kravos.ai/v1/chat \
-H "Authorization: Bearer ${KRAVOS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"message": "Explain the invoice I am viewing",
"externalEndUserId": "customer_456",
"persist": true,
"context": {
"user": { "locale": "en" },
"page": { "name": "Invoice details", "selectedInvoiceId": "inv_123" }
}
}'
Read the successful JSON envelope's data.reply and retain data.conversationId for continuation.
externalEndUserId selects identity; it does not automatically give the model page or account facts.
Continue with updated context
Send this body to either chat endpoint using the same authentication headers. Replace the placeholder with the actual
conversation ID returned by the previous persistent turn. Do not also send externalEndUserId or endUserId.
{
"message": "What changed on this invoice?",
"conversationId": "<conversation-id-from-previous-response>",
"context": {
"user": { "locale": "en" },
"page": { "name": "Invoice details", "selectedInvoiceId": "inv_789" }
}
}
Supply current runtime data on every relevant request. Continuing a conversation does not restore the preceding
request's raw context, toolContext, or toolCredentials, even if that turn used persist: true.
Streaming example: summarize the selected invoice
This standalone backend request uses an agent-scoped chat key from your secret store in KRAVOS_API_KEY.
Derive identity and context from your authenticated host session; the sample values are synthetic.
curl --no-buffer --include --fail-with-body -X POST https://api.kravos.ai/v1/chat/stream \
-H "Authorization: Bearer ${KRAVOS_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"message": "Summarize my selected invoice",
"externalEndUserId": "customer_456",
"persist": true,
"context": {
"page": { "name": "Invoice details", "selectedInvoiceId": "inv_123" }
}
}'
The command displays headers and the stream without buffering. In application code, check HTTP status and
Content-Type before consuming the body as SSE: an early validation or capability failure returns a JSON error
instead. Do not parse the entire successful stream as a JSON response. Use the existing runtime SSE event contract
shown in the streaming endpoint reference below, handling terminal errors and disconnects as well as completion.
The stream carries AI SDK UI-message parts in data: frames: text-delta.delta supplies incremental text,
data-done.data.result supplies the completed result (including reply and conversationId), and an error part
carries errorText. Other parts include text boundaries, citations, tools, media, and trace events; [DONE] terminates
the stream. A model finish part alone does not establish that Kravos has settled and persisted the turn—wait for
data-done for the completed result. Retain its conversation ID for continuation, and treat a disconnect before that
result as an uncertain outcome, not permission to replay the turn.
For durable turns, also handle data-turn-restart by discarding this turn's text since the previous restart before
rendering replacement deltas. Preserve that reset/text state alongside the reconnect cursor, or clear local output
and replay from cursor=0, as described in Restarted execution and partial text.
Optional authorized private-data lookup
Either endpoint also accepts the following body when the tool has stored bindings and a configured billing slot:
{
"message": "Show the selected invoice",
"externalEndUserId": "user_123",
"persist": true,
"context": {
"page": { "name": "Invoice details", "selectedInvoiceId": "inv_456" }
},
"toolContext": {
"accountId": "acct_123"
},
"toolCredentials": {
"billing": "<short-lived-user-token>"
}
}
Runtime data remains request-scoped regardless of persist and must be supplied again for a later turn. Non-cohort requests
discard raw runtime input after execution. Durable cohort requests retain non-secret request snapshots for execution
and recovery. persist: false controls transcript persistence, not immediate deletion of durable turn data.
Credentials are never stored in plaintext in the request snapshot; durable execution uses an encrypted per-turn
credential envelope. Responses and SSE events never echo toolCredentials. The server caller must supply a current
short-lived value when a configured tool requires one.
Follow the HTTP tool bindings and external MCP server bindings guidance for supported
configuration and transports. The stored auth name widget-session also supports runtime slots from public REST
callers; it is not limited to widgets. Unbound data is not automatically sent downstream. Runtime fields do not replace
API authentication, tenant/agent scope, or downstream authorization. Never log request bodies containing these fields
or paste credentials into AI conversations or customer-visible errors.
Model-visible context can be repeated in an assistant reply, and that reply may be persisted. Request scope does not promise secrecy or erasure of those facts from conversation history.
If a configured tool reports SESSION_CREDENTIAL_REQUIRED, this is a safe tool failure naming the missing slot, not
necessarily a top-level HTTP response code. Issue or refresh the named slot and retry according to the
tool's normal idempotency policy. Do not log the value, return it to a browser, or move it into context. A request using
any runtime field may return 503 RUNTIME_DATA_UNAVAILABLE while runtime-data forwarding is unavailable; context-free
requests remain compatible.
For 503 RUNTIME_DATA_UNAVAILABLE, confirm forwarding capability availability with the operator. Do not silently
retry without runtime data or move context into message. Even an explicitly supplied empty object is a runtime field;
omit all three fields only when context-free behavior is intended. Invalid runtime input returns 400 VALIDATION_ERROR:
fix the input rather than retrying unchanged. After a timeout or disconnect, do not automatically repeat a potentially
side-effecting turn—a downstream operation may already have occurred.
Response shape
data.reply is always a string — the complete assistant reply text. data.citations is an array of source
citations used to ground the reply, and data.media is an array of media attachments shared in the turn. Both are
always present and may be empty.
data.endUserId is the resolved internal end user ID. It is present whenever an end user identity was resolved for
the turn — anonymous persistent turns, endUserId, and externalEndUserId, including non-persistent identity-targeted
turns — and omitted for conversation-target and anonymous non-persistent turns.
{
"data": {
"reply": "We are open 9am-6pm Monday to Friday.",
"conversationId": "clx...",
"endUserId": "clx...",
"citations": [],
"media": []
},
"meta": {
"requestId": "req_abc123"
}
}
Realtime token behavior
POST /v1/chat returns realtimeToken and realtimeUrl by default whenever a conversation exists, so realtime
capable clients can upgrade without an extra request. Pass includeRealtimeToken: false to omit both fields
entirely, for example when the client never uses the hosted realtime transport.
Streaming headers
POST /v1/chat/stream returns the SSE stream with content-type: text/event-stream and the x-request-id header.
When an end user identity was resolved for the turn, the stream also sets x-end-user-id to the internal end user
ID. Both x-request-id and x-end-user-id are CORS-exposed, so browser clients can read them.
Endpoints
Chat & Retrieval
Send messages, stream responses, and search the knowledge base on the public /v1 API.
/v1/chatRun one non-streaming chat turn through the agent attached to the API key. Provide at most one of conversationId, endUserId, or externalEndUserId; data.reply is always a string, citations/media are always present, and data.endUserId is present when an end user identity was resolved. With persist: false, existing conversation targets provide read-only context, while end-user targets are resolved without creating a conversation. Realtime fields are included by default; pass includeRealtimeToken: false to omit them. For durable-cohort agents, optional tenant-scoped Idempotency-Key retries with the same request hash attach to the same turn; different hashes return 409 IDEMPOTENCY_CONFLICT. The JSON shape stays the same and x-turn-id is CORS-exposed. Disconnect only detaches; explicitly cancel to stop. A blocked conversation returns 409 CONVERSATION_RECONCILING.
curl -X POST https://api.kravos.ai/v1/chat \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"message":"What are your opening hours?"}'Request body
{
"message": "What are your opening hours?",
"conversationId": "optional",
"endUserId": "optional",
"externalEndUserId": "optional-stable-caller-id",
"persist": true,
"includeRealtimeToken": true
}Response
{
"data": {
"reply": "We are open 9am-6pm Monday to Friday.",
"conversationId": "conv_abc123",
"endUserId": "end_user_abc123",
"citations": [],
"media": [],
"realtimeToken": "rt_abc123",
"realtimeUrl": "wss://realtime.kravos.ai/v1"
},
"meta": {
"requestId": "req_abc123"
}
}/v1/chat/streamStream one chat turn using Server-Sent Events. Provide at most one of conversationId, endUserId, or externalEndUserId: conversationId returns 404 CONVERSATION_NOT_FOUND when outside the key agent scope and 409 CONVERSATION_NOT_ACTIVE when not in AI_ACTIVE state; endUserId returns 404 END_USER_NOT_FOUND when unknown; externalEndUserId is a stable caller-defined identity resolved idempotently. Supplying more than one identity returns 400 VALIDATION_ERROR. With persist: false, existing conversation targets provide read-only context, while end-user targets are resolved without creating a conversation. x-end-user-id is set when identity resolves. For durable-cohort agents, optional tenant-scoped Idempotency-Key retries with the same request hash attach; different hashes return 409 IDEMPOTENCY_CONFLICT. A blocked conversation returns 409 CONVERSATION_RECONCILING. Retain the CORS-exposed x-turn-id for replay/cancel; durable SSE id cursors support reattachment. data-turn-restart has data: { attempt: number } and transient: true, is retained for replay, and means execution resumed from a durable checkpoint. Discard all text for this turn since the previous restart (or turn start), then render following text-delta parts as replacement text, never concatenate abandoned partial text. Save each fully processed cursor with its matching text and restart/reset state across reconnects; if that state is missing, clear local turn output and replay with cursor=0. Disconnect only detaches; explicit cancel is required to stop.
curl -X POST https://api.kravos.ai/v1/chat/stream \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"message":"Tell me about your products","externalEndUserId":"customer-42"}' \
--no-bufferRequest body
{
"message": "Tell me about your products",
"conversationId": "optional",
"endUserId": "optional",
"externalEndUserId": "optional-stable-caller-id",
"persist": true
}Response
HTTP/1.1 200 OK
content-type: text/event-stream
x-request-id: req_abc123
event: ...
data: .../v1/chat/turns/:turnId/streamReattach to an accepted durable-cohort API turn without regenerating. Replay after cursor, then tail live SSE using the original UI-message payloads and id cursors. cursor defaults to 0, must be a nonnegative safe integer, and takes precedence over Last-Event-ID. Replay may contain old-attempt text, data-turn-restart, then replacement text. data-turn-restart carries data: { attempt: number } and transient: true, is retained for replay, and marks execution resumed from a durable checkpoint: discard this turn’s text since the previous restart (or turn start) before rendering replacement deltas. A cursor before the restart receives the reset; a cursor at or after it does not replay that reset. Preserve matching local text and restart/reset state with the last fully processed cursor, applying resets before saving the cursor. If that state is unavailable, clear local turn output and reattach with cursor=0; never concatenate abandoned partial text with replacement text. Use the same API-key authentication and tenant/agent scope as submission. Publishable keys must send a verified x-sdk-session-id and may access only their own end-user turns. Disconnect only detaches; finish alone is not durable completion, so wait for data-done or a terminal error.
curl --no-buffer --include \
"https://api.kravos.ai/v1/chat/turns/turn_abc123/stream?cursor=12" \
-H "Authorization: Bearer spk_your_api_key"Request body
No body. Optional query: ?cursor=12. Alternatively send Last-Event-ID: 12 without cursor.Response
HTTP/1.1 200 OK
content-type: text/event-stream
x-turn-id: turn_abc123
x-request-id: req_abc123
id: 13
data: {"type":"text-delta","id":"old_text","delta":"Abandoned partial reply"}
id: 14
data: {"type":"data-turn-restart","data":{"attempt":2},"transient":true}
id: 15
data: {"type":"text-delta","id":"new_text","delta":"Replacement reply"}
/v1/chat/turns/:turnId/cancelExplicitly request cancellation of a durable-cohort API turn. Disconnecting or aborting a stream does not cancel execution. Uses the same API-key authentication and tenant/agent scope as submission and reattach. Publishable keys require a verified x-sdk-session-id and may cancel only their own end-user turns. Returns 200 with the current state; cancellation may still be in progress. Observe the turn for its terminal outcome. Known spend remains charged and unused holds are released.
curl -X POST https://api.kravos.ai/v1/chat/turns/turn_abc123/cancel \
-H "Authorization: Bearer spk_your_api_key"Request body
{}Response
{
"data": {
"turnId": "turn_abc123",
"state": "CANCELLING"
},
"meta": {
"requestId": "req_abc123"
}
}/v1/retrieveSearch source chunks without running AI generation. topK is 1-20 and minScore is 0-1.
curl -X POST https://api.kravos.ai/v1/retrieve \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"query":"pricing plans","topK":5,"minScore":0.7}'Request body
{
"query": "pricing plans",
"topK": 5,
"minScore": 0.7
}Response
{
"data": {
"chunks": [
{
"id": "chunk_abc123",
"documentId": "doc_abc123",
"sourceName": "Pricing",
"text": "Our plans start at $29/mo...",
"score": 0.94,
"url": "https://example.com/pricing"
}
]
},
"meta": {
"requestId": "req_abc123"
}
}Sources
Manage knowledge sources programmatically. Requires an API key with the ingest permission.
/v1/sourcesList sources with cursor-based pagination. Filter by status or type.
curl 'https://api.kravos.ai/v1/sources?limit=20&status=READY' \
-H "Authorization: Bearer spk_your_api_key"Request body
// Query params: ?limit=20&status=READY&type=URL&cursor=clx...Response
{
"data": {
"sources": [
{
"id": "clx...",
"agentId": "agent_abc123",
"type": "URL",
"name": "Help center",
"status": "READY",
"config": {
"url": "https://help.example.com"
},
"lastSyncedAt": "2026-07-06T12:00:00.000Z",
"lastError": null,
"latestJob": null
}
],
"nextCursor": "clx..."
},
"meta": {
"requestId": "req_abc123"
}
}/v1/sourcesCreate a source. autoSync defaults to true and queues an ingestion job.
curl -X POST https://api.kravos.ai/v1/sources \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-unique-key" \
-d '{"type":"URL","name":"Help center","config":{"url":"https://help.example.com"}}'Request body
{
"type": "URL",
"name": "Help center",
"config": {
"url": "https://help.example.com"
},
"autoSync": true
}Response
{
"data": {
"source": {
"id": "clx...",
"agentId": "agent_abc123",
"type": "URL",
"name": "Help center",
"status": "PENDING",
"config": {
"url": "https://help.example.com"
}
},
"ingestionJob": {
"id": "job_abc123",
"status": "QUEUED"
}
},
"meta": {
"requestId": "req_abc123"
}
}/v1/sources/:idFetch one source and its latest ingestion job.
curl https://api.kravos.ai/v1/sources/SOURCE_ID -H "Authorization: Bearer spk_your_api_key"Request body
// No request bodyResponse
{
"data": {
"id": "clx...",
"agentId": "agent_abc123",
"type": "URL",
"name": "Help center",
"status": "READY",
"config": {
"url": "https://help.example.com"
},
"error": null,
"latestJob": {
"id": "job_abc123",
"type": "INGEST",
"status": "SUCCEEDED",
"progress": 100,
"chunksCreated": 42,
"error": null
}
},
"meta": {
"requestId": "req_abc123"
}
}/v1/sources/:idUpdate a source name, config, or both. At least one field is required.
curl -X PATCH https://api.kravos.ai/v1/sources/SOURCE_ID \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"name":"Updated help center"}'Request body
{
"name": "Updated help center",
"config": {
"url": "https://help.example.com"
}
}Response
{
"data": {
"id": "clx...",
"agentId": "agent_abc123",
"name": "Updated help center",
"type": "URL",
"status": "READY"
},
"meta": {
"requestId": "req_abc123"
}
}/v1/sources/:idStart durable storage cleanup, then delete the source and its associated documents and chunks. Returns 409 while the artifact is preserved or 503 when cleanup cannot start.
curl -X DELETE https://api.kravos.ai/v1/sources/SOURCE_ID -H "Authorization: Bearer spk_your_api_key"Request body
// No request bodyResponse
{
"data": {
"id": "clx...",
"deleted": true
},
"meta": {
"requestId": "req_abc123"
}
}/v1/sources/:id/syncTrigger ingestion or a full re-index for a source. Returns 202 when the job is queued.
curl -X POST https://api.kravos.ai/v1/sources/SOURCE_ID/sync \
-H "Authorization: Bearer spk_your_api_key" \
-H "Content-Type: application/json" \
-d '{"force":false}'Request body
{
"force": false
}Response
{
"data": {
"jobId": "job_abc123",
"status": "QUEUED",
"sourceId": "clx..."
},
"meta": {
"requestId": "req_abc123"
}
}/v1/ingestion-jobs/:idCheck ingestion job status and progress.
curl https://api.kravos.ai/v1/ingestion-jobs/JOB_ID -H "Authorization: Bearer spk_your_api_key"Request body
// No request bodyResponse
{
"data": {
"id": "job_abc123",
"sourceId": "clx...",
"status": "QUEUED",
"progress": 0,
"chunksCreated": null,
"error": null,
"startedAt": null,
"completedAt": null
},
"meta": {
"requestId": "req_abc123"
}
}Conversations & Usage
Read API conversations and current-period usage for the API key agent.
/v1/conversationsList API conversations for the key agent with cursor-based pagination.
curl https://api.kravos.ai/v1/conversations -H "Authorization: Bearer spk_your_api_key"Request body
// Query params: ?limit=20&endUserId=end_user_id&cursor=clx...Response
{
"data": {
"conversations": [
{
"id": "conv_abc123",
"agentId": "agent_abc123",
"endUserId": "end_user_abc123",
"messageCount": 4,
"createdAt": "2026-07-06T12:00:00.000Z",
"updatedAt": "2026-07-06T12:05:00.000Z"
}
],
"nextCursor": null
},
"meta": {
"requestId": "req_abc123"
}
}/v1/conversations/:idFetch one API conversation and its messages for the key agent.
curl https://api.kravos.ai/v1/conversations/CONVERSATION_ID -H "Authorization: Bearer spk_your_api_key"Request body
// No request bodyResponse
{
"data": {
"id": "conv_abc123",
"agentId": "agent_abc123",
"endUserId": "end_user_abc123",
"messages": [
{
"id": "msg_1",
"role": "user",
"content": "Hello",
"createdAt": "2026-07-06T12:00:00.000Z"
},
{
"id": "msg_2",
"role": "assistant",
"content": "Hi!",
"createdAt": "2026-07-06T12:00:01.000Z"
}
],
"createdAt": "2026-07-06T12:00:00.000Z"
},
"meta": {
"requestId": "req_abc123"
}
}/v1/conversations/:id/messagesList every message in a conversation with stable database-key keyset pagination; legacy rows are never silently omitted.
curl 'https://api.kravos.ai/v1/conversations/CONVERSATION_ID/messages?limit=50' \
-H "Authorization: Bearer spk_your_api_key"Request body
// Query params: ?limit=50&cursor=opaque_cursor
// limit defaults to 50 and must be a positive safe integer.
// Ordering: conversation-scoped database pagination key ascending (PostgreSQL BIGINT). Follow nextCursor until it is null; there is no history cap.
// Cursors are HMAC-authenticated with the server REALTIME_TOKEN_SECRET.
// During legacy-key backfill, the endpoint returns 503 PAGINATION_NOT_READY.Response
{
"data": {
"messages": [
{
"id": "msg_1",
"role": "user",
"content": "Hello",
"createdAt": "2026-07-06T12:00:00.000Z"
},
{
"id": "msg_2",
"role": "assistant",
"content": "Hi!",
"createdAt": "2026-07-06T12:00:01.000Z"
}
],
"nextCursor": "opaque_cursor_or_null",
"hasMore": true
},
"meta": {
"requestId": "req_abc123"
}
}/v1/usageRead current-period usage scoped to the key agent: agent turn, ingestion-job, active-source, and input/output token counters, with tenant plan limits reported as limits.
curl https://api.kravos.ai/v1/usage -H "Authorization: Bearer spk_your_api_key"Request body
// No request bodyResponse
{
"data": {
"period": {
"start": "2026-07-01T00:00:00.000Z",
"end": "2026-08-01T00:00:00.000Z"
},
"turns": {
"used": 100,
"limit": 5000,
"overageEnabled": false
},
"ingestionJobs": {
"used": 4,
"limit": 100
},
"sources": {
"active": 5,
"limit": 50
},
"tokens": {
"input": 1200000,
"output": 400000
}
},
"meta": {
"requestId": "req_abc123"
}
}Public Endpoint Index
| Endpoint | Permission | Purpose |
|---|---|---|
POST /v1/chat | chat | Run a synchronous chat turn |
POST /v1/chat/stream | chat | Stream a chat turn as Server-Sent Events |
POST /v1/retrieve | retrieve | Search source chunks without AI generation |
GET /v1/sources | ingest | List sources |
POST /v1/sources | ingest | Create a source and optionally queue ingest |
GET /v1/sources/:id | ingest | Fetch one source and latest job |
PATCH /v1/sources/:id | ingest | Update source name, config, or knowledge metadata |
DELETE /v1/sources/:id | ingest | Delete a source |
POST /v1/sources/:id/sync | ingest | Queue ingest or re-index |
GET /v1/ingestion-jobs/:id | ingest | Check ingestion job status |
GET /v1/conversations | chat | List conversations for the key's agent |
GET /v1/conversations/:id | chat | Fetch one conversation and its messages |
GET /v1/agent-runs/:runId | trace_read | Read one redacted execution trace page |
GET /v1/usage | any public key | Read current-period plan usage |
DELETE /v1/sources/:id starts durable storage cleanup before deleting an artifact-backed Source row. It returns
409 ARTIFACT_DELETE_CONFLICT while another lifecycle operation preserves the artifact, or
503 ARTIFACT_DELETE_UNAVAILABLE when cleanup cannot be started safely. Retry only after resolving or waiting for
the reported condition.
Chat And Retrieval Notes
POST /v1/chat and POST /v1/chat/stream accept JSON only on the public API domain:
{
"message": "What are your opening hours?",
"externalEndUserId": "customer_456",
"persist": true,
"context": { "locale": "en" },
"toolContext": { "accountId": "acct_123" },
"toolCredentials": { "billing": "<short-lived-user-token>" }
}
If no identity field is sent, the API creates an end user for a persistent turn and runs a non-persistent turn anonymously.
persist defaults to true.
The three runtime fields are optional; omit all of them for the existing context-free behavior. The streaming route
returns text/event-stream and forwards runtime SSE events without echoing hidden runtime values.
Agent Run Traces
GET /v1/agent-runs/:runId returns one run only when it belongs to the authenticated key's tenant and agent. It requires
trace_read; runs outside that scope return 404. Every successful trace read is durably audited before the response is
sent.
GET /v1/agent-runs/run_123?limit=20&cursor=40
Authorization: Bearer spk_...
{
"data": {
"id": "run_123",
"status": "complete",
"conversationId": "conv_123",
"messageId": "msg_123",
"startedAt": "2026-07-30T12:00:00.000Z",
"completedAt": "2026-07-30T12:00:03.000Z",
"events": [
{
"id": "run_123:41",
"runId": "run_123",
"sequence": 41,
"type": "tool.completed",
"status": "complete",
"label": "Completed a tool call",
"timestamp": "2026-07-30T12:00:02.000Z"
}
],
"hasMore": true,
"nextCursor": "41"
},
"meta": { "requestId": "req_abc123" }
}
Pass nextCursor as cursor for the next page. limit and cursor must be positive integers; invalid values return
400 VALIDATION_ERROR. Trace event payloads contain developer-visible tool and source context with credentials redacted before storage. Encrypted
payloads are never exposed. Because payloads can include customer and tool data, grant trace_read only to trusted
server-side integrations.
Hosted stream clients with both chat and trace_read receive operator-visible data-agent-run-event parts. A key with
only chat receives end-user-redacted event parts. The chat request authenticates the key once; trace visibility is derived
from that authenticated permission set.
data: {"type":"data-agent-run-event","data":{"id":"run_123:41","runId":"run_123","sequence":41,"type":"tool.completed","status":"complete","label":"Completed a tool call","timestamp":"2026-07-30T12:00:02.000Z"}}
POST /v1/retrieve accepts:
{
"query": "pricing plans",
"topK": 5,
"minScore": 0.7
}
The implemented validation accepts query up to 1,000 characters, topK from 1 to 20, and minScore from 0 to 1.
Source Types
When creating sources via POST /v1/sources, use one of these documented public configurations:
| Type | Required config fields | Notes |
|---|---|---|
URL | config.url | Optional: maxDepth, includePatterns, excludePatterns |
SITEMAP | config.sitemapUrl or config.url | Optional: includePatterns, excludePatterns |
FILE | config.fileUrl | Optional: fileName |
IMAGE | config.fileUrl | Optional: fileName |
BULK_UPLOAD | no route-level required fields | Accepted by the source enum; ZIP extraction is dashboard-managed |
The public create route accepts BULK_UPLOAD because it accepts the source enum. The ZIP extraction workflow itself is an
authenticated dashboard route (/api/sources/bulk-upload), not a separate public /v1 REST endpoint.
Custom Knowledge Metadata
All source types support optional config.knowledgeMetadata when created. It accepts only custom, an object of
string, number, boolean, or null values. Unsupported fields, nested custom values, and non-finite numbers return
400 VALIDATION_ERROR. Standard document metadata, including source dates and content-version identifiers, is
collected automatically and is not writable through this field.
To change metadata without replacing source configuration, use the top-level knowledgeMetadata field on
PATCH /v1/sources/:id:
{
"knowledgeMetadata": {
"custom": { "product": "Enterprise", "approved": true }
}
}
This replaces the custom metadata object and preserves URL, upload, and managed-storage configuration.
Send {"knowledgeMetadata": null} to clear it. Metadata-only edits do not queue ingestion or change ingestion
status. The next knowledge-base search receives the new values even if earlier results were cached.
These values apply to every document in the source and are sent to the model and its provider as source data. They do not override extracted document dates or automatically change source ranking. See Sources → Knowledge Metadata for field semantics and agent instruction examples.
autoSync defaults to true. Set autoSync: false when you want to create the source record first and queue ingestion
later with POST /v1/sources/:id/sync.
With autoSync: true, the Source and ingestion Job are created in one database transaction before queue submission.
Queue delivery is separate from that transaction; this does not guarantee crash-safe delivery or deduplicate concurrent
requests that have not yet cached a successful response.
Idempotent source creation
Pass an Idempotency-Key header on POST /v1/sources to safely retry create requests. Matching
keys for the same API key return the original 201 response for 24 hours.
Web App And SDK Routes
These routes were inspected because they overlap with public API concepts, but they are not the same as the public REST API:
/api/sdk/*powers the embedded widget and suite. It accepts an agent API key or a dashboard session fallback. Routes that read existing end-user state requiresessionId; init and some voice-start paths can create a session when omitted./api/sources*,/api/conversations*, and/api/usageare dashboard session routes. They usenext-auth/RBAC, not public API-key-only auth.POST /api/v1/chatandPOST /api/v1/retrieveon the web app domain return410 GONE; call the public API domain instead.POST /api/v1/chat/streamon the web app domain is the hosted widget stream endpoint. It can accept JSON ormultipart/form-datawith attachments.
File Attachments On Hosted Stream
The hosted web stream endpoint can accept images and PDFs alongside a chat message. Send POST /api/v1/chat/stream on
your Kravos.ai web app domain as multipart/form-data:
curl -X POST https://your-domain.com/api/v1/chat/stream \
-H "Authorization: Bearer spk_..." \
-F "message=What is in this image?" \
-F "files=@photo.png"
Supported MIME types are JPEG, PNG, WebP, GIF, and PDF. The implemented hosted route accepts up to 10 files per request, with 10 MB per image and 25 MB per PDF. Model-specific validation may still reject an attachment type.
OpenAPI And Spec Alignment
The compact OpenAPI spec exposed through platform/MCP docs is generated from
packages/platform/src/read/docs/reference.ts. It lists the implemented public /v1 paths for chat, streaming chat,
durable turn reattach/cancel, retrieval, sources, source detail/update/delete/sync, ingestion-job detail, conversations,
agent-run traces, and usage.
The generated spec includes path parameters, success status codes, permissions, source paths, and update timestamps. It is still compact: request and response schemas are documented on this page and should be treated as the human-readable contract.
Related Docs
API Keys
Create and manage keys for chat, retrieve, and ingest use cases.
Sources
Add and monitor the content your agent retrieves from.
Conversations
Review transcript history and takeover flows.