Skip to content

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 uploadUrlthe simplest option; another program or an AI agent sends the filenot needed
Whole filethe file is on your service, one requestneeded
In partslarge files and unreliable networks: parallel parts, retrying a partneeded
Import from a URLthe file is already at a public https URLnot 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:

Terminal
curl https://api.slicer.media/v1/uploads \
  -H "Authorization: Bearer $SLICER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileName":"interview.mp4","size":48211337}'
Response
{
  "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 use source (the default).

uploadUrl accepts a PUT of the whole file without a token — the permission is built into the link:

Terminal
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, and Content-Length is required (curl -T sets 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:

Terminal
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.mp4

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

JavaScript
Python
upload-parts.mjs
// 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);

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:

Terminal
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 https and 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), pass fileName: "fileName": "product-demo.mp4".
  • The import runs in the background: the upload comes back right away with the uploading status, and progress shows the downloaded share (0–1). Then come processing and ready, or failed with the reason in error.
  • 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
uploadingwaiting for the file or its parts; for imports — downloading
processingthe file has arrived; the server checks the format and makes a cover
readycan be used in a montage; file details are in media
failedthe file was rejected; the reason is in error
abortedthe 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_format400the extension or file content isn't supported
file_too_large400the file exceeds the limit for its kind
empty_file400the file is empty
not_a_video, not_an_image, not_audio, media_unreadable400the content doesn't match the extension or the file is damaged
resolution_too_low, resolution_too_high, image_too_large400the resolution is outside the allowed range
audio_too_long400the audio is longer than allowed
too_many_uploads, daily_upload_limit400too many unfinished uploads or the daily volume is used up
bad_part, bad_part_size400wrong part number or size
parts_missing400complete was called before all parts were sent
length_required411no Content-Length header
bad_content_length, body_too_short, body_too_long400the body size doesn't match the declared size
upload_not_active409the upload is already completed or cancelled
upload_in_parts409the upload is already going in parts
link_not_found404the upload link wasn't found
link_expired, link_used410the link has expired or was already used
bad_url, file_name_required400the import URL isn't https or has no file name
too_many_imports4293 imports are already running — wait for them to finish

What's next