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

# Webhook for the Notify API

> Prelude Notify API can notify your application about events using webhooks. Get notified when your messages are delivered, billed, and when users manage their subscription preferences.

<RequestExample>
  ```json Created theme={null}
  {
    "id": "evt_01htjex3pre54tywgzsdg1jnbn",
    "type": "transactional.message.created",
    "payload": {
      "id": "tx_01htjet67afxhta23j7dtekneh",
      "to": "+3361234567",
      "created_at": "2024-04-03T17:08:01.349000489Z",
      "expires_at": "2024-04-03T17:08:01.349000489Z",
      "customer_uuid": "90c7e8b2-203a-4984-ba25-6cf93d8fdbac",
      "variables": {
        "name": "John"
      },
      "fee": {
        "amount": "0.009",
        "currency": "EUR"
      },
      "correlation_id": "8709c4f8-78b4-4a3c-8167-e99164424d0f",
      "template_id": "template_01hp78bjhcef9tej1tgcg0cpq2",
    },
    "created_at": "2024-04-03T17:08:01.36881397Z"
  }
  ```

  ```json Submitted theme={null}
  {
    "id": "evt_01htjex3pre54tywgzsdg1jnbn",
    "type": "transactional.message.submitted",
    "payload": {
      "message_id": "tx_01htjet67afxhta23j7dtekneh",
      "attempt_id": "att_01htjex3pre54tywgzsdg1jnbn",
      "customer_uuid": "90c7e8b2-203a-4984-ba25-6cf93d8fdbac",
      "created_at": "2024-04-03T17:08:02.123456789Z",
      "correlation_id": "8709c4f8-78b4-4a3c-8167-e99164424d0f",
      "type": "transactional",
      "channel": "sms",
      "template_id": "template_01hp78bjhcef9tej1tgcg0cpq2"
    },
    "created_at": "2024-04-03T17:08:02.123456789Z"
  }
  ```

  ```json Pending theme={null}
  {
    "id": "evt_01htjex3pre54tywgzsdg1jnbn",
    "type": "transactional.message.pending_delivery",
    "payload": {
      "created_at": "2024-04-03T17:08:01.349000489Z",
      "customer_uuid": "90c7e8b2-203a-4984-ba25-6cf93d8fdbac",
      "id": "dls_01htjex3p4e54rgd63kcyrq0eg",
      "message_id": "template_01hp78bjhcef9tej1tgcg0cpq2",
      "price": {
        "amount": "0.03",
        "currency": "EUR"
      },
      "status": "in_transit",
      "mcc": "208",
      "mnc": "01",
      "segment_count": 1,
      "encoding": "GSM-7",
      "correlation_id": "8709c4f8-78b4-4a3c-8167-e99164424d0f",
      "channel": "sms",
      "template_id": "tpl_01htjet67afxhta23j7dtekneh"
    },
    "created_at": "2024-04-03T17:08:01.36881397Z"
  }
  ```

  ```json Delivered theme={null}
  {
    "id": "evt_01htjex3pre54tywgzsdg1jnbn",
    "type": "transactional.message.delivered",
    "payload": {
      "created_at": "2024-04-03T17:08:01.349000489Z",
      "customer_uuid": "90c7e8b2-203a-4984-ba25-6cf93d8fdbac",
      "id": "dls_01htjex3p4e54rgd63kcyrq0eg",
      "message_id": "tx_01htjet67afxhta23j7dtekneh",
      "price": {
        "amount": "0.03",
        "currency": "EUR"
      },
      "status": "delivered",
      "mcc": "208",
      "mnc": "01",
      "segment_count": 1,
      "encoding": "GSM-7",
      "correlation_id": "8709c4f8-78b4-4a3c-8167-e99164424d0f",
      "channel": "sms",
      "template_id": "template_01hp78bjhcef9tej1tgcg0cpq2"
    },
    "created_at": "2024-04-03T17:08:01.36881397Z"
  }
  ```

  ```json Failed theme={null}
  {
    "id": "evt_01htjex3pre54tywgzsdg1jnbn",
    "type": "transactional.message.failed",
    "payload": {
      "created_at": "2024-04-03T17:08:01.349000489Z",
      "customer_uuid": "90c7e8b2-203a-4984-ba25-6cf93d8fdbac",
      "id": "dls_01htjex3p4e54rgd63kcyrq0eg",
      "message_id": "tx_01htjet67afxhta23j7dtekneh",
      "price": {
        "amount": "0.03",
        "currency": "EUR"
      },
      "mcc": "208",
      "mnc": "01",
      "status": "failed",
      "reason": "carrier_rejected",
      "correlation_id": "8709c4f8-78b4-4a3c-8167-e99164424d0f",
      "channel": "sms",
      "template_id": "template_01hp78bjhcef9tej1tgcg0cpq2"
    },
    "created_at": "2024-04-03T17:08:01.36881397Z"
  }
  ```
