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-sdk→webhook-handler.ts - Webhook sender with retries:
webhook-sender.ts