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