HTTP API
Every public endpoint on a Deskbell instance, with request and response fields rendered from the same schemas the server validates with, the guard in front of each, and the event format of the answer stream.
The public API lives under /api/v1 on your instance. It is what the widget
and the SDK call, it reads no cookies, and every route accepts OPTIONS for
preflight. Bodies are JSON. The field tables on this page are generated from
the schemas the server validates with; the same contract is available as an
OpenAPI document for client generators.
Guards
Every request passes the same checks in order before the handler runs. Security explains each; the codes are:
| Code | Cause | Body |
|---|---|---|
403 | The User-Agent looks like a crawler or an HTTP library (curl/, python-requests, axios/, HeadlessChrome…) | {"error": …} |
404 | Unknown project key | {"error": …} |
403 | The request's Origin is not on the project's allowlist | {"error": "Origin not allowed", "reason": …} |
429 | A per-IP or per-project rate limit, the daily token budget, or the per-IP stream cap | {"error": "Rate limited", "retryAfter": seconds}, with Retry-After |
401 | The project requires a visitor token and none was sent, or it failed verification | {"error": "Unauthorized", "reason": "missing" | "invalid"} |
POST routes require an Origin header. A signed-in visitor sends
Authorization: Bearer <token> from the project's identity provider;
Visitor identity says what is verified.
Ask
The one call that spends model tokens. Runs every guard, plus the daily token budget and the per-address stream cap.
POST /api/v1/ask
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
conversationId optional | string | Continue this conversation. Omit to start a new one. Must belong to the caller, or the response is 404. |
message | string | The visitor's question, up to 4000 characters. up to 4000 chars. |
locale optional | string | BCP 47 tag of the visitor's UI language. Retrieval is cross-language. up to 35 chars. |
context optional | object | Where on your site the conversation starts, as identifiers keyed by name. Stored on a new conversation only and ignored when continuing one. |
approval optional | object | The visitor's answer to a confirm event: the call id and whether to go ahead. Continues the turn that paused in conversationId; message may be empty. A call that is not pending there, or has expired, is 404. |
Response 200 text/event-stream
An event stream. Each `data:` line is one JSON event.
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
The response is text/event-stream. Each event is one data: line holding a
JSON object; lines beginning with : are heartbeat comments and carry
nothing. The stream opens with a comment at once so the client knows it is
connected, and sends another after every quiet interval so a proxy does not
close it mid-answer.
| Event | Fields | When |
|---|---|---|
token | value | Once per streamed piece of the answer |
citations | citations[]: documentId, title, snippet, url?, kind? | After the answer, only the sources the model used |
refusal | reason: no_context, no_grounding, off_topic, guardrail | Instead of citations, when there is no grounded answer |
clarify | question, options[] (page titles) | Instead of an answer, when the pages answer the question in more than one way and DESKBELL_CLARIFY=on; send an option back as the next message. See Clarifying questions |
action | id, title, status: running, succeeded, failed | A tool call on the visitor's behalf started or ended (Actions) |
confirm | id, title, summary, details, input, expiresAt | The turn paused on a tool that needs the visitor's confirmation; details is the argument list the card shows (label, value pairs); done follows at once |
done | conversationId, messageId | Last. messageId is what feedback refers to |
kind names the source type of a citation (docs-crawler, corrections for
a written answer, action for a tool result). A citation without url is a
written answer or a tool result.
To answer a confirm event, call /api/v1/ask again with the same
conversationId, an empty message and approval: { callId, approved }.
The rest of the answer streams as usual. See Confirmation.
Config
The project's widget appearance as saved on the Appearance page, plus
poweredBy, which the server decides. Cached for 60 seconds
(Cache-Control: public, max-age=60, stale-while-revalidate=300).
GET /api/v1/config
Query
| Field | Type | Description |
|---|---|---|
key | string | The project's public key, as printed in the embed snippet. Public by design. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
poweredBy | boolean | Whether the Powered by Deskbell badge renders. |
Errors
403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Conversations
All five take projectKey, sessionId and, where named, conversationId,
and answer 404 for a conversation the caller does not own.
List
POST /api/v1/conversations
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
scope optional | object | Keep only conversations whose stored context carries this key with this id. |
limit optional | integer | How many to return, 1 to 50. Default 20. 1 to 50. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
conversations | object[] |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Messages
POST /api/v1/conversations/messages
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
conversationId | string | The conversation to act on. Must belong to the caller, or the response is 404. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
messages | object[] |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Rename
POST /api/v1/conversations/rename
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
conversationId | string | The conversation to act on. Must belong to the caller, or the response is 404. |
title | string | The new title, 1 to 120 characters, trimmed. 1 to 120 chars. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
ok | true | |
title | string |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Delete
POST /api/v1/conversations/delete
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
conversationId | string | The conversation to act on. Must belong to the caller, or the response is 404. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
ok | true |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Preview
For support staff: any conversation of the project, for a bearer token that carries the project's configured staff claim. See Staff preview.
POST /api/v1/conversations/preview
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | An identifier the client generated and keeps (the widget stores it in localStorage). A conversation belongs to the session that created it. 1 to 128 chars. |
conversationId | string | Any conversation of the project. Requires a bearer token carrying the project's staff claim. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
conversation | object | |
endUser | object | null | The signed-in visitor the conversation belongs to, when it has one. |
messages | object[] |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Feedback
Upserts the vote on one assistant message. A down vote is what
notifications fire on and what the
Gaps page counts.
POST /api/v1/feedback
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
sessionId | string | Must match the conversation's session: possession is the authorization. 1 to 128 chars. |
messageId | string | The assistant message being rated, from the ask stream's done event. |
feedback | "up" | "down" | null | The vote. null clears a previous one. |
reason optional | string | null | A downvote reason id: one the project configured, or the built-in incorrect, irrelevant or unaddressed. Kept only with down. |
comment optional | string | null | Free text with a downvote, up to 1000 characters. Kept only with down. up to 1000 chars. |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
ok | true |
Errors
400The body failed validation.401The project requires a visitor token and none was sent, or it failed verification.reasonismissingorinvalid.403The User-Agent looks like a crawler or an HTTP library, or the Origin is not on the project's allowlist.404Unknown project key, or a conversation the caller does not own.429A rate limit, the project's daily token budget, or the per-address stream cap. Retry-After says when.
Ping
The install beacon. Diagnostic only: it records where the widget is live, including hosts not on the allowlist, and grants nothing.
POST /api/v1/ping
Request body
| Field | Type | Description |
|---|---|---|
projectKey | string | The project's public key, as printed in the embed snippet. Public by design. |
Response 204
Recorded.
Errors
400The body failed validation.403The User-Agent looks like a crawler or an HTTP library.404Unknown project key.429Rate limited.
Other routes
| Route | What it is |
|---|---|
GET /deskbell.js | The widget loader, built into the image. 503 until the widget has been built |
GET /api/health | {"ok": true, "service": "deskbell-web"}. What the Compose health check and a load balancer poll |
GET /gaps/export?format=csv|dataset | A dashboard download, session-gated. The dataset format loads into the eval harness |
GET /docs/openapi.json | This contract as OpenAPI 3.1, served by the documentation site |
The dashboard's own pages and /api/auth/* belong to the operator side and
use cookie sessions; they are not part of this API.
Overview
The HTTP API the widget runs on, the typed SDK that wraps it, and where to start when you want to change Deskbell itself or need to know what licence a piece of it carries.
SDK
@deskbell/sdk is the typed client for the public Deskbell API: streamed answers, conversation history and feedback. The widget runs on it, and so can a UI of your own.