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

## Signature parameters

The signature is produced with RSASSA-PSS using the following parameters. Use them exactly when verifying:

| Parameter             | Value                                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------------------- |
| Signature scheme      | RSASSA-PSS                                                                                                  |
| Hash                  | SHA-256                                                                                                     |
| Mask generation (MGF) | MGF1 with SHA-256                                                                                           |
| Trailer field         | `0xBC` (the standard value, sometimes called "trailer field 1")                                             |
| Signing key           | 2048-bit RSA                                                                                                |
| Salt length           | **Maximum** — `ceil((modulusBits − 1) / 8) − hashLen − 2`, i.e. **222 bytes** for the current 2048-bit keys |
| Signed message        | The **raw HTTP request body bytes**, exactly as received — do not re-serialize or reformat the JSON         |
| Signature encoding    | Base64URL (with `=` padding), prefixed with `rsassa-pss-sha256=`                                            |

<Warning>
  **Salt length.** Prelude signs with the *maximum* PSS salt length, not a fixed 32 bytes.
  Most libraries **auto-detect the salt length when verifying** (OpenSSL, Go's `crypto/rsa`,
  Node.js `crypto`, Ruby's `openssl`, and Python's `cryptography`), so you don't need to set it.
  Libraries that require an explicit salt length — such as Java, Kotlin, and PHP (phpseclib) —
  should compute it from the key (`keyBytes − hashLen − 2`) rather than hardcoding `222`, so it
  keeps working if the key size ever changes.
</Warning>

## Verifying the signature

The public key to verify the signature is a PEM-encoded `SUBJECT PUBLIC KEY INFO` (SPKI /
`-----BEGIN PUBLIC KEY-----`). For the Verify and Notify APIs you obtain it from the dashboard
when you generate the signing key.

In the examples below, libraries that support automatic salt-length detection
(Node.js, Python, Go, Ruby, OpenSSL) need no salt configuration. Libraries that
require an explicit salt length (Java, Kotlin, PHP) compute it from the key so
the code keeps working across key sizes.

