Retries & idempotency
Fabric delivers each event at least once. A slow endpoint, a timeout, or a non-2xx response
causes a retry, so the same event can arrive more than once. Your consumer must be idempotent.
Make your consumer idempotent
Section titled “Make your consumer idempotent”- Verify the signature against the raw body.
- Persist the
event.idin a uniqueness-constrained table before you act. - If the id already exists, return
2xxand do nothing — that is the whole point. - Only return
2xxafter the event is durably recorded. A2xxtells Fabric to stop retrying.
async function processOnce(eventId: string, event: WebhookEvent) { const inserted = await db.insertEventIfNew(eventId); // no-op if it already exists if (!inserted) return; // duplicate delivery — ignore await applySideEffect(event);}Never trust ordering. A message.delivered can arrive before the message.sent that logically
precedes it; design state transitions so a later event cannot regress a terminal state.
Delivery states
Section titled “Delivery states”Fabric tracks the state of each attempt to reach your endpoint. Inspect them per endpoint:
const page = await fabric.webhooks.listDeliveries(endpointId, { state: "dead" });console.log(page.data.items.length, page.data.nextCursor);The list is cursor-paginated; iterateDeliveries(endpointId, { state }) walks every page.
| State | Meaning |
|---|---|
pending |
Queued, not yet attempted. |
delivering |
An attempt is in flight. |
delivered |
Your endpoint returned 2xx. |
dead |
Retries were exhausted; the delivery stopped. |
Each WebhookDelivery records attempts, lastHttpStatus, lastErrorCategory, and
nextAttemptAt, plus the endpoint’s rolling health (pending / dead counts).
Replay a dead delivery
Section titled “Replay a dead delivery”After you fix your endpoint, replay a dead delivery instead of resending the original message:
await fabric.webhooks.replayDelivery(endpointId, deliveryId);Replay resets the dead cycle and appends a fresh attempt while keeping the full attempt history. It is audited.