# Guide to Webex Webhooks: Features and Best Practices

Webex webhooks tell your bot or integration what is happening in Webex: someone posts a message or mentions your bot, a user submits an adaptive card, a person joins a space, a meeting starts or ends, a recording or transcript is ready, or a call is placed. Without them, a Webex app has to poll the API for changes. With them, Webex sends each change to your backend as it happens.

This guide covers how to register Webex webhooks, how to sign and verify them, the resources and events available, why the payload rarely contains what you need, and the best practices for production.

## What are Webex webhooks?

A Webex webhook is a registration made through the Webhooks API for one resource and one event, such as `messages` and `created`, with an optional filter to narrow it down to a room, person or meeting host. Users, bots, integrations and org admins can all register them. When a matching event happens, Webex sends a JSON `POST` to your `targetUrl`.

Signing is optional. If you include a `secret` when you create the webhook, Webex adds an `X-Spark-Signature` header with a hex HMAC-SHA1 of the raw body to every delivery. If you don't, deliveries are unsigned. Webex doesn't publish an IP allowlist and says the signature is the way to authenticate requests, so always set a secret.

This guide is about platform webhooks, which send Webex events out to you. Webex's "Incoming Webhooks" work in the other direction, posting into a Webex space from outside.

## Webex webhook features

| Feature | Details |
| --- | --- |
| Configuration | API only: `POST https://webexapis.com/v1/webhooks` with an OAuth token |
| Subscription | One `resource` and `event` per webhook, with an optional `filter`, or a firehose with `all` |
| Org-wide webhooks | `ownedBy: "org"`, for admins and compliance officers |
| Signature header | `X-Spark-Signature`, only when the webhook has a `secret` |
| Scheme | HMAC-SHA1, hex digest |
| Signed content | The raw request body |
| Secret | Chosen by you per webhook |
| Newer header | `X-Webex-Signature` (HMAC-SHA256 or SHA512) on some webhook types, format undocumented |
| Event name | `resource` and `event`, two separate fields |
| Event ID | None. The envelope `id` is the webhook's ID |
| Auto-disable | After 100 failed deliveries within five minutes |
| Source IPs | Not published, by design |

Webex also limits how many webhooks you can register: 30,000 per user for personal webhooks, and one org-wide webhook, with more allowed for different resources and filters.

## Common events

Webex documents 44 resource and event pairs. The main groups are:

| Resource | Events | Fires when |
| --- | --- | --- |
| `messages` | `created`, `deleted` | A message is posted or deleted in a space your app can see |
| `memberships` | `created`, `updated`, `deleted` | A membership of a space is created, updated or deleted |
| `rooms` | `created`, `updated`, `migrated` | A space is created, changed or migrated |
| `attachmentActions` | `created` | A user submits one of your adaptive cards |
| `meetings` | `created`, `updated`, `deleted`, `started`, `ended` | A meeting is scheduled, changed, cancelled, starts or ends |
| `meetingParticipants` | `joined`, `left` | Someone joins or leaves a meeting |
| `recordings`, `convergedRecordings`, `meetingTranscripts` | `created` and others | A recording or transcript is ready, changed or deleted |
| `telephony_calls`, `telephony_conference`, `telephony_mwi` and others | `created`, `updated`, `deleted` | Webex Calling activity for Webex Calling users |
| `applications`, `serviceApp` | `created`, `updated`, `deleted`, `authorized`, `deauthorized` | Apps in your org change, or a service app is authorised or deauthorised |

Bots can only see messages in which they are specifically mentioned, which limits what a bot's `messages` webhooks receive.

The firehose saves registering each pair. Set `event` to `all` for `messages`, `memberships` or `rooms`, or set both `resource` and `event` to `all` to receive all three. The firehose covers only `created`, `updated` and `deleted`, so meeting start and end, and participant join and leave, need their own webhooks.

Every delivery has the same envelope. Here is Webex's example for `messages` and `created`:

