Skip to main content

Chat

The Chat API lets you programmatically converse with a published chatbot flow. This is useful for:

  • Building custom chat UIs outside the web widget
  • Automated testing of conversation flows
  • Integrating chatbot capabilities into your own applications

A conversation over the API is a sequence of request and response pairs on one session. The API never pushes messages: every bot message arrives in the response to a request you make.

  1. Create a session - you get back a session_id. The session is created empty; no bot messages are returned yet
  2. Send messages - pass the session_id with each message and receive the bot's reply synchronously. The reply to your first message is the flow's welcome
  3. Read each reply by its type - see Message shapes
Requires a published flow

The chatbot must have a published conversation flow. Use the Flows API to publish one.


Authentication​

Every request carries an API key. Keys always start with sk_live_.

Authorization: Bearer sk_live_...
StatusdetailWhen
401Authorization header missingNo Authorization header
401Invalid API key formatThe key does not start with sk_live_
401Invalid or expired API keyUnknown, revoked or expired key
401Workspace not found or inactiveThe key's workspace is gone or inactive
404Chatbot not foundThe chatbot does not exist, is inactive, or belongs to another workspace

All error bodies have the same shape: { "detail": "<message>" }. Request body validation errors (422) return detail as a list of field errors instead.

Rate limit​

ScopeLimitWhen exceeded
Per API key, shared by creating sessions and sending messages600 requests per 60 seconds429 with detail Too many requests. Please try again later. and a Retry-After header in seconds

The chat bucket is separate from the read and write limits that apply to the rest of the API. See Rate limits.


Create Chat Session​

Initializes a new conversation session. It returns only the session identifier. The bot does not reply until you send the first message, exactly like the web and playground channels.

POST /api/dev/v1/chatbots/{chatbot_id}/chat/sessions
Authorization: Bearer <api-key>
FieldWhereNotes
chatbot_idpathThe chatbot to talk to. It needs a published flow
curl -X POST https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions \
-H "Authorization: Bearer <your-api-key>"

Response - 201 Created

FieldTypeNotes
session_idstringULID of the conversation session
channelstringAlways "api"
created_atstringISO 8601 timestamp
{
"session_id": "01K5M2Q8Z7C3V9R1T4Y6W0B2NE",
"channel": "api",
"created_at": "2026-09-22T10:40:00+00:00"
}

Each session is a separate end user. The session starts empty: nothing runs until the first message. Send it to Send Message to start the flow.

Errors:

StatusdetailWhen
400No published flow found for this chatbot. Publish a flow first.The chatbot has no published flow
404Chatbot not foundSee Authentication

Send Message​

Sends a user message and returns the bot's reply synchronously.

POST /api/dev/v1/chatbots/{chatbot_id}/chat/sessions/{session_id}/messages
Authorization: Bearer <api-key>
Content-Type: application/json

Request Body

FieldTypeRequiredNotes
messagestringyes1 to 4096 characters. Line breaks are removed before the bot reads it
message_typestringno"text" (default) or "button"
button_idstring | nullnoThe id of the tapped button or list row. When present, the message is always treated as a tap

Response - 200 OK

FieldTypeNotes
session_idstring
messagesarrayBot messages, in the order to show them. See Message shapes
messages_countintegerNumber of entries in messages
current_statestring | nullThe flow state the conversation is now in
timestampstringISO 8601 timestamp

Send the first message​

Any text starts the conversation. The reply is the flow's welcome, and the text of this first message is not treated as an answer to anything.

curl -X POST https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions/<session-id>/messages \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"message": "hi"}'
{
"session_id": "01K5M2Q8Z7C3V9R1T4Y6W0B2NE",
"messages": [
{ "type": "TEXT", "text": "Lorem ipsum dolor sit amet, welcome!\nWhat would you like to do?" },
{
"type": "BUTTONS",
"text": "Choose an option.",
"buttons": [
{ "id": "view_products", "title": "Products" },
{ "id": "support_menu", "title": "Support" }
]
}
],
"messages_count": 2,
"current_state": "welcome",
"timestamp": "2026-09-22T10:40:02+00:00"
}

Send text​

Use for free text: a question, a name, a phone number, or a typed location.

{ "message": "Lorem Ipsum" }

Tap a button​

Send the id of the button the user tapped from the last BUTTONS message. message is still required: send the button's title. message_type can be set to "button" or left out, since button_id alone makes it a tap.

curl -X POST https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions/<session-id>/messages \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"message": "Products", "message_type": "button", "button_id": "view_products"}'
{
"session_id": "01K5M2Q8Z7C3V9R1T4Y6W0B2NE",
"messages": [
{
"type": "LIST",
"text": "Choose a product.",
"button_text": "Products",
"sections": [
{
"title": "Lorem products",
"rows": [
{ "id": "product_alpha", "title": "Product alpha" },
{ "id": "product_beta", "title": "Product beta" },
{ "id": "product_gamma", "title": "Product gamma" }
]
}
]
}
],
"messages_count": 1,
"current_state": "products_menu",
"timestamp": "2026-09-22T10:40:09+00:00"
}
Always send button_id for a tap

