Reference
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.
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:
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. 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 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. |
| 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 theRetry-Afterseconds; every429has it),500 internaland503 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.
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")