--- title: Verify webhook signatures description: Learn how to verify that webhook deliveries come from TheirStack by validating the HMAC-SHA256 signature included in each request. url: https://theirstack.com/en/docs/webhooks/verify-webhook-signatures --- When you configure a signing secret for your webhook, TheirStack signs every delivery with an HMAC-SHA256 signature so you can verify the payload was sent by TheirStack and has not been tampered with. ## How it works 1. You set a **signing secret** (minimum 16 characters) when [creating or editing a webhook](/en/docs/webhooks/how-to-set-up-a-webhook). 2. For every delivery, TheirStack computes an HMAC hex digest of the raw JSON body using your secret and includes it in the `X-TheirStack-Signature-256` header: ``` X-TheirStack-Signature-256: sha256= ``` 3. Your endpoint recomputes the same HMAC and compares the two values using a **timing-safe comparison** to prevent timing attacks. ## Setting up a signing secret You can add a signing secret when creating a new webhook or update an existing one: - **In the app** — use the "Signing Secret" field in the webhook creation/edit form. You can generate a random secret or provide your own (minimum 16 characters). - **Via the API** — include a `secret` field in the request body when calling [POST /v0/webhooks](/en/docs/api-reference/webhooks/post_webhooks_v0) or [PATCH /v0/webhooks/{id}](/en/docs/api-reference/webhooks/patch_webhooks_by_id_v0). > Store your secret securely. Never hardcode it in your application or commit it to version control. Use environment variables or a secrets manager. ## Validating deliveries ``` import hashlib import hmac import os from flask import Flask, request, abort WEBHOOK_SECRET = os.environ["THEIRSTACK_WEBHOOK_SECRET"] def verify_signature(payload: bytes, secret: str, signature_header: str) -> bool: """Verify the X-TheirStack-Signature-256 header.""" expected = "sha256=" + hmac.new( secret.encode("utf-8"), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature_header) app = Flask(__name__) @app.route("/webhook", methods=["POST"]) def webhook(): signature = request.headers.get("X-TheirStack-Signature-256", "") if not verify_signature(request.data, WEBHOOK_SECRET, signature): abort(403) event = request.json # Process the event return "", 200 ``` > Always use a **timing-safe comparison** function like `hmac.compare_digest`. Never use `==` to compare signatures, as this is vulnerable to timing attacks. ## Testing your implementation You can verify your implementation with these test values: | Field | Value | | --- | --- | | Secret | `It's a Secret to Everybody` | | Payload | `Hello, World!` | | Expected signature | `sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17` | ## Troubleshooting - **Signature mismatch** — Make sure you are comparing against the **raw request body** (bytes), not a parsed/re-serialized JSON object. Re-serialization can change key order or whitespace. - **Missing header** — The `X-TheirStack-Signature-256` header is only included when a signing secret is configured for the webhook. - **Encoding issues** — The payload may contain unicode characters. Always handle it as UTF-8.