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

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. Attachment errors are returned as plain JSON before the stream opens, and when the R2 backend stored uploads the done event carries the same attachments array as the non-streaming response below.

{
"reply": "We're available Monday through Friday, 9am to 5pm.",
"sources": [
{ "url": "https://example.com/contact", "title": "Contact", "type": "page" }
],
"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 reserved for retrieval-backed backends — the bundled worker returns only reply today (see RAG).

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

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.