HTTP conventions
Every API operation is under /api/v1 on the Service's public URL. Requests and responses are JSON unless an operation says otherwise (uploads, file content, images and the thread stream). A running Service publishes its OpenAPI document at /api/v1/openapi.json and an interactive page at /api/v1/docs; the HTTP reference is generated from the same document.
Request bodies are strict: unknown fields are refused. Language SDKs and the companion command-line client are developed in independent repositories of converge-ai-labs.
Authentication
Applications authenticate with an API key as a bearer token:
When an Authorization header is present, cookies are ignored. An API key acts only in its own workspace, and never changes the account of the person who created it.
Browsers use the login session cookie set by POST /api/v1/auth/login. For cookie-authenticated requests other than GET, HEAD and OPTIONS:
- send the session's CSRF token in
X-CSRF-Token; login returns it, andGET /api/v1/auth/sessionreturns it again for an existing session; - an
Originheader, if sent, must equal the origin ofserver.public_url.
Account operations (changing your profile, password or email, disabling your account, listing login sessions and your own audit trail), GET /api/v1/auth/session and POST /api/v1/auth/logout require a login session, as do creating an API key, sending or resending an invitation, and starting a browser authorization (an OAuth authorization code or a connector account setup). An API key gets 403 forbidden from all of them, one status everywhere: it never mints something that can outlive it, and never hands a third party a link to complete on your behalf. A request without valid credentials receives 401 unauthenticated; a disabled principal's credentials are treated as invalid.
Every authenticated response, including the route's own answer and any error after authentication, carries Cache-Control: no-store and, when the session was renewed, its refreshed cookie.
Errors
Every error has one shape, and every response carries an X-Request-Id header:
{
"error": {
"code": "precondition_failed",
"message": "Resource changed",
"details": {"current_etag": "\"ap_...:4\""},
"request_id": "req_..."
}
}
| Code | Status | Meaning |
|---|---|---|
invalid_argument |
400 | The request is malformed or refers to something unusable. details.field names the offending field; request validation lists details.fields as {field, reason}. |
invalid_cursor |
400 | The paging cursor is malformed, or was issued for a different collection, scope or set of filters. |
unauthenticated |
401 | No valid credential. |
forbidden |
403 | The credential lacks a verb (details.verb), or a CSRF or origin check failed. |
not_found |
404 | The target does not exist or you cannot read it (details.kind, details.id). |
request_timeout |
408 | The request body did not arrive within server.request_timeout. |
already_exists |
409 | A key or other unique value is taken. |
conflict |
409 | The target's state refuses the operation; details.reason says why, such as archived, builtin, last_organization_admin or idempotency_key_reused. |
precondition_failed |
412 | If-Match is stale; details.current_etag has the current value. |
payload_too_large |
413 | A body or file exceeds its limit (details.limit). |
disabled |
422 | The target, or the workspace, is disabled or archived. |
precondition_required |
428 | The operation requires If-Match. |
rate_limited |
429 | Too many requests; retry after Retry-After seconds (details.retry_after_seconds). |
internal |
500 | An unexpected error; report the request_id. |
unavailable |
503 | A dependency (details.dependency: database, redis, objects, mail, a provider type, ...) cannot serve the request now. Retry later. |
A path no route answers, or a method a path does not accept, is not_found with {"kind": "route", "id": "<METHOD> <path>"}; a body that cannot be parsed as JSON is invalid_argument with {"field": "body", "reason": "unparsable"}.
Messages are for people; branch on code and details.reason. Error details never contain submitted values or secrets.
Concurrency control
Single-resource responses carry a strong ETag, such as "ap_…:4", and views carry the same version. Operations that change an existing resource require the ETag you last read in If-Match:
curl -X PATCH "$A13N_URL/api/v1/workspaces/$WORKSPACE/agents/$AGENT" \
-H "Authorization: Bearer $A13N_API_KEY" -H "Content-Type: application/json" \
-H 'If-Match: "ap_...:4"' -d '{"description": "Answers billing questions"}'
Without If-Match the request fails with 428 precondition_required; with a stale value, with 412 precondition_failed and the current ETag. Read the resource again, reapply your change and retry. The comparison is exact, so weak validators and * never match. The OpenAPI document marks If-Match as an optional header, but every operation that declares it requires it.
Inbox operations (edit, withdraw and reorder queued messages), environment mounts and thread updates take the thread's ETag. Operations that create things, and run commands such as interrupt, take none.
Idempotent requests
These operations require an Idempotency-Key header of 1–512 visible ASCII characters:
POST …/threads(start a thread with its first message) andPOST …/threads/{thread_id}/inbox(submit a message)POST …/runs/{run_id}/forkandPOST …/runs/{run_id}/resumePOST …/uploads
Generate a unique key per logical request and reuse it when retrying after a lost response. Repeating a request with the same key and the same body returns the original result with 200 instead of 201; the same key with a different body or target is 409 conflict with reason idempotency_key_reused. Keys are scoped to the caller and workspace and do not expire.
Paging
Collections return {"items": [...], "next_cursor": "..."}. Pass limit (1–100, default 50) and, for the next page, cursor=<next_cursor>; next_cursor is null on the last page. A cursor is opaque and bound to its collection, its scope and every filter of the query that produced it; reusing it with a different filter is invalid_cursor.
Limits
- Request bodies are bounded by
server.request_bytes(413) and must arrive withinserver.request_timeout(408). Uploads and images have smaller limits. - Password login and other credential checks, uploads and the authorization callback are rate limited (
429withRetry-After). A thread whose inbox is full also answers429.
Identifiers and time
IDs are opaque strings with a kind prefix, such as ws_, ap_ (agent), sess_, thread_, run_. Paths accept workspace, agent and skill keys in place of IDs. Timestamps are RFC 3339 with an offset.
Streams
GET …/threads/{thread_id}/stream is a server-sent event stream of a thread's live output and state changes. See the thread stream.