# Reliability: idempotency and automatic retries

> How the API behaves during outages, duplicates, and replays, and how to write a robust integration.

A messaging integration must survive network failures, restarts, and duplicates. Here is what the API does for you, and what is left for you to do.

## Send idempotency

A send is not trivial: replaying it by mistake would send the same message to a customer **twice**. The `Idempotency-Key` header prevents that.

```bash
curl -X POST "$INBOX_API/messages" \
  -H "Authorization: Bearer $INBOX_KEY" -H "Content-Type: application/json" \
  -H "Idempotency-Key: appointment-2026-11-14-customer-42" \
  -d '{ … }'
```

| Situation | Result |
|---|---|
| First request with this key | The message is accepted and recorded. |
| Same key, **same** request | No new message: you get the original message back, with the `Idempotent-Replayed: true` header. |
| Same key, **different** request | `422 idempotency_conflict`. |
| Two simultaneous requests with the same key | One succeeds, the other is rejected (`409`): retry. |

<ErrorExample name="idempotency_conflict" />

**Choose a key that describes the business intent** (`appointment-reminder-<appointment id>`), not a random identifier regenerated on each attempt: that is what makes replay safe.

## Sending is asynchronous

`202 Accepted` means **accepted**, not **delivered**. The message is queued, then sent:

- **Temporary Meta or network outage, rate limit**: the message is retried automatically, up to 6 attempts with an increasing wait.
- **Permanent rejection** (template not found, recipient unreachable…): no pointless retry; the message goes straight to `failed`, with Meta's code and reason.
- **Throughput**: sends are throttled **per number** (50 messages per second by default): during a spike, they wait their turn without being lost.
- **Restart or queue loss**: a message that stays `queued` for more than 5 minutes is picked up automatically by a maintenance task.

## Statuses: never backward

Meta can deliver its notifications twice or out of order. Bref Point Inbox applies a simple rule: a status **only moves forward** (`queued` → `sent` → `delivered` → `read`), and `failed` does not overwrite a message that is already `read`. Even a `read` that arrives before its `delivered` is accepted, and the missing step is filled in.

## Webhooks: at least once

Each event is delivered **at least once**, never "exactly once". Your receiver must therefore be **idempotent**:

1. Verify the signature.
2. Store the event `id` (a table with a uniqueness constraint is enough).
3. If it already exists, respond `200` without doing anything.
4. Otherwise, process it, then respond `200`.

See [Webhooks](/docs/guides/webhooks) for the retry schedule and replay.

## What's left for you to do

- **Set a timeout** on your calls (10 to 30 seconds) and **retry with the same `Idempotency-Key`** on timeout, network error, or `5xx`.
- **Don't retry** a `4xx` error without fixing the request: it will fail the same way.
- Respond to your webhooks **in under 10 seconds**; hand long-running work off to a queue on your side.
- Monitor `GET /webhook-deliveries?status=failed` and your `failed` messages.

<Callout type="note" title="A known limitation">
If the service stops exactly between the send to Meta and the recording of the result, a new attempt could produce a duplicate. The window is very short and cannot be eliminated, because Meta offers no idempotency key on send.
</Callout>
