Skip to content

Chatbot

The Chatbot API allows you to configure automated responses, keyword rules, conversation flows, and AI-powered responses.

Retrieve current chatbot settings.

Terminal window
GET /api/chatbot/settings

The envelope contains two keys — settings and stats.

{
"status": "success",
"data": {
"settings": {
"enabled": true,
"greeting_message": "Hello! Welcome to our support. How can I help you?",
"greeting_buttons": [
{"id": "btn_1", "title": "Track Order"},
{"id": "btn_2", "title": "Product Info"}
],
"fallback_message": "Sorry, I didn't understand that. Please try again.",
"fallback_buttons": [{"id": "btn_1", "title": "Main Menu"}],
"session_timeout_minutes": 30,
"business_hours_enabled": false,
"business_hours": [],
"out_of_hours_message": "",
"allow_automated_outside_hours": false,
"allow_agent_queue_pickup": true,
"assign_to_same_agent": true,
"agent_current_conversation_only": false,
"ai_enabled": true,
"ai_provider": "openai",
"ai_model": "gpt-4o-mini",
"ai_max_tokens": 500,
"ai_system_prompt": "You are a helpful customer service assistant...",
"sla_enabled": false,
"sla_response_minutes": 0,
"sla_resolution_minutes": 0,
"sla_escalation_minutes": 0,
"sla_auto_close_hours": 0,
"sla_auto_close_message": "",
"sla_warning_message": "",
"sla_escalation_notify_ids": [],
"client_reminder_enabled": false,
"client_reminder_minutes": 0,
"client_reminder_message": "",
"client_auto_close_minutes": 0,
"client_auto_close_message": ""
},
"stats": {
"total_sessions": 120,
"active_sessions": 4,
"messages_handled": 980,
"ai_responses": 210,
"agent_transfers": 33,
"keywords_count": 12,
"flows_count": 3,
"ai_contexts_count": 2
}
}
}

Both greeting_buttons and fallback_buttons are arrays of {"id": ..., "title": ...} objects rendered as WhatsApp interactive buttons:

Button Count Display Type
1-3 buttons Quick reply buttons
4-10 buttons List menu

title is the display text (max 20 characters). id is generated by the UI if you omit it.

Update chatbot settings. Every field is optional — only the keys present in the body are applied, so a partial update leaves the rest untouched. Each settings tab that is touched emits its own audit-log entry (settings.chatbot.messages, .agents, .hours, .sla, .ai).

Terminal window
PUT /api/chatbot/settings

Accepts the same field names as the settings object above, plus a write-only ai_api_key:

