Guides
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.
Files move both ways: the agent hands you what it made, and you send it what it needs.
#Files the agent made
A message lists every file it produced in files, and each job lists its own. Each file looks like this:
{
"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 Failedinstead 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 norevisionorsize.
curl -L -o candle_ad.mp4 "https://api.supercool.sh/api/v1/files/s/eyJmIjoiNWQwYzNl..."#Get a fresh link
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:
curl https://api.supercool.com/v1/files/5d0c3e2a-8f1b-4c6d-9e7f-1a2b3c4d5e6f/candle_ad.mp4 \
-H "Authorization: Bearer $SUPERCOOL_API_KEY"{
"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} 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. Executables are refused either way.
#Large uploads
An upload takes three calls: create it, send the bytes straight to storage, and complete it. Then attach it to a message.
- Create the upload with the file's name, exact size in bytes and type. You get back a presigned POST: a
urland thefieldsto send with it. It's valid for 15 minutes. - POST the file to
urlasmultipart/form-data: every entry offieldsas a form field, exactly as given, and the file last, in a field namedfile. Don't send your API key to this URL. - Complete the upload. SuperCool checks the file (its real size, and that it isn't an executable) and marks it
ready. - Send a message with the
upload_idinupload_ids.
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"])// 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);# 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 "[email protected]"
# 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
sizemust be the exact size in bytes, up to 500 MB. Storage refuses a larger body.- The file must be sent with the same
Content-Typeyou declared (it's one of thefields). - The presigned POST works for 15 minutes. After that, create a new upload.
- Completing is safe to repeat: a
readyupload 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 |
size is missing or not a positive whole number. |
too_large |
Over 500 MB per file, or 1 GB per message. |
not_uploaded |
You completed it before the file reached storage, or the presigned POST expired. |
changed |
The file was replaced while it was being completed. Upload it again. |
executable_refused |
The file is a program. |
not_pending |
The upload was already rejected, or already attached to a message. |
upload_not_ready |
A message named an upload that isn't completed (or isn't yours). |
too_many |
More than 10 uploads on one message. |