# Idempotency

> How the Idempotency-Key header makes POST and PATCH retries safe: what is stored, for how long, and which 409 you get when a key is reused with another body.

Source: https://upfour.io/docs/api/idempotency

Send an `Idempotency-Key` with a `POST` or `PATCH`. If the connection drops and you send the same request again, up4 returns the first answer instead of creating a second monitor.

## Use the header

```
KEY=$(uuidgen)

curl -si -X POST https://app.upfour.io/v1/monitors \
  -H "Authorization: Bearer $UP4_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{"name":"Example","url":"https://example.com","monitor_type":"http"}' \
  | grep -i -E '^(HTTP|idempotency-status)'
```

```
HTTP/2 201
idempotency-status: stored
```

Run the same command again with the same `KEY`:

```
HTTP/2 201
idempotency-status: replayed
```

## The rules

| Topic | Behavior |
| --- | --- |
| **Header** | `Idempotency-Key`, any non-empty text. A UUID is a good choice. An empty or missing header turns the feature off for that request. |
| **Methods** | `POST` and `PATCH`. `PUT`, `DELETE` and `GET` ignore the header. |
| **Not covered** | `POST /monitors/batch` ignores this layer; it requires the header and keeps its own receipt (same key and body: the stored receipt, a different body: a refusal). |
| **Required** | `POST /incidents/{incidentID}/escalate` and `POST /escalation-policies/{policyID}/test` answer `400` without the header and accept at most 128 characters. Everywhere else it is optional and has no length limit. |
| **A key belongs to** | Your Company, your user, the method and path of the request (not the query string), and the key text. The same key on another path is a different key. |
| **Body comparison** | JSON bodies are compared as JSON, so whitespace and key order do not matter. Other bodies are compared byte for byte. |
| **Stored for** | 24 hours after the first response, then deleted. After that the key is free again: the next request with it runs and its response is stored anew. |
| **What is stored** | Responses with a status from 200 to 499 and a JSON body, including validation errors. Not stored: 5xx and responses without a body. |
| **Secrets** | The response is stored as sent. A replay of `POST /webhook-subscriptions` or `POST /webhook-subscriptions/{id}/rotate-secret` therefore carries the same one-time `secret` again within those 24 hours, also after a later rotation made it obsolete. See [Get the signing secret](https://upfour.io/docs/api/webhooks#secret). |
| **Replays** | Return the stored status, content type and body with `Idempotency-Status: replayed`. |
| **Rate limits** | A replay counts as a request. |

## What you get

| Situation | Result |
| --- | --- |
| No receipt for the key | The request runs. A response with status 200 to 499 and a JSON body is stored and carries Idempotency-Status: stored. |
| Receipt, same body | The stored status and body come back with Idempotency-Status: replayed. The request does not run again. |
| Receipt, different body | 409 idempotency_key_conflict. details.original_request_at holds the time of the first request. |
| Receipt, but made with another API key or under changed access | 409 idempotency_replay_unavailable. The request does not run again. |
| Response was 5xx, 204 or had no JSON body | Nothing is stored. A retry with the same key runs the request again. |

```json
{
  "error": {
    "code": "idempotency_key_conflict",
    "message": "Idempotency key was already used with a different request body",
    "request_id": "req_…",
    "details": { "original_request_at": "2026-10-27T09:14:02Z" }
  }
}
```

The receipt remembers who sent the request: the API key, its scopes and access level, and its client access. A retry with another key, or after one of those changed, gets [idempotency_replay_unavailable](https://upfour.io/docs/api/errors#code-idempotency_replay_unavailable) instead of a replay, and the request is not run again. Look at the resource to see what happened.

## Common mistakes

- **A new key on every retry.** Generate the key once per intended operation and reuse it for every attempt of that operation.
- **One key for different operations.** The second body gets [409 idempotency_key_conflict](https://upfour.io/docs/api/errors#code-idempotency_key_conflict). Use one key per operation.
- **Fixing a rejected request under the same key.** A `400` is stored too. Resending the same body replays the `400`; resending a corrected body is a conflict. Use a new key for the corrected request.
- **Retrying while the first request is still running.** The receipt is written when the first request finishes, so an overlapping retry is not guaranteed to be caught. Wait for the response or your timeout.
- **Switching to another API key for the retry.** See `idempotency_replay_unavailable` above.
- **Relying on it for `DELETE` or `PUT`.** The header is ignored on those methods.