Typing a button's title as plain text is not guaranteed to select it.

Choose a list row​

Identical to a button tap, with the row's id as button_id.

{
"message": "Product alpha",
"message_type": "button",
"button_id": "product_alpha"
}

Answer the other message types​

Bot sentHow to answer over the API
LOCATION_REQUESTSend the location as text. The API cannot send a shared map location
CTA_URLNothing to send. The button only opens its link
FLOWNot answerable over the API. The request has no field for a completed WhatsApp Flow form
IMAGE, VIDEO, AUDIO, DOCUMENT, TEXTContinue with the next text or tap

The API also cannot send media from the user: the request has only message, message_type and button_id.

Errors:

StatusdetailWhen
404Chat session not foundUnknown session_id, a session from another chatbot, or a session not created through the API
410Chat session has been closedSee Session Lifecycle
422a list of field errorsmessage missing, empty, or over 4096 characters
500No flow configuredThe chatbot has no flow and none could be created
500Chatbot not foundThe chatbot was deactivated between the checks. Rare
500plain text Internal Server Error, not JSONAn unexpected crash during the turn
200 with the fallback's messagesMost failures inside a turn, such as a failed integration call, are handled by the flow's fallback state and come back as a normal reply
200 with empty messagesThe workspace reached its monthly message limit. The API returns messages: [], messages_count: 0, current_state: null and an empty timestamp, and does not say why. Treat an empty reply as a possible limit and check the plan

Session Lifecycle​

  • Creation: Each POST /sessions initializes a new isolated, empty session with its own state. No messages are returned until you send the first message.
  • Expiry: Sessions expire after 30 minutes of inactivity. After expiry, create a new session; the bot starts again from its initial state on the next message.
SituationWhat happens
Normal turnThe bot answers and the session moves to the state named in current_state
The flow reaches a final stateThe conversation starts over: the next message gets the welcome again. The session_id stays valid
No message for 30 minutesThe session's place in the flow is forgotten. The next message gets the welcome again, on the same session_id
The AI assistant hands the user to a personThe bot's reply arrives as normal, but no one on your team is alerted through the API, and a person cannot reply into an API session because the API never pushes messages
Session closed410 with detail Chat session has been closed. Nothing closes an API session today, so this is reserved
  • Channel isolation: Chat API sessions use the api channel. They are completely isolated from the same chatbot's whatsapp, web, or sms conversations.
  • Conversation history: All sessions and messages are persisted and can be retrieved via the Conversations API using channel=api.

Message shapes​

Read each message by its type. A field that does not apply is left out, never sent as null. Messages are never combined on the API: each one arrives exactly as the flow or the AI assistant produced it.

typeFieldsProduced by
TEXTtext, preview_url, suggestionsFlow messages, AI replies, validation errors, fallbacks
BUTTONStext, buttons, header, footerFlow buttons, AI replies
LISTtext, button_text, sections, header, footerFlow lists, AI replies
IMAGE, VIDEO, AUDIOmedia_url, media_caption, media_filenameFlow media, AI replies, the AI assistant's send-image tool
DOCUMENTmedia_url, media_filename, media_captionFlow media, or a file link whose type cannot be detected
CTA_URLtext, cta_url_display_text, cta_url, header, footerFlow link buttons, AI replies
LOCATION_REQUESTtextFlow location request
TEMPLATEtemplate_name, template_language, template_parametersFlow template send
FLOWtext, flow_id, flow_cta, flow_action, flow_action_payload, flow_token_extra, flow_mode, header, footerFlow sending a WhatsApp Flow form

REACTION exists in the engine but never reaches the API. A reaction needs a WhatsApp message to react to, so the API receives a short TEXT acknowledgement instead.

TEXT​

FieldTypeRequiredNotes
textstringyes
preview_urlbooleannoPresent only when true
suggestionsstring[]noRare on the API. Short follow-up topics, attached to the last text of an AI reply when the model includes them. At most 3
{ "type": "TEXT", "text": "Lorem ipsum dolor sit amet, welcome!" }

BUTTONS​

FieldTypeRequiredNotes
textstringyesBody shown above the buttons
buttons{ id, title }[]yesReply with the tapped id as button_id
headerstringnoText header
footerstringno

BUTTONS never carries media_url on the API.

{
"type": "BUTTONS",
"text": "Choose a payment option.",
"buttons": [
{ "id": "pay_deposit", "title": "Deposit" },
{ "id": "pay_full", "title": "Pay full" },
{ "id": "back_products", "title": "Back" }
]
}

LIST​

FieldTypeRequiredNotes
textstringyes
button_textstringyesLabel of the button that opens the list
sections{ title?, rows: { id, title, description? }[] }[]yesReply with the chosen row's id as button_id
headerstringno
footerstringno
{
"type": "LIST",
"text": "Choose a product.",
"button_text": "Products",
"sections": [
{
"title": "Lorem products",
"rows": [
{ "id": "product_alpha", "title": "Product alpha" },
{ "id": "product_beta", "title": "Product beta", "description": "Lorem ipsum dolor sit amet" }
]
}
]
}