{
"enabled": true,
"ai_enabled": true,
"ai_provider": "anthropic",
"ai_model": "claude-sonnet-4-5",
"ai_api_key": "sk-ant-...",
"ai_max_tokens": 500,
"ai_system_prompt": "You are a helpful assistant for our e-commerce store...",
"greeting_message": "Welcome! How can I assist you today?",
"greeting_buttons": [
{"title": "My Orders"},
{"title": "Get Support"}
],
"fallback_message": "I'm not sure I understand. Please choose an option:",
"fallback_buttons": [
{"title": "Main Menu"}
]
}
Field Type Description
ai_provider string openai, anthropic, or google
ai_api_key string Write-only. Encrypted at rest with app.encryption_key; an empty string is ignored so the stored key is preserved.
Terminal window
GET /api/chatbot/keywords
{
"status": "success",
"data": {
"rules": [
{
"id": "uuid",
"name": "Greeting Response",
"keywords": ["hello", "hi", "hey"],
"match_type": "contains",
"response_type": "text",
"response_content": {"text": "Hello! How can I help you today?"},
"priority": 10,
"enabled": true,
"created_by_name": "Jane Admin",
"updated_by_name": "Jane Admin",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"total": 12,
"page": 1,
"limit": 50
}
}
Parameter Type Description
page integer Page number. Default 1.
limit integer Page size, 1–100. Default 50.
search string Case-insensitive match against the rule name or any of its keywords.

Rules are ordered by priority DESC, created_at DESC.

Terminal window
POST /api/chatbot/keywords
{
"name": "Business Hours",
"keywords": ["hours", "open", "when"],
"match_type": "contains",
"response_type": "text",
"response_content": {"text": "We're open Monday-Friday, 9 AM to 6 PM EST."},
"priority": 5,
"enabled": true
}
Field Type Required Description
keywords string[] Yes At least one keyword, else 400 At least one keyword is required.
name string No Defaults to the first keyword.
match_type string No Defaults to contains.
response_type string No Defaults to text.
response_content object Yes Free-form JSON whose shape depends on response_type.
priority integer No Higher wins when several rules match. Defaults to 0 on create; the column default is 10.
enabled boolean No Defaults to false on create — send true explicitly to activate the rule.
Type Description
exact Message must match keyword exactly
contains Message contains the keyword
starts_with Message starts with the keyword
regex Regular expression pattern match

text, template, media, flow, script, transfer.

Terminal window
GET /api/chatbot/keywords/{id}
Terminal window
PUT /api/chatbot/keywords/{id}
Terminal window
DELETE /api/chatbot/keywords/{id}

AI Contexts provide additional knowledge to the AI for specific topics.

Terminal window
GET /api/chatbot/ai-contexts
{
"status": "success",
"data": {
"contexts": [
{
"id": "uuid",
"name": "Product Catalog",
"trigger_keywords": ["product", "price", "buy"],
"context_type": "static",
"static_content": "Our products include...",
"api_config": null,
"priority": 10,
"enabled": true,
"created_by_name": "Jane Admin",
"updated_by_name": "Jane Admin",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}
],
"total": 2,
"page": 1,
"limit": 50
}
}
Parameter Type Description
page integer Page number. Default 1.
limit integer Page size, 1–100. Default 50.
search string Case-insensitive match against the name, static content, or trigger keywords.

Contexts are ordered by priority DESC, created_at DESC.

Terminal window
POST /api/chatbot/ai-contexts
{
"name": "Shipping Policy",
"trigger_keywords": ["shipping", "delivery", "track"],
"context_type": "static",
"static_content": "We offer free shipping on orders over $50. Standard delivery takes 3-5 business days...",
"priority": 5,
"enabled": true
}
Type Description
static Fixed text content
api Fetched from external API
Terminal window
GET /api/chatbot/ai-contexts/{id}
Terminal window
PUT /api/chatbot/ai-contexts/{id}
Terminal window
DELETE /api/chatbot/ai-contexts/{id}
Terminal window
GET /api/chatbot/flows

Returns {"flows": [...], "total", "page", "limit"}. Supports page, limit and search.

Terminal window
GET /api/chatbot/flows/{id}
Terminal window
POST /api/chatbot/flows

Flows are stored as a graph: nodes (each with an id, type, and type-specific config) connected by edges labelled with the outcome they handle. Execution starts at entry_node and walks edges until a node yields (waiting on user input) or terminates.

{
"name": "Feedback Collection",
"trigger_keywords": ["feedback", "review"],
"initial_message": "Hi! I'd like to collect your feedback.",
"completion_message": "Thank you for your feedback!",
"enabled": true,
"graph": {
"version": 2,
"entry_node": "rating",
"nodes": [
{
"id": "rating",
"type": "buttons",
"label": "Ask for rating",
"position": {"x": 0, "y": 0},
"config": {
"body": "How would you rate your experience?",
"buttons": [
{"id": "excellent", "title": "Excellent"},
{"id": "good", "title": "Good"},
{"id": "poor", "title": "Poor"}
]
}
},
{
"id": "comment",
"type": "prompt",
"label": "Collect comment",
"position": {"x": 250, "y": 0},
"config": {
"body": "Any additional comments?",
"store_as": "comment"
}
},
{
"id": "handoff",
"type": "transfer",
"label": "Transfer to team",
"position": {"x": 500, "y": 0},
"config": {
"body": "Connecting you with our team…",
"team_id": "<uuid>",
"notes": "Rating: {{rating}}"
}
}
],
"edges": [
{"from": "rating", "to": "comment", "condition": "button:excellent"},
{"from": "rating", "to": "comment", "condition": "button:good"},
{"from": "rating", "to": "handoff", "condition": "button:poor"},
{"from": "comment", "to": "handoff", "condition": "default"}
]
}
}

Every node has the shape { "id", "type", "label", "position", "config" }. The config schema depends on type.

Type Purpose Key config fields Outgoing edge conditions
start Entry sentinel. No side effect. — default
message Send a templated text message. message (or text) default
prompt Send a question; wait for and validate a reply. body, store_as, validation_regex, validation_error, max_retries (default 3) default, max_retries
buttons Interactive reply buttons. body, buttons: [{id,title}], store_as button:<id> per button
api_call HTTP request with response capture + optional templated reply. url, method, headers, body, response_mapping, message_template http:2xx, http:non2xx
condition Boolean expression branch (expr-lang syntax) over session data. expression true, false
timing Business-hours routing. schedule: [{day,enabled,start_time,end_time}] in_hours, out_of_hours
set_variable Assign session variables without messaging the user. set: {name: value} — string values are templated default
ai_response Ask the org’s configured LLM and send the answer. prompt_template (falls back to the user’s last message) default
whatsapp_flow Send a native WhatsApp Flow form. flow_id (the Meta flow ID), header, body, cta default
transfer Hand off to a team / queue and end the session. body, team_id, notes (terminal)
goto_flow Jump to another flow in the same org/account. flow_id (handled internally — the runner reloads the target graph)
webhook Fire-and-forget HTTP call; result ignored. url, method, headers, body default
end Optionally send a final message and terminate. message (terminal)

Wherever the table lists body, the runtime also accepts message or text as aliases. {{variable}} placeholders in messages, button titles, URLs and notes are interpolated from the session’s collected variables.

  • prompt — on invalid input it re-sends validation_error and waits again, until max_retries is reached; only then does it emit max_retries. An invalid validation_regex is logged and validation is skipped rather than failing the conversation.
  • api_call — network errors and non-2xx both emit http:non2xx. response_mapping maps variable names to dotted JSON paths ({"customer_id": "data.id"}) and merges the extracted values into session data. phone_number is always available as a variable (digits only, no leading +).
  • condition — unknown identifiers resolve to nil rather than erroring, and any compile or runtime failure is logged and routed as false.
  • timing — days absent from schedule count as out_of_hours. Evaluated in the server’s local time (set TZ).
  • ai_response — if AI is disabled, has no provider, or has no API key, the node logs a warning, sends nothing, and still advances via default. Route a fallback message there.
  • goto_flow — refuses to jump to a disabled flow, a flow on a different WhatsApp account, or one without a v2 graph; each case is logged and ends the flow gracefully. There is no return stack — when the target ends, the session ends.
  • transfer — a team_id of "" or "_general" (or an unparseable UUID) routes to the general queue. The transfer is recorded with source flow and the session is marked completed.

For a node N that produces outcome O, the runtime picks the first edge in edges where from == N.id && condition == O. If no exact match exists it falls back to a condition: "default" edge. With no match at all the session terminates as completed.

The full set of conditions the engine emits is: default, button:<id>, input:<val>, http:2xx, http:non2xx, validation_failed, max_retries, in_hours, out_of_hours, true, false.

A transfer node ends the flow and creates an agent transfer:

{
"id": "handoff",
"type": "transfer",
"config": {
"body": "Connecting you with our support team…",
"team_id": "<uuid>",
"notes": "From flow: {{variable_name}}"
}
}
Field Description
body Optional message sent to the user before handoff (templated).
team_id Target team UUID. Omit or set to "_general" for the shared queue.
notes Internal notes for agents (supports {{variable}} placeholders).

Configure which session variables are displayed in the Contact Info Panel:

{
"panel_config": {
"sections": [
{
"id": "section-1",
"label": "Customer Info",
"columns": 1,
"collapsible": true,
"default_collapsed": false,
"order": 1,
"fields": [
{"key": "customer_name", "label": "Name", "order": 1, "display_type": "text"},
{"key": "status", "label": "Status", "order": 2, "display_type": "badge", "color": "success"}
]
}
]
}
}
Field Type Description
id string Unique section identifier
label string Display label for the section
columns number Layout columns (1 or 2)
collapsible boolean Allow section to be collapsed
default_collapsed boolean Start section in collapsed state
order number Section display order
fields array Fields to display in this section
Field Type Description
key string Session variable name (from store_as or response mapping)
label string Display label for the field
order number Field display order within section
display_type string How to render the value: text (default), badge, or tag
color string Color for badge/tag: default, success, warning, error, or info
Field Type Description
name string Required — 400 Name is required otherwise.
description string Free text.
trigger_keywords string[] Keywords that start this flow for an inbound message.
initial_message string Sent when the flow starts.
completion_message string Sent when the flow completes.
on_complete_action string What to do after completion.
completion_config object Configuration for on_complete_action.
panel_config object Contact Info Panel layout — see Panel Configuration above.
graph object The v2 flow graph.
enabled boolean Whether the flow can be triggered.

Writing a flow requires flows.chatbot:write.

Terminal window
PUT /api/chatbot/flows/{id}
Terminal window
DELETE /api/chatbot/flows/{id}

Get agent transfer requests.

Terminal window
GET /api/chatbot/transfers
Parameter Type Description
status string Filter by status: active, resumed, or expired
team_id string Filter by team ID, or general for the general queue
limit number Page size, 1–100. Default 100.
offset number Rows to skip. Default 0.
include string Comma-separated relations to join: contact, agent, team, transferred_by, resumed_by. Defaults to all; narrowing it skips the joins and omits the corresponding *_name fields.

Ordering is FIFO (transferred_at ASC) except when status=resumed, which returns newest-resumed first.

{
"status": "success",
"data": {
"transfers": [
{
"id": "uuid",
"contact_id": "uuid",
"contact_name": "John Doe",
"phone_number": "1234567890",
"whatsapp_account": "15550001111",
"status": "active",
"source": "flow",
"agent_id": null,
"agent_name": null,
"team_id": "uuid",
"team_name": "Sales Team",
"transferred_by": "uuid",
"transferred_by_name": "Jane Admin",
"notes": "Interested in enterprise plan",
"transferred_at": "2024-01-01T12:00:00Z",
"resumed_at": null,
"resumed_by": null,
"resumed_by_name": null,
"sla_response_deadline": "2024-01-01T12:15:00Z",
"sla_resolution_deadline": "2024-01-01T13:00:00Z",
"sla_breached": false,
"sla_breached_at": null,
"escalation_level": 0,
"escalated_at": null,
"picked_up_at": null,
"expires_at": null
}
],
"general_queue_count": 3,
"team_queue_counts": {
"team-uuid-1": 5,
"team-uuid-2": 2
},
"total_count": 12,
"limit": 100,
"offset": 0
}
}

Nullable fields are omitted from the JSON when unset. general_queue_count and team_queue_counts count only unassigned active transfers; for callers without transfers:write, team counts are limited to teams they belong to.

Callers with transfers:write see every transfer in the org. Everyone else sees their own assigned transfers plus unassigned ones in the general queue and in their own teams’ queues.

Manually transfer a conversation to a human agent or team.

Terminal window
POST /api/chatbot/transfers
{
"contact_id": "uuid",
"whatsapp_account": "15550001111",
"agent_id": "uuid",
"team_id": "uuid",
"notes": "Customer requested human support",
"source": "manual"
}
Field Type Required Description
contact_id uuid Yes The contact to transfer
whatsapp_account string No Phone number ID the conversation belongs to
agent_id uuid No Assign directly to an agent instead of queueing
team_id uuid No Target team (omit for general queue)
notes string No Internal notes for agents
source string No manual, flow, keyword, or chatbot_disabled (the last two are set by the engine, not clients)

Returns 409 Contact already has an active transfer if the contact is already in the queue.

Pick the next unassigned transfer from the queue.

Terminal window
POST /api/chatbot/transfers/pick

Picks the oldest unassigned active transfer (FIFO) and assigns it to the calling user. Uses SELECT … FOR UPDATE SKIP LOCKED so concurrent pickers never get the same transfer.

Parameter Type Description
team_id string Pick from specific team, or general for the general (unassigned-team) queue only

Omitting team_id picks from the general queue plus any teams the caller belongs to. Callers with transfers:write can pick from any queue.

transfers:write grants full access. Otherwise the caller needs transfers:pickup and the org’s chatbot setting allow_agent_queue_pickup must be enabled, else 403 Queue pickup is not allowed. Requesting a team_id the caller is not a member of returns 403 You are not a member of this team.

Assign a transfer to a specific agent.

Terminal window
PUT /api/chatbot/transfers/{id}/assign
{
"agent_id": "uuid",
"team_id": "uuid"
}
Field Type Description
agent_id uuid | null Agent to assign to. Omitting the field (or sending null) means “assign to me” for callers without transfers:write; "" unassigns.
team_id uuid | "" Optional — move the transfer to a different team queue, or "" to move it to the general queue. Requires transfers:write.

Naming an explicit agent_id requires transfers:write (403 You don't have permission to assign transfers to others). The target agent must be available, otherwise 400 Agent is currently away. The transfer must be active, otherwise 400 Transfer is not active.

Resume chatbot after human agent completes interaction.

Terminal window
PUT /api/chatbot/transfers/{id}/resume

View chatbot sessions (for debugging).

Terminal window
GET /api/chatbot/sessions
Parameter Type Description
status string Filter by active, completed, cancelled, or timeout

Not paginated — returns the 100 most recently active sessions under a sessions key, each with its contact preloaded.

{
"status": "success",
"data": {
"sessions": [ /* session objects, see below */ ]
}
}

Get details of a specific session, including its full message history.

Terminal window
GET /api/chatbot/sessions/{id}

The session object is returned directly as data (no wrapper key).

{
"status": "success",
"data": {
"id": "uuid",
"organization_id": "uuid",
"contact_id": "uuid",
"whatsapp_account": "Main Account",
"phone_number": "15551234567",
"status": "active",
"current_flow_id": "uuid",
"current_step": "rating",
"step_retries": 0,
"session_data": {
"name": "John"
},
"started_at": "2024-01-01T12:00:00Z",
"last_activity_at": "2024-01-01T12:05:00Z",
"completed_at": null,
"contact": { },
"messages": [ ]
}
}