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

# Idempotency

> Send an Idempotency-Key on a create call so a lost response can be retried safely, without creating the resource twice.

Networks drop responses. When a create call fails before you see its result, you cannot tell whether it succeeded, and retrying blind risks creating the resource twice.

Send an `Idempotency-Key` header and the retry is safe: we replay the original response instead of doing the work again.

<CodeGroup>
  ```ts SDK theme={null}
  await endstate.units.list();
  ```

  ```bash cURL theme={null}
  curl https://api2.endstate.io/v1/units \
    -H "Authorization: Bearer end_sk_..." \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: jacket-0001" \
    -d '{"collection_id": "8e1a7f50-90ab-4cde-8012-3456789abcde", "external_id": "jacket-0001"}'
  ```
</CodeGroup>

The header is optional everywhere. Omit it and the call behaves exactly as it always has.

## Which calls accept it

Every call that creates something:

| Call                                         |                       |
| -------------------------------------------- | --------------------- |
| `POST /v1/units`                             | Create a unit         |
| `POST /v1/collections`                       | Create a collection   |
| `POST /v1/chips`                             | Pair a chip           |
| `POST /v1/chips/bulk`                        | Pair chips in bulk    |
| `POST /v1/taps`                              | Record a tap          |
| `POST /v1/units/{unit_id}/claims`            | Claim a unit          |
| `POST /v1/units/{unit_id}/transfers`         | Transfer a unit       |
| `POST /v1/units/{unit_id}/chip-replacements` | Replace a unit's chip |

Updates (`PATCH`, `PUT`) do not need a key. They set fields to the values you send, so sending the same update twice leaves the same result - retry them directly.

## Choosing a key

Any string of 1 to 255 characters, unique to **one logical operation**. An identifier you already have is usually the best choice - the resource's own `external_id`, a batch reference, a job id.

Reuse the same key for every retry of that one operation. Do not reuse it for a different operation, and do not generate a fresh key per retry - a new key is a new operation, which is exactly what you are trying to avoid.

## What happens on a retry

**Same key, same request body.** You get the original response back, with `Idempotent-Replayed: true`. The work is not repeated.

```http theme={null}
HTTP/1.1 201 Created
Idempotent-Replayed: true
```

**Same key, different request body.** `409 idempotency.key_conflict`. The key is already bound to a different request, so we will not guess which one you meant. Use a new key.

**Same key, original still in flight.** `409 idempotency.in_progress`. Wait a moment and send it again.

**The original failed.** Nothing is stored for a failed call, so the same key retries through and can succeed. You do not need a fresh key after an error.

## Scope and lifetime

Keys are scoped to your API key and to the specific call, so the same key value used on two different endpoints, or by two different API keys, never collides. For calls authenticated with a tap session token, the scope is the tap.

A stored response is replayable for 24 hours. Session-scoped entries expire with the session token instead, since the credential is gone by then.

## Replays are snapshots

A replayed response is the body we sent the first time, byte for byte. If the resource has changed since - issuance completed, a redirect URL was updated - the replay will not show it.

That is what makes a replay safe, but it means a replay is not a way to poll. Read the resource directly for current state:

<CodeGroup>
  ```ts SDK theme={null}
  const unit = await endstate.units.get(unitId);
  unit.collection?.token.status; // "pending" until issuance completes

  // Or wait for it, instead of polling by hand.
  await endstate.units.waitUntilIssued(unitId);
  ```

  ```ts fetch theme={null}
  const unit = await fetch(`${ENDSTATE}/v1/units/${unitId}`, { headers }).then(
    (r) => r.json(),
  );
  unit.collection.token.status; // "pending" until issuance completes
  ```
</CodeGroup>

## Creating the same resource twice on purpose

The key identifies a request, not an intent. Two calls that send byte-identical bodies look like one operation to us.

This comes up when pairing test chips: `{"unit_id": "...", "is_test": true}` carries nothing that distinguishes the first chip from the second, so reusing a key returns the first chip rather than creating another. When you mean to create two, send two different keys.
