Skip to main content

How signatures work

Every webhook delivery includes two headers for signature verification:
HeaderDescription
X-Signature-TimestampUnix timestamp (seconds) when the signature was computed
X-SignatureHex-encoded HMAC-SHA256 of the signing payload
The signing payload is constructed as:
{timestamp}.{raw_request_body}
The HMAC is computed using your webhook endpoint secret as the key.

Verification steps

  1. Extract X-Signature-Timestamp and X-Signature from the request headers
  2. Construct the signing payload: {timestamp}.{raw_body}
  3. Compute HMAC-SHA256 of the signing payload using your webhook secret
  4. Compare the computed signature with X-Signature using constant-time comparison
  5. Optionally, reject requests with a timestamp more than 5 minutes old to prevent replay attacks

Code examples

import hmac
import hashlib
import time

def verify_webhook(payload: bytes, secret: str, signature: str, timestamp: str) -> bool:
    """Verify a ModelRoute webhook signature."""
    # Check timestamp to prevent replay attacks (5 minute tolerance)
    current_time = int(time.time())
    request_time = int(timestamp)
    if abs(current_time - request_time) > 300:
        return False

    # Construct signing payload
    sign_payload = f"{timestamp}.{payload.decode('utf-8')}"

    # Compute expected signature
    expected = hmac.new(
        secret.encode("utf-8"),
        sign_payload.encode("utf-8"),
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(expected, signature)


# Flask example
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret_here"

@app.route("/webhooks/modelroute", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Signature")
    timestamp = request.headers.get("X-Signature-Timestamp")
    payload = request.get_data()

    if not verify_webhook(payload, WEBHOOK_SECRET, signature, timestamp):
        abort(401)

    event = request.get_json()
    print(f"Received {event['event_type']} for execution {event['execution_id']}")

    # Process the event...
    return "", 200

Common mistakes

Always verify the signature against the raw request body, not a parsed-and-re-serialized version. JSON serialization may reorder keys or change formatting, producing a different signature.
Use hmac.compare_digest (Python), crypto.timingSafeEqual (Node.js), or hmac.Equal (Go). Regular string comparison (==) is vulnerable to timing attacks.
Without timestamp validation, an attacker who intercepts a valid webhook can replay it indefinitely. Reject timestamps older than 5 minutes.