Your first job,
in a few requests.
Upload your media. Send the FFmpeg command you already know. Get a file back, without keeping a request open while it renders.
Custom workloads require a verified browser session or scoped API key. Anonymous visitors can try fixed recipes with the sample. Sign-in methods depend on operator configuration. Paid subscriptions are not enabled in the local preview. A local MCP connector is available for agent integration.
One file in. One file out.
This example extracts an MP3 from a video that contains audio. Pick a language, update the local file path, and run it from your terminal.
# Account mode: create a scoped key in /dashboard. Never commit it.
API_KEY='YOUR_API_KEY'
# POSIX shell; requires curl and jq. Choose a video with audio.
BASE='https://videoworker.dev'
FILE_ID=$(curl --fail-with-body -sS -H "Authorization: Bearer $API_KEY" "$BASE/v1/files" \
-H 'Content-Type: application/octet-stream' \
-H 'X-Filename: clip.mp4' \
--data-binary @clip.mp4 | jq -er '.id')
jq -n --arg id "$FILE_ID" \
'{inputs:[$id],args:["-i","{{input:0}}","-vn","-c:a","libmp3lame","{{output:0}}"],outputs:["audio.mp3"]}' \
| curl --fail-with-body -sS -H "Authorization: Bearer $API_KEY" "$BASE/v1/jobs" \
-H 'Content-Type: application/json' --data-binary @-
# Copy the returned job ID, then check until state is succeeded, failed or cancelled:
curl --fail-with-body -sS -H "Authorization: Bearer $API_KEY" "$BASE/v1/jobs/JOB_ID"
# On success, download the URL returned in outputs[0].url:
curl --fail-with-body -sS -H "Authorization: Bearer $API_KEY" "$BASE/v1/jobs/JOB_ID/outputs/audio.mp3" \
--output audio.mp3Requires a POSIX shell and jq; on Windows use WSL or Git Bash with jq installed. Python and JavaScript include automatic polling and download.
Prefer to click first? Open the playground →Public docs. Private workloads.
Reading documentation should never require an account. Processing your media is a separate permission.
No API key is required on this loopback-only server. This is a development convenience, not anonymous access to a hosted service. Do not expose it to the internet.
In account mode, a verified browser session or a scoped API key authorizes only your own uploads, jobs, and results. Public documentation and health remain readable. Configure Google, GitHub, or email delivery before using those sign-in methods. A development outbox is local-only and sends no external email.
Browser session
Same-origin browser requests send the HttpOnly session cookie.
Before each write, read /v1/me and send its current csrf_token in X-CSRF-Token. Never store this value across session
changes.
const meResponse = await fetch('/v1/me', { credentials: 'same-origin', cache: 'no-store' });
if (!meResponse.ok) throw new Error('Sign in again');
const me = await meResponse.json();
await fetch('/v1/demo-file', {
method: 'POST', credentials: 'same-origin', cache: 'no-store',
headers: { 'X-CSRF-Token': me.csrf_token },
}); Scoped API key
Create and revoke personal keys in your account. The full key appears once. Send Authorization: Bearer YOUR_API_KEY from your secret store;
bearer requests do not need a CSRF header. Key management requires a browser
session.
files:write | Upload media and create tiny samples. |
|---|---|
jobs:write | Submit jobs using your own inputs. |
jobs:read | List and inspect your jobs. |
outputs:read | Download your completed outputs. |
Try the sample, then make it yours.
In Accounts mode, anonymous visitors can run audio, compress or merge with the immutable one-second 160 × 90 sample. Own uploads and arbitrary commands require sign-in. The trusted local-demo mode is a separate operator-controlled development mode.
Only an explicit click should bootstrap POST /v1/demo.
The browser manages its HttpOnly, SameSite=Strict cookie scoped to /v1/demo. Keep the returned csrf_token in memory
and replace it on every bootstrap; bootstrap rotates CSRF without extending
expiry.
// Run from an explicit user interaction, on this same origin.
const session = await fetch('/v1/demo', { method: 'POST', credentials: 'same-origin', cache: 'no-store' });
if (!session.ok) throw new Error('Demo unavailable');
const { csrf_token, limits } = await session.json();
const response = await fetch('/v1/demo/jobs', {
method: 'POST', credentials: 'same-origin', cache: 'no-store',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf_token },
body: JSON.stringify({ recipe: 'audio' }),
});
if (!response.ok) throw new Error('Read detail.code before retrying');
const job = await response.json();
// Poll GET /v1/demo/jobs/{id}; download only returned outputs[].url.
// POST /v1/demo/jobs/{id}/cancel uses the same current CSRF header. The effective execution window is at most five seconds total,
including setup and export, rather than five usable CPU seconds. One
outstanding job per capability, three creation attempts per hour per
IP, and a shared daily demo safety pool (default 1000 seconds)
apply. Every valid-CSRF creation attempt counts before validation,
sample readiness or admission. Use returned limits;
these are operational safety limits, with no billing or
remaining-quota claim.
demo_expired requires an explicit new bootstrap; old
ownership is not recovered. demo_csrf needs the newly
rotated memory token. demo_ip_limit, demo_bootstrap_limit and demo_capacity describe distinct limits; no retry timestamp
is promised. Public SSR never bootstraps a capability.
/v1/filesUpload your source media.
Send the raw file body, not a multipart form. The response contains a file ID; use it when creating a job. Original filenames are metadata and are not used as server paths.
| Header | Value |
|---|---|
Content-Type | application/octet-stream |
X-Filename | URL-encoded original filename, for example my%20clip.mp4. |
{
"id": "0123456789abcdef0123456789abcdef",
"name": "clip.mp4",
"size_bytes": 31000
}/v1/jobsYour command. Familiar syntax.
Pass arguments as a JSON array. Do not include ffmpeg itself or a shell command. Use separate placeholder
arguments for uploaded inputs and declared outputs.
{
"inputs": ["0123456789abcdef0123456789abcdef"],
"args": [
"-i", "{{input:0}}",
"-vn", "-c:a", "libmp3lame",
"{{output:0}}"
],
"outputs": ["audio.mp3"]
} A successful submission returns 202 Accepted with a job ID
and its current state. Submission is asynchronous: accepted does not mean
finished.
For safe retries, send a UUID in Idempotency-Key. Reuse
the same UUID and identical request for 48 hours to retrieve the
original job. Changed JSON returns 409; an expired original job
returns 410. Requests without this optional header create
independent jobs.
Add file IDs in order and reference {{input:0}}, {{input:1}}, and so on. Multiple
declared outputs work the same way. Each input and output must be
referenced by the command.
/v1/jobs/{job_id}Let the worker do its thing.
Poll until the job succeeds, fails or is cancelled. A one-second polling interval is enough for the examples. Keep a timeout in your client so a lost connection does not cause an endless loop.
{
"id": "fedcba9876543210fedcba9876543210",
"state": "succeeded",
"created": 1700000000,
"started": 1700000001,
"finished": 1700000002,
"cancel_requested": null,
"timeout_seconds": 120,
"elapsed": 0.08,
"input_bytes": 31000,
"output_bytes": 5500,
"error": null,
"outputs": [{
"name": "audio.mp3",
"size_bytes": 5500,
"url": "/v1/jobs/fedcba9876543210fedcba9876543210/outputs/audio.mp3"
}]
} On success, download the returned relative URL from the same server.
On failure, error explains what went wrong. Cancelled jobs
have no output.
The response also includes creation, start and finish timestamps. elapsed is wall-clock processing time, not measured CPU time
or a finalized billing quantity.
/v1/jobsReturns the 20 most recent jobs in {"jobs": [...]}. In account mode this list includes only your own jobs. The
trusted local demo shares its list on the machine.
/v1/jobs/{job_id}/cancelRequest cancellation. Confirm the outcome.
Use your current session CSRF header or a key with jobs:write. Only the owner can cancel. Repeated cancellation is idempotent. A queued job can return cancelled immediately; an active job returns cancelling until the worker confirms actual termination and cleanup. Continue polling until succeeded, failed or cancelled; completion may win the race. Cancelled jobs offer no outputs.
curl --fail-with-body -sS -X POST 'YOUR_ORIGIN/v1/jobs/JOB_ID/cancel' \
-H 'Authorization: Bearer YOUR_API_KEY'
# Then GET YOUR_ORIGIN/v1/jobs/JOB_ID until terminal.
# cancel_requested is a nullable Unix timestamp; timeout_seconds is the selected bound. If cancellation loses its acknowledgement, refresh and retry the cancellation safely. Never infer that the process stopped from a network error. An attempted submission with a lost acknowledgement is different: inspect recent jobs before creating another job.
A small request surface.
These fields and constraints come directly from this running API's OpenAPI specification.
| Field | Type | Constraints |
|---|---|---|
| Loading request schema… | ||
Clear limits. Useful errors.
| Upload size | Checking running server… |
|---|---|
| Maximum execution window | Checking running server… |
| Execution | A shared global queue cap applies. Cancelling jobs retain capacity until termination and cleanup are confirmed. |
| Outputs | See GET /v1/limits for your current per-file, combined
input/output and storage limits. Storage has an independent admission
threshold, not a hard disk quota. |
| Retention | Free uploads/results are kept for 24 hours; paid allocations
keep them for 7 days. Upload expiry starts when the upload
finishes; result expiry starts at job completion. Returned
expiry timestamps are authoritative. Job history is kept for
90 days; expired result downloads return HTTP 410. Delete
results early with DELETE /v1/jobs/{job_id}/outputs using jobs:write; shared inputs, history and processing charges
are kept. |
400 | Invalid command, missing input, empty upload, or full local
queue. Read detail before retrying. |
|---|---|
403 | Access, key scope, CSRF token, or browser origin is not allowed. |
404 | Resource is absent, belongs to another account, is unfinished, or has expired. |
413 | The upload exceeds the local size limit. |
422 | Request schema validation failed. Check fields, file IDs, and output names. |
429 | Read detail.code: outstanding_limit,
daily_limit, demo_daily_limit, global_daily_limit,
storage_limit, file_limit or job_limit. No remaining quota
or retry time is implied. |
503 | worker_unavailable or sample_unavailable means the job was not admitted. An unexplained server failure or lost submission acknowledgement can hide an accepted job; refresh activity before resubmitting. |
507 | Local upload storage is full. |
A command can be accepted and then fail during FFmpeg processing.
Check the job's state and error, not just
the submission's HTTP status.
Built for scripts and agents.
Scripts and automation tools can already call the same upload, job, and result endpoints as the playground. The OpenAPI specification provides the machine-readable request schema.
The local STDIO connector uploads authorized media, runs jobs, checks quota and status, saves results and cancels work through the same scoped API. Read the setup guide. Hosted remote MCP with OAuth is a later integration.
You can also use the REST examples above. Keep API credentials in your own secret store; never paste a production key into public code examples.
Less setup. More making.
Open the playground ↗