API Documentation
Use Denser.ai's API to query a chatbot programmatically, keep conversations per end user, and list them. Build custom chat UIs and connect your own apps.
Query a Denser chatbot from your own code, keep each of your users' conversations, and read them back. Before you start, generate an API key and copy your chatbot ID — see the REST API quick start.
| Endpoint | What it does |
|---|---|
POST /api/query | Ask the chatbot a question and get its answer, optionally as one of your users and within an ongoing conversation. |
POST /api/v2/conversations/api | List the chatbot's conversations, optionally only one user's, with their messages. |
Authentication
Each chatbot has its own API key, generated in its API channel (see the quick start). Regenerating the key immediately invalidates the old one.
/api/querytakes the key in thekeyfield of the JSON body./api/v2/conversations/apitakes the key in thex-api-keyheader.
Keep the key on your server
Call the API from your backend, never from a browser. The key gives access to the chatbot and its conversations, and the userId you send is trusted because the key vouches for it.
Both endpoints stop working when the chatbot's API channel is turned off. Every answer from /api/query uses one query from your workspace's plan.
Query a chatbot
Endpoint
POST https://denser.ai/api/query
Send the body as Content-Type: application/json.
Request body
Prop
Type
Choosing a model
model picks a model family, not an exact model. Each family runs one model, chosen by Denser:
| Value | Runs |
|---|---|
claude | Claude Sonnet 5 (Anthropic) |
gpt | GPT-5 (OpenAI) |
gemini | Gemini 2.5 Pro (Google) |
- Versioned names select their family, not that version.
gpt-4o-miniruns GPT-5, andclaude-haiku-4-5runs Claude Sonnet 5. They're accepted so existing code keeps working. - Recognized prefixes:
gpt,openai,chatgpt,o1,o3ando4select GPT;geminiandgoogleselect Gemini;claudeandanthropicselect Claude. Case doesn't matter. - Anything else runs Claude. Other providers, such as Mistral, Llama or DeepSeek, aren't supported, and an unrecognized value doesn't return an error.
- Leave
modelout to use the model set in the chatbot's settings.
The models behind each family are upgraded over time, so the same value can run a newer model later.
Example request
curl -X POST "https://denser.ai/api/query" \
-H "Content-Type: application/json" \
-d '{
"question": "What can you help me with?",
"chatbotId": "11111111-1111-1111-1111-111111111111",
"key": "00000000-0000-0000-0000-000000000000",
"userId": "user_8842"
}'Response
A successful request returns HTTP 200 with:
Prop
Type
{
"statusCode": "200",
"answer": "I can help you with questions about our products, orders and returns...",
"passages": [
{
"source": "https://example.com/help/returns",
"text": "Returns are accepted within 30 days of delivery...",
"score": 7.904721
}
],
"conversationId": "1791582688917-qzltde6lii",
"messageId": "1791582688915-hq4r1oxjrz"
}Streaming the answer
Send "stream": true to receive the answer word by word, as the chat widget shows it, instead of waiting for the whole response. The answer comes back as Server-Sent Events (Content-Type: text/event-stream), in this order:
| Event | data | When |
|---|---|---|
start | { conversationId, messageId } | Once, as the answer starts. |
delta | { text } | For each piece of the answer. Join them to build the answer as it arrives. |
done | The same body as a JSON response | Once, at the end. Its answer is the complete answer, with full citation links. |
error | { statusCode, message } | Instead of done, if the answer fails after it started. |
event: start
data: {"conversationId":"1791582688917-qzltde6lii","messageId":"1791582688915-hq4r1oxjrz"}
event: delta
data: {"text":"Returns are accepted "}
event: delta
data: {"text":"within 30 days [1]."}
event: done
data: {"statusCode":"200","answer":"Returns are accepted within 30 days [1](https://example.com/help/returns PassageID:0).","passages":[…],"conversationId":"1791582688917-qzltde6lii","messageId":"1791582688915-hq4r1oxjrz"}- Citations appear in
deltatext as[1], the same number as the link in thedoneanswer. - A request refused before the answer starts (invalid key, quota used up, conversation not found) is answered as JSON, exactly as without
stream. Check the response'sContent-Typebefore reading it as a stream. - A comment line,
: ping, is sent every 15 seconds while the answer is being prepared, so proxies keep the connection open. Ignore it. - Closing the connection stops the answer.
-N turns off curl's buffering. The jq filter prints only the answer's text, and prints any JSON error as it is:
curl -sN -X POST "https://denser.ai/api/query" \
-H "Content-Type: application/json" \
-d '{
"question": "What can you help me with?",
"chatbotId": "11111111-1111-1111-1111-111111111111",
"key": "00000000-0000-0000-0000-000000000000",
"userId": "user_8842",
"stream": true
}' \
| jq -R --unbuffered -j '
if startswith("data: ") then (.[6:] | fromjson | .text // empty)
elif startswith("event:") or . == "" then empty
else . end'; echoErrors
Some outcomes are reported in the body's statusCode with HTTP 200; others use the HTTP status.
| HTTP | statusCode / body | When |
|---|---|---|
| 200 | "404", answer "Chatbot not found" | The chatbot does not exist. |
| 200 | "404", answer "Conversation not found" | conversationId is not this chatbot's, or not the same userId's. |
| 200 | "429", answer "Quota exceeded" | The workspace has used all its queries. |
| 400 | message lists the invalid fields | A field is missing or invalid, such as a userId over 256 characters. |
| 401 | "Invalid API Key" | The key does not match the chatbot. |
| 403 | "The API channel is turned off for this chatbot" | The chatbot's API channel is turned off. |
| 500 | "Internal Server Error" | Anything else. |
With "stream": true, these are answered the same way, as JSON. A failure after the answer has started is an error event instead.
{
"statusCode": 400,
"error": "Bad Request",
"message": ["question must be a string"]
}End users and conversations
Pass userId to tie each question to one of your users, and conversationId to keep a conversation going. Both are optional: without them every call is a conversation of its own.
-
Ask, with your user's ID. The response's
conversationIdandmessageIdidentify the conversation and the answer.{ "chatbotId": "…", "key": "…", "question": "What is your refund policy?", "userId": "user_8842" } -
Follow up in the same conversation by sending its
conversationIdwith the sameuserId. Withoutcontext, the conversation's recent turns are the history, so a follow-up such as "Does that apply to sale items?" is understood.{ "chatbotId": "…", "key": "…", "question": "Does that apply to sale items?", "userId": "user_8842", "conversationId": "1791582688917-qzltde6lii" } -
Record feedback on your side, such as thumbs up or down, keyed to the answer's
messageId. -
List the user's conversations with List conversations and their
userId.
Conversations made through the API also appear in the chatbot's Chat logs, with the userId as the client ID.
List conversations
Endpoint
POST https://denser.ai/api/v2/conversations/api
Send the API key in the x-api-key header and the filters as a JSON body. Up to 60 requests a minute per key.
Request body
Prop
Type
Example request
curl -X POST "https://denser.ai/api/v2/conversations/api" \
-H "Content-Type: application/json" \
-H "x-api-key: 00000000-0000-0000-0000-000000000000" \
-d '{ "chatbotId": "11111111-1111-1111-1111-111111111111", "userId": "user_8842" }'Response
{
"success": true,
"data": {
"conversations": [
{
"id": "1791582688917-qzltde6lii",
"title": "",
"status": "active",
"clientType": "api",
"userId": "user_8842",
"currentAgent": "bot",
"createdAt": "2026-10-09T18:24:48.917Z",
"updatedAt": "2026-10-09T18:26:05.403Z",
"lastMessage": { "content": "…", "role": "assistant", "createdAt": "2026-10-09T18:26:07.120Z" },
"messages": [
{ "id": "1791582688917-hihu5fongia", "role": "user", "content": "What is your refund policy?", "createdAt": "2026-10-09T18:24:48.917Z" },
{ "id": "1791582688915-hq4r1oxjrz", "role": "assistant", "content": "…", "createdAt": "2026-10-09T18:24:52.310Z" }
]
}
],
"pagination": { "nextCursor": null, "hasMore": false, "count": 1, "limit": 20 }
}
}Each conversation has userId when one was sent (or the website widget's client ID), and each message carries userFeedback when a visitor rated it in the widget. When pagination.hasMore is true, pass pagination.nextCursor back as cursor for the next page.
Errors
Errors have success: false and an error.code:
| HTTP | error.code | When |
|---|---|---|
| 400 | — | A filter is invalid, such as a limit over 100. |
| 401 | INVALID_API_KEY | The key does not match the chatbot. |
| 401 | DATE_RANGE_EXCEEDED | createdAfter to createdBefore spans more than 90 days. |
| 401 | REQUEST_FAILED | The request could not be completed; error.message says why. |
| 403 | CHANNEL_DISABLED | The chatbot's API channel is turned off. |
| 429 | — | More than 60 requests in a minute. |