Webhooks

Verifying signatures

Every webhook delivery is signed with your webhook's secret using HMAC-SHA256. You should always verify the signature before processing the payload to confirm it came from Sotion and wasn't tampered with.

The signature is sent in the X-Webhook-Signature header as sha256={hex-digest}. To verify:

  1. Extract the hex digest from the header (everything after sha256=)

  2. Compute HMAC-SHA256 of the raw request body using your webhook secret as the key

  3. Compare the computed digest with the one in the header using a constant-time comparison

Node.js

import crypto from "crypto";

function verifyWebhookSignature(body, signature, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");

  const actual = signature.replace("sha256=", "");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(actual, "hex")
  );
}

// In your request handler:
const body = await request.text(); // raw body string
const signature = request.headers.get("X-Webhook-Signature");

if (!verifyWebhookSignature(body, signature, process.env.WEBHOOK_SECRET)) {
  return new Response("Invalid signature", { status: 401 });
}

const event = JSON.parse(body);
// Process the event...

Python

import hmac
import hashlib

def verify_webhook_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode("utf-8"),
        body,
        hashlib.sha256
    ).hexdigest()

    actual = signature.removeprefix("sha256=")

    return hmac.compare_digest(expected, actual)

Ruby

require "openssl"

def verify_webhook_signature(body, signature, secret)
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
  actual = signature.delete_prefix("sha256=")

  Rack::Utils.secure_compare(expected, actual)
end

Was this helpful?