Skip to Content
DenserAIDenserAI Docs
API

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.

EndpointWhat it does
POST /api/queryAsk the chatbot a question and get its answer, optionally as one of your users and within an ongoing conversation.
POST /api/v2/conversations/apiList 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/query takes the key in the key field of the JSON body.
  • /api/v2/conversations/api takes the key in the x-api-key header.

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:

ValueRuns
claudeClaude Sonnet 5 (Anthropic)
gptGPT-5 (OpenAI)
geminiGemini 2.5 Pro (Google)
  • Versioned names select their family, not that version. gpt-4o-mini runs GPT-5, and claude-haiku-4-5 runs Claude Sonnet 5. They're accepted so existing code keeps working.
  • Recognized prefixes: gpt, openai, chatgpt, o1, o3 and o4 select GPT; gemini and google select Gemini; claude and anthropic select 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 model out 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:

EventdataWhen
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.
doneThe same body as a JSON responseOnce, 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 delta text as [1], the same number as the link in the done answer.
  • 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's Content-Type before 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'; echo

Errors

Some outcomes are reported in the body's statusCode with HTTP 200; others use the HTTP status.

HTTPstatusCode / bodyWhen
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.
400message lists the invalid fieldsA 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.

  1. Ask, with your user's ID. The response's conversationId and messageId identify the conversation and the answer.

    { "chatbotId": "…", "key": "…", "question": "What is your refund policy?", "userId": "user_8842" }
  2. Follow up in the same conversation by sending its conversationId with the same userId. Without context, 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" }
  3. Record feedback on your side, such as thumbs up or down, keyed to the answer's messageId.

  4. 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:

HTTPerror.codeWhen
400—A filter is invalid, such as a limit over 100.
401INVALID_API_KEYThe key does not match the chatbot.
401DATE_RANGE_EXCEEDEDcreatedAfter to createdBefore spans more than 90 days.
401REQUEST_FAILEDThe request could not be completed; error.message says why.
403CHANNEL_DISABLEDThe chatbot's API channel is turned off.
429—More than 60 requests in a minute.