Uploading files
How to upload videos, photos and audio to Slicer through the API — a one-time link for curl -T, the whole file, in parts, or imported from a public URL.
On this page
A montage is built from uploaded materials: videos, photos and audio. Every file first becomes an upload — it has an
id, a status and media details. Ready uploads (status: ready) go into uploadIds when you
create a project.
Four ways
| Way | Best for | Token when sending the file |
|---|---|---|
One-time link uploadUrl | the simplest option; another program or an AI agent sends the file | not needed |
| Whole file | the file is on your service, one request | needed |
| In parts | large files and unreliable networks: parallel parts, retrying a part | needed |
| Import from a URL | the file is already at a public https URL | not needed — Slicer downloads it |
Create an upload
For the first three ways the upload is created in advance — the server needs to know the file name and size:
curl https://api.slicer.media/v1/uploads \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fileName":"interview.mp4","size":48211337}'{
"ok": true,
"data": {
"upload": {
"id": "7d0c6a4e-2f7b-4f5e-9a0e-3b8f2c1d9e61",
"fileName": "interview.mp4",
"purpose": "source",
"origin": "client",
"sizeBytes": 48211337,
"status": "uploading",
"partSize": 8388608,
"partCount": 6,
"uploadedParts": [],
"progress": null,
"media": null,
"error": null,
"createdAt": "2026-10-16T09:12:04.000Z"
},
"uploadUrl": "https://api.slicer.media/v1/upload-links/slc_ul_…",
"uploadUrlExpiresAt": "2026-10-16T10:12:04.000Z"
}
}fileName— with an extension: it determines the kind of material (video, photo or audio).size— the exact file size in bytes.purpose— what the file is for; montage materials usesource(the default).
One-time link
uploadUrl accepts a PUT of the whole file without a token — the permission is built into the link:
curl -T interview.mp4 "$UPLOAD_URL"Link properties:
- one attempt — the link expires on the first request, even if it was interrupted; to retry, create a new upload or send the whole file to the same upload with your token (below);
- at most an hour — the expiry time is in
uploadUrlExpiresAt; - write-only — you can't read anything through the link or learn anything about the account;
- the body size must match the declared
size, andContent-Lengthis required (curl -Tsets it for you).
Once the file is received, the upload completes on its own and the response is the upload object. The link is handy to give to an AI agent or a script on another machine without revealing your token.
Whole file
The same idea, but with the token and at the upload's permanent address:
curl -X PUT "https://api.slicer.media/v1/uploads/$UPLOAD_ID/content" \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @interview.mp4The server splits the stream into parts, encrypts them and completes the upload. Content-Length must equal the
declared size.
In parts
For large files: parts of partSize bytes (8 MiB), numbered from 1, the last one is the remainder. Parts can go in
parallel and be retried — received parts are listed in uploadedParts. When all parts are in, call complete.
// Node.js 20+: node upload-parts.mjs video.mp4
import { open, stat } from 'node:fs/promises';
import { basename } from 'node:path';
const API = 'https://api.slicer.media/v1';
const auth = { Authorization: `Bearer ${process.env.SLICER_TOKEN}` };
const file = process.argv[2];
async function call(method, path, body, headers = {}) {
const response = await fetch(`${API}${path}`, { method, headers: { ...auth, ...headers }, body });
const json = await response.json();
if (!json.ok) throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
return json.data;
}
const { size } = await stat(file);
const { upload } = await call('POST', '/uploads', JSON.stringify({ fileName: basename(file), size }), {
'Content-Type': 'application/json',
});
const handle = await open(file);
const queue = Array.from({ length: upload.partCount }, (_, i) => i + 1);
async function worker() {
for (let part = queue.shift(); part != null; part = queue.shift()) {
const offset = (part - 1) * upload.partSize;
const length = Math.min(upload.partSize, size - offset);
const { buffer } = await handle.read(Buffer.alloc(length), 0, length, offset);
for (let attempt = 1; ; attempt += 1) {
try {
await call('PUT', `/uploads/${upload.id}/parts/${part}`, buffer, { 'Content-Type': 'application/octet-stream' });
break;
} catch (error) {
if (attempt === 3) throw error;
}
}
}
}
await Promise.all(Array.from({ length: 4 }, worker)); // 4 parts at a time
await handle.close();
const ready = await call('POST', `/uploads/${upload.id}/complete`);
console.log(ready.status, ready.media);# python upload_parts.py video.mp4
import os
import sys
from concurrent.futures import ThreadPoolExecutor
import requests
API = "https://api.slicer.media/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['SLICER_TOKEN']}"}
path = sys.argv[1]
size = os.path.getsize(path)
def call(method, url, **kwargs):
body = requests.request(method, API + url, headers=HEADERS, timeout=300, **kwargs).json()
if not body["ok"]:
raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
return body["data"]
upload = call("POST", "/uploads", json={"fileName": os.path.basename(path), "size": size})["upload"]
def send(part):
offset = (part - 1) * upload["partSize"]
with open(path, "rb") as file:
file.seek(offset)
chunk = file.read(min(upload["partSize"], size - offset))
for attempt in range(3):
try:
return call("PUT", f"/uploads/{upload['id']}/parts/{part}", data=chunk)
except (RuntimeError, requests.RequestException):
if attempt == 2:
raise
with ThreadPoolExecutor(max_workers=4) as pool: # 4 parts at a time
list(pool.map(send, range(1, upload["partCount"] + 1)))
ready = call("POST", f"/uploads/{upload['id']}/complete")
print(ready["status"], ready["media"])Parts count against a separate rate-limit bucket — 1200 requests per minute (Rate limits). An
upload started in parts can't be continued as a whole file (409 upload_in_parts) — keep sending parts.
Import from a URL
If the file is already on the internet, Slicer will download it:
curl https://api.slicer.media/v1/uploads/import \
-H "Authorization: Bearer $SLICER_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://media.example.com/clips/product-demo.mp4"}'- Only
httpsand only public addresses: internal network addresses are rejected, and every redirect is checked again. - The file name is taken from the URL. If the URL has no extension (
…/download?id=42), passfileName:"fileName": "product-demo.mp4". - The import runs in the background: the upload comes back right away with the
uploadingstatus, andprogressshows the downloaded share (0–1). Then comeprocessingandready, orfailedwith the reason inerror. - Up to 3 imports at a time per account; a download may take up to 30 minutes. Size and format limits are the same as for regular uploads.
Links to video hosting pages (rather than to the file itself) won't work: you need a direct link to the file.
Upload statuses
| Status | What's happening |
|---|---|
uploading | waiting for the file or its parts; for imports — downloading |
processing | the file has arrived; the server checks the format and makes a cover |
ready | can be used in a montage; file details are in media |
failed | the file was rejected; the reason is in error |
aborted | the upload was cancelled (DELETE /v1/uploads/{id}) |
Check the status with GET /v1/uploads/{id} or the upload.ready and upload.failed webhooks. The
media field of a ready upload: kind (video, image, audio), durationSec, width, height, hasAudio.
Formats and limits
The limits are the same as in the app and are listed in the catalog GET /v1/styles → materialLimits:
- video —
mp4,mov,m4v,webm,mkv,avi; photos —jpg,jpeg,png,webp; audio —mp3,m4a,aac,wav,ogg,opus,flac; - up to 20 materials per project, with separate limits for videos, photos and audio;
- the format is checked not only by extension but also by the file's actual content.
More on sizes, durations and resolutions — File formats and limits. Unfinished uploads are removed
after a day, project sources are kept for 7 days after the montage (sourcesAvailable on the project), and finished
videos are kept until you delete them.
Upload errors
| Code | Status | Reason |
|---|---|---|
unsupported_format | 400 | the extension or file content isn't supported |
file_too_large | 400 | the file exceeds the limit for its kind |
empty_file | 400 | the file is empty |
not_a_video, not_an_image, not_audio, media_unreadable | 400 | the content doesn't match the extension or the file is damaged |
resolution_too_low, resolution_too_high, image_too_large | 400 | the resolution is outside the allowed range |
audio_too_long | 400 | the audio is longer than allowed |
too_many_uploads, daily_upload_limit | 400 | too many unfinished uploads or the daily volume is used up |
bad_part, bad_part_size | 400 | wrong part number or size |
parts_missing | 400 | complete was called before all parts were sent |
length_required | 411 | no Content-Length header |
bad_content_length, body_too_short, body_too_long | 400 | the body size doesn't match the declared size |
upload_not_active | 409 | the upload is already completed or cancelled |
upload_in_parts | 409 | the upload is already going in parts |
link_not_found | 404 | the upload link wasn't found |
link_expired, link_used | 410 | the link has expired or was already used |
bad_url, file_name_required | 400 | the import URL isn't https or has no file name |
too_many_imports | 429 | 3 imports are already running — wait for them to finish |