{"templateId":"markdown","versions":[{"version":"v1","label":"v1","link":"/pt-br/products/collections/guides/verifying-webhooks","default":true,"active":false,"folderId":"3a2d3fa8"},{"version":"v0","label":"v0","link":"/pt-br/products/collections/v0/guides/verifying-webhooks","default":false,"active":true,"folderId":"3a2d3fa8"}],"sharedDataIds":{"sidebar":"sidebar-@l10n/pt-BR/products/collections/@v0/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Verify webhook signatures","description":"User guides, API reference, and support resources.","siteUrl":"https://docs.ripple.com","lang":"en-US"},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"verify-webhook-signatures","__idx":0},"children":["Verify webhook signatures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When receiving webhooks, the signing operation has already been performed by the platform and you must validate the"," ","signature we provide to ensure the webhook originated from us."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When you create webhooks, the response includes a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["signature_verification_key"]}," for each event type you subscribe to."," ","Each customer has at most one webhook for an event type, with its own key. If a webhook already exists for an event type,"," ","the request returns that existing webhook and key. The key is a base64‑encoded symmetric secret used to compute an"," ","HMAC-SHA256 signature for each webhook payload. Store it securely, as you would any other credential. You can retrieve it again with"," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v0/collections/webhooks/{webhook_id}"]},"."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["NOTE"]},": The returned ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["signature_verification_key"]}," is base64-encoded binary. In the code sample below we base64‑decode it"," ","to raw bytes (you do NOT re‑encode it). You may display it in hex for debugging, but hex is not required for"," ","verification."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["When a webhook is dispatched:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["We include ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Timestamp"]}," (epoch milliseconds)."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["We include ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}," in the form: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t=<timestamp>,v1=<hex_hmac_sha256>"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["We include ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]}," (the event type) and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Subscription-Id"]}," (the ID of the webhook the"," ","delivery belongs to). These headers aren't signed. Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]}," to look up which key to verify with."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Don't look up the key by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Subscription-Id"]},". The platform sets that header when the event occurs, but signs"," ","each delivery, including retries, with the key that's current at send time. If you delete and recreate a webhook while a delivery"," ","is still retrying, the header names the deleted webhook. The signature uses the key of the new webhook, so a lookup by"," ","ID rejects a genuine delivery."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["If one receiver handles several customers, look up the key by customer and event type. No header identifies the"," ","customer, so give each customer its own callback URL and use the URL that received the delivery to tell them apart."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You reconstruct the exact signing string and verify."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"verification-is-a-2-step-process","__idx":1},"children":["Verification is a 2 step process:"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Step"},"children":["Step"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Action"},"children":["Action"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["1"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Reconstruct"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Concatenate the timestamp, a dot (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["."]},"), and the SHA256 hex digest of the raw request body: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<timestamp>.<sha256(body)>"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["2"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Verify"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["HMAC-SHA256 the signing string with the decoded secret; compare the resulting hex digest to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]}," (constant-time)."]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"fields-used-in-the-signature","__idx":2},"children":["Fields Used in the Signature"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Field"},"children":["Field"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Description"},"children":["Description"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Timestamp"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Raw value from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Timestamp"]}," (epoch millis as a string)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Body"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["The exact raw HTTP request body bytes (no parsing / reformatting)."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["body_hash"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["SHA256"]}," hex digest of the raw body."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["signing_string"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<timestamp>.<body_hash>"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Secret"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Base64-decoded ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["signature_verification_key"]}," for the event type in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]},"."]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Signature"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Hex digest of ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["HMAC_SHA256(secret, signing_string)"]}," placed in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1"]},"."]}]}]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If any of these values are transformed (e.g. JSON pretty-printed, numeric formatting changed, timestamp altered) the"," ","verification will fail."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"verification-example","__idx":3},"children":["Verification Example"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import base64\nimport hashlib\nimport hmac\nimport time\nfrom typing import Optional\n\ndef verify_webhook(raw_body: bytes,\n                   timestamp: str,\n                   signature_header: str,\n                   base64_secret: str,\n                   max_age_seconds: int = 300) -> bool:\n    \"\"\"\n    Verify an incoming webhook.\n\n    Parameters:\n      raw_body:        Exact raw request body bytes (no mutation).\n      timestamp:       X-Webhook-Timestamp header (epoch millis as string).\n      signature_header:X-Webhook-Signature header in form \"t=<timestamp>,v1=<hex>\".\n      base64_secret:   subscription signature_verification_key (base64).\n      max_age_seconds: Allowed clock skew / replay window (0 disables freshness check).\n\n    Returns:\n      True if signature is valid, else False.\n    \"\"\"\n    if not (raw_body and timestamp and signature_header and base64_secret):\n        return False\n\n    # Parse signature header\n    parts = {}\n    try:\n        for seg in signature_header.split(\",\"):\n            k, v = seg.split(\"=\", 1)\n            parts[k.strip()] = v.strip()\n    except Exception:\n        return False\n\n    if parts.get(\"t\") != timestamp or \"v1\" not in parts:\n        return False\n\n    # Freshness (optional)\n    if max_age_seconds > 0 and timestamp.isdigit():\n        try:\n            ts_int = int(timestamp)\n            # Detect millis\n            if ts_int > 1_000_000_000_000:\n                ts_int //= 1000\n            if abs(int(time.time()) - ts_int) > max_age_seconds:\n                return False\n        except ValueError:\n            return False\n\n    # SHA256 hex of body\n    body_hash = hashlib.sha256(raw_body).hexdigest()\n    signing_string = f\"{timestamp}.{body_hash}\".encode(\"utf-8\")\n\n    # Decode secret (single base64 decode only)\n    try:\n        secret = base64.b64decode(base64_secret, validate=True)\n    except Exception:\n        return False\n\n    expected = hmac.new(secret, signing_string, hashlib.sha256).hexdigest()\n    provided = parts[\"v1\"]\n    # Constant-time compare\n    return hmac.compare_digest(expected, provided)\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"flask-usage-example","__idx":4},"children":["Flask Usage Example"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"from typing import Optional\n\nfrom flask import Flask, request, Response\n\nfrom verifier import verify_webhook  # assuming the function above saved as verifier.py\n\napp = Flask(__name__)\n\ndef customer_for_callback_url(url: str) -> str:\n    \"\"\"Return the customer that owns this callback URL.\"\"\"\n    ...  # read from your config, keyed by callback URL\n\ndef load_verification_key(customer_id: str, event_type: str) -> Optional[str]:\n    \"\"\"Return the key for this customer and event type, or None if unknown.\"\"\"\n    ...  # read from your secure store, keyed by customer and event type\n\n@app.post(\"/webhook\")\ndef webhook():\n    raw_body = request.get_data()  # exact bytes\n    ts = request.headers.get(\"X-Webhook-Timestamp\", \"\")\n    sig = request.headers.get(\"X-Webhook-Signature\", \"\")\n    customer_id = customer_for_callback_url(request.url)  # each customer has its own callback URL\n    event_type = request.headers.get(\"X-Webhook-Event-Type\", \"\")\n    secret = load_verification_key(customer_id, event_type)\n    if not secret or not verify_webhook(raw_body, ts, sig, secret, max_age_seconds=300):\n        return Response(\"invalid signature\", status=400)\n    # Safe to parse AFTER verification\n    return Response(\"ok\", status=200)\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"key-points","__idx":5},"children":["Key Points"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Always read raw bytes first; only parse JSON after verification."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Each event type has its own key for each customer. Look it up by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]}," and the customer that owns"," ","the callback URL."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Do not pretty-print or modify the JSON before hashing."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Reject if age window exceeded or headers malformed."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["hmac.compare_digest"]}," for constant-time comparison."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"common-pitfalls","__idx":6},"children":["Common Pitfalls"]},{"$$mdtype":"Tag","name":"div","attributes":{"className":"md-table-wrapper"},"children":[{"$$mdtype":"Tag","name":"table","attributes":{"className":"md"},"children":[{"$$mdtype":"Tag","name":"thead","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Issue"},"children":["Issue"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Cause"},"children":["Cause"]},{"$$mdtype":"Tag","name":"th","attributes":{"data-label":"Resolution"},"children":["Resolution"]}]}]},{"$$mdtype":"Tag","name":"tbody","attributes":{},"children":[{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["signature_mismatch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Secret double-base64 encoded"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Use original subscription value directly"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["signature_mismatch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Verified with a different event type's key"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Look up the key by ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]}]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["signature_mismatch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Body re-serialized / pretty-printed"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Hash exact raw bytes"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["signature_mismatch"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Timestamp mismatch between ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t"]}," and header"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ensure they match verbatim"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Stale timestamp"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Clock skew or delayed processing"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Check system clock; adjust max age"]}]},{"$$mdtype":"Tag","name":"tr","attributes":{},"children":[{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Invalid header format"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Missing ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t="]}," or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["v1="]}," segment"]},{"$$mdtype":"Tag","name":"td","attributes":{},"children":["Ensure exact format ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["t=...,v1=..."]}]}]}]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"checklist","__idx":7},"children":["Checklist"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Captured raw body (not parsed then re-stringified)"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Extracted ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Timestamp"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Parsed ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Signature"]}," → t & v1"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Looked up the key for this customer's event type in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["X-Webhook-Event-Type"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Computed SHA256 hex of raw body"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Built signing string ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<timestamp>.<body_hash>"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," Base64-decoded stored secret"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," HMAC-SHA256 computed & constant-time compared"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"input","attributes":{"checked":false,"type":"checkbox","readOnly":true},"children":[]}," (Optional) Freshness window passed"]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If every box is checked and comparison succeeds, the webhook is authentic."]}]},"headings":[{"value":"Verify webhook signatures","id":"verify-webhook-signatures","depth":1},{"value":"Verification is a 2 step process:","id":"verification-is-a-2-step-process","depth":3},{"value":"Fields Used in the Signature","id":"fields-used-in-the-signature","depth":4},{"value":"Verification Example","id":"verification-example","depth":4},{"value":"Flask Usage Example","id":"flask-usage-example","depth":4},{"value":"Key Points","id":"key-points","depth":4},{"value":"Common Pitfalls","id":"common-pitfalls","depth":4},{"value":"Checklist","id":"checklist","depth":4}],"frontmatter":{"tocMaxDepth":3,"seo":{"title":"Verify webhook signatures"}},"lastModified":"2026-11-02T19:41:31.000Z","isTranslationFallback":true,"pagePropGetterError":{"message":"","name":""}},"slug":"/pt-br/products/collections/v0/guides/verifying-webhooks","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}