Gareth Wilson Gareth Wilson

Guide to Webex Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationAPI only: POST https://webexapis.com/v1/webhooks with an OAuth token
SubscriptionOne resource and event per webhook, with an optional filter, or a firehose with all
Org-wide webhooksownedBy: "org", for admins and compliance officers
Signature headerX-Spark-Signature, only when the webhook has a secret
SchemeHMAC-SHA1, hex digest
Signed contentThe raw request body
SecretChosen by you per webhook
Newer headerX-Webex-Signature (HMAC-SHA256 or SHA512) on some webhook types, format undocumented
Event nameresource and event, two separate fields
Event IDNone. The envelope id is the webhook's ID
Auto-disableAfter 100 failed deliveries within five minutes
Source IPsNot 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:

ResourceEventsFires when
messagescreated, deletedA message is posted or deleted in a space your app can see
membershipscreated, updated, deletedA membership of a space is created, updated or deleted
roomscreated, updated, migratedA space is created, changed or migrated
attachmentActionscreatedA user submits one of your adaptive cards
meetingscreated, updated, deleted, started, endedA meeting is scheduled, changed, cancelled, starts or ends
meetingParticipantsjoined, leftSomeone joins or leaves a meeting
recordings, convergedRecordings, meetingTranscriptscreated and othersA recording or transcript is ready, changed or deleted
telephony_calls, telephony_conference, telephony_mwi and otherscreated, updated, deletedWebex Calling activity for Webex Calling users
applications, serviceAppcreated, updated, deleted, authorized, deauthorizedApps 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

  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:
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 /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 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 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.