# 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, `<work_id>/<path>`. 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. |
