Chatbot
Overview
Section titled “Overview”The Chatbot API allows you to configure automated responses, keyword rules, conversation flows, and AI-powered responses.
Chatbot Settings
Section titled “Chatbot Settings”Get Settings
Section titled “Get Settings”Retrieve current chatbot settings.
GET /api/chatbot/settingsResponse
Section titled “Response”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 } }}Button Configuration
Section titled “Button Configuration”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 Settings
Section titled “Update Settings”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).
PUT /api/chatbot/settingsRequest Body
Section titled “Request Body”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. |
Keyword Rules
Section titled “Keyword Rules”List Rules
Section titled “List Rules”GET /api/chatbot/keywordsResponse
Section titled “Response”{ "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 }}Query Parameters
Section titled “Query Parameters”| 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.
Create Rule
Section titled “Create Rule”POST /api/chatbot/keywordsRequest Body
Section titled “Request Body”{ "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. |
Match Types
Section titled “Match Types”| 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 |
Response Types
Section titled “Response Types”text, template, media, flow, script, transfer.
Get Rule
Section titled “Get Rule”GET /api/chatbot/keywords/{id}Update Rule
Section titled “Update Rule”PUT /api/chatbot/keywords/{id}Delete Rule
Section titled “Delete Rule”DELETE /api/chatbot/keywords/{id}AI Contexts
Section titled “AI Contexts”AI Contexts provide additional knowledge to the AI for specific topics.
List Contexts
Section titled “List Contexts”GET /api/chatbot/ai-contextsResponse
Section titled “Response”{ "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 }}Query Parameters
Section titled “Query Parameters”| 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.
Create Context
Section titled “Create Context”POST /api/chatbot/ai-contextsRequest Body
Section titled “Request Body”{ "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}Context Types
Section titled “Context Types”| Type | Description |
|---|---|
static | Fixed text content |
api | Fetched from external API |
Get Context
Section titled “Get Context”GET /api/chatbot/ai-contexts/{id}Update Context
Section titled “Update Context”PUT /api/chatbot/ai-contexts/{id}Delete Context
Section titled “Delete Context”DELETE /api/chatbot/ai-contexts/{id}Conversation Flows
Section titled “Conversation Flows”List Flows
Section titled “List Flows”GET /api/chatbot/flowsReturns {"flows": [...], "total", "page", "limit"}. Supports page, limit and search.
Get Flow
Section titled “Get Flow”GET /api/chatbot/flows/{id}Create Flow
Section titled “Create Flow”POST /api/chatbot/flowsRequest Body
Section titled “Request Body”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"} ] }}Node Types
Section titled “Node Types”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.
Node behaviour notes
Section titled “Node behaviour notes”prompt— on invalid input it re-sendsvalidation_errorand waits again, untilmax_retriesis reached; only then does it emitmax_retries. An invalidvalidation_regexis logged and validation is skipped rather than failing the conversation.api_call— network errors and non-2xx both emithttp:non2xx.response_mappingmaps variable names to dotted JSON paths ({"customer_id": "data.id"}) and merges the extracted values into session data.phone_numberis 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 asfalse.timing— days absent fromschedulecount asout_of_hours. Evaluated in the server's local time (setTZ).ai_response— if AI is disabled, has no provider, or has no API key, the node logs a warning, sends nothing, and still advances viadefault. 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— ateam_idof""or"_general"(or an unparseable UUID) routes to the general queue. The transfer is recorded with sourceflowand the session is markedcompleted.
Edge resolution
Section titled “Edge resolution”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.
Transfer Node Configuration
Section titled “Transfer Node Configuration”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). |
Panel Configuration
Section titled “Panel Configuration”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"} ] } ] }}Section Properties
Section titled “Section Properties”| 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 Properties
Section titled “Field Properties”| 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 |
Flow Fields
Section titled “Flow Fields”| 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.
Update Flow
Section titled “Update Flow”PUT /api/chatbot/flows/{id}Delete Flow
Section titled “Delete Flow”DELETE /api/chatbot/flows/{id}Agent Transfers
Section titled “Agent Transfers”List Transfers
Section titled “List Transfers”Get agent transfer requests.
GET /api/chatbot/transfersQuery Parameters
Section titled “Query Parameters”| 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.
Response
Section titled “Response”{ "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.
Visibility
Section titled “Visibility”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.
Create Transfer
Section titled “Create Transfer”Manually transfer a conversation to a human agent or team.
POST /api/chatbot/transfersRequest Body
Section titled “Request Body”{ "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 Next Transfer
Section titled “Pick Next Transfer”Pick the next unassigned transfer from the queue.
POST /api/chatbot/transfers/pickPicks 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.
Query Parameters
Section titled “Query Parameters”| 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.
Permissions
Section titled “Permissions”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 Transfer
Section titled “Assign Transfer”Assign a transfer to a specific agent.
PUT /api/chatbot/transfers/{id}/assignRequest Body
Section titled “Request Body”{ "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 from Transfer
Section titled “Resume from Transfer”Resume chatbot after human agent completes interaction.
PUT /api/chatbot/transfers/{id}/resumeSessions
Section titled “Sessions”List Sessions
Section titled “List Sessions”View chatbot sessions (for debugging).
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 Session
Section titled “Get Session”Get details of a specific session, including its full message history.
GET /api/chatbot/sessions/{id}Response
Section titled “Response”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": [ ] }}