> For the complete documentation index, see [llms.txt](https://docs.zipchat.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.zipchat.ai/installation-and-setup/chat-settings/integrations/backend-api.md).

# Backend API

The Backend API lets you programmatically access your Zipchat conversation data and manage your AI's configuration. Sync conversations with your CRM, reply to customers, and update the master prompt,

The Backend API lets you programmatically access your Zipchat conversation data and manage your AI's configuration. Use it to sync conversations with your CRM, build custom analytics dashboards, take over conversations from the AI, reply to customers, and update the master prompt, custom tools (skills), and variables — all from your own systems.

### Getting Started

1. Go to **Settings > Integrations** in your Zipchat dashboard
2. Click **Enable** on the Backend API card
3. Copy your API key (prefixed with `zc-`) — store it securely, it is only shown once

### Authentication

Include your API key as a Bearer token in the `Authorization` header:

```
Authorization: Bearer zc-your_api_key_here
```

All requests must be authenticated. Unauthenticated or invalid requests return `401 Unauthorized`.

The API accepts two kinds of token, interchangeably, on every endpoint:

#### Chat-level tokens (`zc-`)

Scoped to a single chat. Generated per chat in **Settings > Integrations > Backend API**. A chat token only authenticates requests whose `:chat_id` matches the chat it belongs to — any other `chat_id` returns `401`.

#### User-level tokens (`zcu-`)

Personal to a user and work against **any chat that user can access** (chats they own or are a member of). Generate or regenerate one under **Account > Settings** (`/account/settings/edit`). Use the same `Authorization: Bearer zcu-...` header. A user token used against a `chat_id` the user cannot access returns `401` and never reveals whether that chat exists.

Both token types can call every endpoint below. Two caveats for user tokens:

* **Role model**: configuration writes (updating the prompt, creating/updating/deleting custom tools and variables) follow the same permissions as the dashboard. A `member`-role (read-only) user's token receives `403 Forbidden` on those endpoints but can still use all read and conversation endpoints.
* **Revocation**: user tokens authenticate independently of the chat's Backend API integration — disabling that integration revokes only the chat-level `zc-` key. To cut off a user token, revoke it in that user's Account > Settings, or remove the user from the chat.

Tokens are environment-specific: a token generated in production only works against production, and vice versa for staging/local environments.

### Base URL

```
https://app.zipchat.ai/api/integrations/backend_api/v1
```

### Endpoints

#### List Chats

```
GET /chats
```

Lists the chats reachable with your credential, so clients can discover chat ids. With a **user token** this returns every chat the user can access; with a **chat token** it returns only that token's chat.

**Parameters**

| Parameter  | Type    | Default | Description                |
| ---------- | ------- | ------- | -------------------------- |
| `page`     | integer | `1`     | Page number                |
| `per_page` | integer | `50`    | Items per page (max `100`) |

**Response**

```json
{
  "chats": [
    {
      "id": 1,
      "name": "Acme Store AI",
      "domain": "acme-store.com",
      "created_at": "2026-01-15T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1,
    "has_next_page": false
  }
}
```

***

#### List Conversations

```
GET /chats/:chat_id/conversations
```

Returns a paginated list of non-empty conversations for the given chat, including archived conversations. Results are ordered by last message time descending, then ID descending.

**Parameters**

| Parameter               | Type    | Default | Description                                                                                                                                  |
| ----------------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `page`                  | integer | `1`     | Page number                                                                                                                                  |
| `per_page`              | integer | `50`    | Items per page (max `100`)                                                                                                                   |
| `last_message_at_since` | string  | —       | ISO 8601 timestamp. Only return conversations with `last_message_at >=` this time. Does not detect status-only changes.                      |
| `updated_at_since`      | string  | —       | ISO 8601 timestamp. Only return conversations with `updated_at >=` this time, including archive, escalation, purchase and sentiment changes. |

**Response**

```json
{
  "conversations": [
    {
      "id": 12345,
      "chat_id": 1,
      "current_url": "https://example.com/products/blue-tee",
      "start_link": "https://example.com/products/red-tee",
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:45:00.123456Z",
      "archived": false,
      "last_message_at": "2026-01-15T10:45:00Z",
      "channel": "web",
      "manual_reply_mode": false,
      "last_escalated_at": null,
      "last_escalation_resolved_at": null,
      "assignee": null,
      "share_url": "https://app.zipchat.ai/chats/1/conversations/12345",
      "lead": {
        "id": 100,
        "full_name": "John Doe",
        "email": "john@example.com",
        "phone_number": "+1234567890",
        "email_marketing_consent": true
      },
      "customer_sentiment": "conviction",
      "customer_sentiment_reason": "Asked whether the Classic Tee runs true to size before buying.",
      "purchase_sentiment": "strongly_influenced",
      "purchase_sentiment_reason": "The assistant answered the sizing doubt about the Classic Tee, which is the item that was ordered.",
      "purchases": [
        {
          "order_id": "5551234567",
          "order_total": "29.9",
          "currency": "USD",
          "line_items": [
            { "id": "9213800579256", "title": "Classic Tee", "price": "29.9", "quantity": 1 }
          ],
          "created_at": "2026-01-15T10:50:00Z"
        }
      ]
    },
    {
      "id": 12346,
      "chat_id": 1,
      "current_url": null,
      "start_link": null,
      "created_at": "2026-01-15T11:00:00Z",
      "updated_at": "2026-01-15T11:20:00.123456Z",
      "archived": true,
      "last_message_at": "2026-01-15T11:20:00Z",
      "channel": "whatsapp",
      "manual_reply_mode": true,
      "last_escalated_at": "2026-01-15T11:10:00Z",
      "last_escalation_resolved_at": "2026-01-15T11:18:00Z",
      "assignee": {
        "id": 42,
        "name": "Stan Marsh",
        "email": "stan@example.com",
        "avatar_url": "https://app.zipchat.ai/rails/active_storage/blobs/redirect/.../avatar.png"
      },
      "share_url": "https://app.zipchat.ai/chats/1/conversations/12346",
      "lead": null,
      "customer_sentiment": null,
      "customer_sentiment_reason": null,
      "purchase_sentiment": null,
      "purchase_sentiment_reason": null,
      "purchases": []
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total_count": 142,
    "total_pages": 3,
    "has_next_page": true
  }
}
```

**Page context**

Both list and detail responses include these nullable page-context fields:

| Field         | Type           | Description                                                                                                                                        |
| ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `current_url` | string or null | Stored page URL associated with the customer's latest storefront message. Sending a message from another page updates it; browsing alone does not. |
| `start_link`  | string or null | First available page URL captured for the conversation. Later messages from other pages do not replace it.                                         |

Use these URLs to map conversations to products in your system. They are page URLs, not product IDs, and a conversation can discuss multiple products even if it started on a product page. Both fields may be `null` when page context is unavailable, including on non-storefront channels or older conversations. Reading either endpoint does not modify them.

`archived` and `updated_at` are included in both list and detail responses. `updated_at` uses microsecond precision and reflects changes to the conversation record; it is not a change log for every nested lead or message edit.

**Incremental status sync**

Use `updated_at_since` alone to pick up archive/unarchive and escalation changes even when no message has been added. If both timestamp filters are provided, both must match (AND). Invalid timestamps return `422`; omitted or blank filters are ignored.

1. Perform an initial paginated fetch and upsert conversations by ID.
2. Record the time at the **start** of each polling cycle, and fetch every page using the same `updated_at_since` value from the previous completed cycle, with an overlap (for example, one minute).
3. Only advance the checkpoint after all pages succeed. Deduplicate overlapping results by conversation ID, using `updated_at` to avoid applying older responses.
4. Periodically reconcile your tracked conversations, including archived ones, via the detail endpoint so missed archive or unarchive changes can be recovered. Pagination uses offsets, so concurrent changes can move records between pages; it is not a snapshot.

Keep archived records in your local sync state so that a later unarchive can return them to your queue. Retain timestamp precision and account for client/server clock skew when choosing an overlap. The filter reflects conversation updates, not deletions or every update to associated records. No status-change webhook is provided.

The `lead` and `assignee` fields are `null` when not set. `last_escalated_at` is set when the conversation was escalated by the AI or via the API; `last_escalation_resolved_at` is set when the escalation was marked resolved in the dashboard or via the API.

**Sentiment and purchases**

Both list and detail responses include the classifications shown on the dashboard's **Insights** card, and the orders attributed to the conversation:

| Field                       | Type           | Description                                                                                                                                                                                                                             |
| --------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `customer_sentiment`        | string or null | The customer's buying stage, shown as "Customer sentiment" on the dashboard: `discovery` (browsing, no product in mind), `conviction` (weighing a specific product), `post_purchase` (asking about an order already placed) or `other`. |
| `customer_sentiment_reason` | string or null | One sentence explaining the `customer_sentiment` verdict.                                                                                                                                                                               |
| `purchase_sentiment`        | string or null | How much the conversation influenced its purchases: `strongly_influenced`, `influenced` or `not_influenced`. One verdict covers every purchase in the conversation. Always `null` when `purchases` is empty.                            |
| `purchase_sentiment_reason` | string or null | One sentence explaining the `purchase_sentiment` verdict.                                                                                                                                                                               |
| `purchases`                 | array          | Orders attributed to the conversation, oldest first. Empty when none.                                                                                                                                                                   |

Each purchase has:

| Field         | Type   | Description                                                                                                                                    |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `order_id`    | string | The store's order ID (the Shopify order ID on Shopify stores).                                                                                 |
| `order_total` | string | Order total as a decimal string (for example `"29.9"`), in `currency`.                                                                         |
| `currency`    | string | ISO 4217 currency code.                                                                                                                        |
| `line_items`  | array  | Ordered items, each with `id` (string), `title`, `price` (decimal string) and `quantity` (integer). Empty when the store did not report items. |
| `created_at`  | string | When Zipchat recorded the purchase.                                                                                                            |

Sentiments are classified once a conversation has been idle for 24 hours, so both are `null` (pending) on recent conversations. Recording a purchase and classifying either sentiment move the conversation's `updated_at`, so the incremental sync on `updated_at_since` described above picks them up.

***

#### Get Conversation

```
GET /chats/:chat_id/conversations/:id
```

Returns a single conversation with its messages.

**Parameters**

| Parameter                  | Type    | Default | Description                                                       |
| -------------------------- | ------- | ------- | ----------------------------------------------------------------- |
| `message_page`             | integer | `1`     | Message page number                                               |
| `message_per_page`         | integer | `100`   | Messages per page (max `200`)                                     |
| `message_created_at_since` | string  | —       | ISO 8601 timestamp. Only return messages created since this time. |

**Response**

```json
{
  "conversation": {
    "id": 12345,
    "current_url": "https://example.com/products/blue-tee",
    "start_link": "https://example.com/products/red-tee",
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-01-15T10:45:00.123456Z",
    "archived": false,
    "last_message_at": "2026-01-15T10:45:00Z",
    "channel": "web",
    "manual_reply_mode": false,
    "last_escalated_at": null,
    "last_escalation_resolved_at": null,
    "assignee": null,
    "share_url": "https://app.zipchat.ai/chats/1/conversations/12345",
    "lead": {
      "id": 100,
      "first_name": "John",
      "last_name": "Doe",
      "full_name": "John Doe",
      "email": "john@example.com",
      "phone_number": "+1234567890",
      "email_marketing_consent": true,
      "phone_marketing_consent": false,
      "shopify_customer_id": "7890123456",
      "additional_attributes": {},
      "created_at": "2026-01-15T10:31:00Z"
    },
    "customer_sentiment": "conviction",
    "customer_sentiment_reason": "Asked whether the Classic Tee comes in blue before buying.",
    "purchase_sentiment": "influenced",
    "purchase_sentiment_reason": "The assistant confirmed the blue Classic Tee was available, and that item was ordered.",
    "purchases": [
      {
        "order_id": "5551234567",
        "order_total": "29.9",
        "currency": "USD",
        "line_items": [
          { "id": "9213800579256", "title": "Classic Tee", "price": "29.9", "quantity": 1 }
        ],
        "created_at": "2026-01-15T10:50:00Z"
      }
    ],
    "messages": [
      {
        "id": 50001,
        "created_at": "2026-01-15T10:30:05Z",
        "role": "user",
        "message": "Do you have this in blue?",
        "count_as_reply": false,
        "manual_reply": false,
        "status": "sent",
        "rate": null,
        "sender_id": null
      },
      {
        "id": 50002,
        "created_at": "2026-01-15T10:30:08Z",
        "role": "assistant",
        "message": "Yes! The Classic Tee is available in blue.",
        "count_as_reply": true,
        "manual_reply": false,
        "status": "sent",
        "rate": "positive",
        "sender_id": null
      }
    ],
    "messages_pagination": {
      "page": 1,
      "per_page": 100,
      "total_count": 2,
      "total_pages": 1,
      "has_next_page": false
    }
  }
}
```

The `current_url` and `start_link` fields have the same meaning and nullable types as in the list response.

The show endpoint returns expanded lead details (including `first_name`, `last_name`, `shopify_customer_id`, and `additional_attributes`) compared to the list endpoint. The `assignee`, `last_escalated_at`, `last_escalation_resolved_at`, sentiment and `purchases` fields follow the same shape as on the list endpoint.

***

#### List Users

```
GET /chats/:chat_id/users
```

Returns the users (the chat owner and any chat participants) who can be assigned to a conversation on this chat.

**Parameters**

| Parameter  | Type    | Default | Description                |
| ---------- | ------- | ------- | -------------------------- |
| `page`     | integer | `1`     | Page number                |
| `per_page` | integer | `50`    | Items per page (max `100`) |

**Response**

```json
{
  "users": [
    {
      "id": 42,
      "name": "Stan Marsh",
      "email": "stan@example.com",
      "avatar_url": "https://app.zipchat.ai/rails/active_storage/blobs/redirect/.../avatar.png"
    }
  ],
  "pagination": {
    "page": 1,
    "per_page": 50,
    "total_count": 1,
    "total_pages": 1,
    "has_next_page": false
  }
}
```

`avatar_url` is `null` when the user has not uploaded an avatar.

***

#### Get User

```
GET /chats/:chat_id/users/:id
```

Returns a single user. Returns `404` if the user is not in the assignable set for this chat — the endpoint will never leak information about users from other chats.

**Response**

```json
{
  "user": {
    "id": 42,
    "name": "Stan Marsh",
    "email": "stan@example.com",
    "avatar_url": null
  }
}
```

***

#### Update Conversation Assignment

```
PATCH /chats/:chat_id/conversations/:id/assignment
```

Takes a conversation over from the AI (manual mode) or hands it back to the AI. This is a single atomic operation: setting `assignee_id` to a user puts the conversation into manual mode; setting it to `null` puts it back into AI mode and unassigns.

Use `GET /chats/:chat_id/users` to discover valid `assignee_id` values.

**Request body**

```json
{ "assignee_id": 42 }
```

Or, to hand back to the AI:

```json
{ "assignee_id": null }
```

**Response**

```json
{
  "conversation": {
    "id": 12345,
    "manual_reply_mode": true,
    "assignee": {
      "id": 42,
      "name": "Stan Marsh",
      "email": "stan@example.com",
      "avatar_url": null
    }
  }
}
```

**Errors**

| Status                     | Error message                                         | Cause                                                  |
| -------------------------- | ----------------------------------------------------- | ------------------------------------------------------ |
| `404 Not Found`            | `Conversation not found`                              | The conversation does not belong to this chat.         |
| `422 Unprocessable Entity` | `assignee_id is required (use null to assign to AI)`  | The body did not include `assignee_id`.                |
| `422 Unprocessable Entity` | `assignee_id is not an assignable user for this chat` | The given user is not the chat owner or a participant. |

***

#### Archive or Unarchive Conversation

```
PATCH /chats/:chat_id/conversations/:id/archive
```

Sets the same `archived` state as the dashboard's Archive/Unarchive action, including for conversations that were never escalated.

**Request body**

```json
{ "archived": true }
```

Send `false` to unarchive. The `archived` field is required and must be a JSON boolean; strings, numbers, null, arrays and objects are rejected. Repeating the same request preserves the state and does not advance `updated_at`.

**Response**

```json
{
  "conversation": {
    "id": 12345,
    "archived": true,
    "updated_at": "2026-01-15T11:30:00.123456Z"
  }
}
```

Archiving does not change `last_message_at`, escalation timestamps, assignment, or AI reply mode. Resolving an escalation does not archive a conversation. The internal conversation lifecycle's closed state is separate from the merchant's archive action and is not exposed by this API.

**Errors**

| Status                     | Error message                                     | Cause                                                            |
| -------------------------- | ------------------------------------------------- | ---------------------------------------------------------------- |
| `404 Not Found`            | `Conversation not found`                          | The conversation does not exist or does not belong to this chat. |
| `422 Unprocessable Entity` | `archived is required and must be a JSON boolean` | Missing or invalid `archived` value.                             |

Standard authentication and rate limits apply.

***

#### Escalate or Resolve Conversation

```
PATCH /chats/:chat_id/conversations/:id/escalation
```

Updates the conversation's escalation timestamps and `updated_at`. It does not archive the conversation, send notifications, or change its assignment.

**Request body**

```json
{ "resolved": false }
```

| Field      | Type    | Description                                                                                                                                                                                                                                                                                                            |
| ---------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `resolved` | boolean | Optional, defaults to `false`. When `false` (or omitted), **escalates**: sets `last_escalated_at` to now and clears `last_escalation_resolved_at` (so a previously resolved conversation is re-opened). When `true`, **resolves**: sets `last_escalation_resolved_at` to now and leaves `last_escalated_at` untouched. |

Sending no body escalates the conversation.

**Response**

The same shape for both operations:

```json
{
  "conversation": {
    "id": 12345,
    "currently_escalated": true,
    "last_escalated_at": "2026-01-15T11:10:00Z",
    "last_escalation_resolved_at": null
  }
}
```

`currently_escalated` is `true` when `last_escalated_at` is set and not yet resolved.

**Errors**

| Status          | Error message            | Cause                                          |
| --------------- | ------------------------ | ---------------------------------------------- |
| `404 Not Found` | `Conversation not found` | The conversation does not belong to this chat. |

***

#### Send Manual Message

```
POST /chats/:chat_id/conversations/:id/messages
```

Sends a manual reply from a human agent on a conversation that is currently in **manual mode**. The message is sent through whichever channel the conversation uses (web, email, WhatsApp, Instagram, Messenger).

You must take the conversation over first with `PATCH /conversations/:id/assignment` — sending while the conversation is still in AI mode returns `422`.

**Request body**

```json
{
  "message": "Hi! Happy to help with that.",
  "sender_id": 42
}
```

| Field       | Type    | Description                                                                                                   |
| ----------- | ------- | ------------------------------------------------------------------------------------------------------------- |
| `message`   | string  | The reply text. Required.                                                                                     |
| `sender_id` | integer | The user id that should be recorded as the sender. Must be a member of `GET /chats/:chat_id/users`. Required. |

**Response**

```json
{
  "message": {
    "id": 50003,
    "created_at": "2026-01-15T10:45:00Z",
    "role": "assistant",
    "message": "Hi! Happy to help with that.",
    "count_as_reply": false,
    "manual_reply": true,
    "status": "sent",
    "rate": null,
    "sender_id": 42
  }
}
```

**Errors**

| Status                     | Error message                                                                     | Cause                                                                                                              |
| -------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `404 Not Found`            | `Conversation not found`                                                          | The conversation does not belong to this chat.                                                                     |
| `422 Unprocessable Entity` | `message is required`                                                             | Empty or missing `message`.                                                                                        |
| `422 Unprocessable Entity` | `sender_id is required`                                                           | Missing `sender_id`.                                                                                               |
| `422 Unprocessable Entity` | `sender_id is not an assignable user for this chat`                               | Sender is not the chat owner or a participant.                                                                     |
| `422 Unprocessable Entity` | `Conversation is in AI mode; assign it to a user before sending a manual message` | Call the assignment endpoint first.                                                                                |
| `422 Unprocessable Entity` | `Cannot send: customer last messaged more than 24 hours ago on a Meta channel`    | WhatsApp / Instagram / Messenger enforce a 24-hour reply window from Meta. Wait for the customer to message again. |

***

#### Get Master Prompt

```
GET /chats/:chat_id/prompt
```

Returns the chat's current master prompt. Use this to read before modifying (e.g. to append to the existing prompt client-side).

**Response**

```json
{
  "prompt": {
    "chat_id": 1,
    "master_prompt": "You are a helpful support assistant for Acme Co.",
    "master_prompt_max_length": 10000
  }
}
```

***

#### Update Master Prompt

```
PATCH /chats/:chat_id/prompt
```

Replaces the chat's master prompt (the bot's core behavior instructions).

