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

# Idempotency

> Why creating a rental requires an Idempotency-Key, what a retry actually does, and how to pick keys.

`POST /rentals` requires an `Idempotency-Key` header. It is the only endpoint
that does, and the reason is narrow: creating a rental spends money, and a
network timeout tells you **nothing** about whether it happened.

```bash theme={null}
IDEMPOTENCY_KEY=$(uuidgen)   # once per intent — NOT once per attempt

curl -s https://api.gpuoutlet.ai/v1/rentals \
  -H "Authorization: Bearer $GPUOUTLET_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{ "offer_id": "off_3kd91mz7", "gpu_count": 2 }'
```

## The problem it solves

Your request times out. Three things could have happened:

1. It never reached us — no rental.
2. It reached us and failed — no rental.
3. It reached us and succeeded, and the **response** was lost — **a rental you
   are now paying for and do not know about.**

From the client, all three look identical. Without an idempotency key you have to
pick between never retrying (and sometimes silently failing to start work) and
always retrying (and sometimes paying for two machines). Neither is acceptable
when the resource costs dollars per hour.

With a key, the retry is safe: we recognise it and return the rental the first
attempt created.

## What a retry returns

Same key, same body → **the original rental**, with `201` and the original `id`.
Not a new machine, not an error.

```bash theme={null}
# First attempt — times out on your side, succeeds on ours
curl … -H "Idempotency-Key: $IDEMPOTENCY_KEY" -d '{"offer_id":"off_3kd91mz7"}'

# Retry — identical key, identical body
curl … -H "Idempotency-Key: $IDEMPOTENCY_KEY" -d '{"offer_id":"off_3kd91mz7"}'
# → the same rnt_… as the first attempt
```

## The three failure modes

<AccordionGroup>
  <Accordion title="Retried too fast — 409 idempotency_key_in_flight" icon="hourglass">
    The first attempt is still being processed. Rather than risk a second machine, we
    refuse and tell you how long to wait:

    ```json theme={null}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "idempotency_key_in_flight",
        "message": "A request with this Idempotency-Key is still being processed. Retry after the interval in `Retry-After`.",
        "retry_after_seconds": 2,
        "doc_url": "https://docs.gpuoutlet.ai/api/errors#idempotency_key_in_flight",
        "request_id": "req_bd913a7e05cc"
      }
    }
    ```

    Wait and retry **with the same key**. A new key here would create the second
    machine you were trying to avoid.
  </Accordion>

  <Accordion title="Same key, different body — 409 idempotency_key_reused" icon="copy">
    ```json theme={null}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "idempotency_key_reused",
        "message": "This Idempotency-Key was already used with a different request body. Use a new key for a new rental.",
        "doc_url": "https://docs.gpuoutlet.ai/api/errors#idempotency_key_reused",
        "request_id": "req_44f0c9b2e178"
      }
    }
    ```

    We compare a canonical hash of the body, so key ordering and whitespace do not
    matter — `{"a":1,"b":2}` and `{"b":2,"a":1}` are the same request.

    The alternatives were both worse. Returning the *first* rental would hand you a
    machine that does not match what you just asked for. Creating a *second* would
    make the key meaningless. An error is the only answer that cannot mislead.

    The usual cause is a key derived from something too coarse — see below.
  </Accordion>

  <Accordion title="No key at all — 400 idempotency_key_required" icon="circle-exclamation">
    ```json theme={null}
    {
      "error": {
        "type": "invalid_request_error",
        "code": "idempotency_key_required",
        "message": "This endpoint spends money, so `Idempotency-Key` is required. Generate one per intent (a UUID is fine).",
        "doc_url": "https://docs.gpuoutlet.ai/api/errors#idempotency_key_required",
        "request_id": "req_02e6bb14c8a9"
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Choosing keys

**One key per intent to create one rental.** 8–255 characters; a UUID v4 is the
easy right answer.

<CodeGroup>
  ```python Python theme={null}
  import uuid

  key = str(uuid.uuid4())          # generate ONCE, outside the retry loop

  for attempt in range(5):
      r = session.post(f"{API}/rentals", headers={"Idempotency-Key": key}, json=body)
      if r.status_code != 409:
          break
      time.sleep(r.json()["error"].get("retry_after_seconds", 2))
  ```

  ```javascript Node theme={null}
  const key = crypto.randomUUID(); // generate ONCE, outside the retry loop

  for (let i = 0; i < 5; i++) {
    const r = await fetch(`${API}/rentals`, {
      method: 'POST',
      headers: { ...auth, 'Content-Type': 'application/json', 'Idempotency-Key': key },
      body: JSON.stringify(body),
    });
    if (r.status !== 409) break;
    const err = (await r.json()).error;
    await new Promise((res) => setTimeout(res, (err.retry_after_seconds ?? 2) * 1000));
  }
  ```
</CodeGroup>

<Warning>
  Generate the key **outside** the retry loop. A `uuid4()` inside the loop gives
  every attempt a fresh key, which is exactly the same as having no idempotency at
  all — and it fails in the worst possible way, by working perfectly until the one
  time the network is slow.
</Warning>

### Deriving keys from your own ids

A deterministic key is fine, and often better, as long as it is unique per
intent:

```python theme={null}
key = f"job-{job_id}-attempt-{attempt_number}"   # good
key = f"job-{job_id}"                            # good if a job rents exactly once
key = f"user-{user_id}"                          # BAD — every rental this user
                                                 # ever makes collides
key = f"{date.today()}"                          # BAD — collides across the day
```

The test is simple: **if two different rentals could ever produce the same
string, the key is too coarse.** A too-coarse key does not create a duplicate
machine — it produces `idempotency_key_reused` and refuses to start the second
rental at all, which is a confusing outage rather than a billing incident.

## Scope and lifetime

* Keys are scoped to your **account**. Two of your keys using the same string
  collide; another customer using it does not affect you.
* Records are retained long enough to cover any realistic retry, then swept. A
  key reused weeks later may no longer be recognised — which is fine, since by
  then it is a new intent anyway.

## Everything else

No other endpoint takes the header. It would be noise:

* `GET` requests change nothing.
* `POST /rental-quotes` reserves nothing and costs nothing.
* `POST /rentals/{id}/stop` is **idempotent by nature** — stopping an
  already-stopping or already-stopped rental returns the rental as it is, not an
  error. Retry it freely.
