# Idempotency

> Send Idempotency-Key on any POST, PATCH or DELETE to make retries safe — a byte-identical retry replays the stored answer; a different body with the same key is refused.

Source: https://developers.processcart.com/guides/idempotency

A network timeout leaves you not knowing whether a write landed. Send an `Idempotency-Key` and retry the identical request: you get the original answer back instead of a second run, a second comment or a second webhook.

## How it works

```bash
curl -X POST https://app.processcart.com/api/v1/runs \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-order-1042-start" \
  -d '{ "workflowId": "…", "name": "Order 1042" }'
```

- The key is any string of 1–255 printable ASCII characters without spaces — a uuid, or something meaningful to you.
- Keys are stored **per workspace**, and a stored response is replayed only to the API key that made the request (or to the key one rotation away from it, in either direction). A **second API key** sending the same `Idempotency-Key` value is refused with 422 `idempotency_key_reused` — so prefix keys with something unique to each integration.
- The first request runs normally and its response is stored for **24 hours**.
- A second request with the same key **and a byte-identical body** (same method, path and `Expected-Revision` too) replays the stored response with the header `Idempotent-Replayed: true` — the same status, body and `Location`. A replay carries a fresh `X-Request-Id`, and a replayed problem body is rewritten to match it.
- A second request with the same key but a **different** method, path, body or `Expected-Revision` — or from a different API key — is refused with 422 `idempotency_key_reused`, and nothing is created.
- While the first request is still running, a duplicate answers 409 `idempotency_in_progress` with `Retry-After: 2`.

The key applies to `POST`, `PATCH` and `DELETE`. `GET` requests are naturally idempotent and ignore it.

## What is stored, and what is not

- Successes (2xx) and request-shaped refusals (most 4xx) are stored and replayed, so a retry of a request that was wrong gets the same honest answer.
- Answers that describe the **state at that moment** are never stored: every 409 (`revision_mismatch`, `idempotency_in_progress`, `run_trashed`, `task_locked`, `task_completed`, …); the 422s that read the run or its workflow — `required_fields_unsatisfied` (a required field still blank) and `field_not_writable` (among others, a field the run's rules hide right now); the 403s that read the current setup — `approver_only`, `workflow_not_runnable`, `not_allowed_for_key`; every 429; every 5xx. A retry re-executes against the current state — the point of retrying. Because nothing was stored, the key is free again: a corrected request may reuse it.
- Responses over 256 KB are not stored; a retry re-executes.
- A request that crashes releases the key, so the retry runs.

## The upload ticket

`POST /v1/runs/{id}/files` returns a signed upload URL that lives two hours. A replay of that request past the ticket's lifetime hands back the stored, now-expired ticket — ask for a new ticket with a new key instead of retrying the old one ([Tasks, approvals & files](https://developers.processcart.com/guides/tasks-approvals-files#files)).

## One-time secrets

A replay returns the stored response as it was — including a webhook's signing secret: the 201 of `POST /v1/webhooks` and the answer of `POST /v1/webhooks/{id}/rotate-secret` come back, secret included, to the same key (or its rotation) for 24 hours. Treat the `Idempotency-Key` on those two calls as secret material ([Webhooks](https://developers.processcart.com/webhooks)).

## Recommendation

Generate the key once per logical operation (before the first attempt), reuse it across every retry of that operation, and never reuse it for a different one. A good shape is `<integration>-<your record id>-<operation>` — `crm-order-1042-start`, `crm-order-1042-complete-review` — so two integrations in one workspace never send the same string.