**Request body**

```json
{ "master_prompt": "You are a helpful support assistant for Acme Co." }
```

| Field           | Type   | Description                                                                                                                     |
| --------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `master_prompt` | string | The new master prompt. Required. Leading/trailing whitespace is stripped. Must not exceed the chat's configured maximum length. |

**Response**

```json
{
  "prompt": {
    "chat_id": 1,
    "master_prompt": "You are a helpful support assistant for Acme Co.",
    "master_prompt_max_length": 10000
  }
}
```

**Errors**

| Status                     | Error message                                     | Cause                                                                                                               |
| -------------------------- | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `403 Forbidden`            | `Your role does not allow configuration changes`  | User token belongs to a read-only (`member`-role) user.                                                             |
| `422 Unprocessable Entity` | `master_prompt is required`                       | The body did not include `master_prompt`.                                                                           |
| `422 Unprocessable Entity` | `master_prompt cannot be blank`                   | The value was `null`, empty, or whitespace-only. Blank values are rejected rather than silently erasing the prompt. |
| `422 Unprocessable Entity` | `Master prompt must be at most N characters long` | The prompt exceeded `master_prompt_max_length`.                                                                     |

***

#### Custom Tools (Skills)

Custom tools (labeled "skills" in the dashboard) are per-chat actions the AI can invoke.

