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:
{
"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
- Get an OAuth token with the read scope for the resource, for example
spark:messages_readfor messages orspark:memberships_readfor memberships. Meeting webhooks have their own scopes, such asmeeting:schedules_readandmeeting:participants_.read - Generate a high-entropy secret, unique to this webhook, and store it, for example as
WEBEX_WEBHOOK_SECRET. - Register the webhook:
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.
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:
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 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 /webhooksand 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
2xxfor 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 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,eventanddata.idtogether, rather than on the envelopeid. - Make side effects safe to repeat.
How Hookdeck Can Help: Hookdeck's 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 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.
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 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 for free and handle Webex webhooks reliably in minutes.