# Idempotency

> Send an Idempotency-Key on a write so retrying it after a timeout can't create a duplicate.

Source: https://docs.xplantpro.com/docs/idempotency

A network can fail after the server has done the work but before you get the answer. Retrying then risks doing the work twice: two tasks, two scans, two runs started. An `Idempotency-Key` header makes the retry safe.

```bash
curl -X POST https://app.xplantpro.com/api/v1/tasks \
  -H "Authorization: Bearer $XPLANT_API_KEY" \
  -H "Idempotency-Key: bench-3-0f2a7c91" \
  -H "Content-Type: application/json" \
  -d '{"title": "Check jar 47"}'
```

A repeat with the same key returns **the first response**, with the same status and body, and the response header `Idempotent-Replay: true`. The write happens once.

## Which endpoints honour it

The header is honoured on these endpoints, and only these:

- [`POST /assets`](https://docs.xplantpro.com/docs/api/media/create-asset.md): Attach a media file
- [`POST /comments`](https://docs.xplantpro.com/docs/api/comments/create-comment.md): Add a comment
- [`POST /contaminations`](https://docs.xplantpro.com/docs/api/contaminations/create-contamination.md): Record a contamination
- [`POST /devices`](https://docs.xplantpro.com/docs/api/devices/create-device.md): Register a device
- [`POST /equipment/{id}/events`](https://docs.xplantpro.com/docs/api/equipment/create-equipment-event.md): Record an equipment event
- [`POST /explants`](https://docs.xplantpro.com/docs/api/explants/create-explant.md): Create an explant
- [`POST /label-scans`](https://docs.xplantpro.com/docs/api/labels/create-label-scan.md): Record a label scan
- [`POST /media-recipes`](https://docs.xplantpro.com/docs/api/media/create-media-recipe.md): Create a media recipe
- [`POST /plants`](https://docs.xplantpro.com/docs/api/plants/create-plant.md): Create a plant
- [`POST /sop-runs`](https://docs.xplantpro.com/docs/api/sops/create-sop-run.md): Start an SOP run
- [`POST /sop-runs/{id}/steps/{stepId}/events`](https://docs.xplantpro.com/docs/api/sops/create-sop-step-event.md): Record step evidence
- [`POST /sop-runs/{id}/steps/{stepId}/measurements`](https://docs.xplantpro.com/docs/api/sops/create-sop-step-measurement.md): Record a step measurement
- [`POST /stages`](https://docs.xplantpro.com/docs/api/transfers-and-stages/create-stage.md): Advance a stage
- [`POST /tasks`](https://docs.xplantpro.com/docs/api/tasks/create-task.md): Create a task
- [`POST /tasks/demand`](https://docs.xplantpro.com/docs/api/tasks/create-demand-signal.md): Record a demand signal
- [`POST /transfers`](https://docs.xplantpro.com/docs/api/transfers-and-stages/create-transfer.md): Record a transfer

Everywhere else the header is ignored. **Sensor readings and device events** make retries safe with your own `external_id` instead (plus `recorded_at` for readings): a repeat of one already stored for the same device is skipped, and a single repeat answers `201` with the stored record and `meta.duplicate: true`.

## The rules

| | |
| --- | --- |
| Format | 8–255 characters from `A–Z a–z 0–9 . _ : ~ -`. A malformed key answers `422 VALIDATION_ERROR` rather than being silently ignored. |
| Scope | Your key plus the endpoint. The same string on two endpoints is two different writes, and two keys never collide. |
| How long | 24 hours. After that, the same key starts a fresh write. |
| Still running | If the first request hasn't finished, a repeat answers `409 IDEMPOTENCY_IN_FLIGHT` with `Retry-After: 1`. Wait and send it again; don't assume it failed. |
| Failed writes | Aren't replayed. If the first attempt failed, the next attempt runs for real, so a passing glitch isn't pinned to your key for a day. |
| No header | The write runs unprotected. Idempotency is opt-in. |

## Choosing keys

Make one key per **intent**, not per attempt. Every retry of the same logical write sends the same value; a new write sends a new one.

Good keys come from your own data: `station-3-run-wk38-start`, `scanner-3-<scan uuid>`, `nightly-sync-2026-09-25-task-8812`. A random UUID generated once, stored with the pending write and reused on each retry, also works.

The JavaScript SDK takes `{ idempotencyKey }` as the last argument of any write, and with `retry: true` it generates a key for you and reuses it across its own retries.