```json
{
  "id": "Y2lzY29zcGFyazovL3VzL1dFQkhPT0svZjRlNjA1NjAtNjYwMi00ZmIwLWEyNWEtOTQ5ODgxNjA5NDk3",
  "name": "New message in 'Project Unicorn' room",
  "resource": "messages",
  "event": "created",
  "filter": "roomId=Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0",
  "orgId": "OTZhYmMyYWEtM2RjYy0xMWU1LWExNTItZmUzNDgxOWNkYzlh",
  "createdBy": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY",
  "appId": "Y2lzY29zcGFyazovL3VzL0FQUExJQ0FUSU9OL0MyNzljYjMwYzAyOTE4MGJiNGJkYWViYjA2MWI3OTY1Y2RhMzliNjAyOTdjODUwM2YyNjZhYmY2NmM5OTllYzFm",
  "ownedBy": "creator",
  "status": "active",
  "actorId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY",
  "data": {
    "id": "Y2lzY29zcGFyazovL3VzL01FU1NBR0UvOTJkYjNiZTAtNDNiZC0xMWU2LThhZTktZGQ1YjNkZmM1NjVk",
    "roomId": "Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0",
    "personId": "Y2lzY29zcGFyazovL3VzL1BFT1BMRS9mNWIzNjE4Ny1jOGRkLTQ3MjctOGIyZi1mOWM0NDdmMjkwNDY",
    "personEmail": "matt@example.com",
    "created": "2015-10-18T14:26:16.000Z"
  }
}

```

`id`, `name`, `filter`, `ownedBy` and `status` describe the webhook registration, not the event. `actorId` is the person who caused it, and `data` is the resource. Note that there's no `text` in `data`: Webex encrypts message text and space titles, so it leaves them out, and you fetch the full message with `GET https://webexapis.com/v1/messages/{id}`. Use the envelope's `orgId`, not the one inside `data`, which Webex says is for internal use and will be removed.

## Setting up Webex webhooks

1. Get an OAuth token with the read scope for the resource, for example `spark:messages_read` for messages or `spark:memberships_read` for memberships. Meeting webhooks have their own scopes, such as `meeting:schedules_read` and `meeting:participants_read`.
2. Generate a high-entropy secret, unique to this webhook, and store it, for example as `WEBEX_WEBHOOK_SECRET`.
3. Register the webhook:

```bash
curl -X POST "https://webexapis.com/v1/webhooks" \
  -H "Authorization: Bearer $WEBEX_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "New message in Project Unicorn",
    "targetUrl": "https://example.com/webhooks/webex",
    "resource": "messages",
    "event": "created",
    "filter": "roomId=Y2lzY29zcGFyazovL3VzL1JPT00vYmJjZWIxYWQtNDNmMS0zYjU4LTkxNDctZjE0YmIwYzRkMTU0",
    "secret": "YOUR_GENERATED_SECRET"
  }'

```

Combine several filters with `&`, for example `personEmail=person@example.com&roomId=abc123`. For org-wide meeting webhooks filtered by `hostEmail` or `hostUserId`, set `ownedBy` to `org`. A webhook keeps sending after the token used to create it expires, as long as your endpoint keeps acknowledging deliveries.

The `targetUrl` must be publicly reachable. For local development, `hookdeck listen 3000 webex --path /webhooks/webex` gives you a public HTTPS URL that forwards to your machine.

## Securing Webex webhooks

Compute an HMAC-SHA1 over the raw request bytes with your webhook's secret, hex-encode it, and compare it with `X-Spark-Signature` in constant time. Webex notes that the header may arrive in lower case over HTTP/2, so look it up case-insensitively.

```javascript
const crypto = require('crypto');
const express = require('express');

function verifyWebexSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  const expected = crypto.createHmac('sha1', secret).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/webex', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.WEBEX_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  // req.get() is case-insensitive, so x-spark-signature over HTTP/2 is found too.
  if (!verifyWebexSignature(req.body, req.get('x-spark-signature'), secret)) {
    return res.sendStatus(401);
  }

  const notification = JSON.parse(req.body.toString('utf8'));
  const event_type = `${notification.resource}.${notification.event}`; // e.g. messages.created
  // Queue the work, then fetch the full resource with notification.data.id if you need it.
  res.sendStatus(200);
});

```

The same check in Python:

```python
import hashlib
import hmac

def verify_webex_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).hexdigest()
    return hmac.compare_digest(signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_webex_signature(raw_body, request.headers.get("x-spark-signature"),
#                        os.environ["WEBEX_WEBHOOK_SECRET"])

```

Webex also sends `X-Webex-Signature`, with HMAC-SHA256 or HMAC-SHA512, for "supported webhook types", and recommends preferring it where present. Its docs don't describe the header's format or list the types that send it, so `X-Spark-Signature`, which Webex calls the compatibility path and sends for every webhook with a secret, is the one you can implement from the documentation.