</RequestExample>

Prelude can notify your application about events using webhooks. There are three types of webhooks:

1. **Message delivery webhooks**: Configure using the `callback_url` parameter of your [send request](/notify/v2/api-reference/send-a-message) to receive delivery updates
2. **Inbound message webhooks**: Receive inbound WhatsApp messages from your users (see [WhatsApp 2-Way messaging](/notify/v2/documentation/whatsapp))
3. **Subscription management webhooks**: Configure through your subscription management config (managed by our Customer Success team) to receive subscription events (STOP, START, HELP)

## Message delivery events

### The event object

<ParamField path="id" type="string">
  The unique identifier of the event.
</ParamField>

<ParamField path="type" type="string">
  The type of the event, possible values are:

  | Type                                     | Description                                                                                    |
  | ---------------------------------------- | ---------------------------------------------------------------------------------------------- |
  | `transactional.message.created`          | The message was created and accepted                                                           |
  | `transactional.message.submitted`        | The message was successfully submitted to the provider API                                     |
  | `transactional.message.pending_delivery` | The message is pending delivery                                                                |
  | `transactional.message.delivered`        | Delivery succeeded                                                                             |
  | `transactional.message.failed`           | Delivery failed, [see status and reason.](/notify/v2/documentation/webhook#delivery-statuses)  |
  | `transactional.message.unknown`          | Unknown delivery, [see status and reason.](/notify/v2/documentation/webhook#delivery-statuses) |
</ParamField>

<ParamField path="payload" type="object">
  The payload of the event, whose structure depends on the event type.
</ParamField>

<ParamField path="created_at" type="RFC3339 date string">
  The timestamp of the event creation.
</ParamField>

### Delivery status payload fields

For delivery status events (`pending_delivery`, `delivered`, `failed`), the payload includes:

<ParamField path="payload.template_id" type="string">
  The unique identifier of the template used to send the message. Useful for differentiating events when multiple templates share the same callback URL.
</ParamField>

<ParamField path="payload.segment_count" type="integer">
  The actual number of SMS segments used, as reported by the provider. Only present for SMS messages.
</ParamField>

<ParamField path="payload.encoding" type="string">
  The SMS encoding type used for the message. Either `GSM-7` (standard characters, up to 160 chars per segment) or `UCS-2` (Unicode including emoji, up to 70 chars per segment). Only present for SMS messages.
</ParamField>

<ParamField path="payload.channel" type="string">
  The delivery channel used. One of `sms`, `rcs` or `whatsapp`.
</ParamField>

<ParamField path="payload.reason" type="string">
  Present on `failed` events. Provides more detail on why delivery failed. See the [Delivery statuses](#delivery-statuses) table for possible values.
</ParamField>

## How to set up your Webhook

To start receiving webhook events in your app, create and register a webhook endpoint by following the steps below.
You can register and create one endpoint to handle several different event types at once, or set up individual endpoints for specific events.

<Steps>
  <Step title="Implement the handler">
    Develop a webhook endpoint function to receive event data POST requests.
  </Step>

  <Step title="Pass the URL">
    Add your webhook endpoint URL to your Notify API requests to start
    receiving message delivery events.
  </Step>

  <Step title="Return OK">
    Return a `200 OK` HTTP response to the POST request to acknowledge receipt
    of the event. If you don't, Prelude will retry sending the event with
    exponential backoff for up to 2 weeks. Retries are spaced progressively
    further apart (1 min, 2 min, 4 min, ... up to 12 hours) to allow your
    endpoint time to recover if it's temporarily down.
  </Step>
</Steps>

## Delivery statuses

Failed delivery statuses come with a `status` field which can have the following values: `failed`, `undeliverable`, or `expired`.

An additional `reason` provides more information about the failure. Some of these may be **retriable** — Prelude will automatically retry delivery until we reach the expiry window defined in the `expires_at` field of the [Send Message request](/notify/v2/api-reference/send-a-message).

Here are the possible `reason` values:

| Reason                | Code                   | Description                                                                                                                     | Retriable                              |
| --------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- |
| Unavailable Recipient | `unavailable`          | The recipient is unavailable due to a temporary problem (e.g. no cell network).                                                 | <Icon icon="check" iconType="solid" /> |
| Generic Reason        | `generic`              | No detailed information has been received from the underlying carrier.                                                          | <Icon icon="check" iconType="solid" /> |
| Network Issue         | `network`              | The recipient's carrier network is temporary unreachable.                                                                       | <Icon icon="check" iconType="solid" /> |
| Unknown Recipient     | `unknown_recipient`    | The number is not associated with an active line.                                                                               |                                        |
| Insufficient Balance  | `insufficient_balance` | Your balance is insufficient to make an attempt.                                                                                |                                        |
| Carrier Rejected      | `carrier_rejected`     | The recipient's mobile carrier blocked the message (e.g. lack of registration, invalid content or local traffic restrictions).  |                                        |
| Unknown               | `unknown`              | The delivery status cannot be determined due to unspecified issues or lack of detailed information from the carrier or network. |                                        |

<Note>
  Delivery statuses are indicative, but they may not always be fully reliable. Depending on the market, a "delivered" confirmation can indicate delivery to the mobile network operator, the cell tower, or — in some cases — the end user's handset.
</Note>

<Note>
  Cost reconciliation can take up to 15 minutes. During this window, the billed amount may not yet reflect the final cost of the message.
</Note>

## Security

Prelude's webhooks support the following security features:

### Webhook Signature

To ensure the authenticity of the webhook events, we use a signature mechanism.

The signature is a **base64 URL-encoded RSASSA-PSS on the SHA256 hash of the payload**.

The signature is sent as a string prefixed with `rsassa-pss-sha256=` in the `X-Webhook-Signature` header of each request to your webhook endpoint.

To enable the webhook signature, go to the [Prelude dashboard](https://app.prelude.so/) in the **Verify API->Configure->Webhooks** section and generate a webhook signing key for your application.

You can then verify the signature of the webhook events in your webhook endpoint and process the event only if the signature is valid.

<Info>
  The same signing key is used for both the Verify and Notify APIs.
</Info>

### IP Whitelisting

You should whitelist the following IP addresses to ensure that your webhook endpoint receives events from Prelude:

```
34.252.67.209
52.30.192.161
34.248.153.151
```

## Inbound message events

When a user sends a WhatsApp message to your business number, Prelude forwards it as a webhook event. Use the `message_id` from the event payload to [send a reply](/notify/v2/documentation/whatsapp) within the 24-hour conversation window.

### Inbound event object

<ParamField path="id" type="string">
  The unique identifier of the event.
</ParamField>

<ParamField path="type" type="string">
  The event type: `inbound.message.received`.
</ParamField>

<ParamField path="payload" type="object">
  <ParamField path="message_id" type="string">
    The unique inbound message identifier (prefixed with `im_`). Use this value in `context.reply_to` when sending a reply.
  </ParamField>

  <ParamField path="channel" type="string">
    The channel the message was received on. Currently `whatsapp`.
  </ParamField>

  <ParamField path="from" type="string">
    The sender's phone number in E.164 format.
  </ParamField>

  <ParamField path="to" type="string">
    Your WhatsApp Business phone number that received the message.
  </ParamField>

  <ParamField path="content" type="object">
    <ParamField path="text" type="string">
      The text content of the inbound message.
    </ParamField>
  </ParamField>

  <ParamField path="conversation" type="object">
    <ParamField path="window_expires_at" type="RFC3339 date string">
      The timestamp when the 24-hour conversation window expires. Replies must be sent before this time.
    </ParamField>
  </ParamField>
</ParamField>

<ParamField path="created_at" type="RFC3339 date string">
  The timestamp of the event creation.
</ParamField>

### Example inbound message event

```json theme={null}
{
  "id": "evt_01k8aq2zggeyssvt53zgvpx63a",
  "type": "inbound.message.received",
  "payload": {
    "message_id": "im_01k8aq2zggeyssvt53zgvpx63a",
    "channel": "whatsapp",
    "from": "+33612345678",
    "to": "+14155551234",
    "content": {
      "text": "Hi, I have a question about my order"
    },
    "conversation": {
      "window_expires_at": "2025-10-25T08:59:10.736909995Z"
    }
  },
  "created_at": "2025-10-24T08:59:10.736925502Z"
}
```

## Subscription management events

When users interact with your messages by sending subscription keywords (`STOP`, `START`, or `HELP`), Prelude will send webhook events to the callback URL configured in your subscription management config.

### Subscription event object

<ParamField path="id" type="string">
  The unique identifier of the event.
</ParamField>

<ParamField path="type" type="string">
  The type of subscription event:

  | Type                           | Description                                 |
  | ------------------------------ | ------------------------------------------- |
  | `marketing.subscription.stop`  | User opted out by sending STOP keyword      |
  | `marketing.subscription.start` | User opted back in by sending START keyword |
  | `marketing.subscription.help`  | User requested help by sending HELP keyword |
</ParamField>

<ParamField path="payload" type="object">
  <ParamField path="action" type="string">
    The subscription action keyword sent by the user. Possible values: `STOP`, `START`, `HELP`.
  </ParamField>

  <ParamField path="from" type="string">
    The phone number that sent the subscription keyword (E.164 format).
  </ParamField>

  <ParamField path="to" type="string">
    The destination phone number (your short code or long code).
  </ParamField>

  <ParamField path="received_at" type="RFC3339 date string">
    The timestamp when the subscription keyword was received.
  </ParamField>
</ParamField>

<ParamField path="created_at" type="RFC3339 date string">
  The timestamp of the event creation.
</ParamField>

### Example subscription webhooks

<CodeGroup>
  ```json STOP Event theme={null}
  {
    "id": "evt_01k8aq2zggeyssvt53zgvpx63a",
    "type": "marketing.subscription.stop",
    "payload": {
      "action": "STOP",
      "from": "+33612345678",
      "to": "36184",
      "received_at": "2025-10-24T08:59:10.736909995Z"
    },
    "created_at": "2025-10-24T08:59:10.736925502Z"
  }
  ```

  ```json START Event theme={null}
  {
    "id": "evt_01k8aq2zggeyssvt53zgvpx63a",
    "type": "marketing.subscription.start",
    "payload": {
      "action": "START",
      "from": "+33612345678",
      "to": "36184",
      "received_at": "2025-10-24T08:59:10.736909995Z"
    },
    "created_at": "2025-10-24T08:59:10.736925502Z"
  }
  ```

  ```json HELP Event theme={null}
  {
    "id": "evt_01k8aq2zggeyssvt53zgvpx63a",
    "type": "marketing.subscription.help",
    "payload": {
      "action": "HELP",
      "from": "+33612345678",
      "to": "36184",
      "received_at": "2025-10-24T08:59:10.736909995Z"
    },
    "created_at": "2025-10-24T08:59:10.736925502Z"
  }
  ```
</CodeGroup>

### Handling subscription events

When you receive a subscription event:

* **STOP**: The user has been automatically unsubscribed. Subsequent API calls for this phone number will return an error.
* **START**: The user has been resubscribed and can receive marketing messages again.
* **HELP**: The user requested information. A default help message is automatically sent, but you may want to log this event.

## Carrier disconnect events

When a phone number is disconnected by its carrier (e.g. line terminated, number ported, or prepaid plan expired), Prelude automatically unsubscribes it from all subscription management configs and sends a webhook event to each config's callback URL.

<Note>
  Unlike subscription events, carrier disconnect events are **not** initiated by the user. The carrier reported the number as disconnected. You should still remove the number from your subscriber lists — disconnected numbers can be reassigned to new subscribers who never consented to your messages.
</Note>

### Carrier disconnect event object

<ParamField path="id" type="string">
  The unique identifier of the event.
</ParamField>

<ParamField path="type" type="string">
  Always `marketing.subscription.carrier_disconnect`.
</ParamField>

<ParamField path="payload" type="object">
  <ParamField path="phone_number" type="string">
    The disconnected phone number in E.164 format.
  </ParamField>

  <ParamField path="disconnected_at" type="string">
    The date the carrier reported the disconnect (`YYYY-MM-DD` format).
  </ParamField>
</ParamField>

<ParamField path="created_at" type="RFC3339 date string">
  The timestamp of the event creation.
</ParamField>

### Example carrier disconnect webhook

<CodeGroup>
  ```json Carrier Disconnect Event theme={null}
  {
    "id": "evt_01k8aq2zggeyssvt53zgvpx63a",
    "type": "marketing.subscription.carrier_disconnect",
    "payload": {
      "phone_number": "+12025551234",
      "disconnected_at": "2026-04-13"
    },
    "created_at": "2026-04-14T15:30:00.000000000Z"
  }
  ```
</CodeGroup>

### Handling carrier disconnect events

When you receive a carrier disconnect event:

* The phone number has been automatically unsubscribed from all subscription management configs.
* Future API calls to send marketing messages to this number will return an error.
* You should remove this number from all your subscriber lists and campaigns.
