# Chatbot

> Configure chatbot automation and AI responses

Source: https://docs.soosh.io/reference/api/chatbot/

## Overview

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

## Chatbot Settings

### Get Settings

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`

### Response

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).

```json
{
  "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
    }
  }
}
```

> **Careful:** The AI API key is **never** returned. There is no `ai_temperature` field, and the system prompt is
> `ai_system_prompt` (not `system_prompt`). The master toggle is `enabled` (not `chatbot_enabled`).

`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.

### 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

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`

### Request Body

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:

```json
{
  "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. |

## Keyword Rules

### List Rules

`GET /api/chatbot/keywords`

### Response

```json
{
  "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

| 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

`POST /api/chatbot/keywords`

### Request Body

```json
{
  "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

| 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

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

### Get Rule

`GET /api/chatbot/keywords/{id}`

### Update Rule

`PUT /api/chatbot/keywords/{id}`

### Delete Rule

`DELETE /api/chatbot/keywords/{id}`

## AI Contexts

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

### List Contexts

`GET /api/chatbot/ai-contexts`

### Response

```json
{
  "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

| 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

`POST /api/chatbot/ai-contexts`

### Request Body

```json
{
  "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

| 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 Context

`GET /api/chatbot/ai-contexts/{id}`

### Update Context

`PUT /api/chatbot/ai-contexts/{id}`

### Delete Context

`DELETE /api/chatbot/ai-contexts/{id}`

## Conversation Flows

### List Flows

`GET /api/chatbot/flows`

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

### Get Flow

`GET /api/chatbot/flows/{id}`

### Create Flow

`POST /api/chatbot/flows`

### 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.

```json
{
  "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

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](https://expr-lang.org/) 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

- **`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`.

### 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`.

> **Careful:** `graph.version` must be `2` and `entry_node` must name a node that exists, otherwise the flow
> fails to load and the chatbot logs an error instead of replying. A graph walk is capped at 100
> node executions per inbound message to guard against cycles.

### Transfer Node Configuration

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

```json
{
  "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

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

```json
{
  "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

| 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

| 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

| 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](#panel-configuration) above. |
| `graph` | object | The v2 flow graph. |
| `enabled` | boolean | Whether the flow can be triggered. |

Writing a flow requires `flows.chatbot:write`.

> **Note:** `GET /api/chatbot/flows` returns a trimmed shape — only `id`, `name`, `description`,
> `trigger_keywords`, `enabled` and `created_at`. Fetch a single flow to get its `graph`,
> `panel_config` and messages.

### Update Flow

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

### Delete Flow

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

## Agent Transfers

### List Transfers

Get agent transfer requests.

`GET /api/chatbot/transfers`

### 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

```json
{
  "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.

### 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

Manually transfer a conversation to a human agent or team.

`POST /api/chatbot/transfers`

### Request Body

```json
{
  "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

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.

### 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

`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

Assign a transfer to a specific agent.

`PUT /api/chatbot/transfers/{id}/assign`

### Request Body

```json
{
  "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

Resume chatbot after human agent completes interaction.

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

## Sessions

### 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.

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

### Get Session

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

`GET /api/chatbot/sessions/{id}`

### Response

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

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

> **Note:** Collected variables live in `session_data` (not `variables`), and the timestamp is
> `last_activity_at` (not `last_activity`). `session_data` also carries an internal `__path__` array
> recording `goto_flow` jumps.

> **Tip:** Use the Sessions API to debug chatbot interactions and understand the conversation state.
