Skip to main content

HTTP API

Every endpoint is under /api on the Worker. Responses are JSON unless stated. Request schemas are zod schemas in packages/shared/src/; a body that fails them is a 400.

MethodPathPurposeEMBED_KEY
GET/api/healthStore, models and gateway in useno
GET/api/claimsPaged claim summariesno
GET/api/claims/scenariosCounts by scenario, and the catalogueno
GET/api/claims/randomA random claim idno
GET/api/claims/:idOne full claimno
POST/api/chatOne assistant turn, streamed as SSEyes
GET/api/voiceWebSocket relay to Gemini Liveyes
GET/api/promptsThe prompt and capability cataloguesno
POST/api/feedbackRecord, replace or withdraw a voteyes
GET/api/feedbackThe feedback reportyes

Conventions​

Cross-origin access. Same-origin callers need nothing. A cross-origin caller must be in ALLOWED_ORIGINS; otherwise every endpoint answers 403 before doing any work. Preflights from allowed origins get 204. See Access control.

The key. When EMBED_KEY is set, the gated endpoints need the header x-claimpilot-key: <key>, or ?key=<key> on the voice socket. A missing or wrong key is 401 with {"error": "Not authorised for this assistant."}.

Errors.

StatusWhenBody
400A body or query that fails its schema{"error": "…", "issues": [ … ]} for chat and feedback; {"error": "<zod message>"} elsewhere
401Wrong or missing key{"error": "Not authorised for this assistant."}
403Origin not allowed{"error": "Origin <o> may not embed this assistant. Add it to ALLOWED_ORIGINS."}
404Unknown claim, unknown /api path{"error": "Claim not found"} · {"error": "Not found"}
500Misconfiguration or an unhandled error{"error": "<message>"}

Any path outside /api is served from the static assets, falling back to index.html.


GET /api/health​

{
"ok": true,
"database": "d1",
"ai": { "configured": true, "model": "gemini-3.5-flash-lite", "liveModel": "gemini-3.8-live", "thinkingLevel": "default", "gateway": "claimpilot-chat" }
}

database is d1, neon or none. gateway is the AI Gateway id, or null when model calls go straight to Google. See Observability.

GET /api/claims​

Paged claim summaries, ordered by id.

QueryTypeMeaning
lobstringLine of business
currencyGBP | USDThe claim's currency
scenariostringScenario id
statusstringClaim status
qstringCase-insensitive search on id, title and claimant name
limitinteger1 to 500, default 50
offsetintegerDefault 0
{
"items": [
{
"id": "CP-AUT-260381", "lineOfBusiness": "auto", "scenario": "auto_policy_lapsed_at_loss",
"title": "Priya Natarajan v Halden Logistics Ltd", "status": "under_review", "currency": "GBP",
"claimantName": "Priya Natarajan", "adjusterName": "Amira Hassan", "dateOfLoss": "2026-07-22",
"totalReserve": 27800, "totalPaid": 380, "riskFlagCount": 2
}
],
"total": 1
}

total counts every match, not just the page. With DATABASE_PROVIDER=none the list is always empty.

GET /api/claims/scenarios​

{
"loaded": [{ "lineOfBusiness": "auto", "scenario": "auto_policy_lapsed_at_loss", "count": 12 }],
"catalogue": [{ "id": "auto_policy_lapsed_at_loss", "lineOfBusiness": "auto", "title": "…", "brief": "…", "showcases": ["…"], "gotchas": ["…"], "hint": "…" }]
}

loaded is what the store holds; catalogue is every scenario brief.

GET /api/claims/random​

QueryMeaning
lobWithin one line of business
currencyWithin one currency
capabilityA claim whose demo notes showcase this capability id
{ "id": "CP-PRO-260214" }

id is null when nothing matches.

GET /api/claims/:id​

The full Claim document, parsed (every default applied), or 404.

POST /api/chat​

One assistant turn. The response is text/event-stream: one JSON ChatEvent per data: line.

