Retry a write, run it once.
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: storedRun the same command again with the same KEY:
HTTP/2 201
idempotency-status: replayedThe 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. |
| 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. |
{
"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 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. Use one key per operation. - Fixing a rejected request under the same key. A
400is stored too. Resending the same body replays the400; 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_unavailableabove. - Relying on it for
DELETEorPUT. The header is ignored on those methods.
Was this page helpful?