> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xpressbot.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Outbound Webhook

> Send every message your customers send, and every delivery update of the messages you send, to a URL you own.

## What is this?

The **Outbound Webhook** tab pushes your channel's messages to a URL you control — your own server, Zapier, Make, or any HTTP endpoint. It is useful for logging, syncing with your CRM, or triggering your own systems. You get two kinds of events:

* **`message.received`** — a message a customer sent to you.
* **`message.status`** — a delivery update (**sent**, **delivered**, **read** or **failed**) for a message *you* sent.

This is a **plan feature or add-on**. If it isn't available, the tab is hidden or shows an upgrade prompt.

> [!NOTE]
>
> This is different from the **Outbound Webhook** automation step, which calls a URL from inside one flow. This tab covers every message on a channel type.

## What can I do here?

* Set one webhook **per channel type**: WhatsApp, Instagram, Facebook, Telegram, TikTok, or Web Chat
* Sign each request so your server can verify it came from the platform
* Add up to 10 **extra headers** your server needs
* Turn delivery on or off without removing the webhook
* **Send a test** and see the response status
* See the **Last delivery** result — so a failing webhook is easy to spot
* Open **What is sent** to see the exact payload for any message type or delivery update

## How to use it

### Set up a webhook

1. Open **Settings → Outbound Webhook** and pick a channel type tab.
2. Enter your **Webhook URL** — it must start with `https://`.
3. Optionally add a **Signing secret** (at least 16 characters, or click **Generate**). It is stored encrypted and shown once — copy it before saving. When set, every request carries a signature (see below).
4. Use **Extra headers (optional)** for any custom headers your server needs (up to 10).
5. Turn **Send messages** on or off. Off stops delivery (messages and updates) without removing the configuration.
6. Click **Save**. Use **Send a test** to check the connection straight away.

Under the fields, **Last delivery** shows what happened to the most recent request: *Delivered. Your server answered 200.* or *Failed.* with the reason. **What is sent** lets you pick a message type or a delivery update to see exactly what your URL receives (values are made up).

### What is sent

Each event is a `POST` with a JSON body and an `event` field you can switch on.

**A received message** (`event: "message.received"`):

```json theme={null}
{
  "event": "message.received",
  "id": "<webhook id>.<message id>",
  "sent_at": "2026-01-01T00:00:00.000Z",
  "data": {
    "channel_type": "whatsapp",
    "channel_id": "...",
    "conversation_id": "...",
    "contact": { "id": "...", "name": "...", "phone": "+15550100", "wa_id": "15550100", "bsuid": null },
    "message": {
      "id": "...", "external_id": "wamid...", "kind": "text", "type": "text", "message_type": null,
      "text": "...", "media_url": null, "media_mime_type": null, "reply_to_message_id": null,
      "button_id": null, "list_id": null, "received_at": "...",
      "reaction": null, "location": null, "flow": null
    }
  }
}
```

`contact.wa_id` is the WhatsApp number in digits (WhatsApp only). Switch on `message.kind` to know what the customer sent:

| `kind` | What it is | Fields to read |
| - | - | - |
| `text` | Plain text | `text` |
| `image`, `video`, `document`, `sticker` | A file | `media_url`, `media_mime_type`, `text` (caption or file name) |
| `audio` / `voice` | An audio file / a voice note | `media_url`, `media_mime_type` |
| `location` | A shared place | `location: { latitude, longitude, name, address }` |
| `contacts` | Shared contact cards | `text` lists the names |
| `reaction` | An emoji reaction | `reaction: { emoji, action, target_external_id }` |
| `button_reply`, `list_reply` | A tap on a button or list row | `button_id` / `list_id`, `text` is the label |
| `flow_reply` | A WhatsApp Flow submission | `flow: { id, token, response }` |
| `address`, `call_permission_reply`, `order` | Address form, call-permission answer, catalog order | `text` |
| `edited`, `deleted` | The customer edited or deleted a message | `text` |
| `shared_post`, `comment` | A shared post, or a comment | `text` |
| `system`, `unsupported`, `other` | A network notice, a type the network doesn't pass on, or anything else | `text`, or `type` / `message_type` |

Not every network sends every kind (Telegram has no read receipts, Instagram has no Flows).

`media_url` is a public link to the file on the platform's storage: no API key is needed and it doesn't expire. If your platform has no file storage, it is `null`.

**A delivery update** (`event: "message.status"`) arrives for every message *you* send:

```json theme={null}
{
  "event": "message.status",
  "id": "<webhook id>.<message id>.read",
  "sent_at": "...",
  "data": {
    "channel_type": "whatsapp", "channel_id": "...", "conversation_id": "...",
    "contact": { "id": "...", "name": "...", "phone": "+15550100", "wa_id": "15550100", "bsuid": null },
    "message": { "id": "...", "external_id": "wamid...", "kind": "text", "type": "text", "message_type": null, "text": "..." },
    "status": { "value": "read", "at": "...", "error": null }
  }
}
```

`status.value` is `sent`, `delivered`, `read` or `failed`; on `failed`, `status.error` is `{ code, message }` from the network. Each status is posted once, so a message that is sent, delivered and read produces up to three requests. **Telegram** has no delivery or read receipts, so you won't get `read` updates for it. A receiver that only cares about customer messages should ignore events other than `message.received`.

### Verify the signature

If you set a signing secret, each request includes these headers:

| Header | Value |
| - | - |
| `X-Webhook-Id` | The webhook's id |
| `X-Webhook-Timestamp` | When the request was made |
| `X-Webhook-Signature` | `sha256=` followed by the `HMAC-SHA256` of `<timestamp>.<raw body>` using your secret |

Sign the **raw request body exactly as received**, prepend `<timestamp>.`, compute `HMAC-SHA256` with your secret, and compare. Reject requests whose timestamp is too old.

### Remove a webhook

Click **Remove webhook** at the bottom of the tab. This deletes the URL, secret and headers for that channel type.

> [!WARNING]
>
> Every delivery attempt counts as data transfer — including retries and attempts to a server that doesn't respond. Requests time out after 10 seconds and are retried up to 3 times. Check your plan's data-transfer allowance before enabling.

## Troubleshooting / Technical Notes

* **The Outbound Webhook tab is missing.** The feature isn't in your plan. Ask your platform admin to add the Outbound Webhook add-on, or upgrade your plan.
* **Test request fails.** Check the URL starts with `https://`, your server is reachable from the internet (public addresses only), and returns a 2xx status. The tab shows the HTTP status and response time.
* **Messages aren't arriving.** Confirm **Send messages** is on and the webhook is saved. **Last delivery** shows the most recent failure reason.
* **I only want customer messages.** Ignore requests whose `event` is not `message.received`.
* **Signature verification fails.** Sign the raw body exactly as received with the `<timestamp>.` prefix, using the secret you saved. A secret is shown once — generate a new one if you lost it.
* **`media_url` is null.** Your platform has no file storage configured, or the network doesn't provide a file for that message.

## Related docs

* [Settings](https://docs.xpressbot.org/workspace/settings/overview.md)
* [Webhooks](https://docs.xpressbot.org/workspace/integrations/webhooks.md)
* [Developer Portal](https://docs.xpressbot.org/workspace/developer-portal.md)
* [Inbox](https://docs.xpressbot.org/workspace/inbox/overview.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.