Skip to main content
When CharityStack delivers a webhook event to your server, it signs the request using your webhook secret. Verifying this signature before processing the payload confirms two things: the request genuinely came from CharityStack, and the payload was not modified in transit. Skipping verification leaves your endpoint open to spoofed or replayed requests.

Headers sent with every webhook

Each webhook delivery includes three security headers:
HeaderDescription
X-Webhook-SignatureHMAC-SHA256 signature in the format sha256=<hex_digest>
X-Webhook-TimestampUnix timestamp (seconds) of when the request was signed
X-Webhook-IDUnique delivery identifier for this event

Verification algorithm

To verify the signature, you reconstruct the signed string from the timestamp and raw request body, compute the expected HMAC, and compare it to the value in X-Webhook-Signature.
1

Read the headers

Extract X-Webhook-Signature and X-Webhook-Timestamp from the incoming request headers.
2

Build the signed string

Concatenate the timestamp and the raw request body separated by a period:
signed_string = timestamp + "." + raw_body
Use the raw request body bytes — do not parse JSON first. Parsing and re-serializing the body can alter its byte representation and cause the signature check to fail.
3

Compute the expected signature

Compute an HMAC-SHA256 of the signed string using your webhook secret as the key, then hex-encode the result.
4

Compare signatures

Prefix your computed digest with sha256= and compare it to the value of X-Webhook-Signature. Use a constant-time comparison to prevent timing attacks.
5

Check the timestamp

Verify that the timestamp is within 5 minutes of your server’s current time. Reject requests with timestamps that are too old to protect against replay attacks.
Your webhook secret is shown only once when you create the webhook. Store it in an environment variable or secrets manager immediately — you cannot retrieve it again from the API.

Code examples

import hashlib
import hmac
import time
from flask import Flask, request, abort

app = Flask(__name__)

WEBHOOK_SECRET = "your_webhook_secret_here"  # From environment variable


def verify_signature(raw_body: bytes, timestamp: str, signature: str) -> bool:
    # Reject requests older than 5 minutes
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        return False

    # Build signed string: timestamp + "." + raw body
    signed_string = f"{timestamp}.".encode() + raw_body

    # Compute expected HMAC-SHA256 signature
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_string,
        hashlib.sha256,
    ).hexdigest()

    # Constant-time comparison to prevent timing attacks
    return hmac.compare_digest(expected, signature)


@app.route("/webhook", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-Webhook-Signature", "")
    timestamp = request.headers.get("X-Webhook-Timestamp", "")
    raw_body = request.get_data()  # Raw bytes before JSON parsing

    if not verify_signature(raw_body, timestamp, signature):
        abort(401, "Invalid webhook signature")

    event = request.get_json()
    print(f"Received event: {event['type']}")
    # Process the event here...

    return "", 200

Security best practices

Compute the signature against the raw bytes of the request body, not a parsed and re-serialized version. JSON serializers can reorder keys or change whitespace, which will break signature verification.
An attacker who intercepts a valid signed request could resubmit it later. Rejecting requests with timestamps older than 5 minutes eliminates this risk without affecting legitimate deliveries.
Use hmac.compare_digest (Python), crypto.timingSafeEqual (Node.js), or hash_equals (PHP) instead of == when comparing signatures. Standard string comparison can leak timing information that helps an attacker forge signatures.
Keep your webhook secret in an environment variable or a secrets manager. Never hardcode it in source code or commit it to version control.