{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-guides/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Verifying signatures","description":"Start today using our public API to power your next project."},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"verifying-signatures"},"children":["Verifying signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Always verify the HMAC-SHA256 signature before processing any payload. Without verification you risk acting on forged requests."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"how-to-verify"},"children":["How to verify"]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Read the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["raw, unparsed"]}," request body as bytes."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compute ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["sha256={hex}"]}," where ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["{hex}"]}," is the HMAC-SHA256 hex digest of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["(secret, raw_body)"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Compare your computed value to the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}," header using a ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["constant-time"]}," comparison."]}]},{"$$mdtype":"Tag","name":"blockquote","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Warning — Use the raw bytes"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["raw bytes"]},", not re-serialized JSON. Re-encoding the body (e.g. ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON.parse"]}," then ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["JSON.stringify"]},") will change byte ordering and whitespace, breaking verification."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"node.js-express"},"children":["Node.js (Express)"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require('crypto');\n\nfunction verifyWebhookSignature(rawBody, signatureHeader, secret) {\n  const expected = `sha256=${crypto\n    .createHmac('sha256', secret)\n    .update(rawBody)\n    .digest('hex')}`;\n\n  // During secret rotation the header may contain two comma-separated signatures.\n  const signatures = signatureHeader.split(',').map(s => s.trim());\n  return signatures.some(sig => {\n    if (sig.length !== expected.length) return false;\n    return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));\n  });\n}\n\n// Use express.raw() so you receive the unparsed body.\napp.post(\n  '/webhooks/jeeves',\n  express.raw({ type: 'application/json' }),\n  (req, res) => {\n    const valid = verifyWebhookSignature(\n      req.body,\n      req.headers['x-webhook-signature'],\n      process.env.JEEVES_WEBHOOK_SECRET\n    );\n    if (!valid) return res.status(401).send('Invalid signature');\n\n    const event = JSON.parse(req.body);\n    // Enqueue for background processing — do NOT block here.\n    res.status(200).send('OK');\n  }\n);\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"python-fastapi-/-flask"},"children":["Python (FastAPI / Flask)"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import hmac, hashlib\n\ndef verify_webhook_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:\n    expected = 'sha256=' + hmac.new(\n        secret.encode('utf-8'),\n        raw_body,\n        hashlib.sha256,\n    ).hexdigest()\n    # During secret rotation, header may contain two comma-separated signatures.\n    signatures = [s.strip() for s in signature_header.split(',')]\n    return any(hmac.compare_digest(sig, expected) for sig in signatures)\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"go"},"children":["Go"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"go","header":{"controls":{"copy":{}}},"source":"import (\n    \"crypto/hmac\"\n    \"crypto/sha256\"\n    \"encoding/hex\"\n    \"strings\"\n)\n\nfunc VerifyWebhookSignature(rawBody []byte, signatureHeader, secret string) bool {\n    mac := hmac.New(sha256.New, []byte(secret))\n    mac.Write(rawBody)\n    expected := \"sha256=\" + hex.EncodeToString(mac.Sum(nil))\n    for _, sig := range strings.Split(signatureHeader, \",\") {\n        sig = strings.TrimSpace(sig)\n        if hmac.Equal([]byte(sig), []byte(expected)) {\n            return true\n        }\n    }\n    return false\n}\n","lang":"go"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"dual-signatures-during-secret-rotation"},"children":["Dual signatures during secret rotation"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["While a rotation is in progress (24-hour grace period), ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}," contains ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["two"]}," signatures separated by a comma:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"header":{"controls":{"copy":{}}},"source":"X-Webhook-Signature: sha256=<new_secret_sig>, sha256=<old_secret_sig>\n"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The verifiers above already accept the request if either signature matches."]}]},"headings":[{"value":"Verifying signatures","id":"verifying-signatures","depth":1},{"value":"How to verify","id":"how-to-verify","depth":2},{"value":"Node.js (Express)","id":"node.js-express","depth":2},{"value":"Python (FastAPI / Flask)","id":"python-fastapi-/-flask","depth":2},{"value":"Go","id":"go","depth":2},{"value":"Dual signatures during secret rotation","id":"dual-signatures-during-secret-rotation","depth":2}],"frontmatter":{"seo":{"title":"Verifying signatures"}},"lastModified":"2026-06-15T18:50:49.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/guides/integration-guides/webhooks/signatures","userData":{"isAuthenticated":false,"teams":["anonymous"]}}