Skip to content
ReferenceChatbot and AI

Chatbot

Configure chatbot automation and AI responses

DevelopersUpdated

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

Retrieve current chatbot settings. The settings come in groups, each guarded by its own workspace component and permission:

Group Fields Permission
Messages enabled, greeting and fallback messages and buttons, session_timeout_minutes settings.chatbot
Agent routing allow_agent_queue_pickup, assign_to_same_agent, agent_current_conversation_only chatbot.routing
Business hours business_hours_enabled, business_hours, out_of_hours_message, allow_automated_outside_hours chatbot.hours
SLA sla_* and client_* (client inactivity) chatbot.sla
AI ai_* (prompt, handoff, typing indicator, reply wait, memory switch) and ai_mode settings.chatbot, while the AI replies and knowledge component is on; the memory fields also need the Customer memory component

Read access to any group opens the endpoint; only the groups you can read are returned.

GET/api/chatbot/settings

The envelope contains settings (the groups you can read), access (per group, whether you can read and write it) and stats (with read access to the messages group).

{
"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_mode": "managed",
"ai_typing_indicator": null,
"ai_debounce_ms": null,
"ai_debounce_default_ms": 3000,
"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
}
}
}

ai_system_prompt can be at most 8,000 characters. A changed prompt that is longer gets a 400 and nothing is saved; a prompt saved before this limit is accepted unchanged until it is edited.

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. Every group the body touches needs that group’s write permission (see the table above); otherwise the request is refused with 403 and nothing is saved. Field names must match exactly: an unknown or differently cased name is refused with 400. The customer memory fields are left out of the response, and refused, while the workspace doesn’t have Customer memory. Each settings tab that is touched emits its own audit-log entry (settings.chatbot.messages, .agents, .hours, .sla, .ai).

PUT/api/chatbot/settings

Accepts the same field names as the settings object above, except the read-only ai_mode (only a super admin changes it) and ai_debounce_default_ms (the platform’s default reply wait). Sending either is refused with 400, as is an unknown or differently cased field name:

{
"enabled": true,
"ai_enabled": true,
"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_typing_indicator boolean or null Whether the customer sees “typing…” (which also marks their message read) while the AI writes a reply. null follows each number’s auto_read_receipt. A workspace setting: it applies to every number. Send null to go back to following read receipts; leave the field out to keep it as it is.
ai_debounce_ms integer or null How long, in milliseconds (0 to 8000), the AI waits after a customer’s message for more before it answers them all in one reply. 0 answers each message at once. null uses the platform’s default (ai_debounce_default_ms). A workspace setting: it applies to every number, to the bot’s AI replies and to AI steps in flows. Send null to go back to the default; leave the field out to keep it as it is.
ai_debounce_default_ms integer Read-only. The platform’s default wait, used while ai_debounce_ms is null.
ai_mode string Read-only. managed: replies and customer memory use Soosh’s AI, which picks the provider and model (there is no provider, model or key to set; ai_provider, ai_model and ai_api_key are ignored if sent). off: AI is switched off for the workspace.
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.

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.

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

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

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.

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

trigger_keywords limits when a context is used. With none, it goes to the AI with every message. With some, it’s used only when one of them appears as a whole word, ignoring case, in the customer’s latest message or one of the 2 before it. An api context calls its API only then.

static_content can be at most 16,000 characters. Creating a context with longer content, or changing it to longer content, gets a 400; content saved before this limit is accepted unchanged until it is edited. An api context’s response goes to the AI with its JSON whitespace removed, cut to 4,000 characters.

GET/api/chatbot/ai-contexts/{id}
PUT/api/chatbot/ai-contexts/{id}
DELETE/api/chatbot/ai-contexts/{id}
GET/api/chatbot/flows

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

GET/api/chatbot/flows/{id}
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.

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

Get agent transfer requests.

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",
"ai_summary": "Wants a quote for 40 seats on the enterprise plan; was sent the price list. Still open: a discount for annual billing.",
"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
}
}

ai_summary is the conversation summed up by AI for the agents, when the workspace has AI transfer summaries. It is written in the background a few seconds after the transfer, so it is missing at first; an agent_transfer_summary WebSocket event ({id, contact_id}) says when it is there. A transfer may never get one: the AI’s own hand-overs have none (their notes say why), and none is written when AI can’t run for the workspace (off, paused or out of credit), when the customer hasn’t written in the conversation, or when the server is already writing as many summaries as it can at once.

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.

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.

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.

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.

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

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 details of a specific session, including its full message history.

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": [ ]
}
}