> ## Documentation Index
> Fetch the complete documentation index at: https://docs.blujay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry sends and other changes safely, without ever sending a message twice.

Networks fail. A request can time out after Blujay has already sent your message. An idempotency key lets you retry without risk: Blujay recognizes the key and returns the original result instead of doing the work again.

```bash theme={null}
curl https://api.blujay.io/v1/chats \
  -H "Authorization: Bearer blu_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-welcome" \
  -d '{"from":"+15555550100","to":["+15555550123"],"message":{"parts":[{"type":"text","text":"Hi Ava, your order has shipped."}]}}'
```

## How it works

| You send | Blujay returns |
| - | - |
| A new key | Does the work and remembers the key with the result. |
| The same key and the same request, after it finished | The original response body with `200 OK` (instead of `202`). Nothing is sent again. |
| The same key while the first request is still running | [`IDEMPOTENCY_IN_PROGRESS`](/errors/idempotency-in-progress) (`409`, retryable). Retry shortly. |
| The same key with a different request | [`IDEMPOTENCY_REQUEST_MISMATCH`](/errors/idempotency-request-mismatch) (`409`, not retryable). Nothing is sent. |

* **Format:** any string of 1 to 255 characters. Use something tied to the business event, such as `order-1042-welcome`, or a UUID you store with the job.
* **Scope:** a key is scoped to your workspace, your API key and a route family (below). The same key on a different route family is independent.
* **Lifetime:** Blujay keeps a key for 24 hours after the request that used it. Retry within that window.
* **Optional, but use it:** without a key, every retry is a new request and can send a duplicate message.

## Route families

Each idempotent endpoint belongs to a route family. A key is unique within its family, not within one URL.

| Route family | Endpoint | Purpose |
| - | - | - |
| `chats.create` | `POST /v1/chats` | Start a chat with a first message |
| `chats.messages.create` | `POST /v1/chats/{chat_id}/messages` | Send a follow-up message |
| `chats.participants.add` | `POST /v1/chats/{chat_id}/participants` | Add a participant to a group chat |
| `chats.participants.remove` | `DELETE /v1/chats/{chat_id}/participants` | Remove a participant from a group chat |
| `messages.edit` | `PATCH /v1/messages/{message_id}` | Edit an iMessage |
| `messages.unsend` | `POST /v1/messages/{message_id}/unsend` | Unsend an iMessage |
| `messages.retry` | `POST /v1/messages/{message_id}/retry` | Retry a failed message |
| `webhook_events.replay` | `POST /v1/webhook-subscriptions/{subscription_id}/events/{event_id}/replay` | Replay a webhook event |

> [!WARNING]
>
> **Common mistake:** `chats.messages.create` is one family for every chat. Sending `Idempotency-Key: order-1042-link` to `chat_abc123` and then the same key to `chat_grp789` is a different request under the same key, so the second returns [`IDEMPOTENCY_REQUEST_MISMATCH`](/errors/idempotency-request-mismatch). Put something unique to the chat or the business event in the key.

The two send endpoints also accept the key as an `idempotency_key` field in the body. If you send both, they must match, or you get [`IDEMPOTENCY_KEY_MISMATCH`](/errors/idempotency-key-mismatch).

## Writes that ignore the key

These endpoints accept an `Idempotency-Key` header without error but don't use it. A retry does the work again, so check before you repeat one.

| Endpoint | What a repeat does | How to stay safe |
| - | - | - |
| `POST /v1/webhook-subscriptions` | Creates a second subscription, and your endpoint gets every event twice | List subscriptions before retrying |
| `POST /v1/vcards` | Creates a second saved contact card | List saved cards first |
| `POST /v1/contacts` | Returns [`CONTACT_ALREADY_EXISTS`](/errors/contact-already-exists) for the same phone number | Treat that error as success |
| `POST /v1/contacts/import` | Matches the same contact again and updates it; nothing is duplicated | Safe to repeat |
| `POST /v1/attachments` | Starts a second upload | Reuse the `attachment_id` you already have |
| `PATCH /v1/chats/{chat_id}` (rename) | Renames again to the same name | Harmless to repeat |
| `POST /v1/chats/{chat_id}/leave` | Tries to take the number out again | Read the chat's participants first |
| `POST /v1/chats/{chat_id}/typing`, `DELETE …/typing`, `POST …/read` | Repeats the signal | Harmless to repeat |

Workflow enrollments use their own required `request_id` field instead. See [Workflows](/workflows.md).

## Rejected requests are not remembered

Blujay remembers a key only when the request was accepted (`202`, or `200` for an edit or unsend). A request rejected with a `4xx`, for example [`VALIDATION_ERROR`](/errors/validation-error) or [`PRE_REPLY_CONTENT_BLOCKED`](/errors/pre-reply-content-blocked), releases the key. You can send the fixed request with the same key or a new one; both run as a new request.

If Blujay never finished the original request (for example your connection dropped mid-way and the work was lost), the key is released after about 5 minutes, and a retry with the same key runs the request. Until then the retry returns [`IDEMPOTENCY_IN_PROGRESS`](/errors/idempotency-in-progress).

## When to retry

| Result | What to do |
| - | - |
| Timeout, connection error, `500`, `503` | Retry with the same key. You get the original result if it had already succeeded. |
| `429` | Wait for `Retry-After` seconds, then retry with the same key. |
| `409` with `retryable: true` | Retry with the same key after about 1 second, backing off. |
| Any other `4xx` | Don't retry as is. Fix the request; the key was released, so reuse it or pick a new one. |
| `202 Accepted`, then the message fails later | Don't resend automatically. Read the reason from the `message.failed` webhook (`failure.code`), or `failure_code` on the message when it has one, fix the cause, then try `POST /v1/messages/{message_id}/retry`. It works only for direct-chat messages that failed before leaving Blujay; anything else returns [`RETRY_NOT_ALLOWED`](/errors/retry-not-allowed), and the message may already have arrived. Check before sending a new message with a new key. |

Back off between retries, for example 1, 2, 4, 8 and 16 seconds with some random jitter, and give up after about 5 attempts or a few minutes. Keep the key with the job so a retry after a restart uses the same key. If a `Retry-After` is longer than you want to wait (for example a daily limit), schedule the send for later; a key is honored for 24 hours.

## Next steps

> **[Errors](/errors.md)**
>
> Every error code and whether it is retryable.

> **[Rate limits](/rate-limits.md)**
>
> Request limits and how to back off.

> **[Track a message](/messaging/lifecycle.md)**
>
> What happens after `202`, and when to retry a failed message.


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