**List**

```
GET /chats/:chat_id/custom_tools
```

Paginated (`page`, `per_page`, max `100`).

```json
{
  "custom_tools": [
    {
      "id": 12,
      "name": "Order lookup",
      "instructions": "Fetch orders from the API",
      "active": true,
      "channels": ["all channels"],
      "icon_id": null,
      "custom_tool_template_id": null,
      "variable_ids": [5, 7],
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total_count": 1, "total_pages": 1, "has_next_page": false }
}
```

**Create**

```
POST /chats/:chat_id/custom_tools
```

```json
{
  "custom_tool": {
    "name": "Order lookup",
    "instructions": "Fetch orders from the API",
    "channels": ["all channels"],
    "active": true,
    "variable_ids": [5, 7]
  }
}
```

| Field          | Type    | Description                                                                                                                                                    |
| -------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`         | string  | Required, max 255 chars.                                                                                                                                       |
| `instructions` | string  | Required, max 10000 chars.                                                                                                                                     |
| `channels`     | array   | Channels the tool is active on. When omitted, defaults to the template's install channels if `custom_tool_template_id` is given, otherwise `["all channels"]`. |
| `active`       | boolean | Optional, defaults to `true`.                                                                                                                                  |
| `variable_ids` | array   | Optional. IDs of existing pool variables to attach to this tool. Must be an array of ids (`422` otherwise).                                                    |

Returns `201` with the serialized custom tool. Validation failures return `422` with an `error` message — including an unknown `icon_id` or `custom_tool_template_id` (`icon_id does not exist` / `custom_tool_template_id does not exist`).

**Update**

```
PATCH /chats/:chat_id/custom_tools/:id
```

Same body shape as create (all fields optional; `custom_tool_template_id` is create-only and ignored here). `variable_ids`, when present, is the **full desired set** of attached variables: missing ones are attached and absent ones detached (detaching only removes the link — the pooled variable itself is untouched). Omit the key to leave attachments unchanged. Returns `200`, or `404` if the tool does not belong to this chat.

**Delete**

```
DELETE /chats/:chat_id/custom_tools/:id
```

Returns `204 No Content`, or `404` if the tool does not belong to this chat.

***

#### Variables

Variables are a per-chat pool of secrets (e.g. API keys) that custom tools reference. **Values are write-only** — they are encrypted at rest and never returned by the API; responses expose only a `has_value` flag.

**List**

```
GET /chats/:chat_id/variables
```

Paginated (`page`, `per_page`, max `100`).

```json
{
  "variables": [
    {
      "id": 5,
      "name": "API_KEY",
      "kind": "user",
      "has_value": true,
      "usage_count": 2,
      "custom_tool_ids": [12],
      "created_at": "2026-01-15T10:30:00Z",
      "updated_at": "2026-01-15T10:30:00Z"
    }
  ],
  "pagination": { "page": 1, "per_page": 50, "total_count": 1, "total_pages": 1, "has_next_page": false }
}
```

`kind` is `user` for normal variables or `managed_shopify` for the reserved, auto-managed `SHOPIFY_API_KEY`.

**Create**

```
POST /chats/:chat_id/variables
```

```json
{
  "skill_variable": {
    "name": "API_KEY",
    "value": "sk_live_...",
    "custom_tool_ids": [12]
  }
}
```

| Field             | Type   | Description                                                                                  |
| ----------------- | ------ | -------------------------------------------------------------------------------------------- |
| `name`            | string | Required. Uppercase letters, digits, and underscores only (e.g. `API_KEY`). Unique per chat. |
| `value`           | string | Required. Stored encrypted; never returned.                                                  |
| `custom_tool_ids` | array  | Optional. Custom tools to attach this variable to.                                           |

Returns `201`. Validation failures (bad name format, duplicate, reserved name) return `422`.

**Update**

```
PATCH /chats/:chat_id/variables/:id
```

Updates the value only. The change propagates to every custom tool the variable is attached to.

```json
{ "skill_variable": { "value": "sk_live_new..." } }
```

Returns `200`. Returns `422` if `value` is blank, `404` if the variable is not on this chat, or `422` when targeting the managed `SHOPIFY_API_KEY` (it cannot be edited via the API).

**Delete**

```
DELETE /chats/:chat_id/variables/:id
```

Returns `204 No Content`. Returns `404` if the variable is not on this chat, or `422` for the managed `SHOPIFY_API_KEY` (it cannot be deleted).

### Rate Limiting

The API allows **120 requests per 60-second window** per credential (per chat integration for chat tokens, per user for user tokens).

Every response includes rate limit headers:

| Header                  | Description                              |
| ----------------------- | ---------------------------------------- |
| `X-RateLimit-Limit`     | Maximum requests per window (`120`)      |
| `X-RateLimit-Remaining` | Requests remaining in the current window |
| `X-RateLimit-Reset`     | Unix timestamp when the window resets    |

When the limit is exceeded, the API returns `429 Too Many Requests`.

### Error Codes

| Status                     | Description                                                                              |
| -------------------------- | ---------------------------------------------------------------------------------------- |
| `401 Unauthorized`         | Missing or invalid API key                                                               |
| `403 Forbidden`            | User token's role does not allow configuration changes (prompt, custom tools, variables) |
| `404 Not Found`            | Conversation, user, custom tool, or variable not found                                   |
| `422 Unprocessable Entity` | Invalid parameter (e.g., malformed timestamp, missing field, ineligible assignee)        |
| `429 Too Many Requests`    | Rate limit exceeded                                                                      |

### Examples

#### Discover your chats with a user token

```bash
curl -H "Authorization: Bearer zcu-your_user_token" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats"
```

#### Read, then append to the master prompt

```bash
# 1. Read the current prompt
curl -H "Authorization: Bearer zcu-your_user_token" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/prompt"

