Using a Webhook Secret
March 1, 2023

Updated September 2026: This post has been refreshed with current Webex webhook security guidance.
Webhooks send HTTP POST requests to the public URL you provide. A shared secret lets your receiver verify the request before it parses or processes the payload.
anchorCreate a webhook with a secret
anchorAdd a secret when you create a webhook. For example:
{
"name": "New message in a project room",
"targetUrl": "https://example.com/webex-events",
"resource": "messages",
"event": "created",
"filter": "roomId=YOUR_ROOM_ID",
"secret": "replace-with-a-strong-random-secret"
}
Store the secret in your server's secret manager or environment configuration. Do not put it in source control, a client application, or a log.
anchorVerify the signature
anchorWhen a webhook has a secret, Webex includes an X-Spark-Signature header containing an HMAC-SHA1 signature of the raw JSON request body. This example uses SHA-1 only because it is part of the Webex webhook contract, not as a general cryptographic recommendation. Header names are case-insensitive, and HTTP/2 clients can deliver the field name in lowercase, so use your framework's case-insensitive header lookup.
Some webhook types also provide HMAC-SHA256 and HMAC-SHA512 signatures in the X-Webex-Signature header. Prefer that header when it is available; this example covers the HMAC-SHA1 X-Spark-Signature path. See the API changelog for the signature options.
Verify the signature against the raw request bytes before parsing JSON. Compare the expected and received signatures in constant time, then reject the request when the header is missing or does not match.
This Flask example returns 401 for an invalid request and only reaches application logic after verification succeeds:
import hashlib
import hmac
import os
from typing import Optional
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["WEBEX_WEBHOOK_SECRET"].encode("utf-8")
def is_valid_webhook(raw_body: bytes, received_signature: Optional[str]) -> bool:
if not received_signature:
return False
expected_signature = hmac.new(
WEBHOOK_SECRET,
raw_body,
hashlib.sha1,
).hexdigest()
return hmac.compare_digest(expected_signature, received_signature)
@app.post("/webex-events")
def receive_webhook():
raw_body = request.get_data()
signature = request.headers.get("x-spark-signature")
if not is_valid_webhook(raw_body, signature):
return "Invalid signature", 401
# Parse and process raw_body only after signature verification succeeds.
return "", 204
Return a successful 2xx response after you accept the event. Requests that fail verification should not reach the rest of your bot or integration logic. For a broader webhook security example, see Building a More Secure Bot.
For the complete webhook API, see the Webhooks guide.