Webhooks

Retries and duplicate handling

We attempt each delivery up to three times. Design your handler so receiving the same event twice is harmless.

Retry schedule

  • Attempt 1 immediately, attempt 2 after about one second, attempt 3 after about two seconds.
  • A delivery fails when your endpoint returns a non-2xx status, refuses the connection, or takes longer than 10 seconds.
  • After the third attempt we stop. The event is not resent later.
  • Every attempt — status code, error and payload — is recorded and visible in your portal under Developer API.

Retries are short by design

Because delivery attempts stop after three tries, never treat webhooks as your only path to state. Reconcile with `GET /orders` and `GET /esims/{id}` on a schedule.

Duplicates

A retry happens whenever we do not see a clean 2xx — including when your handler succeeded but the response was lost. Assume at-least-once delivery.

idempotent handler
// Every envelope carries a unique event id — use it as the dedupe key.
const seen = await db.webhookEvents.findUnique({ where: { id: event.id } });
if (seen) return ok();                       // already processed
await db.webhookEvents.create({ data: { id: event.id, event: event.event } });
await handle(event);
  • Dedupe on the envelope id (evt_…), which is unique per delivery attempt group.
  • Make the effect itself idempotent too — setting an order to completed twice should be a no-op.
  • Events can arrive out of order under retry. Check the resource state rather than assuming sequence.

Debugging a failing endpoint

  • Open Developer API in the portal and read the delivery log: status code, error message and the exact payload we sent.
  • Fire test traffic by placing a sandbox order — it produces a real order.completed delivery to your sandbox URL.
  • 401s in the log almost always mean the body was parsed before verification.