> **Make Webex webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies `X-Spark-Signature`, accepts every Webex delivery so a slow backend never trips the auto-disable limit, and durably queues messages, meeting and calling events.

## Webex webhook limitations and pain points

### Webhooks without a secret are unsigned

**The Problem:** A webhook created without a `secret` arrives with no signature, and anyone who learns your URL can send fake events to your bot.

**Why It Happens:** `secret` is an optional field on the create call, and Webex publishes no IP list to fall back on.

**Workarounds:**

* Always set a unique secret per webhook, and reject unsigned requests.
* List your webhooks with `GET /webhooks` and re-create any that were registered without one.

**How Hookdeck Can Help:** Hookdeck's Webex source type verifies `X-Spark-Signature` with the secret you set. Verification is optional on the source, so leave it off for a webhook that has no secret, and turn it on once you have re-created the webhook with one.

### The auto-disable threshold

**The Problem:** A short outage switches your integration off, and it stays off.

**Why It Happens:** If 100 deliveries within five minutes fail, because your server is unreachable or returns a non-`2xx`, Webex disables the webhook. It is not re-enabled automatically. You have to set `status` back to `active` with the Update a Webhook API, and if you do that within five minutes, the next failure disables it again immediately. Busy firehose webhooks can reach 100 deliveries quickly.

**Workarounds:**

* Fix the cause before re-enabling, and check webhook status with Get Webhook Details after any incident.
* Acknowledge fast, and return `2xx` for anything you have verified, even if you can't process it yet.

**How Hookdeck Can Help:** Hookdeck accepts Webex deliveries even when your service is down, so Webex sees success and never counts failures towards the limit. Hookdeck then [retries delivery](/docs/retries) to your service until it recovers.

### No event ID, and duplicates happen

**The Problem:** Your bot replies twice to the same message.

**Why It Happens:** Webex says your app can receive more than one notification for an event. The envelope `id` identifies the webhook, not the event, so it's the same on every delivery from that webhook.

**Workarounds:**

* Deduplicate on `resource`, `event` and `data.id` together, rather than on the envelope `id`.
* Make side effects safe to repeat.

**How Hookdeck Can Help:** Hookdeck's [deduplication](/docs/deduplication) can drop repeats based on `resource`, `event` and `data.id` before they reach your bot.

### The payload doesn't have what you need

**The Problem:** Your bot receives `messages.created` but can't read the message.

**Why It Happens:** Message text and space titles are encrypted, so Webex omits them from `data`. For meetings, Webex says the REST resource can take up to 10 minutes to catch up with the webhook in 99.5% of cases, so an immediate `GET /meetingParticipants/{id}` can return `404`.

**Workarounds:**

* Fetch messages with `GET /messages/{id}` using your token.
* For meeting resources, retry the follow-up read with backoff on `404`.

**How Hookdeck Can Help:** Hookdeck can [limit the delivery rate](/docs/destinations#set-a-max-delivery-rate) to your service, so a burst of firehose events doesn't turn into a burst of follow-up API calls.

## Best practices

### Verify against the raw body

Webex says not to parse, re-serialise or normalise the JSON before verifying. Use `express.raw()` in Express, `await request.text()` in Next.js, and `await request.body()` in FastAPI.

### Build the event name from two fields

Combine `resource` and `event` into one key, such as `messages.created`, and route on that. `event` alone, such as `created`, is shared by many resources.

### Use filters to cut volume

Filter by `roomId`, `personEmail` or meeting host rather than taking everything and discarding most of it. Fewer deliveries means less load and less risk of reaching the disable threshold.

### Acknowledge fast and process asynchronously

Webex doesn't document a timeout, but every failure counts towards the disable threshold. Verify, store, return `200`, and process in the background. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

Webex signs webhooks only when you register them with a `secret`, using a hex HMAC-SHA1 of the raw body in `X-Spark-Signature`. Always set a secret, verify the raw bytes, combine `resource` and `event` to route, fetch sensitive data such as message text from the API, and keep your endpoint healthy, because 100 failures in five minutes disable the webhook until you turn it back on.

[Hookdeck Event Gateway](https://hookdeck.com) verifies every Webex signature, answers Webex even while your service is down, and holds each event until your bot or integration has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle Webex webhooks reliably in minutes.