Request
{
"claim": { "id": "CP-AUT-260381", "…": "…" },
"messages": [{ "role": "user", "content": "Was the policy in force at the date of loss?" }],
"capability": "policy_validation",
"locale": "en-GB",
"brand": "clonepilot",
"screenActions": true,
"promptOverrides": { "system.base": "…" }
}
FieldRequiredMeaning
claimone of claim or claimIdThe document itself, as the page shows it
claimIdone of claim or claimIdA claim to load from the store
messagesyes1 to 60 text turns, user or assistant, each at most 20,000 characters
capabilitynoRun this capability's playbook as a quick action
promptOverridesnoPrompt id → text for this turn. Unknown ids and values over 20,000 characters are dropped
localenoen-GB (default) or en-US
brandnoclaimpilot (default) or clonepilot
screenActionsnoThe caller can perform client_action events. Default false
Response (abridged)
data: {"type":"activity","tool":"check_policy_in_force","label":"Checking the policy period and coverages…"}

data: {"type":"tool_result","tool":"check_policy_in_force","summary":"{\"dateOfLoss\":\"2026-07-22\",…}"}

data: {"type":"text_delta","text":"Not safely. The loss on 22/07/2026 falls inside "}

data: {"type":"text_delta","text":"the policy period, but the policy is recorded as lapsed…"}

data: {"type":"done","model":"gemini-3.5-flash-lite","usage":{"inputTokens":5210,"outputTokens":412,"cacheReadTokens":3968},"turnId":"4f6b2c1e-…","prompts":{"fingerprint":"default","overridden":[]}}

Problems after the stream has started arrive as an error event, not an HTTP status. A malformed body is a 400 before any streaming. See The chat pipeline.

From a terminal
curl -N http://localhost:8787/api/chat \
-H 'content-type: application/json' \
-d '{"claimId":"CP-AUT-260381","messages":[{"role":"user","content":"Summarise this claim."}]}'

GET /api/voice​

A WebSocket upgrade. A plain GET is 426; no model key is 503.

QueryMeaning
localeen-GB or en-US
claimIdOptional, older path: load the claim from the store; 404 before the upgrade if it does not exist
keyThe embed key, when one is set

The first client frame may be {"clientConfig": {"promptOverrides", "claim", "screenActions", "brand"}}. After that, client frames are audio and text for Gemini Live; server frames are Live's own, plus {"chat": ChatEvent} for activity and screen actions and {"error": {"message"}}. See Voice.

GET /api/prompts​

{
"prompts": [{ "id": "system.base", "label": "System — identity & working style", "description": "…", "defaultText": "…" }],
"capabilities": [{ "id": "claim_summary", "label": "Summarise claim", "description": "…", "group": "understand", "pinned": true, "quickActionMessage": "…" }],
"groups": [{ "id": "understand", "label": "Understand the claim", "blurb": "Get up to speed fast." }]
}

Read-only: the code defaults. Versions live in the browser.

POST /api/feedback​

Record, replace or withdraw the vote on one answer. Body: FeedbackRequest.

{
"turnId": "4f6b2c1e-8a9d-4c3b-9e2f-1d7a5b0c6e93",
"rating": "down",
"reasons": ["missed"],
"note": "It ignored the lapse notice in the diary.",
"claimId": "CP-AUT-260381",
"capability": "policy_validation",
"channel": "text",
"surface": "app",
"locale": "en-GB",
"model": "gemini-3.5-flash-lite",
"promptFingerprint": "default",
"question": "Was the policy in force at the date of loss?",
"answer": "…",
"tools": [{ "tool": "check_policy_in_force", "summary": "…" }]
}
  • 204 on success.
  • rating: null withdraws the vote.
  • The same turnId again replaces the earlier vote.
  • reasons and note are kept only for a thumbs down.
  • An invalid body is 400 with {"error": "Invalid feedback", "issues": [ … ]}.

GET /api/feedback​

QueryMeaning
ratingup or down: filters items only
capabilityA capability id: filters items only
limit1 to 200, default 50
{
"totals": { "up": 41, "down": 9 },
"byCapability": [{ "capability": "policy_validation", "up": 12, "down": 3 }],
"byPrompt": [{ "capability": "policy_validation", "promptFingerprint": "default", "promptOverridden": [], "up": 10, "down": 3 }],
"items": [{ "turnId": "…", "rating": "down", "reasons": ["missed"], "…": "…", "createdAt": "2026-09-15T10:11:12Z", "updatedAt": "2026-09-15T10:11:12Z" }]
}

The totals and tallies cover every vote; the filters apply to items. With DATABASE_PROVIDER=none the report is empty and votes go to the Worker log.