# Idempotency & retries

> Retry SuperCool API requests safely. How the Idempotency-Key header works, how long keys and messages are kept, and which requests are safe to repeat.

Source: https://supercool.com/docs/api/idempotency

Networks fail. A request can time out after SuperCool received it, and if you just send it again, you might start the same video twice and pay for it twice. The `Idempotency-Key` header prevents that.

## How it works

Send a unique `Idempotency-Key` with every `POST /v1/messages`, and reuse it when you retry that same request:

```bash
curl https://api.supercool.com/v1/messages \
  -H "Authorization: Bearer $SUPERCOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-8812-product-video" \
  -d '{"message": "A 20 second product video for the lavender candle, SKU 8812"}'
```

| You send | You get |
|---|---|
| A new key | A new message, `201 Created`. |
| The same key and the same body | The **same message** as it is now, `200 OK`. No work starts twice, even if the first request is still running. |
| The same key with a different body | [`409 idempotency_conflict`](https://supercool.com/docs/api/errors#idempotency_conflict). Nothing is sent to the agent. |
| No key | A new message every time. |

"The same body" means the same `message` text (ignoring leading and trailing spaces), the same `files` and the same `upload_ids`.

## The rules

- **Scoped to the API key.** The same idempotency key sent with two different API keys is two different requests. After you [rotate](https://supercool.com/docs/api/authentication#rotate-a-key) an API key, retries with the new key start new messages.
- **Kept for 7 days.** After that, the same key starts a new message.
- **Up to 128 characters.** Longer keys are cut to 128, so make keys unique within their first 128 characters.
- **Message ids are always ours.** The message `id` (`msg_…`) is generated by SuperCool, never taken from your key, so a key reused after 7 days can't collide with an old message.

Good keys are unique per logical request and stable across retries: a UUID generated once and stored with the job, or an id from your own system, like `order-8812-product-video`.

## Two clocks

Idempotency keys and messages are kept for different lengths of time:

| Record | Kept for |
|---|---|
| Idempotency key → message | 7 days after the first request |
| The message, its jobs and results | At least 14 days after its last job's deadline (up to about 3 weeks for long work). See [retention](https://supercool.com/docs/api/results#retention). |
| The work (chat) and its files | As long as the chat exists in your account |

## Which requests are safe to retry

| Request | Safe to retry? |
|---|---|
| Any `GET` | Yes, always. |
| `POST /v1/messages` with an `Idempotency-Key` | Yes. |
| `POST /v1/messages` without one | **No.** A retry can start the work again. |
| `POST /v1/messages/{id}/recover` | Yes. It never starts new work. |
| `POST /v1/uploads` | Yes, but each call creates a new upload. Unused ones are deleted after 24 hours. |
| `POST /v1/uploads/{id}/complete` | Yes. A ready upload returns the same result. |
| `POST /v1/webhooks` | No. Each call creates another endpoint. List them first. |
| `PATCH /v1/webhooks/{id}` | Yes. |
| `DELETE /v1/webhooks/{id}` | Yes. A repeat returns `404 not_found`. |
| `POST /v1/webhooks/{id}/rotate-secret` | No. Each call makes a new secret. |

## When to retry

Retry with exponential backoff and a little jitter on:

- network errors and timeouts,
- `429 rate_limited` (wait the `Retry-After` seconds; every `429` has it),
- `500 internal` and `503 unavailable`.

Don't retry other `4xx` errors unchanged: fix the request first.

If a message comes back `failed` with `reason: "agent_busy"`, your agent was busy with another message and nothing ran. Send the **same request with the same `Idempotency-Key`** after `retry_after_seconds`: it runs the message this time and keeps the same message id.

```python
import os
import random
import time
import uuid

import requests

API = "https://api.supercool.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SUPERCOOL_API_KEY']}"}

def send(message: str, key: str | None = None) -> dict:
    key = key or str(uuid.uuid4())  # store this with your job to retry after a crash
    for attempt in range(6):
        try:
            r = requests.post(f"{API}/messages", json={"message": message},
                              headers={**HEADERS, "Idempotency-Key": key}, timeout=60)
        except requests.RequestException:
            r = None
        if r is not None and r.status_code < 500 and r.status_code != 429:
            r.raise_for_status()
            msg = r.json()
            if msg.get("reason") != "agent_busy":
                return msg
            time.sleep(msg.get("retry_after_seconds", 15))
            continue
        delay = float(r.headers.get("Retry-After", 0)) if r is not None else 0
        time.sleep(max(delay, min(30, 2 ** attempt)) + random.random())
    raise RuntimeError("SuperCool API unavailable, try again later")
```
