videoworker.dev
Menu
FFMPEG, THROUGH AN API

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.

You're exploring account mode.

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.

01 / GET STARTED

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

Requires 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 →
02 / ACCESS

Public docs. Private workloads.

Reading documentation should never require an account. Processing your media is a separate permission.

Trusted local-demo mode

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.

Available key scopes
files:writeUpload media and create tiny samples.
jobs:writeSubmit jobs using your own inputs.
jobs:readList and inspect your jobs.
outputs:readDownload your completed outputs.
PUBLIC / FIXED RECIPES

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.

POST/v1/files

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

Request headers
HeaderValue
Content-Typeapplication/octet-stream
X-FilenameURL-encoded original filename, for example my%20clip.mp4.
201 CREATEDExample response
{
  "id": "0123456789abcdef0123456789abcdef",
  "name": "clip.mp4",
  "size_bytes": 31000
}
POST/v1/jobs

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

More than one input? Same API.

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.

GET/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.

queuedrunningsucceeded / failed / cancelled
{
  "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.

GET/v1/jobs

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

POST/v1/jobs/{job_id}/cancel

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

REFERENCE / LIVE SCHEMA

A small request surface.

These fields and constraints come directly from this running API's OpenAPI specification.

JobRequest
FieldTypeConstraints
Loading request schema…
View the complete OpenAPI JSON →
REFERENCE / BEHAVIOR

Clear limits. Useful errors.

Operational safety limits
Upload sizeChecking running server…
Maximum execution windowChecking running server…
ExecutionA shared global queue cap applies. Cancelling jobs retain capacity until termination and cleanup are confirmed.
OutputsSee 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.
RetentionFree 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.
HTTP errors
400Invalid command, missing input, empty upload, or full local queue. Read detail before retrying.
403Access, key scope, CSRF token, or browser origin is not allowed.
404Resource is absent, belongs to another account, is unfinished, or has expired.
413The upload exceeds the local size limit.
422Request schema validation failed. Check fields, file IDs, and output names.
429Read 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.
503worker_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.
507Local 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.

AUTOMATION / WHAT WORKS TODAY

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.

Connect a local MCP client.

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 ↗