Skip to content

Verify webhook signatures

Validate every SeatLayer webhook using the raw body, HMAC-SHA256, and a timing-safe comparison.

Updated View as Markdown

Every webhook delivery is signed. Verify the signature before parsing or trusting its payload.

Signature contract

Field Value
Secret The subscription’s whsec_… secret
Signed message Exact raw request-body bytes
Algorithm HMAC-SHA256
Encoding Lowercase hexadecimal
Header X-SeatLayer-Signature: sha256=<hex>

Legacy deliveries may also include X-SeatMap-Signature and X-SeatMap-Event. New integrations should verify X-SeatLayer-Signature.

Implement verification

server/webhooks.jsjs
import crypto from "node:crypto";
import express from "express";

function isValidSignature(rawBody, header, secret) {
  if (
    typeof header !== "string" ||
    !/^sha256=[0-9a-f]{64}$/i.test(header)
  ) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest();
  const provided = Buffer.from(header.slice("sha256=".length), "hex");

  return (
    provided.length === expected.length &&
    crypto.timingSafeEqual(provided, expected)
  );
}

app.post(
  "/webhooks/seatlayer",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const valid = isValidSignature(
      req.body,
      req.header("X-SeatLayer-Signature"),
      process.env.SEATLAYER_WEBHOOK_SECRET,
    );

    if (!valid) return res.status(401).send("invalid signature");

    const delivery = JSON.parse(req.body.toString("utf8"));
    await enqueueIdempotently(delivery);
    return res.status(200).end();
  },
);
server/webhooks.pypython
import hashlib
import hmac
import os
from flask import request

def is_valid_signature(raw_body: bytes, header: str, secret: str) -> bool:
    if not header.startswith("sha256="):
        return False
    expected = hmac.new(
        secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    provided = header.removeprefix("sha256=")
    return hmac.compare_digest(expected, provided)

@app.post("/webhooks/seatlayer")
def seatlayer_webhook():
    raw_body = request.get_data(cache=True)
    header = request.headers.get("X-SeatLayer-Signature", "")
    secret = os.environ["SEATLAYER_WEBHOOK_SECRET"]

    if not is_valid_signature(raw_body, header, secret):
        return "invalid signature", 401

    enqueue_idempotently(request.get_json())
    return "", 200
server/webhooks.phpphp
<?php
$rawBody = file_get_contents("php://input");
$header = $_SERVER["HTTP_X_SEATLAYER_SIGNATURE"] ?? "";
$secret = getenv("SEATLAYER_WEBHOOK_SECRET");

if (!str_starts_with($header, "sha256=")) {
    http_response_code(401);
    exit("invalid signature");
}

$expected = hash_hmac("sha256", $rawBody, $secret);
$provided = substr($header, strlen("sha256="));

if (!hash_equals($expected, $provided)) {
    http_response_code(401);
    exit("invalid signature");
}

$delivery = json_decode($rawBody, true, flags: JSON_THROW_ON_ERROR);
enqueue_idempotently($delivery);
http_response_code(200);

Process after verification

Reject invalid signatures

Return a non-2xx response and record a security-safe diagnostic. Do not log the secret or full signature.

Parse the verified body

Only now decode JSON and validate the expected envelope.

Deduplicate

Use the delivery identity and your domain state so retries cannot apply the same transition twice.

Acknowledge quickly

Persist or enqueue the event, return 2xx, and perform slow downstream work asynchronously.

Secret handling and rotation

  • Store each whsec_… value in a secret manager, never source control.
  • Associate the secret with its subscription; do not try every organization secret for every request.
  • Restrict who can reveal, replace, or delete subscriptions.
  • During a planned rotation, support the old and new subscription secrets only for the bounded overlap required by your rollout.
  • Treat a leaked webhook secret as compromised even though it cannot call the Server API.

Test vectors

Test at least:

  1. valid signature and unchanged raw body;
  2. one-byte body modification;
  3. missing prefix;
  4. malformed hex and wrong length;
  5. wrong subscription secret;
  6. valid delivery received twice; and
  7. parser failure after a valid signature.

Never use a plain string == or === for the final comparison.

Verification checklist

  • Raw bytes are captured before JSON middleware.
  • The exact X-SeatLayer-Signature header is required.
  • HMAC uses SHA-256 and the correct subscription secret.
  • The comparison is timing-safe and length-safe.
  • Invalid signatures receive a non-2xx response.
  • Verified deliveries are deduplicated and acknowledged quickly.
  • Secrets and full signatures are absent from logs.

Continue with integrate a ticketing backend.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close