SuperCoolDocs Get an API key

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.

View as Markdown

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:

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.

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

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

  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
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"])
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);
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 "[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

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

Last updated