# 2. Send back the full prompt with your addition appended
curl -X PATCH \
  -H "Authorization: Bearer zcu-your_user_token" \
  -H "Content-Type: application/json" \
  -d '{"master_prompt": "<existing prompt>\n\nAlways mention our 30-day return policy."}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/prompt"
```

#### Create a custom tool with a secret variable

```bash
# 1. Create the variable (value is write-only; it is never returned)
curl -X POST \
  -H "Authorization: Bearer zcu-your_user_token" \
  -H "Content-Type: application/json" \
  -d '{"skill_variable": {"name": "ORDERS_API_KEY", "value": "sk_live_..."}}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/variables"

# 2. Create the tool and attach the variable (use the id from step 1)
curl -X POST \
  -H "Authorization: Bearer zcu-your_user_token" \
  -H "Content-Type: application/json" \
  -d '{"custom_tool": {"name": "Order lookup", "instructions": "Call GET https://api.example.com/orders/{id} with the ORDERS_API_KEY header...", "variable_ids": [5]}}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/custom_tools"
```

#### Copy a custom tool to another chat

```bash
# 1. List tools on the source chat, pick the one to copy
curl -H "Authorization: Bearer zcu-your_user_token" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/custom_tools"

# 2. POST its name/instructions/channels to the destination chat
curl -X POST \
  -H "Authorization: Bearer zcu-your_user_token" \
  -H "Content-Type: application/json" \
  -d '{"custom_tool": {"name": "Order lookup", "instructions": "...", "channels": ["all channels"]}}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/2/custom_tools"
