API quickstart
Your first video through the Slicer API in 10 minutes — upload a file, get the price, start the montage, wait and download, in curl, JavaScript and Python.
On this page
In a handful of requests you will upload a video, get the montage price, start the montage and download the finished
vertical video. First we walk through it step by step with curl, then the same flow as a single JavaScript or Python
script.
You will need:
- a Slicer account and tokens on the balance (your first video is free, with a watermark);
- a video with speech, for example
interview.mp4; curlandjq(opens in a new tab) for the terminal steps, Node.js 20+ or Python 3.9+ for the scripts.
Step by step
Create an API token
Open Profile → Developers → Create token in the web app or in the iPhone and iPad app. Choose the Montage preset — it allows uploads, starting a montage and edits. The token is shown once: copy it right away. Details — Tokens and scopes.
Put the token in an environment variable
All examples in these docs read the token from
SLICER_TOKEN:Terminal export SLICER_TOKEN="slc_…" export API=https://api.slicer.media/v1Check that the token works:
Terminal curl -s "$API/me" -H "Authorization: Bearer $SLICER_TOKEN" | jq .data.walletResponse { "balance": 240, "held": 0, "available": 240, "freeMontageAvailable": false, "montageTokens": 24 }Upload a video
Create an upload — the response contains a one-time
uploadUrl. Send the file to it withcurl -T, without the token header: the permission is built into the link itself.Terminal FILE=interview.mp4 SIZE=$(wc -c < "$FILE" | tr -d ' ') CREATED=$(curl -s "$API/uploads" \ -H "Authorization: Bearer $SLICER_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"fileName\":\"$FILE\",\"size\":$SIZE}") UPLOAD_ID=$(echo "$CREATED" | jq -r .data.upload.id) UPLOAD_URL=$(echo "$CREATED" | jq -r .data.uploadUrl) curl -s -T "$FILE" "$UPLOAD_URL" | jq .data.statusOnce the file is received, the server checks it. Wait for the
readystatus:Terminal until [ "$(curl -s "$API/uploads/$UPLOAD_ID" -H "Authorization: Bearer $SLICER_TOKEN" | jq -r .data.status)" = "ready" ]; do sleep 2 doneIf the status becomes
failed, the reason is in theerrorfield (for example, an unsupported format). Other ways to upload — in parts and from a public URL — are covered in Uploading files.Get the price
A project creation request without
confirmCostcreates and spends nothing — it only names the price:Terminal BODY="{\"uploadIds\":[\"$UPLOAD_ID\"],\"title\":\"A reel in 3 minutes\",\"settings\":{\"templateId\":\"bold\"}}" QUOTE=$(curl -s "$API/projects" \ -H "Authorization: Bearer $SLICER_TOKEN" \ -H "Content-Type: application/json" \ -d "$BODY") echo "$QUOTE" | jq .data COST=$(echo "$QUOTE" | jq .data.cost)Response { "status": "quote", "cost": 24, "free": false, "available": 240, "enough": true, "spendLimitLeft": 200 }enough: falsemeans you don't have enough tokens — top up your balance in the app. If your account still has the free first video, you will get"cost": 0, "free": true.Start the montage
Send the same request with
confirmCostequal to the quoted price. TheIdempotency-Keyheader protects you from a double start if you have to retry after a network failure (Idempotency).Terminal PROJECT_ID=$(curl -s "$API/projects" \ -H "Authorization: Bearer $SLICER_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d "$(echo "$BODY" | jq --argjson cost "$COST" '. + {confirmCost: $cost}')" | jq -r .data.id) echo "Project $PROJECT_ID"If the price changed in the meantime, you get
409 cost_changed— request the price again.Wait for the result
The montage takes a few minutes. Poll the project every 5 seconds — or receive the
project.succeededwebhook:Terminal while true; do STATUS=$(curl -s "$API/projects/$PROJECT_ID" -H "Authorization: Bearer $SLICER_TOKEN" | jq -r '.data | "\(.status) \(.stage) \(.progress)%"') echo "$STATUS" case "$STATUS" in succeeded*|failed*|cancelled*) break ;; esac sleep 5 doneThe montage produces one version: the director reviews the finished result itself and fixes the order and logic before the project is ready. Earlier versions after a rebuild are covered in Video versions.
Download the video
Terminal curl -f -o reel.mp4 "$API/projects/$PROJECT_ID/result" \ -H "Authorization: Bearer $SLICER_TOKEN"Your video is ready. The same project opens in the web app at the
appUrllink — you can polish it there in the editor.
The whole script
The same flow in one file: upload → price → start → wait → download. The script takes the path to a video as an argument.
// Node.js 20+: node quickstart.mjs interview.mp4
import { createWriteStream, openAsBlob } from 'node:fs';
import { stat } from 'node:fs/promises';
import { basename } from 'node:path';
import { randomUUID } from 'node:crypto';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const API = 'https://api.slicer.media/v1';
const TOKEN = process.env.SLICER_TOKEN;
const file = process.argv[2] ?? 'interview.mp4';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function api(method, path, body, headers = {}) {
const response = await fetch(`${API}${path}`, {
method,
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body ? { 'Content-Type': 'application/json' } : {}),
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await response.json();
if (!json.ok) throw new Error(`${response.status} ${json.error.code}: ${json.error.message}`);
return json.data;
}
// 1. Upload: create it and send the file to the one-time link (no token)
const { size } = await stat(file);
const created = await api('POST', '/uploads', { fileName: basename(file), size });
const put = await fetch(created.uploadUrl, { method: 'PUT', body: await openAsBlob(file) });
const putJson = await put.json();
if (!putJson.ok) throw new Error(`Upload: ${putJson.error.message}`);
let upload = putJson.data;
while (upload.status !== 'ready') {
if (upload.status === 'failed' || upload.status === 'aborted') throw new Error(`File rejected: ${upload.error}`);
await sleep(2000);
upload = await api('GET', `/uploads/${upload.id}`);
}
// 2. Price: nothing starts without confirmCost
const request = { uploadIds: [upload.id], title: 'A reel in 3 minutes', settings: { templateId: 'bold' } };
const quote = await api('POST', '/projects', request);
console.log(`Price: ${quote.cost} tokens, available: ${quote.available}`);
if (!quote.enough) throw new Error('Not enough tokens — top up your balance in the Slicer app');
// 3. Start with the confirmed price
let project = await api('POST', '/projects', { ...request, confirmCost: quote.cost }, { 'Idempotency-Key': randomUUID() });
// 4. Wait
while (project.status === 'queued' || project.status === 'processing') {
console.log(`${project.stage ?? 'queued'}: ${project.progress}%`);
await sleep(5000);
project = await api('GET', `/projects/${project.id}`);
}
if (project.status !== 'succeeded') throw new Error(`Montage did not finish: ${project.error?.message ?? project.status}`);
// 5. Stream the download
const output = `reel.${project.result.container}`;
const video = await fetch(project.result.url, { headers: { Authorization: `Bearer ${TOKEN}` } });
if (!video.ok) throw new Error(`Download: ${video.status}`);
await pipeline(Readable.fromWeb(video.body), createWriteStream(output));
console.log(`Done: ${output}. Project in the app: ${project.appUrl}`);# Python 3.9+: pip install requests && python quickstart.py interview.mp4
import os
import sys
import time
import uuid
import requests
API = "https://api.slicer.media/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['SLICER_TOKEN']}"
def api(method, path, json=None, headers=None):
response = session.request(method, API + path, json=json, headers=headers, timeout=60)
body = response.json()
if not body["ok"]:
error = body["error"]
raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")
return body["data"]
path = sys.argv[1] if len(sys.argv) > 1 else "interview.mp4"
# 1. Upload: create it and send the file to the one-time link (no token)
created = api("POST", "/uploads", {"fileName": os.path.basename(path), "size": os.path.getsize(path)})
with open(path, "rb") as file:
put = requests.put(created["uploadUrl"], data=file, timeout=3600)
put_body = put.json()
if not put_body["ok"]:
raise RuntimeError(f"Upload: {put_body['error']['message']}")
upload = put_body["data"]
while upload["status"] != "ready":
if upload["status"] in ("failed", "aborted"):
raise RuntimeError(f"File rejected: {upload['error']}")
time.sleep(2)
upload = api("GET", f"/uploads/{upload['id']}")
# 2. Price: nothing starts without confirmCost
request = {"uploadIds": [upload["id"]], "title": "A reel in 3 minutes", "settings": {"templateId": "bold"}}
quote = api("POST", "/projects", request)
print(f"Price: {quote['cost']} tokens, available: {quote['available']}")
if not quote["enough"]:
raise RuntimeError("Not enough tokens — top up your balance in the Slicer app")
# 3. Start with the confirmed price
project = api(
"POST",
"/projects",
{**request, "confirmCost": quote["cost"]},
headers={"Idempotency-Key": str(uuid.uuid4())},
)
# 4. Wait
while project["status"] in ("queued", "processing"):
print(f"{project['stage'] or 'queued'}: {project['progress']}%")
time.sleep(5)
project = api("GET", f"/projects/{project['id']}")
if project["status"] != "succeeded":
raise RuntimeError(f"Montage did not finish: {(project['error'] or {}).get('message', project['status'])}")
# 5. Stream the download
output = f"reel.{project['result']['container']}"
with session.get(project["result"]["url"], stream=True, timeout=600) as video:
video.raise_for_status()
with open(output, "wb") as out:
for chunk in video.iter_content(chunk_size=1 << 20):
out.write(chunk)
print(f"Done: {output}. Project in the app: {project['appUrl']}")