Chatbot API Flow
This guide explains how the public chatbot API fits together from an integrator's point of view. It focuses on the flow and data model so you can build a client without guessing how responses should be interpreted.
For the full schema, field-level validation, and complete endpoint reference, see the API reference.
Flow at a glance
- Your backend calls
POST /api/v1/chat/authwith the chatbot API key and achatbot_id. - The API returns a visitor Bearer token.
- Your client calls
GET /api/v1/chat/configto load branding and startup configuration. - Your client sends user input to
POST /api/v1/chat/messagesand receives a streamed response. - The stream emits structured content as
part_deltaandpartevents, then ends withdoneorerror. - If the assistant returns an actionable marker, your client collects the required data and sends
it to
POST /api/v1/chat/actions. - You can optionally read message history, save a conversation rating, or delete the visitor's data.
Auth and config
The API has two authentication layers:
| Step | Who calls it | Purpose |
|---|---|---|
POST /api/v1/chat/auth | Your backend | Exchange the API key for a visitor token |
| Visitor-scoped endpoints | Your client or trusted backend | Use the returned Bearer token for config, messages, actions, history, rating, and deletion |
Keep the API key on your server. Do not expose it in browser code, mobile apps, or public frontend bundles.
/auth uses HTTP Basic Auth with:
- the API key as the username
- an empty password
When the token expires, call /auth again to create a new visitor token. There is no refresh
token flow.
Example: create a visitor token
curl -X POST "https://api.dialogintelligens.dk/api/v1/chat/auth" \
-u "$CHATBOT_API_KEY:" \
-H "Content-Type: application/json" \
-d '{
"chatbot_id": "shop-bot"
}'The response contains a JWT token:
{
"token": "eyJhbGciOiJIUzI1NiIs..."
}After that, your client can load config with the Bearer token. GET /api/v1/chat/config
returns the public UI configuration for the chatbot, including:
- name
- avatar
- welcome message
- theme colors
- popup messages
How message data is structured
Every conversation item is a message. A message contains ordered parts.
For user input, message.parts is how you send text and attachments in one request.
For assistant output, message.parts is also how you render the response. Parts are already
structured for you, so the client should render them directly instead of trying to parse raw text.
Mental model
| Term | Meaning |
|---|---|
message | One user, assistant, or system entry in the conversation |
part | A top-level renderable unit inside a message |
block | A section inside a rich_text part or table cell |
span | Inline formatted text inside a paragraph or bullet item |
message
`-- parts[]
|-- rich_text
| `-- blocks[]
| |-- paragraph
| | `-- spans[] -> text | bold | strike | link
| `-- bullet_list
|-- image
|-- table
|-- products
`-- show_contact_formParts
Common assistant part types are:
rich_textfor formatted textimagefor image contenttablefor structured tablesproductsfor product cards- marker parts such as
show_contact_formorrequest_image_upload
The important detail is that all of these are siblings in the same parts array. That means a
single assistant message can look like:
- Some text
- A marker telling the client to open a form
- More text after the marker
Blocks and spans
Inside a rich_text part:
blocksdescribe larger sections such as paragraphs and bullet listsspansdescribe inline formatting inside those blocks, such as plain text, bold text, struck text, and links
This lets a client render formatted content without having to parse markdown or custom marker syntax.
Example: a message with text and an action
{
"message_id": "msg_123",
"role": "assistant",
"parts": [
{
"type": "rich_text",
"part_id": "part_1",
"blocks": [
{
"type": "paragraph",
"spans": [{ "type": "text", "text": "Need help with your order?" }]
}
]
},
{
"type": "show_contact_form",
"part_id": "part_2",
"fields": [
{ "key": "name", "label": "Your name", "required": true },
{ "key": "email", "label": "Email address", "type": "email", "required": true }
]
},
{
"type": "rich_text",
"part_id": "part_3",
"blocks": [
{
"type": "paragraph",
"spans": [{ "type": "text", "text": "Fill in the form and we will follow up." }]
}
]
}
]
}In this example, the form is not separate from the message. It is one ordered part of the message.
How streaming works
POST /api/v1/chat/messages returns a Server-Sent Events stream. Because this is a POST
endpoint, the browser's built-in EventSource API is not a good fit. Use fetch() directly or a
helper such as @microsoft/fetch-event-source.
The stream uses five public event types:
| Event | Meaning |
|---|---|
status | Lifecycle updates such as connected or processing |
part_delta | Incremental updates for a streamed part |
part | A finalized structured part |
done | The final assembled assistant message |
error | A terminal stream error |
Typical flow:
status(connected)
status(processing)
part_delta / part ...
done or errorpart_delta is useful when you want progressive rendering for content such as:
- rich text
- products
- tables
part gives you a finalized structured part. Marker parts typically arrive this way because they
do not need progressive updates.
done.message is the final source of truth for the assistant response. If you rendered draft
content while streaming, replace or reconcile it with the message from done.
Example: consume the message stream
import { fetchEventSource } from "@microsoft/fetch-event-source";
await fetchEventSource("https://api.dialogintelligens.dk/api/v1/chat/messages", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: {
parts: [{ type: "text", text: "What is your return policy?" }],
},
context: {
page: window.location.href,
},
}),
onmessage(event) {
const data = JSON.parse(event.data);
switch (event.event) {
case "status":
updateStatus(data.status);
break;
case "part_delta":
applyPartDelta(data.part_id, data.delta);
break;
case "part":
renderFinalPart(data.part);
break;
case "done":
replaceDraftMessage(data.message);
break;
case "error":
showError(data.message);
break;
}
},
});How markers and actions work together
Markers are assistant parts that tell the client to do something beyond rendering text.
The key link is part_id:
- the assistant sends a marker part with a
part_id - your client renders the related UI
- when the user completes the action, your client sends that same
part_idto/actions
Marker behavior
| Marker part | What the client does | Follow-up |
|---|---|---|
show_contact_form | Render the fields from the marker | Submit action.type = "contact_form" to /actions |
show_support_ticket | Render the ticket form and attachment rules from the marker | Submit action.type = "support_ticket" to /actions |
request_image_upload | Prompt the user to upload an image that matches the marker constraints | Send a later /messages request with an image part |
request_human_agent | Show a handoff option in your UI | Client-defined flow |
custom | Handle your own key and optional payload | Client-defined flow |
Only show_contact_form and show_support_ticket are submitted to POST /api/v1/chat/actions.
Example: submit an action
{
"part_id": "part_2",
"action": {
"type": "contact_form",
"fields": {
"name": "Jane Doe",
"email": "jane@example.com",
"message": "I need help with my order"
}
}
}The same pattern applies to support_ticket, but with the support ticket payload shape defined in
the API reference.
History, feedback, and deletion
Once a visitor is authenticated, you can also use:
GET /api/v1/chat/messagesto load paginated message historyPOST /api/v1/chat/rateto save a1-5conversation rating and optional feedbackDELETE /api/v1/chatto remove all data for the current visitor
These endpoints use the same Bearer token as config, messages, and actions.