IMAGE, VIDEO, AUDIO, DOCUMENT​

FieldTypeRequiredNotes
media_urlstringyesPublic link to the file
media_captionstringnoLeft out when there is no caption
media_filenamestringnoSet for documents, and for other media only when the flow sets a file name
{
"type": "IMAGE",
"media_url": "https://example.com/media/lorem.png",
"media_caption": "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
}

Caption rules:

  • A caption written in the flow arrives in full. A flow whose caption is over 1,024 characters fails validation and cannot be published.
  • Captions in AI replies and structured responses are cut at 1,024 characters.
  • A variable in the caption with no value becomes empty text: Hi {{variables.name}}! arrives as Hi !.
  • The media type comes from the link, not from the flow's media_type: first the file extension (.jpg, .jpeg, .png, .gif, .webp are images), then the file server's content type. If neither works, the message arrives as DOCUMENT and the caption is also used as the file name.
  • In AI replies, a link that is not an image arrives as a CTA_URL whose text is the caption, and a caption with no link arrives as a TEXT.

CTA_URL​

FieldTypeRequiredNotes
textstringyesBody
cta_url_display_textstringyesButton label
cta_urlstringyesLink the button opens
headerstringno
footerstringno
{
"type": "CTA_URL",
"text": "Track your order online.",
"cta_url_display_text": "Open tracking",
"cta_url": "https://example.com/track"
}

LOCATION_REQUEST​

FieldTypeRequiredNotes
textstringyesPrompt asking the user to share a location
{
"type": "LOCATION_REQUEST",
"text": "Please share your location or type where you are."
}

TEMPLATE​

FieldTypeRequiredNotes
template_namestringyesWhatsApp template name
template_languagestringyesLanguage code, for example en
template_parametersobjectnoValues per template component
{ "type": "TEMPLATE", "template_name": "order_update", "template_language": "en" }

FLOW​

FieldTypeRequiredNotes
textstringyesBody shown with the form button
flow_idstringyesWhatsApp Flow id
flow_ctastringyesButton label that opens the form
flow_actionstringnoFor example navigate
flow_action_payloadobjectnoFor example the first screen
flow_token_extraobjectnoExtra keys the flow author added to the form token
flow_modestringnopublished or draft
headerstringno
footerstringno
{
"type": "FLOW",
"text": "Book your appointment.",
"flow_id": "1234567890",
"flow_cta": "Book now",
"flow_action": "navigate",
"flow_action_payload": { "screen": "BOOKING" }
}

History of combined messages​

Until 2026-09-21, the API combined an image and the buttons after it into one BUTTONS message carrying media_url, with the caption placed above the buttons text. Until 2026-09-22, AI replies also folded text into the message after it. Neither happens on the API any more.

History rows recorded before those dates are returned as follows:

RecordedHow it comes back
Before 2026-09-21: image combined with buttonsSplit into an IMAGE with media_url only, then the BUTTONS. The buttons text still begins with the old caption, then a blank line
Before 2026-09-22: an AI replyAs recorded. Messages combined at the time stay combined

Full Example​

A complete conversation using curl:

# 1. Create a session
SESSION=$(curl -s -X POST \
https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions \
-H "Authorization: Bearer <your-api-key>" | jq -r '.session_id')

echo "Session: $SESSION"

# 2. Send the first message - the reply is the flow's welcome
curl -s -X POST \
"https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions/$SESSION/messages" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"message": "hi"}' | jq

# 3. Tap a button from the welcome
curl -s -X POST \
"https://developers.sarufi.io/api/dev/v1/chatbots/<chatbot-id>/chat/sessions/$SESSION/messages" \
-H "Authorization: Bearer <your-api-key>" \
-H "Content-Type: application/json" \
-d '{"message": "Products", "message_type": "button", "button_id": "view_products"}' | jq

Python example​

import requests

BASE = "https://developers.sarufi.io/api/dev/v1"
HEADERS = {"Authorization": "Bearer <your-api-key>"}
CHATBOT_ID = "<chatbot-id>"

# Create session (returns session_id only)
resp = requests.post(f"{BASE}/chatbots/{CHATBOT_ID}/chat/sessions", headers=HEADERS)
session_id = resp.json()["session_id"]

# Send the user's first message - the reply is the flow's welcome
resp = requests.post(
f"{BASE}/chatbots/{CHATBOT_ID}/chat/sessions/{session_id}/messages",
headers=HEADERS,
json={"message": "hi"},
)
for msg in resp.json()["messages"]:
if msg["type"] == "TEXT":
print("Bot:", msg["text"])
elif msg["type"] == "BUTTONS":
print("Bot:", msg["text"], [b["id"] for b in msg["buttons"]])
else:
print("Bot:", msg["type"], msg)