```

#### Fetch the latest conversations

```bash
curl -H "Authorization: Bearer zc-your_api_key" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations"
```

#### Fetch conversations with new message activity since a timestamp

```bash
curl -H "Authorization: Bearer zc-your_api_key" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations?last_message_at_since=2026-01-15T09:30:00Z"
```

#### Fetch a single conversation with messages

```bash
curl -H "Authorization: Bearer zc-your_api_key" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345"
```

#### Paginate through messages

```bash
curl -H "Authorization: Bearer zc-your_api_key" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345?message_page=2&message_per_page=50"
```

#### Take over a conversation, reply, and hand it back to the AI

```bash
# 1. Take the conversation over and assign it to user 42
curl -X PATCH \
  -H "Authorization: Bearer zc-your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"assignee_id": 42}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/assignment"

# 2. Send a manual reply on behalf of user 42
curl -X POST \
  -H "Authorization: Bearer zc-your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hi! Happy to help.", "sender_id": 42}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/messages"

# 3. Hand the conversation back to the AI
curl -X PATCH \
  -H "Authorization: Bearer zc-your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"assignee_id": null}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/assignment"
```

#### Escalate, then resolve a conversation

```bash
# Escalate (no body needed)
curl -X PATCH \
  -H "Authorization: Bearer zc-your_api_key" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/escalation"

# Resolve the escalation
curl -X PATCH \
  -H "Authorization: Bearer zc-your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"resolved": true}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/escalation"
```

#### Archive and sync status changes

```bash
# Archive; send {"archived": false} to unarchive.
curl -X PATCH \
  -H "Authorization: Bearer zc-your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"archived": true}' \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations/12345/archive"

# Poll changes, including status-only updates. Follow pagination for all results.
curl --get \
  -H "Authorization: Bearer zc-your_api_key" \
  --data-urlencode "updated_at_since=2026-01-15T11:00:00.000000Z" \
  "https://app.zipchat.ai/api/integrations/backend_api/v1/chats/1/conversations"
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.zipchat.ai/installation-and-setup/chat-settings/integrations/backend-api.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
