# 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 (`<work_id>/<path>`); 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 (`<work_id>/<path>`, 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. |
| &nbsp;&nbsp;`job_id` | string | `exe_…` |
| &nbsp;&nbsp;`message_id` | string |  |
| &nbsp;&nbsp;`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 | `<work_id>/<path>`. 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 | `<work_id>/<path>`. 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 |  |
| &nbsp;&nbsp;`work_id` | string |  |
| &nbsp;&nbsp;`state` | string | One of `settled`, `watching`, `recovering`, `exhausted`. |
| &nbsp;&nbsp;`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 |  |
| &nbsp;&nbsp;`name` | string |  |
| `key` | object |  |
| &nbsp;&nbsp;`id` | string |  |
| &nbsp;&nbsp;`name` | string |  |
| `limits` | object |  |
| &nbsp;&nbsp;`running_jobs` | integer | Running jobs allowed per account (5). |
| &nbsp;&nbsp;`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 |  |
| &nbsp;&nbsp;`object` | "job" |  |
| &nbsp;&nbsp;`message_id` | string |  |
| &nbsp;&nbsp;`job_id` | string | `exe_…` for work that ran or is queued; `ref_…` for work refused before it was queued. |
| &nbsp;&nbsp;`kind` | string | One of `execution`, `refusal`. |
| &nbsp;&nbsp;`role` | string | `follow_up` when the message joined work that was already running; its status follows that work. One of `execution`, `follow_up`. |
| &nbsp;&nbsp;`work_id` | string \| null | The chat the work runs in. |
| &nbsp;&nbsp;`title` | string \| null |  |
| &nbsp;&nbsp;`status` | string (JobStatus) | One of `running`, `completed`, `failed`, `cancelled`, `blocked`, `expired`. |
| &nbsp;&nbsp;`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`. |
| &nbsp;&nbsp;`final` | boolean | `false` while running, and for queued work waiting for credits (it can still run). |
| &nbsp;&nbsp;`queued` | boolean \| null | `true` for work that is queued and waiting for credits; otherwise `null`. |
| &nbsp;&nbsp;`resume_by` | string \| null | For queued work waiting for credits, the deadline to add them (24 hours after it was queued). |
| &nbsp;&nbsp;`link` | string \| null | The work in the SuperCool app. |
| &nbsp;&nbsp;`started_at` | string \| null |  |
| &nbsp;&nbsp;`settled_at` | string \| null |  |
| &nbsp;&nbsp;`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). |
| &nbsp;&nbsp;`field` | string \| null | The dotted location of the field (like `files.0.url` or `wait`), or `null` for the body as a whole. |
| &nbsp;&nbsp;`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#<code>`.

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`.
