Skip to main content

EDI webhooks

Register HTTPS webhook endpoints to receive document validation results, routing updates, and partner acknowledgement signals without polling.

Quick answer

What HTTP status should handlers return?

Return 2xx quickly after enqueueing async work. Timeouts cause retries — respond fast and process in a background queue.

More common questions →

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.

Common questions

© 2026 SignalEDI Inc. All rights reserved.