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

# Vobiz Messaging Webhooks

> Receive SMS, MMS and RCS events on your server - create webhook subscriptions with the Vobiz Messaging API, verify the X-Webhook-Signature HMAC, de-duplicate on X-Webhook-ID, and handle delivery-status and inbound-message payloads in Node or Python.

A webhook subscription tells Vobiz to `POST` messaging events to a URL you own: delivery status changes, inbound messages and broadcast progress. Each delivery is signed with a secret you choose, so your server can confirm it came from Vobiz.

You manage subscriptions through the [`/webhooks`](/docs/sms/api/webhooks/create-webhook) endpoints or in the console under **SMS & RCS → Webhooks**.

<Note>
  Messaging webhooks use their own headers and signing scheme, described on this page. Voice callbacks (call status, VobizXML requests) use the `X-Vobiz-Signature-*` headers described in [Validating callbacks](/docs/concepts/validating-callbacks). Keep the two verifiers separate.
</Note>

## Events

Subscribe to any of these event names. The default subscription, used when you omit `events`, is `message.received`, `message.delivered` and `message.failed`.

| Event | When it fires |
| - | - |
| `message.status` | The delivery state of a message you sent changed. |
| `message.delivered` | A message you sent reached the handset. |
| `message.failed` | A message you sent could not be delivered. |
| `message.received` | A contact sent a message to one of your numbers or RCS senders. |
| `broadcast.updated` | A broadcast changed status or its counters moved. |

Vobiz delivers an event to a subscription only when that subscription is `active` and its `events` list contains the exact event name.

## Manage subscriptions

### Create

[`POST /webhooks`](/docs/sms/api/webhooks/create-webhook) with `url` (required), `secret` and `events`. Use an HTTPS URL and a long random secret.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.vobiz.ai/api/v1/sms/webhooks" \
    -H "X-Auth-ID: MA_XXXXXXXX" \
    -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://example.com/webhooks/vobiz-messaging",
      "secret": "a-long-random-secret-you-generate",
      "events": ["message.received", "message.delivered", "message.failed"]
    }'
  ```

  ```python Python theme={null}
  import requests

  BASE = "https://api.vobiz.ai/api/v1/sms"
  HEADERS = {
      "X-Auth-ID": "MA_XXXXXXXX",
      "X-Auth-Token": "YOUR_AUTH_TOKEN",
      "Content-Type": "application/json",
  }

  r = requests.post(
      f"{BASE}/webhooks",
      headers=HEADERS,
      json={
          "url": "https://example.com/webhooks/vobiz-messaging",
          "secret": "a-long-random-secret-you-generate",
          "events": ["message.received", "message.delivered", "message.failed"],
      },
  )
  r.raise_for_status()
  print(r.json()["id"])
  ```

  ```javascript Node theme={null}
  const BASE = "https://api.vobiz.ai/api/v1/sms";
  const headers = {
    "X-Auth-ID": "MA_XXXXXXXX",
    "X-Auth-Token": "YOUR_AUTH_TOKEN",
    "Content-Type": "application/json",
  };

  const res = await fetch(`${BASE}/webhooks`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      url: "https://example.com/webhooks/vobiz-messaging",
      secret: "a-long-random-secret-you-generate",
      events: ["message.received", "message.delivered", "message.failed"],
    }),
  });
  if (!res.ok) throw new Error(await res.text());
  console.log((await res.json()).id);
  ```
</CodeGroup>

The response is `201` with the subscription:

```json theme={null}
{
  "id": "c4b1d7e2-9f3a-4c5d-8e6f-1a2b3c4d5e6f",
  "accountId": "MA_XXXXXXXX",
  "url": "https://example.com/webhooks/vobiz-messaging",
  "secret": "a-long-random-secret-you-generate",
  "events": ["message.received", "message.delivered", "message.failed"],
  "active": true,
  "createdAt": "2026-10-12T10:30:00Z"
}
```

<Warning>
  The API returns `secret` in clear text on create, list and get. Anyone who can read your subscriptions can forge signed deliveries, so protect your Auth ID and Auth Token as you would the secret itself, and rotate the secret with `PUT` if it leaks.
</Warning>

### List, get, update, delete

| Action | Endpoint | Result |
| - | - | - |
| List | [`GET /webhooks`](/docs/sms/api/webhooks/list-webhooks) | `200` array of subscriptions, newest first |
| Get | [`GET /webhooks/{id}`](/docs/sms/api/webhooks/get-webhook) | `200` subscription |
| Update | [`PUT /webhooks/{id}`](/docs/sms/api/webhooks/update-webhook) | Any subset of `url`, `secret`, `events`, `active`. `200 {"status": "ok"}` |
| Delete | [`DELETE /webhooks/{id}`](/docs/sms/api/webhooks/delete-webhook) | `200 {"status": "ok"}` |

Set `active: false` to pause deliveries without losing the subscription, for example during a deployment.

```bash cURL theme={null}
curl -X PUT "https://api.vobiz.ai/api/v1/sms/webhooks/c4b1d7e2-9f3a-4c5d-8e6f-1a2b3c4d5e6f" \
  -H "X-Auth-ID: MA_XXXXXXXX" \
  -H "X-Auth-Token: YOUR_AUTH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"active": false}'
