Webhooks
EDI webhooks
Quick answer
What HTTP status should handlers return?
Event types
The document contract includes document.accepted.v1, document.available.v1, document.transport_delivered.v1, document.delivery_failed.v1, document.acknowledgement_received.v1, and document.held.v1.
- Metadata-only JSON envelopes with documentId, environment, state dimensions, capabilities, and pull links
- Separate Certification and Production endpoints prevent cross-lane delivery; Lab uses a signed one-shot probe instead of a persistent subscription
- Retry with exponential backoff on 5xx
Signature headers
Every versioned document event (document.*.v1) carries X-SignalEDI-Signature (sha256=<hex>), X-SignalEDI-Timestamp (unix seconds), X-SignalEDI-Event, X-SignalEDI-Delivery-ID, and the lane in Signal-Environment (sent lowercase; compare it case-insensitively). The signature is an HMAC-SHA256 over the string timestamp + '.' + rawBody, using your webhook signing secret. Legacy events (transaction.completed, transaction.failed, file.processed, document.partner_ack, document.ack_overdue) are signed over the raw body only and carry no timestamp header; partner.connected is a retired name that nothing emits.
- Reject deliveries whose X-SignalEDI-Timestamp skews more than ~5 minutes (replay protection)
- During key rotation a second X-SignalEDI-Signature-Legacy header lets you accept both secrets
Verification
Verify webhook signatures using the shared secret configured in the developer console. See the verify-signed-webhooks guide for code samples.
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"));
}Idempotency
Use X-SignalEDI-Delivery-ID to dedupe retries. Your handler should be safe to run twice for the same logical transition.
FAQ
Common questions
Return 2xx quickly after enqueueing async work. Timeouts cause retries — respond fast and process in a background queue.
Use a tunnel (ngrok or Cloudflare Tunnel) for your dev URL and run the signed one-shot probe from /dashboard/lab. Persistent lifecycle subscriptions are available only in Certification and Production.