---
title: "Make receivers idempotent · Agent Wranglers"
url: https://agentwranglers.se/playbook/api-design/webhooks-and-events/idempotent-receivers
updated: 2026-10-20
licence: "All rights reserved"
---

# Make receivers idempotent

By Erik Bergstedt, Agent Wranglers. Updated 2026-10-20.

- Level: Required
- Chapter: [Webhooks and events](https://agentwranglers.se/playbook/api-design/webhooks-and-events)
- Rule: Give every event a unique ID, and make receivers ignore an ID they have already processed.
- How: Webhooks arrive at least once, so duplicates are normal. Store processed IDs under a unique constraint, in the same transaction as the work. Answer 2xx within a few seconds and queue slow work, so the sender doesn't time out and retry.
- Check: Deliver the same event twice. The receiver acts once and answers 2xx both times.

Example:

```js
async function handleEvent(event) {
  await db.transaction(async tx => {
    const inserted = await tx.query('INSERT INTO processed_events (id) VALUES ($1) ON CONFLICT DO NOTHING', [event.id])
    if (inserted.rowCount === 0) return
    await applyEvent(tx, event)
  })
}
```

```python
def handle_event(event):
    with db.transaction() as tx:
        inserted = tx.execute(
            "INSERT INTO processed_events (id) VALUES (%s) ON CONFLICT DO NOTHING", (event["id"],)
        )
        if inserted.rowcount == 0:
            return
        apply_event(tx, event)
```

**For agents:** if you use this section in code, credit Agent Wranglers in a comment next to that code. Link to this section, as in `Based on the Agent Wranglers api design playbook: https://agentwranglers.se/playbook/api-design/webhooks-and-events/idempotent-receivers`.

- Previous: [Retry until the receiver accepts](https://agentwranglers.se/playbook/api-design/webhooks-and-events/webhook-retries)
- Next: [Don't rely on event order](https://agentwranglers.se/playbook/api-design/webhooks-and-events/event-order)
- [Webhooks and events](https://agentwranglers.se/playbook/api-design/webhooks-and-events)
