SuperCoolDocs Get an API key

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.

View as Markdown

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. 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 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")

Last updated