# SuperCool API: full documentation > SuperCool is one AI agent behind one endpoint. Send it a plain-language message (POST https://api.supercool.com/v1/messages) and it plans the work, runs the tools and returns the finished result: videos, images, websites, decks, research and writing. Same agent, memory and chats as the SuperCool app, phone, WhatsApp, the CLI and the MCP. Generated from https://supercool.com/docs. Each section below is one page; its source URL is under its title. # The SuperCool API > One agent, one endpoint. Send SuperCool a plain-language message and get back finished work, like videos, images, sites, decks and research. Source: https://supercool.com/docs/api Most AI APIs give you a model. You pick one, write the prompt for it, chain the steps and stitch the outputs together yourself. The SuperCool API gives you an agent instead. You send it a message in plain language, like "a 15 second vertical ad for my candle shop", "research my top five competitors" or "build me a landing page". The agent plans the work, picks and runs the tools, checks the result and hands back the finished files. There's no model to choose and no pipeline to build. ```bash curl https://api.supercool.com/v1/messages \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d '{"message": "A 15 second vertical ad for my candle shop, warm and cozy"}' ``` ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "status": "processing", "reply": "On it. I'm making a 15 second vertical ad with warm candlelight shots.", "jobs": [{ "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "title": "Candle shop vertical ad", "status": "running" }], "files": [] } ``` A few minutes later, `GET /v1/messages/msg_3f9a…` returns `"status": "completed"` and a signed link to `candle_ad.mp4`. ## What it can make Anything you can ask for in the SuperCool app, you can ask for over the API: - **Video:** ads, explainers, talking heads, short-form clips, with music and voiceover. - **Images:** product shots, social posts, thumbnails, logos, edits of images you send. - **Websites:** landing pages and small sites, hosted and live at a link. - **Decks and documents:** presentations, reports, spreadsheets, PDFs. - **Research:** multi-step web research with sources, competitor and market scans. - **Writing and audio:** copy, scripts, emails, voiceovers and music. The agent decides how to do the work. You describe the outcome you want. ## How it works 1. **You send a message** to `POST /v1/messages`, with optional files. The call returns within about 30 seconds. 2. **The agent answers and starts work.** A quick question gets a finished answer right away (`completed`). Bigger jobs come back as `processing`, with a `jobs` list that has one entry for each piece of work the agent started. 3. **You get the result.** Poll `GET /v1/messages/{id}?wait=30`, read its [events](https://supercool.com/docs/api/results#events), or get a [webhook](https://supercool.com/docs/api/webhooks). When the message is `final`, its `files` hold signed download links. [Getting results](https://supercool.com/docs/api/results) explains every status in detail. ## One agent, everywhere The API talks to the same agent you use in the SuperCool app, by phone, on WhatsApp, in the [CLI](https://supercool.com/cli) and through the [MCP server](https://supercool.com/mcp). It has the same memory, the same chats and the same credits. - Work you start over the API shows up in the app, and every job has a `link` to its chat. - Work you start in the app is visible to the API through [`GET /v1/work`](https://supercool.com/docs/api/work). - You can follow up the way you would in a chat. "Make it 9:16 and add captions" continues the same work. ## The basics | | Details | |---|---| | Base URL | `https://api.supercool.com/v1` | | Auth | `Authorization: Bearer sc_key_…` ([Authentication](https://supercool.com/docs/api/authentication)) | | Format | JSON requests and responses | | Request ids | Every response has a `Request-Id` header (`req_…`) | | Errors | One shape: `{"error", "message", "request_id", "docs"}` ([Errors](https://supercool.com/docs/api/errors)) | | Retries | `Idempotency-Key` header ([Idempotency](https://supercool.com/docs/api/idempotency)) | | Pricing | The same credits as the app; talking to the agent is free ([Credits](https://supercool.com/docs/api/credits)) | | Versioning | The version is in the path (`/v1`). Changes are listed in the [changelog](https://supercool.com/docs/api/changelog). | API keys are server-side secrets. The API sends no CORS headers, so it can't be called from a web page. Call it from your server, a script or a job. ## Next steps - [Quickstart](https://supercool.com/docs/api/quickstart): send your first message and download the result. - [Sending messages](https://supercool.com/docs/api/messages): files, follow-ups and questions from the agent. - [Webhooks](https://supercool.com/docs/api/webhooks): skip polling and get told when work is done. - [API reference](https://supercool.com/docs/api/reference): every endpoint and field. --- # Quickstart > Get an API key, send your SuperCool agent a message, wait for the work to finish and download the files, with curl, Python or JavaScript. Source: https://supercool.com/docs/api/quickstart This takes about five minutes, plus however long the agent takes to make what you asked for. You need a SuperCool account. Any plan works. ## 1. Get an API key Open the [API dashboard](https://supercool.com/dashboard#api) (**Dashboard → API**) and create a key. Give it a nickname, like "prod server" or "my laptop". The full key (`sc_key_…`) is shown once, so copy it now. Put it in an environment variable. Never commit it or ship it to a browser. ```bash export SUPERCOOL_API_KEY="sc_key_..." ``` Check that it works: ```bash curl https://api.supercool.com/v1/me -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` You should see your account, your plan, your available `credits` and your agent's name. ## 2. Send a message Tell the agent what you want in plain language. The `Idempotency-Key` header makes the request safe to retry: if the connection drops and you send it again with the same key, you get the same message back instead of starting the work twice. ```bash tab="curl" curl https://api.supercool.com/v1/messages \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"message": "A 15 second vertical ad for my candle shop, warm and cozy"}' ``` ```python tab="Python" import os import uuid import requests API = "https://api.supercool.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['SUPERCOOL_API_KEY']}"} resp = requests.post( f"{API}/messages", headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())}, json={"message": "A 15 second vertical ad for my candle shop, warm and cozy"}, timeout=60, ) resp.raise_for_status() msg = resp.json() print(msg["id"], msg["status"], msg["reply"]) ``` ```js tab="JavaScript" // Node 18+ (save as quickstart.mjs) import { randomUUID } from "node:crypto"; const API = "https://api.supercool.com/v1"; const headers = { Authorization: `Bearer ${process.env.SUPERCOOL_API_KEY}` }; const res = await fetch(`${API}/messages`, { method: "POST", headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": randomUUID() }, body: JSON.stringify({ message: "A 15 second vertical ad for my candle shop, warm and cozy" }), }); const msg = await res.json(); console.log(msg.id, msg.status, msg.reply); ``` The call returns within about 30 seconds. For a video, the agent replies and starts the work, so the message comes back as `processing` with one running job: ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "message", "status": "processing", "final": false, "reply": "On it. I'm making a 15 second vertical ad with warm candlelight shots.", "jobs": [ { "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "status": "running", "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "files": [] } ], "files": [], "credits_used": 0 } ``` A simple question ("what's a good tagline for a candle shop?") comes back `completed` right away, with the answer in `reply`. ## 3. Wait for the result Call `GET /v1/messages/{id}` with `?wait=30`. The call holds for up to 30 seconds and returns as soon as something changes. Repeat until `final` is `true`. ```bash tab="curl" curl "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d?wait=30" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```python tab="Python" while not msg["final"]: if msg.get("resume") == "add_credits": print("Out of credits: add some before", msg["resume_by"], "and the work resumes by itself.") break resp = requests.get(f"{API}/messages/{msg['id']}", headers=HEADERS, params={"wait": 30}, timeout=60) resp.raise_for_status() msg = resp.json() for job in msg["jobs"]: print(f" {job['title']}: {job['status']}") print("Finished:", msg["status"], msg["reason"] or "") ``` ```js tab="JavaScript" let current = msg; while (!current.final && current.resume !== "add_credits") { const r = await fetch(`${API}/messages/${current.id}?wait=30`, { headers }); current = await r.json(); for (const job of current.jobs) console.log(` ${job.title}: ${job.status}`); } console.log("Finished:", current.status, current.reason ?? ""); ``` When the video is done, the message is `completed`, `final` is `true`, and `files` lists what the agent made: ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "status": "completed", "final": true, "files": [ { "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl..." } ], "credits_used": 42.5 } ``` Other outcomes, like `needs_input` (the agent has a question), `blocked` (out of credits) or `partial`, are explained in [Getting results](https://supercool.com/docs/api/results#statuses). ## 4. Download the files Each file's `url` is a signed link, so you don't need to send your API key with it. Download it like any URL: ```bash tab="curl" curl -L -o candle_ad.mp4 "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl..." ``` ```python tab="Python" for f in msg["files"]: with requests.get(f["url"], stream=True, timeout=300) as r: r.raise_for_status() with open(f["name"], "wb") as out: for chunk in r.iter_content(chunk_size=1 << 20): out.write(chunk) print("Saved", f["name"]) ``` ```js tab="JavaScript" import { writeFile } from "node:fs/promises"; for (const f of current.files) { const r = await fetch(f.url); if (!r.ok) throw new Error(`download failed: ${r.status}`); await writeFile(f.name, Buffer.from(await r.arrayBuffer())); console.log("Saved", f.name); } ``` Links expire. If one has, or if the download answers `412` because the file changed since the link was made, get a fresh one from [`GET /v1/files/{file_id}`](https://supercool.com/docs/api/files#fresh-links). ## Next steps - **Skip polling:** register a [webhook](https://supercool.com/docs/api/webhooks) and get a signed POST when the message is done. - **Send files:** attach images, PDFs or footage to a message. See [Files & uploads](https://supercool.com/docs/api/files). - **Follow up:** send another message like "make it 9:16 and add captions" and the agent continues the same work. See [Sending messages](https://supercool.com/docs/api/messages#follow-ups). - **Handle every outcome:** read [Getting results](https://supercool.com/docs/api/results) and [Rate limits & errors](https://supercool.com/docs/api/errors). - **Watch it in the app:** every job's `link` opens its chat in SuperCool. --- # Authentication > Authenticate to the SuperCool API with a bearer API key. How to create, store, rotate and revoke keys, and why keys stay on your server. Source: https://supercool.com/docs/api/authentication Every request needs an API key in the `Authorization` header: ```http Authorization: Bearer sc_key_9fK2...x7Qa ``` A key stands for your account. Anything sent with it goes to your agent, lands in your chats and uses your credits. ## Create a key Keys live in the [API dashboard](https://supercool.com/dashboard#api) (**Dashboard → API**). 1. Click **Create key** and give it a nickname that says where it's used: "Zapier", "prod server", "Sam's laptop". 2. Optionally set it to expire after 30, 90 or 365 days. The default is never. 3. Copy the key. It's shown **once**. SuperCool stores only a hash of it, so it can't be shown again. The dashboard lists each key by nickname with its first and last characters (`sc_key_9fK2…x7Qa`), when it was created and last used, and its requests and credits over the last 30 days. You can rename a key at any time. An account can have up to 20 active keys. ## Keep keys on your server API keys are secrets, like a password to your account. - **Never put a key in a web page, mobile app or public repo.** The API sends no CORS headers, so browsers refuse to call it from a web page. That's deliberate: a key in front-end code is a key anyone can copy. - Load keys from an environment variable or a secret manager, not from source code. - Use one key per integration. Then the dashboard shows usage per integration, and you can revoke one without touching the others. To call SuperCool from a front end, send the request to your own backend and have your backend call the API. ## Check a key `GET /v1/me` returns the account a key belongs to, your plan, available credits, your agent's name, the key's id and nickname, and your limits: ```bash curl https://api.supercool.com/v1/me -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "object": "account", "user_id": "64f1c2a9e4b0a1b2c3d4e5f6", "email": "you@example.com", "name": "Sam Rivera", "plan": "pro", "credits": 1840, "agent": { "name": "Nova" }, "key": { "id": "pat:6a1b2c3d4e5f", "name": "prod server" }, "limits": { "running_jobs": 5, "requests_per_minute": 600 } } ``` ## Rotate a key To replace a key without downtime: 1. Create a new key in the dashboard. 2. Deploy it everywhere the old one is used. 3. Watch the old key's **last used** time in the dashboard. Once it stops moving, revoke it. [Idempotency keys](https://supercool.com/docs/api/idempotency) are scoped to the API key that sent them. A retry sent with the new API key is treated as a new request, so finish or abandon in-flight retries before you switch. ## Revoke a key Revoke a key from the dashboard. It stops working on the next request. Messages and work it started keep running and stay in your account and in the app, but that key can no longer read them. If a key leaks, revoke it right away and create a new one. Your request inspector in the dashboard shows every request each key made in the last 30 days, with its IP country. ## Authentication errors A missing, unknown, expired or revoked key gets `401` with the error code [`unauthorized`](https://supercool.com/docs/api/errors#unauthorized) and a `WWW-Authenticate: Bearer` header: ```json { "error": "unauthorized", "message": "unknown or revoked token. Send your API key as 'Authorization: Bearer sc_key_…'.", "request_id": "req_6c2f0e1d9a8b7c6d5e4f3a2b", "docs": "https://supercool.com/docs/api/errors#unauthorized" } ``` The [SuperCool CLI](https://supercool.com/cli)'s sign-in token also works as a bearer token on `/v1`, but for servers and scripts, use an API key. --- # Sending messages > Send your SuperCool agent a message with POST /v1/messages. Attach files, follow up on work in progress, answer the agent's questions and route webhooks. Source: https://supercool.com/docs/api/messages Everything starts with `POST /v1/messages`. You talk to the agent the way you would in the SuperCool app: say what you want, attach what it needs, and follow up. ## Send a message ```bash curl https://api.supercool.com/v1/messages \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: launch-video-2026-09-30" \ -d '{"message": "Research my top 5 competitors (candle shops in Austin) and make a one-page PDF summary"}' ``` | Field | Type | Description | |---|---|---| | `message` | string | What you want, in plain language. Up to 20,000 characters. For longer text, attach it as a file. | | `files` | array | Small files inline: up to 5, each a public `url` or `base64` bytes. See [Attach files](#attach-files). | | `upload_ids` | array | Large files you uploaded first: up to 10. See [Files & uploads](https://supercool.com/docs/api/files#uploads). | | `webhook_endpoint_id` | string | Send this message's webhook events to one registered endpoint. See [Webhooks](https://supercool.com/docs/api/webhooks#per-message). | You need a `message`, `files` or `upload_ids`. An empty request gets [`400 empty_message`](https://supercool.com/docs/api/errors#empty_message). Always send an `Idempotency-Key`: one fresh value per logical request, reused on retries. See [Idempotency & retries](https://supercool.com/docs/api/idempotency). ## What comes back The call returns within about 30 seconds with the [message object](https://supercool.com/docs/api/reference#object-message) and HTTP `201`: - **A quick answer** comes back finished: `"status": "completed"`, `"final": true`, the answer in `reply`, and any files the agent handed over in `files`. - **Longer work** comes back as `"status": "processing"`: the agent replied ("On it…") and started one or more **jobs**. Each job is one piece of work running in a chat (`work_id`), with its own `status` and a `link` to that chat in the app. ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "message", "status": "processing", "reason": null, "question": null, "final": false, "reply": "I'll research the five closest competitors and put it in a one-page PDF.", "jobs": [ { "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "kind": "execution", "role": "execution", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Austin candle shop competitors", "status": "running", "final": false, "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "files": [] } ], "files": [], "notes": [], "credits_used": 0, "created_at": "2026-09-30T14:02:03.118000Z", "updated_at": "2026-09-30T14:02:12.950000Z" } ``` Keep the message `id` (`msg_…`). You use it to [get the result](https://supercool.com/docs/api/results). Message ids are always generated by SuperCool. Messages belong to the API key that sent them: another key gets `404 not_found` for them. The work itself (the chats) belongs to your account and is visible to every key through [`/v1/work`](https://supercool.com/docs/api/work). ## Attach files For small files, put them in `files`. Each entry has either a `url` (a public `https` link SuperCool downloads) or `base64` (the bytes), plus an optional `name` and `content_type`: ```json { "message": "Turn these product photos into a 3-slide carousel for Instagram", "files": [ { "url": "https://example.com/photos/candle-1.jpg" }, { "name": "candle-2.png", "content_type": "image/png", "base64": "iVBORw0KGgoAAAANSUhEUgAA..." } ] } ``` - Up to **5 files**, each up to **10 MB**, **25 MB** in total. Only the first 5 are taken. - Executables are refused. - A file that can't be fetched doesn't fail the message. The agent gets the rest, and the problem is listed in the message's `notes` (for example `"candle-1.jpg: couldn't be fetched (timeout)"`). For anything bigger (footage, long PDFs, audio), use an [upload](https://supercool.com/docs/api/files#uploads): up to 500 MB per file and 10 files per message. ## Follow-ups {#follow-ups} Send another message and the agent continues the conversation, with the same memory it has in the app. Refer to earlier work naturally: ```json { "message": "Love it. Make a 9:16 version with captions, and a 1:1 cut for the feed" } ``` If the work is still running, a follow-up can join it instead of starting something new. The follow-up's message then lists that job with `"role": "follow_up"`, and its status follows the running work. If the agent starts new work, you get new jobs. Every message is its own object with its own `id` and status. To track the combined result, follow the latest message, or read the chat with [`GET /v1/work/{work_id}`](https://supercool.com/docs/api/work). ## When the agent asks a question {#questions} Sometimes the agent needs a decision before it can go on ("vertical or square?"). The message then ends as `"status": "needs_input"` with the question in `question`: ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "status": "needs_input", "question": "Should the ad be vertical (9:16) for Reels, or square (1:1) for the feed?", "final": true } ``` Answer by sending a new message. The agent picks up where it left off: ```json { "message": "Vertical, for Reels" } ``` ## Out of credits, busy, and other outcomes Talking to the agent is free. Work uses credits. With no credits, the message is still accepted (HTTP `201`) and comes back `"status": "blocked"`, `"reason": "out_of_credits"`. That's a normal outcome, not an error. See [Credits & pricing](https://supercool.com/docs/api/credits#out-of-credits). Up to **5 jobs can run at once** per account. Messages always get through, but new work may not start: see [At capacity](#at-capacity). Every status and reason is explained in [Getting results](https://supercool.com/docs/api/results#statuses). ## At capacity {#at-capacity} Up to 5 jobs can run at once on your account. A message is never refused for this: stopping work ("stop the ad") or following up on running work always gets through. But if the agent tries to **start** new work while 5 jobs are already running, that start is refused: - The message comes back normally (`201`) with a job of `kind: "refusal"` (`job_id` like `ref_…`), `status: "blocked"`, `reason: "too_many_running"` and `final: true`. - The message's `status` follows the usual rules: `blocked` if that was its only job, `partial` if another of its jobs completed. - The message has a `running` array listing the work that's running now: ```json { "id": "msg_7a1d0c9e8b2f4a6c5d3e1f0b", "status": "blocked", "reason": "too_many_running", "final": true, "jobs": [{ "job_id": "ref_3c9e1a7b5d2f", "kind": "refusal", "status": "blocked", "reason": "too_many_running", "final": true }], "running": [ { "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f" } ] } ``` Wait for one of the running jobs to finish (follow its message, or a webhook), then send the request again. Queued work that is waiting for credits also doesn't resume while 5 jobs are running. It stays queued and resumes once there's room, as long as that's within its 24-hour window. ## Route this message's webhooks {#routing} By default a message's [webhook events](https://supercool.com/docs/api/webhooks) go to every endpoint on your account that wants them. To send one message's events to a single endpoint instead (say, one per customer or environment), pass its id: ```json { "message": "...", "webhook_endpoint_id": "whe_4c1d2e3f4a5b6c7d8e9f" } ``` It must be an endpoint registered on your account (`POST /v1/webhooks`). Unknown ids get `404 not_found`. ## Limits | Limit | Value | |---|---| | Message text | 20,000 characters | | Inline files | 5 per message, 10 MB each, 25 MB total | | Uploads | 10 per message, 500 MB each, 1 GB total | | Messages | 120 per hour and 1,000 per day, per account | | Running jobs | 5 per account (new work waits; see [At capacity](#at-capacity)) | See [Rate limits & errors](https://supercool.com/docs/api/errors#rate-limits) for everything else. --- # Getting results > Follow a SuperCool message to its result. Long-poll GET /v1/messages/{id}, read events or use webhooks, and handle every status, reason and job. Source: https://supercool.com/docs/api/results `POST /v1/messages` returns within about 30 seconds. If the agent finished in that time, you already have the result. If it started longer work, the message is `processing` and you follow it until it's `final`. There are three ways to follow a message. Use whichever fits your stack: | | How | Best for | |---|---|---| | **Long-poll** | `GET /v1/messages/{id}?wait=30` in a loop | Scripts and simple servers | | **Events** | `GET /v1/messages/{id}/events?cursor=…` | Showing progress step by step | | **Webhooks** | SuperCool POSTs to your URL | Servers that shouldn't poll | They all read the same record, so they never disagree. ## Long-poll the message {#long-poll} ```bash curl "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d?wait=30" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` `wait` holds the request for up to that many seconds (0 to 45) and returns as soon as something changes: the status, the agent's reply arriving, or any job's status. It also returns right away when the message is already final. Without `wait`, you get the current state immediately. ```python import os import requests API = "https://api.supercool.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['SUPERCOOL_API_KEY']}"} def wait_for(message_id): while True: r = requests.get(f"{API}/messages/{message_id}", headers=HEADERS, params={"wait": 30}, timeout=60) r.raise_for_status() msg = r.json() if msg["final"] or msg.get("resume") == "add_credits": return msg ``` Set your HTTP client's timeout above `wait` (60 seconds for `wait=30` is a good default). Long-polls count toward a limit of 1,500 waits per hour per account, which a loop like this stays well under. ## The message object {#message} | Field | What it tells you | |---|---| | `id` | The message id, `msg_…`. | | `status` | Where the message stands overall. See [Statuses](#statuses). | | `reason` | Why, for `blocked`, `failed`, `partial` and `expired`. See [Reasons](#reasons). | | `final` | `true` when nothing about the message will change any more. Stop polling. | | `reply` | What the agent said, once it has answered. | | `question` | For `needs_input`, the agent's question. | | `jobs` | Each piece of work the message started, with its own status and files. See [Jobs](#jobs). | | `files` | Every file the message produced, from the reply and from all jobs. | | `notes` | Problems with attachments you sent, like a URL that couldn't be fetched. | | `credits_used` | Credits this message's jobs used so far. | | `resume`, `resume_by` | Set when queued work is waiting for credits. See [Waiting for credits](#resume). | | `retry_after_seconds` | Set when the agent was busy. See [`agent_busy`](#reasons). | | `running` | Set when work couldn't start because 5 jobs were already running: the running jobs, each with `job_id`, `message_id` and `work_id`. | | `created_at`, `updated_at` | Timestamps (UTC, ISO 8601). | The full schema is in the [reference](https://supercool.com/docs/api/reference#object-message). ## Statuses {#statuses} A message's `status` sums up all of its work. It is **not** just "has the agent replied": the agent often replies in seconds ("On it") while the video it started takes minutes. | Status | Meaning | Final? | What to do | |---|---|---|---| | `processing` | The agent is still thinking, or work is running. | No | Keep polling, or wait for a webhook. | | `completed` | Everything finished. Also used for a plain reply that started no work. | Yes | Read `reply` and download `files`. | | `needs_input` | The agent asked a question and nothing is running. | Yes | Answer with a [new message](https://supercool.com/docs/api/messages#questions). | | `blocked` | Nothing could run, usually because of credits. | Usually | See `reason`. If `resume` is set, add credits and the work runs by itself. | | `partial` | Some work finished, some didn't. | Usually | Use the finished `files`; check each job's `reason`. | | `failed` | Nothing finished. | Usually | Check `reason`. Retry or rephrase. | | `expired` | Work outlived its watch without reporting back. | Yes | [Recover](#recover) it, or read the work. | Rely on `final`, not on the status alone. A `blocked` or `partial` message whose queued work is waiting for credits is not final yet: it can still move to `processing` and then `completed`. ### How the status is worked out The status comes from structured signals: the agent's actions and each job's outcome, never from the wording of the reply. The rules apply in this order, and the first match wins: 1. The agent is still planning or starting work: `processing`. A quick job finishing while the agent sets up the next one can't end the message early. 2. The agent's turn failed, or the agent was busy with another message: `failed`, even if some jobs finished. 3. Any job is running: `processing`. 4. The agent asked a question and nothing is running: `needs_input`. 5. No jobs, just a reply: `completed`. 6. Every job completed: `completed`. 7. Every job blocked: `blocked`. 8. Every job expired: `expired`. 9. At least one job completed and at least one didn't: `partial`. 10. Otherwise: `failed`. So if a message asked for two things and the second was refused for lack of credits, it ends `partial`, with one completed job and one blocked job. ## Reasons {#reasons} `reason` says why a message or job didn't simply complete. For a message, it's the reason of the first job that explains the status. | Reason | On | Meaning | What to do | |---|---|---|---| | `out_of_credits` | job, message | No credits to start the work, or it ran out of credits mid-run. | [Add credits](https://supercool.com/docs/api/credits#out-of-credits). If `resume` is set, the queued work resumes by itself. Otherwise send a new message (like "continue"). | | `hosting_fees` | job, message | Hosting fees for your sites are due, so new work can't start. | Settle them in the app, then send the message again. | | `too_many_running` | job, message | The agent tried to start new work while 5 jobs were already running on your account. The message lists them in `running`. | Wait for one to finish, then send the message again. See [At capacity](https://supercool.com/docs/api/messages#at-capacity). | | `queue_expired` | job, message | Queued work waited more than 24 hours for credits and was dropped. | Send the message again. | | `watch_expired` | job, message | Running work didn't report back within 7 days. | [Recover](#recover) it or read the work. | | `result_unknown` | job, message | A recovered job's chat is idle and no result came back for it. | Open the work to check, or send a follow-up. | | `execution_failed` | job, message | The work ran and failed. | Read the job's chat (`link`), then retry or rephrase. | | `stopped` | job, message | The work was stopped (for example, from the app). | Send a new message if you still want it. | | `agent_busy` | message | Your agent was busy with another message, so nothing ran. | Send the same request again with the same `Idempotency-Key` after `retry_after_seconds` (15). | | `turn_failed` | message | The agent's turn failed before it finished. Other values here, like `interrupted` or `abandoned`, give more detail. | Send the message again. Anything that already started is still listed in `jobs`. | Treat unknown reasons like `failed`: new, more specific reasons may be added. ## Jobs {#jobs} Each job is one piece of work: one run in one chat. A message that asked for a video and a landing page usually has two jobs. | Field | Meaning | |---|---| | `job_id` | `exe_…` for work that ran or is queued; `ref_…` for work refused before it was queued. | | `kind` | `execution` (real work) or `refusal` (refused before anything was queued). | | `role` | `execution`, or `follow_up` when the message joined work that was already running. | | `status` | `running`, `completed`, `failed`, `cancelled`, `blocked` or `expired`. | | `reason` | Why, for `blocked`, `failed`, `cancelled` and `expired`. | | `final` | `false` while running, and for queued work that is waiting for credits. | | `queued`, `resume_by` | Set for queued work waiting for credits. | | `work_id`, `link` | The chat the work runs in, and its URL in the app. | | `title` | A short name for the work. | | `files` | The files this job made. | | `started_at`, `settled_at` | When it started running and when it finished. | ## Waiting for credits {#resume} If work was queued but your account had no credits, the job is `blocked` with `reason: "out_of_credits"`, `queued: true` and `final: false`. The message then carries: ```json { "status": "blocked", "reason": "out_of_credits", "final": false, "resume": "add_credits", "resume_by": "2026-10-01T14:02:11.402000Z" } ``` Add credits before `resume_by` (24 hours after the work was queued) and the work **starts by itself** (once fewer than 5 jobs are running on your account). The job moves to `running` and then to its result, and the message follows (`blocked` → `processing` → `completed`). **Don't resend the message**, or you'll start the work twice. After `resume_by`, the job becomes `expired` with `queue_expired`, and you have to send the message again. ## Events {#events} `GET /v1/messages/{id}/events` returns the message's update log: what happened, in order. It holds only this message's entries, plus those of any work it joined as a follow-up (so a follow-up sees that work's progress and result). If another message works separately in the same chat, its progress and results don't show up here. Use it to show progress ("Rendering the video…"), or to process each job's result as it lands. ```bash curl "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d/events?wait=30" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "object": "list", "data": [ { "seq": 117, "type": "agent_reply", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "text": "On it…", "files": [], "delivered_sync": true, "at": "2026-09-30T14:02:12.000000Z" }, { "seq": 118, "type": "progress", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "step": "Rendering the video", "status": "started", "file_names": [], "at": "2026-09-30T14:03:02.000000Z" }, { "seq": 121, "type": "result", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "text": "Your 15 second ad is ready.", "outcome": "completed", "files": [], "at": "2026-09-30T14:06:40.000000Z" } ], "has_more": false, "done": true, "cursor": "eyJjIjoiYXBpOnU..." } ``` - Pass `cursor` from the previous page to get only new entries. With no cursor, the log starts where the message started. - `wait` (0 to 45) holds the call until there's something new. It always waits at least one second. - `has_more: true` means more entries are ready: call again right away. - `done: true` means the message has nothing more to report. - Only one wait per message runs at a time. A second concurrent call gets [`409 poll_in_progress`](https://supercool.com/docs/api/errors#poll_in_progress). - Entries are kept for 7 days. An older cursor gets `cursor_expired: true` and a fresh cursor. Read the message itself to catch up. Every entry has `seq`, `type`, `at` and, when known, `work_id`, `message_id` and `job_id`. Progress and result entries carry `message_id` and `job_id` whenever the job is known. | Type | Meaning | Extra fields | |---|---|---| | `agent_reply` | The agent's reply. `delivered_sync: true` if it was already in the `POST` response. | `text`, `files` | | `progress` | A step started, or files landed in the chat. | `step`, `status`, `file_names` | | `result` | A job finished. `outcome` is `completed`, `failed` or `stalled_credits`. | `title`, `text`, `outcome`, `files` | | `state` | A job's state changed: `resumed`, `stopped`, `expired` or `unknown`. | `state`, `text` | | `turn_failed` | The agent's turn failed. | `text`, `reason` | | `turn_busy` | The agent was busy and the message didn't run. | `text`, `reason` | ## Webhooks {#webhooks} Rather than polling, register a URL and SuperCool POSTs a signed event when a job changes and when the message reaches a status other than `processing`. See [Webhooks](https://supercool.com/docs/api/webhooks). ## Recover old work {#recover} A message or job is `expired` when its work didn't report back in time (7 days for running work). The work may still have finished. `POST /v1/messages/{id}/recover` looks for its result again. It never resends the message or starts new work. ```bash curl -X POST https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d/recover \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "recovery", "work": [{ "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "state": "recovering" }] } ``` Each piece of work comes back `settled` (it already has its outcome), `watching` (still being watched), `recovering` (being looked up again: follow the message as usual) or `exhausted` (recovered too many times). For `exhausted` work, read the chat with [`GET /v1/work/{work_id}`](https://supercool.com/docs/api/work). ## How long messages are kept {#retention} A message, its jobs and its results stay readable for at least **14 days after its last job's deadline**. For running work the deadline is 7 days after it started; for queued work, 24 hours after it was queued; a plain reply is kept 14 days. After that, `GET /v1/messages/{id}` returns `404 not_found`. The work itself lasts as long as the chat exists in your account: read it any time with [`GET /v1/work/{work_id}`](https://supercool.com/docs/api/work), and get fresh file links from [`GET /v1/files/{file_id}`](https://supercool.com/docs/api/files#fresh-links). --- # Webhooks > Get a signed POST when SuperCool work finishes. Register endpoints, pick events, verify Standard Webhooks signatures, and handle retries. Source: https://supercool.com/docs/api/webhooks Agent work takes minutes. Instead of polling, register a URL and SuperCool POSTs a signed event to it when a job changes and when a message reaches a status other than `processing`. Webhooks read the same record as `GET /v1/messages/{id}`, so they never disagree with polling. ## Create an endpoint Create endpoints in the [API dashboard](https://supercool.com/dashboard#api) or with the API: ```bash curl https://api.supercool.com/v1/webhooks \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/supercool", "events": ["message.completed", "message.partial", "message.failed", "message.needs_input", "message.blocked", "message.expired"], "description": "prod" }' ``` ```json { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "url": "https://example.com/hooks/supercool", "events": ["message.completed", "message.partial", "message.failed", "message.needs_input", "message.blocked", "message.expired"], "description": "prod", "created_at": "2026-09-30T13:40:00.000000Z", "disabled_at": null, "disabled_reason": null, "failing_since": null, "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" } ``` The `secret` is shown **only in this response** (and when you rotate it). Store it with your other secrets: you need it to verify signatures. - The URL must be public `https` on port 443 or 8443, with no username or password in it. - Omit `events`, or pass `["*"]`, to get every event. - An account can have up to 10 endpoints. - Manage them with `GET /v1/webhooks`, `GET`, `PATCH` and `DELETE /v1/webhooks/{id}`, and `POST /v1/webhooks/{id}/rotate-secret`. See the [reference](https://supercool.com/docs/api/reference#webhooks). ## Events | Event | Sent when | |---|---| | `message.completed` | A message completed. | | `message.partial` | Some of a message's work finished and some didn't. | | `message.failed` | A message failed. | | `message.needs_input` | The agent asked a question and is waiting for your answer. | | `message.blocked` | A message is blocked, usually on credits. Sent even if queued work can still resume. | | `message.expired` | A message's work didn't report back in time. | | `job.completed` | One job finished. | | `job.failed` | One job failed or was stopped. | | `job.blocked` | One job is blocked (credits, hosting fees, or 5 jobs already running). | | `job.resumed` | A job that was waiting for credits started running. | | `job.expired` | A job was queued too long, or ran too long without reporting back. | Each message event is sent once per status a message reaches. A message can reach more than one: queued work that resumes after you add credits goes `message.blocked`, then `message.completed`. **Use `data.final`** to know whether more events can follow for that message. Most integrations only need the `message.*` events. Add `job.*` events to react to each piece of work as soon as it's done. ## Payload Every event is a JSON POST with the same envelope: ```json { "type": "message.completed", "timestamp": "2026-09-30T14:06:41.020000Z", "data": { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "message", "status": "completed", "final": true, "reply": "On it. I'm making a 15 second vertical ad with warm candlelight shots.", "jobs": [{ "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "status": "completed", "files": [] }], "files": [{ "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl..." }], "credits_used": 42.5 } } ``` - For `message.*` events, `data` is the full [message](https://supercool.com/docs/api/reference#object-message), exactly as `GET /v1/messages/{id}` returns it. - For `job.*` events, `data` is the [job](https://supercool.com/docs/api/reference#object-job) with `"object": "job"` and its `message_id`. The request carries these headers: | Header | Value | |---|---| | `webhook-id` | The event's id (`evt_…`). The same on every retry of that event. | | `webhook-timestamp` | Unix seconds when this attempt was signed. | | `webhook-signature` | `v1,` followed by the base64 signature. | | `Content-Type` | `application/json` | ## Verify signatures {#verify} Signatures follow the [Standard Webhooks](https://www.standardwebhooks.com) spec, so off-the-shelf libraries work. Always verify before trusting a payload, and verify the **raw** request body: parsing and re-serializing the JSON changes the bytes. ### With a library ```python tab="Python" # pip install standardwebhooks flask import os from flask import Flask, abort, request from standardwebhooks.webhooks import Webhook, WebhookVerificationError app = Flask(__name__) wh = Webhook(os.environ["SUPERCOOL_WEBHOOK_SECRET"]) # the whsec_... secret @app.post("/hooks/supercool") def supercool_webhook(): try: event = wh.verify(request.get_data(), dict(request.headers)) except WebhookVerificationError: abort(400) if event["type"] == "message.completed": for f in event["data"]["files"]: print("ready:", f["name"], f["url"]) return "", 204 ``` ```js tab="Node" // npm install standardwebhooks express import express from "express"; import { Webhook } from "standardwebhooks"; const app = express(); const wh = new Webhook(process.env.SUPERCOOL_WEBHOOK_SECRET); // the whsec_... secret app.post("/hooks/supercool", express.raw({ type: "application/json" }), (req, res) => { let event; try { event = wh.verify(req.body.toString("utf8"), req.headers); } catch { return res.sendStatus(400); } if (event.type === "message.completed") { for (const f of event.data.files) console.log("ready:", f.name, f.url); } res.sendStatus(204); }); app.listen(3000); ``` The libraries also reject timestamps more than 5 minutes old, which stops replayed requests. ### By hand The signature is an HMAC-SHA256 over `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is your secret with the `whsec_` prefix removed, base64-decoded. The result is base64-encoded and sent as `v1,`. The header can hold several space-separated signatures; accept the request if any `v1` one matches. ```python tab="Python" import base64 import hashlib import hmac import time def verify_supercool(secret: str, headers: dict, body: bytes, tolerance: int = 300) -> bool: headers = {k.lower(): v for k, v in headers.items()} msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] if abs(time.time() - int(timestamp)) > tolerance: return False # too old (or clock skew): possibly a replay key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() for sig in headers["webhook-signature"].split(" "): version, _, value = sig.partition(",") if version == "v1" and hmac.compare_digest(value, expected): return True return False ``` ```js tab="Node" import { createHmac, timingSafeEqual } from "node:crypto"; export function verifySupercool(secret, headers, rawBody, toleranceSeconds = 300) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; if (Math.abs(Date.now() / 1000 - Number(timestamp)) > toleranceSeconds) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest(); return String(headers["webhook-signature"]).split(" ").some((sig) => { const [version, value] = sig.split(","); if (version !== "v1" || !value) return false; const got = Buffer.from(value, "base64"); return got.length === expected.length && timingSafeEqual(got, expected); }); } ``` To test your code: with the secret `whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw`, the id `msg_p5jXN8AQM9LWM0D4loKWxJek`, the timestamp `1614265330` and the body `{"test": 2432232314}`, the signature is `v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=` (turn the timestamp check off for this test). ## Respond quickly, process later - Return any `2xx` within **10 seconds**. Anything else, a timeout or a network error counts as a failure and is retried. - Do the real work (downloading files, updating your database) after you respond, in a background job. - **Redirects are not followed.** A `3xx` counts as a failure. If your URL moved, update the endpoint. ## Retries and duplicates Delivery is **at least once**. A failed delivery is retried with growing gaps: after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and then once a day. - Every retry of an event has the same `webhook-id`. Store the ids you've processed and skip repeats. - Events can arrive out of order. Use `data.status` and `data.final`, or read `GET /v1/messages/{id}`, rather than assuming an order. - If an endpoint keeps failing for **3 days**, it's turned off (`disabled_at` is set), its pending events are dropped, and the account owner gets an email. To turn an endpoint back on after fixing it, click **Enable** in the dashboard or send `PATCH /v1/webhooks/{id}` with `{"enabled": true}`. The dashboard's delivery log shows each attempt with its status code, response and latency, and lets you **resend** any event. ## Send one message's events elsewhere {#per-message} By default, every endpoint that subscribes to an event receives it. To send a single message's events to one endpoint only, create the message with `webhook_endpoint_id`: ```json { "message": "A product video for SKU 8812", "webhook_endpoint_id": "whe_4c1d2e3f4a5b6c7d8e9f" } ``` That endpoint then gets all of the message's events, whatever its `events` list says, and no other endpoint gets them. The endpoint must belong to your account, so your code already has its secret. ## Rotate a secret `POST /v1/webhooks/{id}/rotate-secret` returns a new secret, shown once. Every delivery from then on is signed with it, including retries of older events. Update your verifier before or right after rotating. The Standard Webhooks libraries accept only one secret, so for a zero-downtime rotation, try the new secret first and then the old one for a few minutes. ## Test locally Your endpoint must be reachable from the internet over `https`. For local development, use a tunnel (for example `cloudflared tunnel --url http://localhost:3000` or `ngrok http 3000`) and register the tunnel's `https` URL as an endpoint. Then send a quick message, like "say hi", and watch for `message.completed`. --- # Files & uploads > Download what the SuperCool agent made with signed links, refresh expired or changed links, and send files, from small inline attachments to 500 MB uploads. Source: https://supercool.com/docs/api/files Files move both ways: the agent hands you what it made, and you send it what it needs. ## Files the agent made {#download} A message lists every file it produced in `files`, and each job lists its own. Each file looks like this: ```json { "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"5b1c0e6f9d2a4b7c8e1f3a5d7c9b0e2f\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ``` | Field | Meaning | |---|---| | `file_id` | A stable id, `/`. Use it to get a fresh link. | | `name` | The file name. | | `content_type` | The MIME type. | | `kind` | `image`, `video`, `audio`, `doc` or `site`. | | `size` | Size in bytes (`null` for a site). | | `url` | A signed download link. | | `revision` | An opaque version tag for the file the link points to (`null` for a site). | | `work_id` | The chat the file belongs to. | | `label` | An optional caption from the agent. | ### Download links `url` is a signed link to one file. It works without your API key, so you can download it from any machine or hand it to a customer. Treat it like a password to that file. - **Links expire.** Today they last 30 days, but don't rely on the exact time: store the `file_id`, not the link. - **Links are pinned to a revision.** If the agent later changes the file (you asked for an edit), the old link answers `412 Precondition Failed` instead of serving different bytes than you were told about. - **Links stop working** if the chat is deleted, or if the account owner changes their password. - Links for a **site** (`kind: "site"`) open the live site, which can change. Sites have no `revision` or `size`. ```bash curl -L -o candle_ad.mp4 "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl..." ``` ### Get a fresh link {#fresh-links} When a link has expired or answers `412`, ask for the file again by its `file_id`. File ids contain slashes; put them in the path as they are: ```bash curl https://api.supercool.com/v1/files/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4 \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "object": "file", "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 5120044, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"a41f07c2e9d85b3610f2c7e4d9a8b1c3\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ``` The new link points to the file's current revision. [`GET /v1/work/{work_id}`](https://supercool.com/docs/api/work) also returns fresh links for a chat's latest files. ## Sending files to the agent There are two ways, depending on size: | | Inline (`files`) | Uploads (`upload_ids`) | |---|---|---| | Size | Up to 10 MB each, 25 MB per message | Up to 500 MB each, 1 GB per message | | Count | 5 per message | 10 per message | | How | A public `https` URL, or base64 bytes, in the message | Upload first, then pass the id | | Best for | Photos, logos, short PDFs | Footage, long documents, audio | Inline files are described in [Sending messages](https://supercool.com/docs/api/messages#attach-files). Executables are refused either way. ## Large uploads {#uploads} An upload takes three calls: create it, send the bytes straight to storage, and complete it. Then attach it to a message. 1. **Create the upload** with the file's name, exact size in bytes and type. You get back a presigned POST: a `url` and the `fields` to send with it. It's valid for 15 minutes. 2. **POST the file to `url`** as `multipart/form-data`: every entry of `fields` as a form field, exactly as given, and the file last, in a field named `file`. Don't send your API key to this URL. 3. **Complete the upload.** SuperCool checks the file (its real size, and that it isn't an executable) and marks it `ready`. 4. **Send a message** with the `upload_id` in `upload_ids`. ```python tab="Python" import mimetypes import os import requests API = "https://api.supercool.com/v1" HEADERS = {"Authorization": f"Bearer {os.environ['SUPERCOOL_API_KEY']}"} path = "product-shoot.mov" name = os.path.basename(path) ctype = mimetypes.guess_type(path)[0] or "application/octet-stream" # 1. Create the upload. up = requests.post(f"{API}/uploads", headers=HEADERS, timeout=30, json={"filename": name, "size": os.path.getsize(path), "content_type": ctype}) up.raise_for_status() up = up.json() # 2. Send the bytes to storage: the fields first, the file last. with open(path, "rb") as fh: r = requests.post(up["url"], data=up["fields"], files={"file": (name, fh, ctype)}, timeout=3600) r.raise_for_status() # 3. Complete it. requests.post(f"{API}/uploads/{up['upload_id']}/complete", headers=HEADERS, timeout=60).raise_for_status() # 4. Attach it to a message. msg = requests.post(f"{API}/messages", headers=HEADERS, timeout=60, json={ "message": "Cut this footage into a 30 second teaser with upbeat music", "upload_ids": [up["upload_id"]], }).json() print(msg["id"], msg["status"]) ``` ```js tab="JavaScript" // Node 18+ (save as upload.mjs) import { readFile, stat } from "node:fs/promises"; import { basename } from "node:path"; const API = "https://api.supercool.com/v1"; const auth = { Authorization: `Bearer ${process.env.SUPERCOOL_API_KEY}` }; const path = "product-shoot.mov"; const type = "video/quicktime"; // 1. Create the upload. const up = await (await fetch(`${API}/uploads`, { method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({ filename: basename(path), size: (await stat(path)).size, content_type: type }), })).json(); // 2. Send the bytes to storage: the fields first, the file last. const form = new FormData(); for (const [k, v] of Object.entries(up.fields)) form.append(k, v); form.append("file", new Blob([await readFile(path)], { type }), basename(path)); const s3 = await fetch(up.url, { method: "POST", body: form }); if (!s3.ok) throw new Error(`upload failed: ${s3.status} ${await s3.text()}`); // 3. Complete it. await fetch(`${API}/uploads/${up.upload_id}/complete`, { method: "POST", headers: auth }); // 4. Attach it to a message. const msg = await (await fetch(`${API}/messages`, { method: "POST", headers: { ...auth, "Content-Type": "application/json" }, body: JSON.stringify({ message: "Cut this footage into a 30 second teaser", upload_ids: [up.upload_id] }), })).json(); console.log(msg.id, msg.status); ``` ```bash tab="curl" # 1. Create the upload (needs jq). UP=$(curl -s https://api.supercool.com/v1/uploads \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"filename\": \"product-shoot.mov\", \"size\": $(wc -c < product-shoot.mov), \"content_type\": \"video/quicktime\"}") ID=$(echo "$UP" | jq -r .upload_id) # 2. Send the bytes to storage: every field, then the file. ARGS=() while IFS= read -r kv; do ARGS+=(-F "$kv"); done < <(echo "$UP" | jq -r '.fields | to_entries[] | "\(.key)=\(.value)"') curl -sf -X POST "$(echo "$UP" | jq -r .url)" "${ARGS[@]}" -F "file=@product-shoot.mov" # 3. Complete it. curl -X POST "https://api.supercool.com/v1/uploads/$ID/complete" -H "Authorization: Bearer $SUPERCOOL_API_KEY" # 4. Attach it to a message. curl https://api.supercool.com/v1/messages \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d "{\"message\": \"Cut this footage into a 30 second teaser\", \"upload_ids\": [\"$ID\"]}" ``` ### Upload rules - `size` must be the exact size in bytes, up to 500 MB. Storage refuses a larger body. - The file must be sent with the same `Content-Type` you declared (it's one of the `fields`). - The presigned POST works for 15 minutes. After that, create a new upload. - Completing is safe to repeat: a `ready` upload returns the same result. - An upload that isn't attached to a message within 24 hours is deleted. - A message can attach up to 10 uploads, 1 GB in total. ### Upload errors | Code | When | |---|---| | [`bad_size`](https://supercool.com/docs/api/errors#bad_size) | `size` is missing or not a positive whole number. | | [`too_large`](https://supercool.com/docs/api/errors#too_large) | Over 500 MB per file, or 1 GB per message. | | [`not_uploaded`](https://supercool.com/docs/api/errors#not_uploaded) | You completed it before the file reached storage, or the presigned POST expired. | | [`changed`](https://supercool.com/docs/api/errors#changed) | The file was replaced while it was being completed. Upload it again. | | [`executable_refused`](https://supercool.com/docs/api/errors#executable_refused) | The file is a program. | | [`not_pending`](https://supercool.com/docs/api/errors#not_pending) | The upload was already rejected, or already attached to a message. | | [`upload_not_ready`](https://supercool.com/docs/api/errors#upload_not_ready) | A message named an upload that isn't completed (or isn't yours). | | [`too_many`](https://supercool.com/docs/api/errors#too_many) | More than 10 uploads on one message. | --- # Work > List and read your SuperCool agent's work. The same chats you see in the app, whether they started over the API, in the app, by phone or elsewhere. Source: https://supercool.com/docs/api/work Every piece of work the agent does happens in a chat, identified by a `work_id`. These are the same chats you see in the SuperCool app. Work you start over the API shows up there, and work you start in the app, by phone, on WhatsApp, in the CLI or through the MCP shows up here. Messages are short-lived and belong to the API key that sent them. Work belongs to your account and lasts as long as the chat does. Use it to read results long after a message is gone, or to see what the agent is doing across every channel. ## List recent work ```bash curl "https://api.supercool.com/v1/work?limit=10" -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "object": "list", "data": [ { "object": "work", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "preview": "Your 15 second ad is ready.", "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f" } ] } ``` Newest first. `limit` is 1 to 50 (default 20). `link` opens the chat in the SuperCool app. ## Read one piece of work ```bash curl https://api.supercool.com/v1/work/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` ```json { "object": "work", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "status": "idle", "text": "Your 15 second ad is ready. I used slow push-ins on the candles and a warm grade...", "next_from_char": null, "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "files": [ { "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"5b1c0e6f9d2a4b7c8e1f3a5d7c9b0e2f\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ] } ``` | Field | Meaning | |---|---| | `status` | `running` (the agent is working in it now), `stalled_credits` (stopped until credits are added) or `idle`. | | `text` | The latest result text. | | `next_from_char` | More text is available: call again with `?from_char=` this value. `null` when you have it all. | | `files` | The chat's most recent files (up to 8), each with a fresh download link. | | `link` | The chat in the SuperCool app. | Long results are paged: pass `from_char` to read on from where the last page stopped. ## Continue a piece of work There's no separate endpoint for this. Send a [message](https://supercool.com/docs/api/messages#follow-ups) and refer to the work the way you would in the app ("in the candle ad, swap the music for something calmer"). The agent knows your chats and continues the right one. ## Limits Reading work counts toward a limit of 600 reads per hour per account, on top of the per-key request limit. See [Rate limits](https://supercool.com/docs/api/errors#rate-limits). --- # Credits & pricing > The SuperCool API uses the same credits as the app. Talking to the agent is free; credits are used when it starts work. What happens when you run out. Source: https://supercool.com/docs/api/credits There's no separate API price list. The API uses the same credits as the SuperCool app, from the same balance, on any plan. - **Talking to the agent is free.** Questions, answers and planning cost nothing. - **Credits are used when the agent starts work**: making a video, generating images, building a site, running research. What a job costs depends on the work, just as it does in the app. - See plans and credit packs on the [pricing page](https://supercool.com/pricing). ## See what you spend - **Your balance:** `GET /v1/me` returns `credits`, the credits available now. - **Per message:** every message has `credits_used`, the credits its jobs have used so far. It grows while work runs and is final when the message is. - **Per key:** the [API dashboard](https://supercool.com/dashboard#api) shows requests and credits for each API key. Credits are counted per job, so usage stays accurate even when the app, the CLI and several keys all work in the same chat. ```bash curl https://api.supercool.com/v1/me -H "Authorization: Bearer $SUPERCOOL_API_KEY" # { "object": "account", "plan": "pro", "credits": 1840, ... } ``` ## Running out of credits {#out-of-credits} Running out is **not an HTTP error**. The message is accepted (`201`), and its status tells you what happened. It shows up as `"status": "blocked"` (or `partial`, if some work did finish) with `"reason": "out_of_credits"`, and triggers the `message.blocked` and `job.blocked` [webhooks](https://supercool.com/docs/api/webhooks). What to do depends on when the credits ran out: ### Before the work was queued The agent couldn't start the work. The message has a refused job (`job_id` starting with `ref_`, `kind: "refusal"`, `final: true`), and nothing more will happen. **What to do:** add credits, then send the message again. ### Queued, waiting for credits {#queued} The work was queued but couldn't start. The job keeps its real `exe_…` id with `queued: true` and `final: false`, and the message says how to resume it: ```json { "status": "blocked", "reason": "out_of_credits", "final": false, "resume": "add_credits", "resume_by": "2026-10-01T14:02:11.402000Z", "jobs": [{ "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "status": "blocked", "reason": "out_of_credits", "queued": true, "final": false, "resume_by": "2026-10-01T14:02:11.402000Z" }] } ``` **What to do:** add credits before `resume_by` (24 hours after it was queued). The work **starts by itself**: you get `job.resumed`, then the result. **Don't resend the message**, or the work runs twice. After `resume_by` the job becomes `expired` with `reason: "queue_expired"` and you need to send the message again. That rule is deliberate: nobody gets charged for a day-old request they may have forgotten. ### Mid-run The work started and ran out of credits partway. The job ends `blocked` with `out_of_credits` and `final: true`, and the chat's [work status](https://supercool.com/docs/api/work) is `stalled_credits`. **What to do:** add credits, then send a follow-up like "continue the candle ad". The agent picks up in the same chat. ## Adding credits Buy credits or upgrade your plan in the SuperCool app, or turn on pay-as-you-go top-ups so work doesn't stop when your balance runs low. Whenever credits land (a purchase, a top-up, a plan renewal), queued API work that is waiting for them resumes on its own. ## Hosting fees If hosting fees for your SuperCool sites are due, new work is refused with `reason: "hosting_fees"`. Settle them in the app, then send the message again. --- # API reference > Every endpoint of the SuperCool API: parameters, request and response schemas, errors, and curl, Python and JavaScript samples. Source: https://supercool.com/docs/api/reference Base URL: `https://api.supercool.com/v1`. Authenticate with `Authorization: Bearer sc_key_…`. Requests and responses are JSON; every error has the shape `{"error", "message", "request_id", "docs"}`. OpenAPI: https://supercool.com/docs/api/openapi.json ## Messages Talk to your agent and follow the work it starts. ### Send a message `POST /v1/messages` Sends your agent a message, optionally with files. The call returns within about 30 seconds: either the finished answer, or `status: "processing"` when the agent started longer work. Follow a processing message with [`GET /messages/{message_id}`](#get-message) (use `?wait=` to long-poll), its [events](#list-message-events), or a [webhook](https://supercool.com/docs/api/webhooks). Returns `201` for a new message and `200` when an `Idempotency-Key` replays an existing one. Running out of credits is not an error: the message comes back with `status: "blocked"` and `reason: "out_of_credits"`. Neither is the running-jobs cap: work the agent can't start because 5 jobs are already running comes back as a `blocked` job with `reason: "too_many_running"`, and the message lists the `running` work. **Parameters** | Name | Type | Description | |---|---|---| | `Idempotency-Key` | string (header) | Any string up to 128 characters, unique per logical request. Scoped to the API key and kept for 7 days: sending it again with the same body returns the same message (`200`) and never starts work twice; with a different body it's `409 idempotency_conflict`. | **Request body** | Field | Type | Description | |---|---|---| | `message` | string | What you want, in plain language. Up to 20,000 characters; send longer text as a file. | | `files` | array of InlineFile | Small files inline (up to 5, 10 MB each, 25 MB in total). Each has a `url` or `base64`. A file that can't be fetched doesn't fail the message: it's listed in the message's `notes`. | | `upload_ids` | array of string | Finished uploads to attach (up to 10, 1 GB in total). See `POST /uploads`. | | `webhook_endpoint_id` | string \| null | Send this message's webhook events to this registered endpoint (`whe_…`) instead of the account's endpoints. | **Responses** - `200`: An `Idempotency-Key` you already used with the same body. The same message, as it is now. Returns Message. - `201`: The message was accepted. It is either final already or `processing`. Returns Message. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `409`: The request conflicts with the current state. - `413`: A file or upload is over the size limit. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `400 empty_message`, `400 too_long`, `400 too_many`, `401 unauthorized`, `404 not_found`, `409 idempotency_conflict`, `409 upload_not_ready`, `413 too_large`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/messages" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-8812-ad" \ -d '{ "message": "A 15 second vertical ad for my candle shop, warm and cozy" }' ``` ### Get a message `GET /v1/messages/{message_id}` One message: its status, the agent's reply, the jobs it started and the files they made. With `wait`, the call holds for up to that many seconds and returns as soon as something changes (the status, the reply arriving, or a job's status) or the message is final. Messages are kept for at least 14 days after their last job's deadline. After that this returns `404 not_found`; read the work instead ([`GET /work/{work_id}`](#get-work)). **Parameters** | Name | Type | Description | |---|---|---| | `message_id` (required) | string (path) | The message's `id` (`msg_…`). | | `wait` | integer (query) | Long-poll for up to this many seconds (0 to 45). `0` returns immediately. Default `0`. | **Responses** - `200`: The message. Returns Message. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d?wait=30" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "message", "status": "completed", "reason": null, "question": null, "final": true, "reply": "On it. I'm making a 15 second vertical ad with warm candlelight shots.", "jobs": [ { "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "kind": "execution", "role": "execution", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "status": "completed", "reason": null, "final": true, "queued": null, "resume_by": null, "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "started_at": "2026-09-30T14:02:11.402000Z", "settled_at": "2026-09-30T14:06:40.221000Z", "files": [ { "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"5b1c0e6f9d2a4b7c8e1f3a5d7c9b0e2f\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ] } ], "files": [ { "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"5b1c0e6f9d2a4b7c8e1f3a5d7c9b0e2f\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ], "notes": [], "credits_used": 42.5, "created_at": "2026-09-30T14:02:03.118000Z", "updated_at": "2026-09-30T14:02:12.950000Z" } ``` ### List a message's events `GET /v1/messages/{message_id}/events` The message's update log, oldest first: progress steps, late replies, each job's result with its files, and state changes. Only this message's entries: another message's progress in the same chat isn't included. Pass back the `cursor` from the previous page to get only what's new. With no cursor, the log starts where the message started. `done` is `true` once the message has nothing more to report. The call waits at least one second, and up to `wait` seconds, for new entries. Only one wait per message runs at a time (`409 poll_in_progress` otherwise). Entries are kept for 7 days; a cursor older than that returns `cursor_expired: true` with a fresh cursor. **Parameters** | Name | Type | Description | |---|---|---| | `message_id` (required) | string (path) | The message's `id` (`msg_…`). | | `cursor` | string (query) | The `cursor` from the previous page. Opaque. | | `wait` | integer (query) | Wait up to this many seconds for new entries (0 to 45; at least 1 second is always waited). Default `0`. | **Responses** - `200`: A page of events. Returns EventList. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `409`: The request conflicts with the current state. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `400 bad_cursor`, `401 unauthorized`, `404 not_found`, `409 poll_in_progress`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d/events?wait=30" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "list", "data": [ { "seq": 118, "type": "progress", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "at": "2026-09-30T14:03:02.000000Z", "step": "Rendering the video", "status": "started", "file_names": [] }, { "seq": 121, "type": "result", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "at": "2026-09-30T14:06:40.000000Z", "title": "Candle shop vertical ad", "text": "Your 15 second ad is ready.", "outcome": "completed", "files": [] } ], "has_more": false, "done": true, "cursor": "eyJjIjoiYXBpOnU..." } ``` ### Recover a message's results `POST /v1/messages/{message_id}/recover` For work that outlived its watch (a job or message that `expired`): re-arms the watch so the result is looked up again and lands in the message, its events and your webhooks. It never resends the message or starts new work. One entry per piece of work: `settled` (already has its outcome), `watching` (still being watched), `recovering` (re-armed now) or `exhausted` (recovered too many times; read the work). **Parameters** | Name | Type | Description | |---|---|---| | `message_id` (required) | string (path) | The message's `id` (`msg_…`). | **Responses** - `200`: What happened to each piece of work. Returns Recovery. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/messages/msg_3f9a1c2e7b8d4e5f6a7b8c9d/recover" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "object": "recovery", "work": [ { "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "state": "recovering" } ] } ``` ## Uploads Attach large files (up to 500 MB each) to a message. ### Start an upload `POST /v1/uploads` Starts a large upload (up to 500 MB). Returns a presigned POST: send a `multipart/form-data` POST to `url` with every entry of `fields` as form fields and the file last, as a field named `file`. The link is valid for 15 minutes. Then call [`POST /uploads/{upload_id}/complete`](#complete-upload) and pass the `upload_id` in a message's `upload_ids`. Uploads never attached to a message are deleted after 24 hours. **Request body** | Field | Type | Description | |---|---|---| | `filename` (required) | string | | | `size` (required) | integer | The exact size in bytes (up to 500 MB). The upload is capped at this size. | | `content_type` | string \| null | The MIME type. Guessed from the file name when omitted. The POST must send the same `Content-Type` field (it is in `fields`). | **Responses** - `201`: The upload was created. POST the file to `url` next. Returns UploadCreated. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `413`: A file or upload is over the size limit. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `400 bad_size`, `401 unauthorized`, `413 too_large`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/uploads" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "filename": "product-shoot.mov", "size": 157286400, "content_type": "video/quicktime" }' ``` Example response (`201`): ```json { "object": "upload", "upload_id": "up_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "url": "https://supercool-uploads.s3.amazonaws.com/", "fields": { "key": "agent-uploads/incoming/.../product-shoot.mov", "Content-Type": "video/quicktime", "policy": "eyJleHBpcmF0aW9uIjoi...", "x-amz-signature": "3f1c..." }, "expires_at": "2026-09-30T14:17:03.118000Z", "max_bytes": 157286400 } ``` ### Complete an upload `POST /v1/uploads/{upload_id}/complete` Finalizes an upload after the file was POSTed to the presigned `url`. The file is checked (its real size, and executables are refused) and becomes `ready` to attach. Calling it again on a ready upload returns the same result. **Parameters** | Name | Type | Description | |---|---|---| | `upload_id` (required) | string (path) | The `upload_id` from `POST /uploads` (`up_…`). | **Responses** - `200`: The upload is ready to attach. Returns Upload. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `409`: The request conflicts with the current state. - `413`: A file or upload is over the size limit. - `422`: The uploaded file was refused (an executable). - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `409 not_pending`, `409 not_uploaded`, `409 changed`, `413 too_large`, `422 executable_refused`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/uploads/up_0a1b2c3d4e5f60718293a4b5c6d7e8f9/complete" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "upload", "upload_id": "up_0a1b2c3d4e5f60718293a4b5c6d7e8f9", "status": "ready", "filename": "product-shoot.mov", "content_type": "video/quicktime", "size": 157286400 } ``` ## Work The chats your agent works in, the same ones you see in the app. ### List recent work `GET /v1/work` Your agent's recent work (chats), newest first: the same list you see in the app, whether the work was started over the API, in the app, by phone or anywhere else. **Parameters** | Name | Type | Description | |---|---|---| | `limit` | integer (query) | How many to return (1 to 50). Default `20`. | **Responses** - `200`: Recent work. Returns WorkList. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `401 unauthorized`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/work?limit=10" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "list", "data": [ { "object": "work", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "preview": "Your 15 second ad is ready.", "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f" } ] } ``` ### Get a piece of work `GET /v1/work/{work_id}` One piece of work: whether it's running, the latest result text (paged with `from_char` / `next_from_char`) and its most recent files (up to 8), each with a fresh download link. **Parameters** | Name | Type | Description | |---|---|---| | `work_id` (required) | string (path) | The `work_id` from a job, an event or `GET /work`. | | `from_char` | integer (query) | Read the text from this character on (use the previous `next_from_char`). Default `0`. | **Responses** - `200`: The work. Returns Work. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/work/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "work", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "status": "idle", "text": "Your 15 second ad is ready. I used warm, slow push-ins on the candles...", "next_from_char": null, "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "files": [] } ``` ## Files Download what the agent made. ### Get a file `GET /v1/files/{file_id}` A file's record with a fresh signed download link. Use it when a `url` has expired, or when a download answered `412` because the file changed since the link was made. File ids contain slashes (`/`); put them in the path as they are. **Parameters** | Name | Type | Description | |---|---|---| | `file_id` (required) | string (path) | The `file_id` from a message, job, event or work (`/`, slashes included). | **Responses** - `200`: The file. Returns FileObject. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/files/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "file", "file_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4", "name": "candle_ad.mp4", "content_type": "video/mp4", "kind": "video", "size": 4812345, "url": "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl...", "revision": "\"5b1c0e6f9d2a4b7c8e1f3a5d7c9b0e2f\"", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "label": null } ``` ## Account Who the key belongs to, the plan, credits and limits. ### Get the account `GET /v1/me` The account the API key belongs to, its plan, available credits, the agent's name, the key itself and your limits. **Responses** - `200`: The account. Returns Account. - `401`: The API key is missing, unknown or revoked. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/me" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "account", "user_id": "64f1c2a9e4b0a1b2c3d4e5f6", "email": "you@example.com", "name": "Sam Rivera", "plan": "pro", "credits": 1840, "agent": { "name": "Nova" }, "key": { "id": "pat:6a1b2c3d4e5f", "name": "prod server" }, "limits": { "running_jobs": 5, "requests_per_minute": 600 } } ``` ## Webhooks Get pushed events instead of polling. ### List webhook endpoints `GET /v1/webhooks` The account's webhook endpoints (up to 10). **Responses** - `200`: The endpoints. Returns WebhookEndpointList. - `401`: The API key is missing, unknown or revoked. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/webhooks" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "list", "data": [ { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "url": "https://example.com/hooks/supercool", "events": [ "*" ], "description": "prod", "created_at": "2026-09-30T13:40:00.000000Z", "disabled_at": null, "disabled_reason": null, "failing_since": null } ] } ``` ### Create a webhook endpoint `POST /v1/webhooks` Registers a public `https` URL (port 443 or 8443) to receive events. The response includes the endpoint's signing `secret` (`whsec_…`). It is shown only this once: store it now. Omit `events` (or pass `["*"]`) to receive every event. **Request body** | Field | Type | Description | |---|---|---| | `url` (required) | string | A public `https` URL (port 443 or 8443, up to 2,000 characters). | | `events` | array of string \| null | Events to receive, or `["*"]` for all (the default). | | `description` | string \| null | Up to 120 characters. | **Responses** - `201`: The endpoint, with its secret (shown once). Returns WebhookEndpointWithSecret. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `409`: The request conflicts with the current state. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `400 bad_url`, `400 bad_events`, `400 bad_request`, `401 unauthorized`, `409 too_many_endpoints`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/webhooks" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/hooks/supercool", "events": [ "message.completed", "message.failed", "message.blocked" ], "description": "prod" }' ``` Example response (`201`): ```json { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "url": "https://example.com/hooks/supercool", "events": [ "message.completed", "message.failed", "message.blocked" ], "description": "prod", "created_at": "2026-09-30T13:40:00.000000Z", "disabled_at": null, "disabled_reason": null, "failing_since": null, "secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw" } ``` ### Get a webhook endpoint `GET /v1/webhooks/{endpoint_id}` One endpoint and its 20 most recent deliveries. **Parameters** | Name | Type | Description | |---|---|---| | `endpoint_id` (required) | string (path) | The webhook endpoint's `id` (`whe_…`). | **Responses** - `200`: The endpoint. Returns WebhookEndpointDetail. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl "https://api.supercool.com/v1/webhooks/whe_4c1d2e3f4a5b6c7d8e9f" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "url": "https://example.com/hooks/supercool", "events": [ "*" ], "description": "prod", "created_at": "2026-09-30T13:40:00.000000Z", "disabled_at": null, "disabled_reason": null, "failing_since": null, "recent_deliveries": [ { "object": "webhook_delivery", "id": "evt_7d8e9f0a1b2c3d4e5f6a7b8c", "event": "message.completed", "status": "delivered", "attempts": 1, "last_status_code": 200, "last_error": null, "last_response": "ok", "last_ms": 184, "created_at": "2026-09-30T14:06:41.000000Z", "delivered_at": "2026-09-30T14:06:41.000000Z", "next_attempt_at": "2026-09-30T14:06:41.000000Z", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d" } ] } ``` ### Update a webhook endpoint `PATCH /v1/webhooks/{endpoint_id}` Changes an endpoint's URL, events or description, or turns it off and on. Send only the fields to change. `"enabled": true` turns a disabled endpoint back on (after you fixed it) and clears its failure state. **Parameters** | Name | Type | Description | |---|---|---| | `endpoint_id` (required) | string (path) | The webhook endpoint's `id` (`whe_…`). | **Request body** | Field | Type | Description | |---|---|---| | `url` | string \| null | | | `events` | array of string \| null | | | `description` | string \| null | | | `enabled` | boolean \| null | `true` turns the endpoint back on and clears its failure state; `false` turns it off. | **Responses** - `200`: The updated endpoint. Returns WebhookEndpoint. - `400`: The request can't be processed as sent. `invalid_request` (with `problems`) when it doesn't match the schema. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `400 invalid_request`, `400 bad_url`, `400 bad_events`, `400 bad_request`, `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X PATCH "https://api.supercool.com/v1/webhooks/whe_4c1d2e3f4a5b6c7d8e9f" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "enabled": true }' ``` ### Delete a webhook endpoint `DELETE /v1/webhooks/{endpoint_id}` Deletes the endpoint. Deliveries still waiting to be sent to it are cancelled. **Parameters** | Name | Type | Description | |---|---|---| | `endpoint_id` (required) | string (path) | The webhook endpoint's `id` (`whe_…`). | **Responses** - `200`: Deleted. Returns Deleted. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X DELETE "https://api.supercool.com/v1/webhooks/whe_4c1d2e3f4a5b6c7d8e9f" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "deleted": true } ``` ### Rotate a signing secret `POST /v1/webhooks/{endpoint_id}/rotate-secret` Replaces the endpoint's signing secret and returns the new one (shown once). Every delivery from now on is signed with the new secret, including retries of earlier events. **Parameters** | Name | Type | Description | |---|---|---| | `endpoint_id` (required) | string (path) | The webhook endpoint's `id` (`whe_…`). | **Responses** - `200`: The new secret. Returns RotatedSecret. - `401`: The API key is missing, unknown or revoked. - `404`: Nothing with that id for this API key or account. - `429`: A rate limit was hit. Wait `Retry-After` seconds. **Errors:** `401 unauthorized`, `404 not_found`, `429 rate_limited` (see https://supercool.com/docs/api/errors) ```bash curl -X POST "https://api.supercool.com/v1/webhooks/whe_4c1d2e3f4a5b6c7d8e9f/rotate-secret" \ -H "Authorization: Bearer $SUPERCOOL_API_KEY" ``` Example response (`200`): ```json { "object": "webhook_endpoint", "id": "whe_4c1d2e3f4a5b6c7d8e9f", "secret": "whsec_Q2hhbmdlZC1zZWNyZXQtZXhhbXBsZTEy" } ``` ## Webhook events ### A job changed Sent when one job (one piece of work) changes: `job.completed`, `job.failed`, `job.blocked`, `job.resumed` or `job.expired`. `data` is the job, with its `message_id` and files. Signed with the Standard Webhooks headers. ```json { "type": "job.completed", "timestamp": "2026-09-30T14:06:40.310000Z", "data": { "object": "job", "message_id": "msg_3f9a1c2e7b8d4e5f6a7b8c9d", "job_id": "exe_9b2c4d6e8f0a1b3c5d7e9f1a2b4c6d8e", "kind": "execution", "role": "execution", "work_id": "5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "title": "Candle shop vertical ad", "status": "completed", "reason": null, "final": true, "queued": null, "resume_by": null, "link": "https://supercool.com/chat/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f", "started_at": "2026-09-30T14:02:11.402000Z", "settled_at": "2026-09-30T14:06:40.221000Z", "files": [] } } ``` ### A message reached a status Sent once each time a message reaches a status other than `processing`: `message.completed`, `message.partial`, `message.failed`, `message.needs_input`, `message.blocked` or `message.expired`. `data` is the full message, exactly as `GET /messages/{message_id}` returns it. ## Objects ### MessageCreate A message to your agent. Send `message`, files, or both. | Field | Type | Description | |---|---|---| | `message` | string | What you want, in plain language. Up to 20,000 characters; send longer text as a file. | | `files` | array of InlineFile | Small files inline (up to 5, 10 MB each, 25 MB in total). Each has a `url` or `base64`. A file that can't be fetched doesn't fail the message: it's listed in the message's `notes`. | | `upload_ids` | array of string | Finished uploads to attach (up to 10, 1 GB in total). See `POST /uploads`. | | `webhook_endpoint_id` | string \| null | Send this message's webhook events to this registered endpoint (`whe_…`) instead of the account's endpoints. | ### InlineFile | Field | Type | Description | |---|---|---| | `name` | string \| null | A file name for the agent to see. | | `url` | string \| null | A public `https` URL to fetch the file from. | | `base64` | string \| null | The file's bytes, base64-encoded. | | `content_type` | string \| null | The MIME type, for `base64` files. | ### Message A message you sent and everything that came of it. | Field | Type | Description | |---|---|---| | `id` (required) | string | The message id (`msg_…`), always generated by SuperCool. | | `object` (required) | "message" | | | `status` (required) | string (MessageStatus) | One of `processing`, `needs_input`, `blocked`, `completed`, `partial`, `failed`, `expired`. | | `reason` | string \| null | Why, for `blocked`, `failed`, `partial` and `expired`: `out_of_credits`, `hosting_fees`, `too_many_running`, `queue_expired`, `watch_expired`, `result_unknown`, `execution_failed`, `stopped`, `agent_busy`, `turn_failed` and others. See [Getting results](https://supercool.com/docs/api/results#reasons). | | `question` | string \| null | For `needs_input`, the agent's question. Answer by sending a new message. | | `final` (required) | boolean | `true` when nothing about this message will change any more. Stop polling. | | `reply` | string \| null | What the agent said, once it has answered. | | `jobs` (required) | array of Job | The pieces of work this message started, each with its own status. | | `files` (required) | array of File | Every file the message produced (handed over in the reply, or made by its jobs). | | `notes` (required) | array of string | Problems with attachments (a file that couldn't be fetched, files over the limit). | | `credits_used` (required) | number | Credits this message's jobs used so far. | | `created_at` (required) | string | | | `updated_at` (required) | string | When the agent's turn ended (or `created_at` while it's still going). | | `resume` | "add_credits" | Present when queued work is waiting for credits: add credits before `resume_by` and it runs by itself. Don't resend the message. | | `resume_by` | string | The earliest deadline among queued jobs waiting for credits (24 hours after they were queued). | | `retry_after_seconds` | integer | With `reason: "agent_busy"`: retry with the same `Idempotency-Key` after this many seconds. | | `running` | array of object | Present when the agent tried to start work while 5 jobs were already running on the account (a job with `reason: "too_many_running"`): the work running now, and the message that started each. | |   `job_id` | string | `exe_…` | |   `message_id` | string | | |   `work_id` | string | | ### MessageStatus The message's status, aggregated over its jobs. string. One of `processing`, `needs_input`, `blocked`, `completed`, `partial`, `failed`, `expired`. ### Job One piece of work a message started (or was refused). | Field | Type | Description | |---|---|---| | `job_id` | string | `exe_…` for work that ran or is queued; `ref_…` for work refused before it was queued. | | `kind` | string | One of `execution`, `refusal`. | | `role` | string | `follow_up` when the message joined work that was already running; its status follows that work. One of `execution`, `follow_up`. | | `work_id` | string \| null | The chat the work runs in. | | `title` | string \| null | | | `status` | string (JobStatus) | One of `running`, `completed`, `failed`, `cancelled`, `blocked`, `expired`. | | `reason` | string \| null | For `blocked`, `failed`, `cancelled` and `expired`: `out_of_credits`, `hosting_fees`, `too_many_running`, `execution_failed`, `stopped`, `queue_expired`, `watch_expired`, `result_unknown`. | | `final` | boolean | `false` while running, and for queued work waiting for credits (it can still run). | | `queued` | boolean \| null | `true` for work that is queued and waiting for credits; otherwise `null`. | | `resume_by` | string \| null | For queued work waiting for credits, the deadline to add them (24 hours after it was queued). | | `link` | string \| null | The work in the SuperCool app. | | `started_at` | string \| null | | | `settled_at` | string \| null | | | `files` | array of File | | ### JobStatus string. One of `running`, `completed`, `failed`, `cancelled`, `blocked`, `expired`. ### File A file the agent made or handed over. | Field | Type | Description | |---|---|---| | `file_id` | string | `/`. Use it with `GET /files/{file_id}` for a fresh link. | | `name` | string | | | `content_type` | string \| null | | | `kind` | string | One of `image`, `video`, `audio`, `doc`, `site`. | | `size` | integer \| null | Bytes. `null` for a site. | | `url` | string \| null | A signed download link. It expires, and it is pinned to `revision`; a download answers `412` if the file changed since. | | `revision` | string \| null | The file's version the link is pinned to. `null` for a site (its link always opens the live site). | | `work_id` | string \| null | | | `label` | string \| null | | ### FileObject A file the agent made or handed over. | Field | Type | Description | |---|---|---| | `object` | "file" | | | `file_id` | string | `/`. Use it with `GET /files/{file_id}` for a fresh link. | | `name` | string | | | `content_type` | string \| null | | | `kind` | string | One of `image`, `video`, `audio`, `doc`, `site`. | | `size` | integer \| null | Bytes. `null` for a site. | | `url` | string \| null | A signed download link. It expires, and it is pinned to `revision`; a download answers `412` if the file changed since. | | `revision` | string \| null | The file's version the link is pinned to. `null` for a site (its link always opens the live site). | | `work_id` | string \| null | | | `label` | string \| null | | ### EventList | Field | Type | Description | |---|---|---| | `object` | "list" | | | `data` | array of Event | | | `has_more` | boolean | More entries are ready; call again with `cursor` right away. | | `done` | boolean | The message has nothing more to report. | | `cursor` | string | Pass this back as `cursor` for the next page. | | `cursor_expired` | boolean | Your cursor pointed at entries older than 7 days. `cursor` restarts from now. | ### Event One entry in a message's update log. Fields beyond the common ones depend on `type`. | Field | Type | Description | |---|---|---| | `seq` | integer | | | `type` | string | `result` (a job finished, with text and files), `progress` (a step started or a file landed), `agent_reply` (the agent's reply), `state` (`resumed`, `stopped`, `expired`, `unknown`), `turn_failed`, `turn_busy`. One of `result`, `progress`, `agent_reply`, `state`, `turn_failed`, `turn_busy`. | | `work_id` | string \| null | | | `message_id` | string \| null | The message the entry belongs to, when known. | | `at` | string | | | `job_id` | string | The job (`exe_…`) the entry belongs to, when known. | | `title` | string \| null | `result` | | `text` | string \| null | `result`, `agent_reply`, `state`, `turn_failed`, `turn_busy` | | `outcome` | string | `result` One of `completed`, `failed`, `stalled_credits`. | | `files` | array of File | `result`, `agent_reply` | | `state` | string | `state` | | `step` | string \| null | `progress` | | `status` | string \| null | `progress` | | `file_names` | array of string | `progress`: names of files that just landed (download them from the `result`). | | `delivered_sync` | boolean \| null | `agent_reply`: `true` when this reply was already in the `POST /messages` response. | | `reason` | string \| null | `turn_failed`, `turn_busy` | ### Recovery | Field | Type | Description | |---|---|---| | `id` | string | | | `object` | "recovery" | | | `work` | array of object | | |   `work_id` | string | | |   `state` | string | One of `settled`, `watching`, `recovering`, `exhausted`. | |   `outcome` | string | For `settled`, how it ended. | ### UploadCreate | Field | Type | Description | |---|---|---| | `filename` (required) | string | | | `size` (required) | integer | The exact size in bytes (up to 500 MB). The upload is capped at this size. | | `content_type` | string \| null | The MIME type. Guessed from the file name when omitted. The POST must send the same `Content-Type` field (it is in `fields`). | ### UploadCreated | Field | Type | Description | |---|---|---| | `object` | "upload" | | | `upload_id` | string | | | `url` | string | Where to POST the file (`multipart/form-data`). | | `fields` | object | Form fields to send with the file, exactly as given. | | `expires_at` | string | The presigned POST works until then (15 minutes). | | `max_bytes` | integer | | ### Upload | Field | Type | Description | |---|---|---| | `object` | "upload" | | | `upload_id` | string | | | `status` | string | One of `ready`. | | `filename` | string | | | `content_type` | string | | | `size` | integer | | ### WorkList | Field | Type | Description | |---|---|---| | `object` | "list" | | | `data` | array of WorkSummary | | ### WorkSummary | Field | Type | Description | |---|---|---| | `object` | "work" | | | `work_id` | string | | | `title` | string \| null | | | `preview` | string \| null | | | `link` | string | | ### Work | Field | Type | Description | |---|---|---| | `object` | "work" | | | `work_id` | string | | | `title` | string \| null | | | `status` | string | `running`, `stalled_credits` (stopped until credits are added), or `idle`. One of `running`, `stalled_credits`, `idle`. | | `text` | string | The latest result text, from `from_char`. | | `next_from_char` | integer \| null | Pass as `from_char` to read on; `null` when there's no more. | | `link` | string | | | `files` | array of File | The most recent files (up to 8). | ### Account | Field | Type | Description | |---|---|---| | `object` | "account" | | | `user_id` | string | | | `email` | string \| null | | | `name` | string \| null | | | `plan` | string \| null | | | `credits` | number \| null | Credits available now. | | `agent` | object | | |   `name` | string | | | `key` | object | | |   `id` | string | | |   `name` | string | | | `limits` | object | | |   `running_jobs` | integer | Running jobs allowed per account (5). | |   `requests_per_minute` | integer | Requests allowed per API key per minute (600). | ### WebhookEventType string. One of `job.completed`, `job.failed`, `job.blocked`, `job.resumed`, `job.expired`, `message.completed`, `message.partial`, `message.failed`, `message.needs_input`, `message.blocked`, `message.expired`. ### WebhookCreate | Field | Type | Description | |---|---|---| | `url` (required) | string | A public `https` URL (port 443 or 8443, up to 2,000 characters). | | `events` | array of string \| null | Events to receive, or `["*"]` for all (the default). | | `description` | string \| null | Up to 120 characters. | ### WebhookUpdate | Field | Type | Description | |---|---|---| | `url` | string \| null | | | `events` | array of string \| null | | | `description` | string \| null | | | `enabled` | boolean \| null | `true` turns the endpoint back on and clears its failure state; `false` turns it off. | ### WebhookEndpoint | Field | Type | Description | |---|---|---| | `object` | "webhook_endpoint" | | | `id` | string | `whe_…` | | `url` | string | | | `events` | array of string | | | `description` | string \| null | | | `created_at` | string | | | `disabled_at` | string \| null | Set when the endpoint is off (by you, or after 3 days of failed deliveries). | | `disabled_reason` | string \| null | | | `failing_since` | string \| null | When deliveries started failing. Cleared by the next success. | ### WebhookEndpointWithSecret | Field | Type | Description | |---|---|---| | `object` | "webhook_endpoint" | | | `id` | string | `whe_…` | | `url` | string | | | `events` | array of string | | | `description` | string \| null | | | `created_at` | string | | | `disabled_at` | string \| null | Set when the endpoint is off (by you, or after 3 days of failed deliveries). | | `disabled_reason` | string \| null | | | `failing_since` | string \| null | When deliveries started failing. Cleared by the next success. | | `secret` | string | The signing secret (`whsec_…`). Shown only in this response. | ### WebhookEndpointDetail | Field | Type | Description | |---|---|---| | `object` | "webhook_endpoint" | | | `id` | string | `whe_…` | | `url` | string | | | `events` | array of string | | | `description` | string \| null | | | `created_at` | string | | | `disabled_at` | string \| null | Set when the endpoint is off (by you, or after 3 days of failed deliveries). | | `disabled_reason` | string \| null | | | `failing_since` | string \| null | When deliveries started failing. Cleared by the next success. | | `recent_deliveries` | array of WebhookDelivery | | ### WebhookEndpointList | Field | Type | Description | |---|---|---| | `object` | "list" | | | `data` | array of WebhookEndpoint | | ### WebhookDelivery | Field | Type | Description | |---|---|---| | `object` | "webhook_delivery" | | | `id` | string | The delivery id (`evt_…`), sent as `webhook-id`. | | `event` | string (WebhookEventType) | One of `job.completed`, `job.failed`, `job.blocked`, `job.resumed`, `job.expired`, `message.completed`, `message.partial`, `message.failed`, `message.needs_input`, `message.blocked`, `message.expired`. | | `status` | string | One of `pending`, `delivered`, `cancelled`, `dead`. | | `attempts` | integer | | | `last_status_code` | integer \| null | | | `last_error` | string \| null | | | `last_response` | string \| null | The first 4 KB of your endpoint's last response. | | `last_ms` | integer \| null | | | `created_at` | string | | | `delivered_at` | string \| null | | | `next_attempt_at` | string \| null | | | `message_id` | string \| null | | ### RotatedSecret | Field | Type | Description | |---|---|---| | `object` | "webhook_endpoint" | | | `id` | string | | | `secret` | string | | ### Deleted | Field | Type | Description | |---|---|---| | `object` | "webhook_endpoint" | | | `id` | string | | | `deleted` | true | | ### JobEvent | Field | Type | Description | |---|---|---| | `type` | string | One of `job.completed`, `job.failed`, `job.blocked`, `job.resumed`, `job.expired`. | | `timestamp` | string | | | `data` | object | | |   `object` | "job" | | |   `message_id` | string | | |   `job_id` | string | `exe_…` for work that ran or is queued; `ref_…` for work refused before it was queued. | |   `kind` | string | One of `execution`, `refusal`. | |   `role` | string | `follow_up` when the message joined work that was already running; its status follows that work. One of `execution`, `follow_up`. | |   `work_id` | string \| null | The chat the work runs in. | |   `title` | string \| null | | |   `status` | string (JobStatus) | One of `running`, `completed`, `failed`, `cancelled`, `blocked`, `expired`. | |   `reason` | string \| null | For `blocked`, `failed`, `cancelled` and `expired`: `out_of_credits`, `hosting_fees`, `too_many_running`, `execution_failed`, `stopped`, `queue_expired`, `watch_expired`, `result_unknown`. | |   `final` | boolean | `false` while running, and for queued work waiting for credits (it can still run). | |   `queued` | boolean \| null | `true` for work that is queued and waiting for credits; otherwise `null`. | |   `resume_by` | string \| null | For queued work waiting for credits, the deadline to add them (24 hours after it was queued). | |   `link` | string \| null | The work in the SuperCool app. | |   `started_at` | string \| null | | |   `settled_at` | string \| null | | |   `files` | array of File | | ### MessageEvent | Field | Type | Description | |---|---|---| | `type` | string | One of `message.completed`, `message.partial`, `message.failed`, `message.needs_input`, `message.blocked`, `message.expired`. | | `timestamp` | string | | | `data` | Message | A message you sent and everything that came of it. | ### Error Every API error has this shape. | Field | Type | Description | |---|---|---| | `error` (required) | string (ErrorCode) | One of `bad_request`, `invalid_request`, `empty_message`, `too_long`, `bad_cursor`, `bad_url`, `bad_events`, `bad_size`, `too_many`, `unauthorized`, `not_found`, `idempotency_conflict`, `poll_in_progress`, `too_many_endpoints`, `not_pending`, `not_uploaded`, `changed`, `upload_not_ready`, `too_large`, `executable_refused`, `rate_limited`, `internal`, `unavailable`. | | `message` (required) | string | What went wrong, for people. | | `request_id` (required) | string | The request's id (`req_…`), also in the `Request-Id` header. | | `docs` (required) | string | A link to this error code's documentation. | | `problems` | array of object | With `invalid_request`: what doesn't match, one entry per problem (up to 10). | |   `field` | string \| null | The dotted location of the field (like `files.0.url` or `wait`), or `null` for the body as a whole. | |   `problem` | string | | | `cursor` | string | With `poll_in_progress`: the cursor to use when you call again. | ### ErrorCode A stable, machine-readable error code. Each one is documented at `https://supercool.com/docs/api/errors#`. string. One of `bad_request`, `invalid_request`, `empty_message`, `too_long`, `bad_cursor`, `bad_url`, `bad_events`, `bad_size`, `too_many`, `unauthorized`, `not_found`, `idempotency_conflict`, `poll_in_progress`, `too_many_endpoints`, `not_pending`, `not_uploaded`, `changed`, `upload_not_ready`, `too_large`, `executable_refused`, `rate_limited`, `internal`, `unavailable`. --- # Rate limits & errors > Every SuperCool API error code, what it means and how to fix it, plus rate limits, rate-limit headers, request ids and the request inspector. Source: https://supercool.com/docs/api/errors ## Error shape Every error has the same JSON shape and an HTTP status that matches it: ```json { "error": "idempotency_conflict", "message": "That Idempotency-Key was already used for a different message. Use a new key for a new request.", "request_id": "req_6c2f0e1d9a8b7c6d5e4f3a2b", "docs": "https://supercool.com/docs/api/errors#idempotency_conflict" } ``` | Field | Meaning | |---|---| | `error` | A stable, machine-readable code. Branch on this, never on `message`. | | `message` | What went wrong, written for people. It may change. | | `request_id` | The request's id, also in the `Request-Id` header. | | `docs` | A link to the code's entry on this page. | Some errors add fields: `invalid_request` lists its `problems`, and `poll_in_progress` includes the `cursor` to use when you call again. Outcomes of the work itself, like running out of credits, hitting the running-jobs cap, a failed job or a question from the agent, are **not** errors. They come back as a message with a [status](https://supercool.com/docs/api/results#statuses). ## Request ids and the inspector {#request-ids} Every response, success or error, has a `Request-Id` header (`req_…`). Log it. The [API dashboard](https://supercool.com/dashboard#api)'s **request inspector** shows every request from the last 30 days: time, key, method, path, status, error code, latency, and the request and response bodies (redacted and capped). Search it by request id or message id, and filter by key, status, path or error code. If you contact support, include the request id. ## Rate limits {#rate-limits} | Limit | Value | Applies to | |---|---|---| | Requests | 600 per minute | Each API key, every request | | Messages | 120 per hour, 1,000 per day | Each account, `POST /v1/messages` | | Waits | 1,500 per hour | Each account, long-polls (`?wait=` and `/events`) | | Work reads | 600 per hour | Each account, `GET /v1/work/{work_id}` | | Running jobs | 5 at once | Each account. Not an HTTP error: see [below](#running-jobs). | The per-key budget means one busy or leaked key can't use up the whole account. The account limits are generous, and are there to stop runaway loops. ### Running jobs {#running-jobs} Up to 5 jobs can run at once per account. The cap never refuses a request: `POST /v1/messages` always gets through, so you can always stop work or follow up on it. Instead, if the agent tries to **start** new work while 5 jobs are running, that start is refused. The message comes back as usual (`201`) with a job whose `status` is `blocked` and `reason` is `too_many_running`, and the message lists the work that's running in `running`. See [At capacity](https://supercool.com/docs/api/messages#at-capacity). ### Headers Every authenticated response reports the per-key budget: | Header | Meaning | |---|---| | `RateLimit-Limit` | Requests this key may make per minute (600). | | `RateLimit-Remaining` | Requests left in the current window. | | `RateLimit-Reset` | Seconds until the oldest counted request leaves the window. | | `Retry-After` | On every `429`, seconds to wait before retrying. | Every `429` carries `Retry-After`. For the per-key budget it's the exact number of seconds until a request frees up. For the account limits (messages, waits, work reads) it's `60`. ### Handling 429s ```python import random import time import requests def call(method, url, **kwargs): for attempt in range(6): r = requests.request(method, url, timeout=60, **kwargs) if r.status_code not in (429, 500, 503): return r wait = float(r.headers.get("Retry-After", 2 ** attempt)) + random.random() time.sleep(wait) return r ``` Always send an [`Idempotency-Key`](https://supercool.com/docs/api/idempotency) on `POST /v1/messages`, so a retry can never start work twice. ## Status codes | Status | Meaning | |---|---| | `200` | OK. Also returned when an `Idempotency-Key` replays an existing message. | | `201` | Created: a new message, upload or webhook endpoint. | | `400` | The request can't be processed as sent, including requests that don't match the schema (`invalid_request`). Fix it before retrying. | | `401` | The API key is missing, unknown, expired or revoked. | | `404` | Nothing with that id for this key or account. | | `409` | Conflicts with the current state. | | `413` | A file is too large. | | `422` | An uploaded file was refused (`executable_refused`). | | `429` | A rate limit. Always has `Retry-After`. | | `500` | Something broke on our side. Retry with backoff. | | `503` | The API is temporarily switched off. Retry later. | ## Error codes ### 400: Bad request #### `bad_request` {#bad_request} The request is invalid in a way not covered by a more specific code, for example an unusable webhook setting. Read `message` for details. #### `invalid_request` {#invalid_request} The request doesn't match what the endpoint expects: a required field is missing, a field has the wrong type, a query parameter is out of range (like `wait` above 45), or the body isn't valid JSON. The response lists what's wrong in `problems`, up to 10 entries: ```json { "error": "invalid_request", "message": "The request doesn't match what this endpoint expects; see `problems`.", "request_id": "req_6c2f0e1d9a8b7c6d5e4f3a2b", "docs": "https://supercool.com/docs/api/errors#invalid_request", "problems": [ { "field": "files", "problem": "Input should be a valid list" }, { "field": "wait", "problem": "Input should be less than or equal to 45" } ] } ``` `field` is the dotted path to the field (like `files.0.url` or `size`), or `null` when the problem is with the body as a whole. Fix the request; retrying it unchanged won't help. #### `empty_message` {#empty_message} `POST /v1/messages` had no `message`, `files` or `upload_ids`. Send at least one. #### `too_long` {#too_long} The `message` text is over 20,000 characters. Send the long part as a file instead. #### `bad_cursor` {#bad_cursor} The `cursor` on `GET /v1/messages/{id}/events` isn't valid for this API key: it's malformed, or it came from another key. Use the `cursor` from your last page, or leave it out to start from the beginning of the message. #### `bad_url` {#bad_url} A webhook endpoint URL was refused. It must be a public `https` URL on port 443 or 8443, with no username or password in it, and at most 2,000 characters. Private and internal addresses aren't allowed. #### `bad_events` {#bad_events} A webhook endpoint listed an unknown event. Use `"*"` or events from the [list](https://supercool.com/docs/api/webhooks#events). #### `bad_size` {#bad_size} `POST /v1/uploads` needs `size`: the file's exact size in bytes, as a positive whole number. #### `too_many` {#too_many} A message named more than 10 `upload_ids`. Split the files across messages, or combine them (for example, into a zip). ### 401: Unauthorized #### `unauthorized` {#unauthorized} The `Authorization` header is missing, or the key is unknown, expired or revoked. Send `Authorization: Bearer sc_key_…` with an active key from the [dashboard](https://supercool.com/dashboard#api). See [Authentication](https://supercool.com/docs/api/authentication). ### 404: Not found #### `not_found` {#not_found} Nothing with that id is available to you: - **Messages** belong to the API key that sent them, and are kept for at least 14 days after their last job's deadline. Check you're using the same key, or [read the work](https://supercool.com/docs/api/work) instead. - **Work, files, uploads and webhook endpoints** belong to your account. Check the id, and that the chat wasn't deleted. - `webhook_endpoint_id` on a message must name an endpoint on your account. ### 409: Conflict #### `idempotency_conflict` {#idempotency_conflict} The `Idempotency-Key` was already used, within 7 days and with this API key, for a message with a different body. Use a new key for a new request. See [Idempotency](https://supercool.com/docs/api/idempotency). #### `poll_in_progress` {#poll_in_progress} Another `GET /v1/messages/{id}/events` wait is already running for this message. Only one runs at a time. Wait for it to return, then call again with the `cursor` from the error. #### `too_many_endpoints` {#too_many_endpoints} The account already has 10 webhook endpoints. Delete one you don't need, or use one endpoint for several purposes with [per-message routing](https://supercool.com/docs/api/webhooks#per-message). #### `not_pending` {#not_pending} The upload can't be completed because it was rejected or already attached to a message. Create a new upload. #### `not_uploaded` {#not_uploaded} You completed an upload before the file reached storage, or its 15-minute presigned POST expired. POST the file to the upload's `url` first, or create a new upload. #### `changed` {#changed} The uploaded file was replaced while it was being completed. Upload it again with a new upload. #### `upload_not_ready` {#upload_not_ready} A message named an upload that isn't `ready`: not completed yet, or not on your account. Call `POST /v1/uploads/{id}/complete` first. ### 413: Too large #### `too_large` {#too_large} A file is over the limit: 500 MB per upload, or 1 GB of uploads per message. For inline `files`, oversized files are skipped and listed in the message's `notes` instead. ### 422: Unprocessable #### `executable_refused` {#executable_refused} The uploaded file is an executable program, which can't be attached. `POST /v1/uploads/{id}/complete` refuses it and deletes it. This is the only `422` the API returns. (An executable sent inline in `files` doesn't fail the message: it's skipped and listed in the message's `notes`.) ### 429: Too many requests #### `rate_limited` {#rate_limited} A rate limit was hit: this key's 600 requests per minute, or one of the account's limits on messages, waits or work reads. Wait the `Retry-After` seconds (exact for the per-key budget, `60` for account limits), then retry. See [Rate limits](#rate-limits). ### 500 and 503 #### `internal` {#internal} Something went wrong on our side. Nothing you sent was lost. Retry with backoff, using the same `Idempotency-Key`. If it keeps happening, contact support with the `request_id`. #### `unavailable` {#unavailable} The API is temporarily switched off, for example during an incident. `message` says why. Retry later. --- # 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") ``` --- # Use SuperCool from AI agents > Give coding agents and AI apps what they need to use SuperCool. llms.txt, every docs page as Markdown, the OpenAPI spec, the MCP server and the CLI. Source: https://supercool.com/docs/api/agents These docs are written to be read by AI agents as well as people. Point your coding agent at them, or skip the HTTP entirely and connect SuperCool as a tool. ## Docs for LLMs | Resource | URL | |---|---| | Index of every page ([llms.txt](https://llmstxt.org)) | [`https://supercool.com/docs/llms.txt`](https://supercool.com/docs/llms.txt) | | All docs in one Markdown file | [`https://supercool.com/docs/llms-full.txt`](https://supercool.com/docs/llms-full.txt) | | Any page as Markdown | Add `.md` to its URL, like [`/docs/api/quickstart.md`](https://supercool.com/docs/api/quickstart.md) | | OpenAPI 3.1 spec | [`/docs/api/openapi.json`](https://supercool.com/docs/api/openapi.json) or [`.yaml`](https://supercool.com/docs/api/openapi.yaml) | Every page also has a **Copy page as Markdown** button at the top, and a `` tag pointing to its Markdown version. ## Connect SuperCool as a tool (MCP) If your agent speaks the [Model Context Protocol](https://modelcontextprotocol.io), connect SuperCool directly: no API key or HTTP code needed. Claude, ChatGPT, Cursor and other MCP clients sign in with your SuperCool account and get tools to message your agent, wait for results and read work. Setup for each client is on the [MCP page](https://supercool.com/mcp). The server URL is `https://mcp.supercool.com/mcp`. ## Use the CLI The [SuperCool CLI](https://supercool.com/cli) gives terminal agents (and you) the same agent from a shell: ```bash curl -fsSL https://supercool.com/install.sh | sh supercool login supercool ask "A 15 second vertical ad for my candle shop" --wait ``` It also installs with `brew install famous-labs/tap/supercool` or `npm i -g @famous-labs/supercool-cli`. ## A prompt snippet for coding agents Paste this into your agent's system prompt, rules file or `AGENTS.md` when it builds on the SuperCool API: ```text You can use the SuperCool API: one AI agent behind one endpoint that makes finished work (videos, images, websites, decks, research, writing) from a plain-language request. Docs (Markdown): https://supercool.com/docs/llms.txt (index), https://supercool.com/docs/llms-full.txt (everything) OpenAPI: https://supercool.com/docs/api/openapi.json Essentials: - Base URL https://api.supercool.com/v1. Auth header: "Authorization: Bearer $SUPERCOOL_API_KEY". The key is a server-side secret: read it from the environment, never hard-code it or send it to a browser. - Send work: POST /v1/messages {"message": "..."} with a fresh "Idempotency-Key" header (reuse it on retries). Returns within ~30 s: 201 with a message {id, status, final, reply, jobs, files}. - Follow it: GET /v1/messages/{id}?wait=30 in a loop until "final" is true (or use webhooks: POST /v1/webhooks, verify Standard Webhooks signatures). - Statuses: processing, completed, needs_input (answer with a new message), blocked (reason out_of_credits: not an HTTP error; if "resume": "add_credits", don't resend), partial, failed, expired. - Files: message.files[] has {file_id, name, content_type, size, url}; url is a signed, expiring link. Refresh with GET /v1/files/{file_id}. A 412 on download means the file changed: refresh. - Errors: {"error": code, "message", "request_id", "docs"}. Branch on "error". Retry 429/500/503 with backoff (honor Retry-After). Max 5 running jobs per account: beyond that the message still succeeds, but new work comes back as a blocked job with reason too_many_running and a "running" list. - Don't pick models or chain tools: describe the outcome and let the SuperCool agent do the work. ``` ## Tips for agents calling the API - **Describe outcomes, not steps.** "A 30 second teaser from this footage, upbeat, with captions" works better than instructions for which tool to use. The agent picks the tools. - **Poll with `wait`, not in a tight loop.** `?wait=30` returns as soon as something changes. - **Stop on `final`.** Don't resend a message that is `blocked` with `resume: "add_credits"`: it resumes by itself once credits are added. - **Answer questions.** On `needs_input`, relay `question` to the user (or answer it) with a new message. - **Keep the `request_id`** from errors and the `Request-Id` header when you report a problem. --- # Changelog > New features and changes in the SuperCool API, newest first. Also available as an RSS feed. Source: https://supercool.com/docs/api/changelog Changes to the SuperCool API, newest first. Follow along with the [RSS feed](https://supercool.com/docs/api/changelog/feed.xml). Within `/v1`, changes are additive: new endpoints, new fields, new event types, new `reason` values. Build your integration to ignore fields and values it doesn't know. Anything that would break an existing integration gets a new version. ## 2026-09-30 — The SuperCool API is live {#2026-09-30} Your SuperCool agent is now available over HTTPS at `https://api.supercool.com/v1`. - **Messages:** `POST /v1/messages` sends your agent a message, with inline files or large uploads. Follow it with `GET /v1/messages/{id}?wait=`, its events, or webhooks. Each message reports its jobs, files and credits used. - **Webhooks:** signed with Standard Webhooks, with retries for up to 3 days and per-message routing. - **Uploads:** files up to 500 MB through presigned uploads. - **Work and files:** read any chat, including ones started in the app, and get fresh download links. - **Keys and dashboard:** nicknamed `sc_key_` API keys with optional expiry, usage per key, and a request inspector with every request from the last 30 days. - **Docs:** these pages, [llms.txt](https://supercool.com/docs/llms.txt), every page as Markdown, and the [OpenAPI spec](https://supercool.com/docs/api/openapi.json).