Skip to content

Idempotency

Writes are safe to retry. Reads are naturally idempotent. Webhooks are guaranteed-delivery with dedupe on eventId.

Write endpoints — Idempotency-Key header

All POST endpoints that create state accept an Idempotency-Key header:

http
POST /api/deals
Idempotency-Key: 01HQXZABCD...
Content-Type: application/json

{ ... }

Guarantees:

  • Same key within 24h with the same body → identical response, no duplicate resource
  • Same key with a different body → 409 Conflict, code: "idempotency.body_mismatch"
  • Key omitted → non-idempotent, at-your-own-risk

Use a ULID or UUIDv4. Store the key with the request in your caller — on retry, re-send the same key.

Webhook delivery — dedupe on eventId

Webhooks are at-least-once. The eventId field in the payload is a UUID that the consumer MUST dedupe against for at least 24 hours:

ts
if (await seenEventIds.has(event.eventId)) {
  return new Response('', { status: 200 })  // already processed
}
await process(event)
await seenEventIds.add(event.eventId, { ttl: 86_400 })

The SDK createWebhookHandler does this for you if you pass a store option.

Outbound HTTP — SDK defaults

The SDK's createWebhookSender retries 5xx and 429 with exponential backoff + jitter, up to 5 attempts over ~10 minutes. Each retry carries the same eventId so consumers dedupe correctly.

Gotchas

  • Don't generate idempotency keys server-side. The caller owns the key — they know when they're retrying.
  • Dedupe store must be fast. Put it on Redis / Upstash / an equivalent. Database-backed dedupe gets slow under webhook storms.
  • 24h window is the floor, not the ceiling. Long-running manual retries (e.g., fixing a bug and replaying events) can exceed this — coordinate out-of-band in those cases.

Source of truth

  • Webhook handler with dedupe: @r-bsoftware/ecosystem-sdkwebhook-handler.ts
  • Webhook sender with retries: webhook-sender.ts

Red Broom Software Ecosystem