Skip to content

REST API

Base URL: your deployed worker, e.g. https://claudius-chat-worker.<you>.workers.dev. CORS restricts callers to the configured ALLOWED_ORIGIN list (plus http://localhost:*). Allowed methods: GET, POST, OPTIONS; allowed header: Content-Type.

Send the conversation so far; receive the assistant’s reply.

{
"messages": [
{ "role": "user", "content": "Hello" },
{ "role": "assistant", "content": "Hi there!" },
{ "role": "user", "content": "What are your hours?" }
],
"conversationId": "optional-opaque-id"
}
FieldTypeNotes
messagesarray, requiredFull conversation history, oldest first. Max 100 messages; each content is truncated to 2,000 characters
messages[].role"user" | "assistant"Other roles are rejected
messages[].contentstringMay be empty only when the message has attachments
messages[].attachmentsarray, optionalFiles on a user message; see below
conversationIdstring, optionalOpaque id used only for analytics correlation
citationsboolean, optionaltrue asks the worker to number its RAG excerpts to match sources and to have the model cite them as [n]. Anything else is ignored

Each entry in messages[].attachments is:

{
"id": "att-1",
"name": "receipt.png",
"mediaType": "image/png",
"size": 48213,
"data": "<base64, optional>",
"key": "att/<tenant>/<uuid> (optional)"
}
FieldNotes
idClient-generated, [A-Za-z0-9_-]{1,64}, unique per request. Names the multipart file part
nameFilename; shown to the model as the document title
mediaTypeMust be on the worker’s allowlist and match the file’s leading bytes
sizeBytes. Recomputed server-side whenever bytes are present
dataInline base64 (no data: prefix). Omit when sending multipart or referencing a stored file
keyStorage key returned by a previous response (R2 mode). The worker loads the file itself

An attachment with neither data nor key (or whose stored copy expired) is described to the model as “no longer available” rather than failing the request. Attachments are forwarded to Claude as image blocks (JPEG, PNG, GIF, WebP) or document blocks (PDF), placed before the message text.

To avoid base64 overhead, send multipart/form-data instead of JSON:

  • a payload text field containing the JSON body above, with attachment entries without data;
  • one file part per new upload, whose field name is the attachment id.
Terminal window
curl https://<worker>/api/chat \
-F 'payload={"messages":[{"role":"user","content":"What is the total?","attachments":[{"id":"f1","name":"receipt.png","mediaType":"image/png","size":48213}]}]}' \
-F 'f1=@receipt.png;type=image/png'

Stray file parts that match no attachment are rejected. The widget’s client switches to multipart automatically whenever a message carries inline bytes.

POST /api/chat/stream accepts the same JSON or multipart body.

{
"reply": "We're available Monday through Friday, 9am to 5pm.",
"sources": [
{
"url": "https://example.com/contact",
"title": "Contact",
"type": "page",
"snippet": "We're available Monday through Friday, 9am to 5pm."
}
],
"attachments": [
{
"id": "att-1",
"key": "att/example.com/6f1c…",
"url": "https://<worker>/api/attachments/att/example.com/6f1c…?exp=1750000000&sig=…",
"expiresAt": "2026-06-16T12:00:00.000Z"
}
]
}

sources is optional and present when RAG retrieved pages for the reply. Each source has url, title, type, and, from 1.18.0, a snippet of about 200 characters.

attachments is present only when the worker’s R2 storage backend stored new uploads during this request. Reference key on later turns instead of re-sending the bytes; url is an HMAC-signed download link valid until expiresAt.

All errors share one envelope:

{ "error": "Human-readable message", "code": "MACHINE_CODE", "limitType": "minute" }
StatuscodeWhenExtra
400VALIDATION_ERROREmpty/missing messages, more than 100 messages, invalid role, message with neither text nor attachments
400ATTACHMENTS_DISABLEDRequest carried attachments but ATTACHMENTS_ENABLED=false
400ATTACHMENT_INVALIDDisallowed or mismatched type, too many files, malformed id/key, or Claude could not process the file
413ATTACHMENT_TOO_LARGEFile over ATTACHMENT_MAX_BYTES, or this message’s uploads over ATTACHMENT_MAX_REQUEST_BYTES
413ATTACHMENT_QUOTA_EXCEEDEDDaily per-IP or per-tenant upload quota reachedRetry-After header (seconds to next UTC midnight)
429RATE_LIMITEDPer-IP limit exceeded (default 10/min, 50/hr)Retry-After header (seconds); limitType: "minute" or "hour"
500CONFIG_ERRORWorker misconfiguration (e.g. bad API key, R2 mode without bucket/secret)
503SERVICE_ERRORClaude temporarily unavailable/overloaded
500UNKNOWN_ERRORAnything else

Accepts the same JSON or multipart body as /api/chat and answers with text/event-stream. Failures before the first byte (validation, rate limit, attachments, a bad API key) return the same JSON errors as /api/chat, so clients can share their error handling. When the R2 backend stored uploads, the done event carries the same attachments array as the JSON response.

EventDataWhen
sources{ "sources": [...] }Once, before the first chunk, when retrieval found sources
chunk{ "text": "..." }One per model text delta
toola tool-use summaryOne per executed tool call
done{ "reply", "sources"?, "toolUses"?, "attachments"? }Last event: the full reply plus everything the JSON response would carry
error{ "error", "code": "STREAM_ERROR" }A failure after streaming began; the stream ends
event: sources
data: {"sources":[{"url":"https://example.com/contact","title":"Contact","type":"page","snippet":"We're available Monday through Friday."}]}
event: chunk
data: {"text":"We're available Monday through Friday [1]."}
event: done
data: {"reply":"We're available Monday through Friday [1].","sources":[...]}

sources on done is the authoritative list; the early event exists so a widget can render citation chips while the text is still arriving.

Serves a stored attachment (R2 mode only). The full URL, including the exp and sig query parameters, comes from a chat response’s attachments[].url; it cannot be constructed by hand.

StatusWhen
200Bytes with the original Content-Type, Content-Disposition: inline, and Cache-Control: private
403Signature invalid or link expired
404Unknown key, object expired/deleted, or storage is passthrough
{ "ok": true }

Use for uptime checks; it does not call Claude.

The widget’s built-in API client retries up to 2 times (3 attempts total): 429 waits for the server’s Retry-After; 503 backs off exponentially (1 s, then 3 s). Sends are debounced (300 ms default). If you build your own client, mirroring this behavior plays well with the worker’s limits.