Skip to main content

Verify signed webhooks

Verify SignalEDI webhook signatures with HMAC-SHA256, timestamp tolerance, and replay protection — Node and Python examples included.

Quick answer

Which headers should my handler read?

X-SignalEDI-Signature, X-SignalEDI-Timestamp, X-SignalEDI-Event, and X-SignalEDI-Delivery-ID. Verify before parsing JSON.

More common questions

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)

Common questions

© 2026 SignalEDI Inc. All rights reserved.