<CodeGroup>
  ```js Node.js theme={null}
  const crypto = require("crypto");

  // rawBody must be a Buffer of the exact bytes received (e.g. express.raw()),
  // NOT a re-stringified JSON object.
  function verifyWebhook(rawBody, signatureHeader, publicKeyPem) {
    const signature = Buffer.from(
      signatureHeader.replace(/^rsassa-pss-sha256=/, ""),
      "base64url",
    );

    return crypto.verify(
      "sha256",
      rawBody,
      {
        key: publicKeyPem,
        padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
        // Auto-detect the salt length used at signing time.
        saltLength: crypto.constants.RSA_PSS_SALTLEN_AUTO,
      },
      signature,
    );
  }
  ```

  ```python Python theme={null}
  import base64
  from cryptography.exceptions import InvalidSignature
  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import padding


  # raw_body must be the exact bytes received, not a re-serialized dict.
  def verify_webhook(raw_body: bytes, signature_header: str, public_key_pem: bytes) -> bool:
      public_key = serialization.load_pem_public_key(public_key_pem)
      signature = base64.urlsafe_b64decode(
          signature_header.removeprefix("rsassa-pss-sha256=")
      )
      try:
          public_key.verify(
              signature,
              raw_body,
              # PSS.AUTO auto-detects the salt length (cryptography >= 42.0.0).
              padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.AUTO),
              hashes.SHA256(),
          )
          return True
      except InvalidSignature:
          return False
  ```

  ```go Go theme={null}
  import (
  	"crypto"
  	"crypto/rsa"
  	"crypto/sha256"
  	"encoding/base64"
  	"strings"
  )

  // body is the exact request body bytes.
  func verifyWebhook(body []byte, signatureHeader string, pub *rsa.PublicKey) error {
  	raw, err := base64.URLEncoding.DecodeString(
  		strings.TrimPrefix(signatureHeader, "rsassa-pss-sha256="),
  	)
  	if err != nil {
  		return err
  	}

  	sum := sha256.Sum256(body)
  	// Passing nil PSSOptions verifies with an auto-detected salt length.
  	return rsa.VerifyPSS(pub, crypto.SHA256, sum[:], raw, nil)
  }
  ```

  ```java Java theme={null}
  import java.security.Signature;
  import java.security.interfaces.RSAPublicKey;
  import java.security.spec.MGF1ParameterSpec;
  import java.security.spec.PSSParameterSpec;
  import java.util.Base64;

  // rawBody must be the exact bytes received, not a re-serialized JSON string.
  // Java (JCA) cannot auto-detect the PSS salt length, so compute it from the key.
  static boolean verifyWebhook(byte[] rawBody, String signatureHeader, RSAPublicKey publicKey)
          throws Exception {
      byte[] signature = Base64.getUrlDecoder()
              .decode(signatureHeader.replaceFirst("^rsassa-pss-sha256=", ""));

      int emLen = (publicKey.getModulus().bitLength() - 1 + 7) / 8;
      int saltLength = emLen - 32 - 2; // hashLen = 32 for SHA-256; == 222 for a 2048-bit key

      Signature verifier = Signature.getInstance("RSASSA-PSS");
      verifier.setParameter(new PSSParameterSpec(
              "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, saltLength, 1));
      verifier.initVerify(publicKey);
      verifier.update(rawBody);
      return verifier.verify(signature);
  }
  ```

  ```kotlin Kotlin theme={null}
  import java.security.Signature
  import java.security.interfaces.RSAPublicKey
  import java.security.spec.MGF1ParameterSpec
  import java.security.spec.PSSParameterSpec
  import java.util.Base64

  // rawBody must be the exact bytes received, not a re-serialized JSON string.
  // Like Java, Kotlin/JCA cannot auto-detect the salt length, so compute it.
  fun verifyWebhook(rawBody: ByteArray, signatureHeader: String, publicKey: RSAPublicKey): Boolean {
      val signature = Base64.getUrlDecoder()
          .decode(signatureHeader.removePrefix("rsassa-pss-sha256="))

      val emLen = (publicKey.modulus.bitLength() - 1 + 7) / 8
      val saltLength = emLen - 32 - 2 // hashLen = 32 for SHA-256; == 222 for a 2048-bit key

      return Signature.getInstance("RSASSA-PSS").run {
          setParameter(PSSParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, saltLength, 1))
          initVerify(publicKey)
          update(rawBody)
          verify(signature)
      }
  }
  ```

  ```ruby Ruby theme={null}
  require "openssl"
  require "base64"

  # raw_body must be the exact request body bytes.
  def verify_webhook(raw_body, signature_header, public_key_pem)
    signature = Base64.urlsafe_decode64(
      signature_header.sub(/\Arsassa-pss-sha256=/, "")
    )
    public_key = OpenSSL::PKey::RSA.new(public_key_pem)

    # salt_length: :auto detects the salt length used at signing time.
    public_key.verify_pss("SHA256", signature, raw_body,
                          salt_length: :auto, mgf1_hash: "SHA256")
  end
  ```

  ```php PHP theme={null}
  <?php
  // composer require phpseclib/phpseclib
  use phpseclib3\Crypt\PublicKeyLoader;
  use phpseclib3\Crypt\RSA;

  // $rawBody must be the exact request body bytes (e.g. file_get_contents("php://input")).
  function verifyWebhook(string $rawBody, string $signatureHeader, string $publicKeyPem): bool {
      $b64 = strtr(preg_replace('/^rsassa-pss-sha256=/', '', $signatureHeader), '-_', '+/');
      $signature = base64_decode($b64);

      $key = PublicKeyLoader::loadPublicKey($publicKeyPem);
      $saltLength = intdiv($key->getLength(), 8) - 32 - 2; // == 222 for a 2048-bit key

      $key = $key
          ->withPadding(RSA::SIGNATURE_PSS)
          ->withHash('sha256')
          ->withMGFHash('sha256')
          ->withSaltLength($saltLength);

      return $key->verify($rawBody, $signature);
  }
  ```

  ```bash OpenSSL (CLI) theme={null}
  # body.bin   : the raw request body bytes
  # public_key.pem : the signing public key
  # Extract and base64url-decode the signature from the header value:
  printf '%s' "$X_WEBHOOK_SIGNATURE" \
    | sed 's/^rsassa-pss-sha256=//' \
    | tr '_-' '/+' \
    | base64 -d > sig.bin

  # rsa_pss_saltlen:-2 auto-detects the salt length.
  openssl dgst -sha256 \
    -sigopt rsa_padding_mode:pss \
    -sigopt rsa_pss_saltlen:-2 \
    -sigopt rsa_mgf1_md:sha256 \
    -verify public_key.pem \
    -signature sig.bin \
    body.bin
  ```
</CodeGroup>

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