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

# Idempotent requests

> Retry a write that timed out without running it twice: send an Idempotency-Key, and a retry under the same key gets the first response back instead of creating another alert, connection, or transcription request.

A `POST` that times out leaves you guessing: the alert, webhook connection, or transcription request may already exist. Send an `Idempotency-Key` header with the write, resend the same key on every retry, and the request runs at most once. A retry gets the first response back, so you can retry a write the same way you retry a read.

## Send a key

Generate one key per logical write, such as a UUID, before the first attempt, and reuse it for every retry of that write:

```bash theme={"dark"}
KEY=$(uuidgen)

curl -X POST "https://api.particle.pro/v1/projects/$PARTICLE_PROJECT_ID/alerts" \
  -H "X-API-Key: $PARTICLE_API_KEY" \
  -H "Idempotency-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d @alert.json
```

Run the same command again with the same `$KEY` and you get the first response back, with `Idempotent-Replayed: true`, and no second alert. Mint a new key for a new write: a key belongs to one request.

A key is 8 to 128 printable ASCII characters. The header is optional; a write sent without it behaves as it always has.

## Endpoints that take a key

| Endpoint | A retry without a key would |
| - | - |
| [`POST /v1/projects/{projectId}/alerts`](/alerts/create) | Create a second alert and use another alert from your allowance |
| [`POST /v1/projects/{projectId}/webhooks/connections`](/alerts/webhooks) | Create a second connection with a different secret |
| [`POST /v1/projects/{projectId}/webhooks/connections/{id}/secret/rotate`](/alerts/webhooks#manage-connections) | Rotate again, invalidating the secret the first rotation returned before you ever saw it |
| [`POST /v1/alerts/{id}/test-webhook`](/alerts/webhooks#send-a-test-event) | Send the test event again |
| [`POST /v1/podcasts/transcription/requests`](/podcasts/transcription) | Return your existing request, billed as a lookup |

Each of these carries `x-idempotent: true` and the `Idempotency-Key` header parameter in the [OpenAPI document](https://api.particle.pro/openapi.json), so a generated client can find them. Other endpoints ignore the header. Reads are already safe to retry, and an alert update or a delete can simply be sent again.

## What a retry gets back

| When the retry arrives | Response |
| - | - |
| After the first request succeeded | The first response: the same status and body, with `Idempotent-Replayed: true`. Nothing runs again and nothing is billed. |
| While the first request is still running | `409` [`idempotency_key_in_use`](/errors/idempotency_key_in_use) with `Retry-After`. Wait and retry with the same key. |
| After the first request failed | The request runs again. Only a success uses up a key, so a `4xx` or `5xx` never locks you out of retrying once you fix the cause. |
| With a different body, path, or query | `422` [`idempotency_key_reused`](/errors/idempotency_key_reused). The key belongs to the request it was first sent with. |

A request is the same when its method, path, query string, and body bytes all match. Resend the body exactly as you first sent it rather than rebuilding it.

A success is replayed for **24 hours**. After that the key is forgotten, and a request under it runs as new.

## Scope

A key belongs to the API key that sent it. Two API keys can use the same key string without colliding, and one project's key can never replay another project's response. A revoked key gets `401` on its retries like on any other request. The header is ignored on a keyless [x402](/x402) request, which has no credential to hold a key, and on a signed-in session at platform.particle.pro, whose access to a project can be withdrawn while a key is still being replayed.

## Limits

* **An organization can start 50,000 keyed writes in 24 hours.** Past that, a request with a new key gets `429` [`rate_limit_exceeded`](/errors/rate_limit_exceeded) with `Retry-After` and does not run. Retries of keys already in use still get their answers, and requests without a key are unaffected.
* **If we cannot check the key, the request does not run.** A `503` with `Retry-After` means the key could not be looked up, so the write was refused rather than run unrecorded. Retry with the same key.
* **A failure on our side can still run a write twice.** If our server stops, or loses the store that records keys, after the write commits but before its response is recorded, a retry gets `409 idempotency_key_in_use` until shortly after the request's own time limit, and a retry after that runs the write again. This takes a failure on our side at one exact moment; a timeout or a dropped connection on your side does not cause it.
* **MCP tool calls do not take a key.** The `particle_alert_*` tools on the [MCP server](/mcp/overview) run without one; check with `particle_alert_list` before creating an alert again.

Webhook deliveries are the other direction: we retry them to you, and your receiver de-duplicates them on the match `id`. See [Delivery, retries, and idempotency](/alerts/webhooks#idempotency).


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