Journal · October 12, 2026

Webhooks that survive retries

At-least-once delivery, HMAC signatures over the raw body, replay windows and idempotency on event IDs — the parts that make webhooks safe to act on.

Webhooks are the moment a payment system hands responsibility to your code. They arrive over the public internet, sometimes twice, sometimes late. Designing for that is the difference between a reliable integration and a support queue.

What GRYPHEM PAY guarantees

  • At-least-once delivery. Every event is written to an outbox in the same transaction as the state change, then delivered with exponential backoff and jitter. After the maximum number of attempts, failed deliveries are kept in a dead-letter list you can replay.
  • A stable identity. Each event carries an event_id. Retries reuse it.
  • A verifiable signature. The Gryphem-Signature header contains a timestamp and an HMAC-SHA256 over timestamp + "." + raw_body, computed with your endpoint’s secret.

What your endpoint should do

import { verifyWebhook } from '@gryphem-pay/webhooks';

// Rejects stale timestamps (300 s tolerance) and accepts the previous secret during rotation.
const event = await verifyWebhook({
  payload: rawBody, // the exact bytes received
  header: request.headers['gryphem-signature'],
  secrets: [process.env.GRYPHEM_WEBHOOK_SECRET, process.env.GRYPHEM_WEBHOOK_SECRET_PREVIOUS].filter(Boolean),
});

if (await alreadyProcessed(event.event_id)) return ok();

Three habits matter most:

  1. Verify against the raw body. Re-serialised JSON will not match the signature.
  2. Deduplicate on event_id. Treat a repeated event as success, not an error.
  3. Confirm before irreversible actions. For high-value fulfilment, fetch the payment from the API and check its state before shipping.

Rotating secrets without downtime

When you rotate an endpoint secret, deliveries are signed with both the new and the previous secret for a short overlap window, so verifiers can switch over without dropping events. Secrets are shown once, encrypted at rest and never logged.

← All journal entries