> ## Documentation Index
> Fetch the complete documentation index at: https://relay-demo.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry a request safely without sending the same event twice.

Networks fail. If your request to `POST /v1/events` times out, you can't tell whether Relay stored the event. Send an `Idempotency-Key` header and retry with the same key: Relay stores the event once, whatever happens to the responses.

```bash theme={null}
curl https://api.relay.example/v1/events \
  -H "Authorization: Bearer $RELAY_API_KEY" \
  -H "Idempotency-Key: invoice-inv_123-paid" \
  -H "Content-Type: application/json" \
  -d '{"type": "invoice.paid", "data": {"invoice_id": "inv_123"}}'
```

![Sequence diagram between your app and the Relay API: your app sends POST /v1/events with Idempotency-Key invoice-inv\_123-paid; Relay stores event evt\_2Lk9xQ and remembers the key and response for 24 hours, but the 202 is lost and the request times out; your app retries with the same key; the key matches, so Relay stores no new event and returns the first 202 with Idempotent-Replayed: true.](https://mintcdn.com/relay-demo/EZNTzLZZIWlaFK8l/images/idempotent-retry.png?fit=max\&auto=format\&n=EZNTzLZZIWlaFK8l\&q=85\&s=5c1f3f50986e2b54a88dd54eb55df38d)

The first response is lost, so your app retries with the same key. Relay stores nothing new and replays the first response.

## How keys are matched

| The request | What Relay does |
| - | - |
| A new key | Stores the event and remembers the key and the response for 24 hours. |
| The same key and the same body, within 24 hours | Stores nothing and returns the first response, with the header `Idempotent-Replayed: true`. |
| The same key and a different body | Answers `409` with `error.type` `idempotency_conflict`. |
| The same key while the first request is still running | Answers `409` with `error.type` `request_in_progress`. Retry after a second. |
| A key older than 24 hours | Treated as new. |

Use a key that names the thing that happened, such as `invoice-inv_123-paid`, rather than a random value made per attempt. Then a retry from a different process, or after a deploy, still matches.

## Duplicates on the receiving side

Relay delivers at least once. A delivery can arrive twice, for example when your endpoint processed it but its `200` was lost on the way back. Every event has a stable `id` (`evt_...`), and every delivery carries it in the `Relay-Event-Id` header. Record the ids you have processed and skip any you have already seen.

![Sequence diagram between Relay and your webhook endpoint: Relay delivers invoice.paid with Relay-Event-Id evt\_2Lk9xQ; the endpoint handles it and records the id, but its 200 is lost; Relay retries with the same event id; the endpoint finds evt\_2Lk9xQ already recorded, skips the work and returns 200; the delivery is marked delivered.](https://mintcdn.com/relay-demo/EZNTzLZZIWlaFK8l/images/duplicate-delivery.png?fit=max\&auto=format\&n=EZNTzLZZIWlaFK8l\&q=85\&s=3c2955b75e5da6aabf25dd19a9275b85)

Your endpoint's 200 is lost, so Relay delivers the event again. The endpoint has already recorded the event id, skips the work and answers 200.

```js theme={null}
if (await seen.has(event.id)) return res.sendStatus(200);
await handle(event);
await seen.add(event.id);
res.sendStatus(200);
```

Answer `200` for a duplicate. An error makes Relay retry it again.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.