Query endpoints
Two endpoints, one request shape. Both need a scoped widget token (see Developer access and widget tokens), and both are stateless: each call is answered on its own, with no memory of the last one.
Ask a question
curl -X POST "$BASE_URL/kb-api/api/v1/kb/$KB_ID/widget/query" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"question": "What is the notice period for a fixed-term contract?"}'
Request
| Field | Type | Notes |
|---|---|---|
question | string | Required. The question, in plain language. The field is question, not query. |
top_k | integer | Accepted and ignored. |
options | object | Accepted and ignored. |
conversation_id | string | Accepted and ignored. These endpoints hold no conversation. |
Response
{
"answer": "A fixed-term contract requires 30 days' written notice.",
"citations": [
{ "index": 1, "doc_id": "doc_01J...", "kb_id": "kb_01J...", "source_span": { } }
],
"provenance": "generated",
"abstained": false
}
| Field | Type | Notes |
|---|---|---|
answer | string | The grounded answer. When abstained is true, this is the refusal message. |
citations | array | What the answer was drawn from. Empty when nothing was cited. |
citations[].index | integer | The marker used in answer. |
citations[].doc_id | string | The document the passage came from. |
citations[].kb_id | string | The knowledge base it belongs to. |
citations[].source_span | object | An opaque pointer to the passage. Pass it back to us when you report a problem; don't parse it. |
provenance | string | generated, abstained or qa_shortcut (answered from a curated question and answer pair). |
abstained | boolean | True when Elie declined to answer. Always present. |
Fields with no value are left out rather than sent as null.
Declining to answer is a success, not an error. It arrives as 200 with abstained: true,
and it means the knowledge base held no evidence good enough to answer from. Treat it as an answer
your interface should show, not as a failure to retry.
Stream the answer
curl -N -X POST "$BASE_URL/kb-api/api/v1/kb/$KB_ID/widget/query/stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"question": "What is the notice period?"}'
Same request body. The response is text/event-stream, and every frame carries a JSON object
whose type repeats the event name.
| Event | Payload | What to do with it |
|---|---|---|
queued | { "type": "queued", "stage": "..." } | The answer is waiting its turn. Show that you are working on it. |
heartbeat | { "type": "heartbeat", "stage": "..." } | Sent every 10 seconds or so to keep the connection open. Nothing to show. |
token | { "type": "token", "text": "..." } | Append text to what you are showing. |
citations | { "type": "citations", "citations": [ ... ] } | Show the sources. Same fields as above, plus text when the passage can be quoted. May arrive before the answer finishes. |
abstain | { "type": "abstain", "answer": "..." } | Replace everything shown so far with answer. |
reset | { "type": "reset" } | Discard everything shown so far and keep listening. |
suggestions | { "type": "suggestions", "questions": [ ... ] } | Optional follow-up questions to offer. |
done | { "type": "done", "abstained": false } | The answer is complete. Close the stream. |
error | { "type": "error", "code": "...", "message": "..." } | Writing the answer failed part-way. Show what you have, plus a failure notice. |
Handle reset and abstain, or you will show text Elie has withdrawn: both exist because the
service may retract a partial answer it no longer stands behind.
A refused request never becomes a stream. The token, the knowledge base, billing and the question
are all checked before the response opens, so a refusal is an ordinary status code with a JSON
body. That includes 503 dependency_unavailable with Retry-After when the service already has
too many answers streaming.
Next
- Errors - status codes and the error body.
- Embed the chat widget - the same endpoints, no code.