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
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();
},
);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<?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:
- valid signature and unchanged raw body;
- one-byte body modification;
- missing prefix;
- malformed hex and wrong length;
- wrong subscription secret;
- valid delivery received twice; and
- 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-Signatureheader 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.