```

## The delivery request

Vobiz sends an HTTP `POST` to your `url` with a JSON body and these headers:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `X-Webhook-ID` | A UUID that identifies this event. The same event keeps the same ID across redeliveries, so use it to de-duplicate. |
| `X-Webhook-Event` | The event name, for example `message.delivered`. Route on it before parsing the body. |
| `X-Webhook-Signature` | Lower-case hex HMAC-SHA256 of the raw request body, keyed with your subscription `secret`. No prefix. |

The body is always the same envelope:

```json theme={null}
{
  "event": "message.delivered",
  "timestamp": "2026-10-12T10:31:04.512Z",
  "data": { }
}
```

### Delivery-status payload

For `message.status`, `message.delivered` and `message.failed`, `data` describes the message and its new state:

```json theme={null}
{
  "event": "message.delivered",
  "timestamp": "2026-10-12T10:31:04.512Z",
  "data": {
    "messageId": "9b2e1c40-8a3d-4f5e-9c1a-7d6b2e8f4a01",
    "from": "+14155550100",
    "to": "+14155550123",
    "data": {
      "status": "delivered"
    }
  }
}
```

On a failure, the inner `data` also carries the reason:

```json theme={null}
{
  "event": "message.failed",
  "timestamp": "2026-10-12T10:31:09.004Z",
  "data": {
    "messageId": "9b2e1c40-8a3d-4f5e-9c1a-7d6b2e8f4a02",
    "from": "+14155550100",
    "to": "+14155550124",
    "data": {
      "status": "failed",
      "errorCode": "EC_ABSENT_SUBSCRIBER_SM",
      "errorMessage": "Absent Subscriber"
    }
  }
}
```

`errorCode` and `errorMessage` are present only when the network reported them. Look the message up with [`GET /messages/{id}`](/docs/sms/api/messages/get-message) for the full record and its `statusHistory`.

### Responding

* Return any `2xx` status within **30 seconds**. A `3xx`, `4xx`, `5xx` or a timeout counts as a failed delivery.
* Vobiz redelivers a failed event until your endpoint accepts it. Because a slow response can also trigger a redelivery, treat every delivery as possibly repeated and de-duplicate on `X-Webhook-ID`.
* Verify the signature first, acknowledge, then do your own processing asynchronously.

## Verify the signature

Compute HMAC-SHA256 over the **raw** request bytes with your secret, hex-encode it in lower case, and compare it to `X-Webhook-Signature` with a constant-time comparison. Do not re-serialise the parsed JSON; whitespace changes break the digest.

<CodeGroup>
  ```javascript Node (Express) theme={null}
  import express from "express";
  import crypto from "node:crypto";

  const SECRET = process.env.VOBIZ_WEBHOOK_SECRET; // the secret you registered
  const seen = new Set(); // replace with a shared store in production

  function verify(rawBody, signatureHeader) {
    const expected = crypto.createHmac("sha256", SECRET).update(rawBody).digest("hex");
    const a = Buffer.from(expected, "utf8");
    const b = Buffer.from(signatureHeader ?? "", "utf8");
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  }

  const app = express();

  app.post(
    "/webhooks/vobiz-messaging",
    express.raw({ type: "application/json" }), // keep the raw bytes
    (req, res) => {
      if (!verify(req.body, req.get("X-Webhook-Signature"))) {
        return res.status(401).end();
      }
      const id = req.get("X-Webhook-ID");
      if (seen.has(id)) return res.status(200).end(); // redelivery
      seen.add(id);

      const envelope = JSON.parse(req.body.toString("utf8"));
      res.status(200).end(); // acknowledge quickly

      switch (req.get("X-Webhook-Event")) {
        case "message.delivered":
        case "message.failed":
        case "message.status":
          console.log(envelope.data.messageId, envelope.data.data.status);
          break;
        case "message.received":
          console.log("inbound from", envelope.data.from);
          break;
      }
    }
  );

  app.listen(3000);
  ```

  ```python Python (Flask) theme={null}
  import hmac
  import hashlib
  import os

  from flask import Flask, request, abort

  SECRET = os.environ["VOBIZ_WEBHOOK_SECRET"].encode()  # the secret you registered
  seen = set()  # replace with a shared store in production

  app = Flask(__name__)


  def verify(raw_body: bytes, signature: str | None) -> bool:
      expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
      return hmac.compare_digest(expected, signature or "")


  @app.post("/webhooks/vobiz-messaging")
  def vobiz_messaging():
      raw = request.get_data()  # raw bytes, before any JSON parsing
      if not verify(raw, request.headers.get("X-Webhook-Signature")):
          abort(401)

      event_id = request.headers.get("X-Webhook-ID")
      if event_id in seen:
          return "", 200  # redelivery
      seen.add(event_id)

      envelope = request.get_json(force=True)
      event = request.headers.get("X-Webhook-Event")
      if event in ("message.delivered", "message.failed", "message.status"):
          print(envelope["data"]["messageId"], envelope["data"]["data"]["status"])
      elif event == "message.received":
          print("inbound from", envelope["data"]["from"])
      return "", 200
  ```
</CodeGroup>

<Tip>
  To test locally, create a subscription pointing at a tunnel URL, send yourself a message with [`POST /messages`](/docs/sms/api/messages/send-message), and log the headers and body you receive. Replace the in-memory `seen` set with your database or cache before going to production.
</Tip>

## Webhooks or WebSocket?

Webhooks are the durable path: events are redelivered until your backend accepts them, and they work without a persistent connection. The [WebSocket](/docs/sms/websocket) is for live dashboards and consoles that need updates on screen while they are open. Use both when you need both.

## Next steps

<CardGroup cols={2}>
  <Card title="Webhook endpoints" icon="code" href="/docs/sms/api/webhooks/create-webhook">
    Full subscription schema.
  </Card>

  <Card title="Conversations" icon="comments" href="/docs/sms/conversations">
    Threads built from the inbound messages your webhook receives.
  </Card>
</CardGroup>

## Before you go live

<CardGroup cols={2}>
  <Card title="Messaging numbers" icon="hashtag" href="/docs/sms/numbers">
    Get a number that sends and receives.
  </Card>

  <Card title="Compliance overview" icon="shield-check" href="/docs/sms/compliance/overview">
    Register before production traffic.
  </Card>
</CardGroup>


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