# Idempotency and references

Your IDs make every call safe to repeat.

> **In a nutshell:** Use your own IDs everywhere - external_id for businesses, reference for messages, the delivery ID for callbacks. Repeating a request with the same ID never creates a second business or sends a second message.

## Three IDs, three guarantees

| ID | Where | Guarantee |
|---|---|---|
| `external_id` | [`POST /businesses`](/docs/api/upsert-business) | Same ID = same business. Repeating updates it. |
| `reference` | [`POST .../messages`](/docs/api/send-message) | Same reference for the same business = same message. Repeating returns the original (`200`) and sends nothing. |
| `X-PaalChat-Delivery` | [Callbacks](/docs/callbacks) | Same ID = same event. Process it once. |
| `Idempotency-Key` header | Any write (`POST`, `PUT`, `PATCH`, `DELETE`) | Same key + same request within 24 hours = the first response again, nothing done twice. |

## The Idempotency-Key header

Any write accepts an `Idempotency-Key` header - your own unique ID for that request
(1-255 printable characters). A retry with the same key and the same body gets the
first response back, with `Idempotent-Replayed: true`, and nothing happens twice.

- The same key with a **different** request is refused: `409 idempotency_key_reused`.
- A retry while the first request is still running gets `409 idempotency_in_progress` -
  wait a moment and retry with the same key.
- Keys are kept for 24 hours, per product and per environment (live and test).
- Server errors (`5xx`) and `429` are not kept, so retrying with the same key works.
- For messages, `reference` does the same job without a time limit - use both if you like.

## References for messages

Use your own database ID for the message, for example `notification-4411` or
`m8812`. Rules: 1-191 printable characters, no spaces, unique per business.

```text
First call   POST .../messages  reference=notification-4411  →  202 Accepted (queued)
Retry        POST .../messages  reference=notification-4411  →  200 OK (the same message, not sent again)
```

> [!WARNING]
> Never generate a new reference for a retry. A new reference is a new message.

Without a reference, every call sends a new message - a retry after a timeout may
send it twice. Always send one.

## References in callbacks

[`message.status`](/docs/api/callbacks/message-status) callbacks carry your
`reference`, so you can update your own row without storing PaalChat's IDs. The
platform's `message_id` and Meta's `wamid` are included too.

## Callbacks can repeat

A callback can arrive more than once (retries, resends) and out of order. Store
`X-PaalChat-Delivery` and skip deliveries you have processed. For messages, also
key on `wamid` and apply statuses only forward.
