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 CountDisplay Type
1-3 buttonsQuick reply buttons
4-10 buttonsList 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"}
]
}
FieldTypeDescription
ai_providerstringopenai, anthropic, or google
ai_api_keystringWrite-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
}
}
ParameterTypeDescription
pageintegerPage number. Default 1.
limitintegerPage size, 1–100. Default 50.
searchstringCase-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
}
FieldTypeRequiredDescription
keywordsstring[]YesAt least one keyword, else 400 At least one keyword is required.
namestringNoDefaults to the first keyword.
match_typestringNoDefaults to contains.
response_typestringNoDefaults to text.
response_contentobjectYesFree-form JSON whose shape depends on response_type.
priorityintegerNoHigher wins when several rules match. Defaults to 0 on create; the column default is 10.
enabledbooleanNoDefaults to false on create — send true explicitly to activate the rule.
TypeDescription
exactMessage must match keyword exactly
containsMessage contains the keyword
starts_withMessage starts with the keyword
regexRegular 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
}
}
ParameterTypeDescription
pageintegerPage number. Default 1.
limitintegerPage size, 1–100. Default 50.
searchstringCase-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
}
TypeDescription
staticFixed text content
apiFetched 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.

TypePurposeKey config fieldsOutgoing edge conditions
startEntry sentinel. No side effect.default
messageSend a templated text message.message (or text)default
promptSend a question; wait for and validate a reply.body, store_as, validation_regex, validation_error, max_retries (default 3)default, max_retries
buttonsInteractive reply buttons.body, buttons: [{id,title}], store_asbutton:<id> per button
api_callHTTP request with response capture + optional templated reply.url, method, headers, body, response_mapping, message_templatehttp:2xx, http:non2xx
conditionBoolean expression branch (expr-lang syntax) over session data.expressiontrue, false
timingBusiness-hours routing.schedule: [{day,enabled,start_time,end_time}]in_hours, out_of_hours
set_variableAssign session variables without messaging the user.set: {name: value} — string values are templateddefault
ai_responseAsk the org's configured LLM and send the answer.prompt_template (falls back to the user's last message)default
whatsapp_flowSend a native WhatsApp Flow form.flow_id (the Meta flow ID), header, body, ctadefault
transferHand off to a team / queue and end the session.body, team_id, notes(terminal)
goto_flowJump to another flow in the same org/account.flow_id(handled internally — the runner reloads the target graph)
webhookFire-and-forget HTTP call; result ignored.url, method, headers, bodydefault
endOptionally 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}}"
}
}
FieldDescription
bodyOptional message sent to the user before handoff (templated).
team_idTarget team UUID. Omit or set to "_general" for the shared queue.
notesInternal 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"}
]
}
]
}
}
FieldTypeDescription
idstringUnique section identifier
labelstringDisplay label for the section
columnsnumberLayout columns (1 or 2)
collapsiblebooleanAllow section to be collapsed
default_collapsedbooleanStart section in collapsed state
ordernumberSection display order
fieldsarrayFields to display in this section
FieldTypeDescription
keystringSession variable name (from store_as or response mapping)
labelstringDisplay label for the field
ordernumberField display order within section
display_typestringHow to render the value: text (default), badge, or tag
colorstringColor for badge/tag: default, success, warning, error, or info
FieldTypeDescription
namestringRequired — 400 Name is required otherwise.
descriptionstringFree text.
trigger_keywordsstring[]Keywords that start this flow for an inbound message.
initial_messagestringSent when the flow starts.
completion_messagestringSent when the flow completes.
on_complete_actionstringWhat to do after completion.
completion_configobjectConfiguration for on_complete_action.
panel_configobjectContact Info Panel layout — see Panel Configuration above.
graphobjectThe v2 flow graph.
enabledbooleanWhether 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
ParameterTypeDescription
statusstringFilter by status: active, resumed, or expired
team_idstringFilter by team ID, or general for the general queue
limitnumberPage size, 1–100. Default 100.
offsetnumberRows to skip. Default 0.
includestringComma-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"
}
FieldTypeRequiredDescription
contact_iduuidYesThe contact to transfer
whatsapp_accountstringNoPhone number ID the conversation belongs to
agent_iduuidNoAssign directly to an agent instead of queueing
team_iduuidNoTarget team (omit for general queue)
notesstringNoInternal notes for agents
sourcestringNomanual, 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.

ParameterTypeDescription
team_idstringPick 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"
}
FieldTypeDescription
agent_iduuid | nullAgent to assign to. Omitting the field (or sending null) means "assign to me" for callers without transfers:write; "" unassigns.
team_iduuid | ""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
ParameterTypeDescription
statusstringFilter 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": [ ]
}
}