Webhook security
Verify signed webhooks
Quick answer
Which headers should my handler read?
Why signatures matter
Webhook endpoints are public URLs. SignalEDI signs every delivery so you can trust document.validated and document.partner_ack payloads.
Signing scheme
Headers: X-SignalEDI-Timestamp and X-SignalEDI-Signature (sha256=<hex>). Signed string is `${timestamp}.${rawBody}` using your webhook signing secret.
- Reject timestamps older than five minutes
- Legacy secrets honored during seven-day rotation grace
Node verification
Read the raw request body before JSON parsing — re-serialized JSON will not match the signature.
import crypto from "node:crypto";
function verifySignalEdiWebhook(input: {
rawBody: string;
signatureHeader: string;
timestampHeader: string;
secret: string;
}): boolean {
const provided = input.signatureHeader.replace(/^sha256=/i, "").toLowerCase();
const expected = crypto
.createHmac("sha256", input.secret)
.update(`${input.timestampHeader}.${input.rawBody}`, "utf8")
.digest("hex");
// Use a timing-safe compare so signature checks do not leak partial matches.
return crypto.timingSafeEqual(Buffer.from(provided, "hex"), Buffer.from(expected, "hex"));
}Python verification
Use hmac.compare_digest. Return 401 for failed verification.
import hmac
import hashlib
def verify_signaledi_webhook(raw_body: str, signature: str, timestamp: str, secret: str) -> bool:
provided = signature.removeprefix("sha256=").lower()
expected = hmac.new(
secret.encode(),
f"{timestamp}.{raw_body}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(provided, expected)FAQ
Common questions
X-SignalEDI-Signature, X-SignalEDI-Timestamp, X-SignalEDI-Event, and X-SignalEDI-Delivery-ID. Verify before parsing JSON.
During the grace window, deliveries signed with the previous secret may include X-SignalEDI-Signature-Legacy — accept either until grace expires.