Inbox Ledger
FeaturesWebhooks

Securing and verifying webhooks

Verify the HMAC-SHA256 signature on every webhook delivery, and learn the retry, backoff, and public-HTTPS rules.

Securing and verifying webhooks

Admin+

Anyone who learns your endpoint URL could POST fake data to it. Inbox Ledger signs every delivery so you can prove the request came from us, and your endpoint should reject any request whose signature does not match.

The signature header

Each delivery includes three headers alongside Content-Type: application/json:

  • X-Signature-256: the signature, formatted as sha256=<hex>.
  • X-Event: the event name, such as invoice.created.
  • X-Delivery-Id: a unique ID for this delivery, useful as an idempotency key.

The signature is an HMAC-SHA256 digest computed over the raw JSON request body, keyed by your webhook's signing secret. The hex digest is prefixed with sha256=.

Verify in Node

Compute the same HMAC over the raw body and compare it to the header with a constant-time check. Read the body as raw bytes, not a parsed and re-serialized object, or the digest will not match.

import crypto from 'node:crypto';

function verifyWebhook(rawBody: string, signatureHeader: string, secret: string): boolean {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');

  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);

  // Length guard: timingSafeEqual throws on mismatched buffer lengths.
  if (a.length !== b.length) return false;

  return crypto.timingSafeEqual(a, b);
}

Always capture the raw request body before any JSON middleware parses it. In Express, mount express.raw({ type: 'application/json' }) on the webhook route. A parsed object that you stringify again will reorder or reformat keys, and the recomputed digest will fail.

Delivery, retries, and the dead state

Inbox Ledger waits up to 10 seconds for your endpoint to respond. A 2xx status marks the delivery succeeded.

Any other status, a timeout, or a network error marks the delivery failed, and Inbox Ledger retries with exponential backoff. After 5 failed attempts the delivery is marked dead and is not retried again.

Return 2xx as soon as you have stored the payload, then do slower work in the background. Use X-Delivery-Id to skip a delivery you have already handled, since a retry can arrive after your endpoint recovers.

The endpoint must be public HTTPS

Your URL must be a reachable public HTTPS address. Inbox Ledger runs an SSRF guard that rejects private, internal, and metadata hosts at save time and again before every send. A localhost, 10.x, or 169.254.x URL is refused.

Ready to try this?

Inbox Ledger turns your inbox into clean accounting data. The free tier includes 10 credits, refilled every 30 days.

Start free

On this page