FFmpeg in the cloud
FFmpeg in the cloud,
for agents & humans.
POST your FFmpeg command, get the output in your bucket. No containers to build, no servers to size, no timeouts to dodge.
$ curl -s xora.sh/llms.txt # onboard your agent Add to your agent →
- First job in under 2 min
- No credit card
- MCP + OpenAPI + llms.txt
The problem
Wherever your code runs, FFmpeg doesn't fit.
Agents work in sandboxes. Apps ship to serverless. Pipelines land on boxes someone has to size. FFmpeg — a hundred-megabyte binary that wants every core you've got — punishes all three.
in a sandbox
Agent sandboxes are built for code, not codecs. No FFmpeg binary installed, shared vCPUs, a disk that can't hold a 2 GB source — and a wall-clock timeout that dies mid-encode.
on serverless
The binary barely squeezes into a 250 MB package. /tmp caps at 512 MB. CPU only comes bundled with memory. And at minute 15 the function ends — finished or not.
on your own boxes
A Docker image, an instance size, an encode queue, retries, autoscaling. One 1080p encode pins a core for minutes. Congratulations: you operate a media pipeline now.
The command was never the problem.
Every model writes working FFmpeg args on the first try — and so do you. What's missing is the machine. Xora keeps your command and moves the compute: isolated workers with real cores and real disk, long jobs fanned out across dozens of parallel chunks, state reported back as JSON your code can branch on.
The fix
One POST replaces the pipeline.
POST the job
A recipe or raw FFmpeg args, plus an input URL. You get a job ID back immediately.
Watch the state
Poll the job or take a webhook. Every transition is explicit — queued, transcoding, completed, failed — with progress while it runs.
Collect the output
A signed download URL — or nothing to collect at all, because it's already in your bucket.
Agents & humans
One endpoint. Two kinds of callers.
The same job contract, ergonomic for a person with curl at midnight and for an agent that has never seen your codebase.
for_agents
-
args as JSONA tool call, not a shell string. Nothing to quote, nothing to inject.
-
deterministic statequeued → transcoding → completed, progress in percent, idempotency keys, safe retries.
-
errors worth parsingFailures come back as structured JSON to branch on — not stderr soup.
-
a readable surfaceOpenAPI spec, llms.txt, plain REST. No SDK required.
-
an MCP servercreate_job, get_job, list_recipes as tool calls — same key, same limits. Connect your agent.
You can process media with the Xora FFmpeg API.
Create a job:
POST https://api.xora.sh/v1/jobs
Authorization: Bearer YOUR_API_KEY
{
"mode": "ffmpeg",
"input_files": { "in_1": "<https url>" },
"output_files": { "out_1": "output.mp4" },
"ffmpeg": { "args": ["-i", "{{in_1}}", ...your args, "{{out_1}}"] }
}
Poll GET /v1/jobs/{id} until state is terminal
(completed | failed | rejected | cancelled), then use the
signed download URL on the job — or pass "webhookUrl"
to be called back instead.
Prefer tool calls? Remote MCP server, same key:
claude mcp add --transport http xora \
https://api.xora.sh/v1/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
Full API surface: https://xora.sh/llms.txt for humans
-
Recipes for the 90%
web-ready, thumbnail, trim, concat, compress — tuned so you don't relearn flag order at 1 a.m.
-
Presets
Save a configuration once, run it forever with a single ID.
-
A dashboard worth opening
Live job logs, key management, usage — when you'd rather look than curl.
-
Docs that answer
Zero to first job in under two minutes, with a recipe catalog and an interactive API reference.
No translation step
Bring the command you already have.
Wrap your local FFmpeg invocation in a JSON payload and it behaves the same way in the cloud. {{in_1}} and {{out_1}} name your files; inputs come from any HTTPS origin — S3 and R2 presigned URLs included.
On your laptop
$ ffmpeg -i raw.mov -vf "scale=1280:720" \
-c:v libx264 -crf 20 output.mp4 Same args, different machine.
curl -X POST https://api.xora.sh/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"mode": "ffmpeg",
"input_files": { "in_1": "https://cdn.example.com/raw.mov" },
"output_files": { "out_1": "output.mp4" },
"ffmpeg": {
"args": ["-i", "{{in_1}}",
"-vf", "scale=1280:720",
"-c:v", "libx264", "-crf", "20",
"{{out_1}}"]
}
}' const job = await fetch('https://api.xora.sh/v1/jobs', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
mode: 'ffmpeg',
input_files: { in_1: 'https://cdn.example.com/raw.mov' },
output_files: { out_1: 'output.mp4' },
ffmpeg: {
args: ['-i', '{{in_1}}',
'-vf', 'scale=1280:720',
'-c:v', 'libx264', '-crf', 20,
'{{out_1}}']
}
})
}).then(r => r.json()); import requests
job = requests.post(
"https://api.xora.sh/v1/jobs",
headers={
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
json={
"mode": "ffmpeg",
"input_files": {"in_1": "https://cdn.example.com/raw.mov"},
"output_files": {"out_1": "output.mp4"},
"ffmpeg": {
"args": ["-i", "{{in_1}}",
"-vf", "scale=1280:720",
"-c:v", "libx264", "-crf", 20,
"{{out_1}}"]
},
},
).json() payload := `{
"mode": "ffmpeg",
"input_files": { "in_1": "https://cdn.example.com/raw.mov" },
"output_files": { "out_1": "output.mp4" },
"ffmpeg": { "args": ["-i", "{{in_1}}",
"-vf", "scale=1280:720",
"-c:v", "libx264", "-crf", "20", "{{out_1}}"] }
}`
req, _ := http.NewRequest("POST",
"https://api.xora.sh/v1/jobs", strings.NewReader(payload))
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close() let job = reqwest::Client::new()
.post("https://api.xora.sh/v1/jobs")
.bearer_auth("YOUR_API_KEY")
.json(&serde_json::json!({
"mode": "ffmpeg",
"input_files": { "in_1": "https://cdn.example.com/raw.mov" },
"output_files": { "out_1": "output.mp4" },
"ffmpeg": { "args": ["-i", "{{in_1}}",
"-vf", "scale=1280:720",
"-c:v", "libx264", "-crf", 20, "{{out_1}}"] }
}))
.send()
.await?; Capabilities
Small API. No small print.
A handful of endpoints that hold up whether the job is one thumbnail or a batch of a thousand transcodes.
Your bucket, not ours
Deliver outputs straight to your Cloudflare R2 bucket — your keys, your access rules, no egress markup. Or use managed storage with signed URLs and a Files API to list and delete. You always know where media lives.
No abstraction ceiling
Filter graphs, codec parameters, multi-input args — if FFmpeg takes the flag, the API takes the flag.
"probe": {
"codec": "h264",
"width": 3840,
"duration": 127.4,
"faststart": false
} Probe before you process
mode: "probe" returns enriched ffprobe metadata, and the web-ready recipe re-encodes only when a file actually needs it.
Range: bytes=0–2097151 of 2.1 GB
Ranged reads
Byte-range staging cuts a thumbnail from a 2 GB file without moving 2 GB — and falls back to a full copy automatically when the origin can't seek.
Parallel by default
Submit the batch and let workers absorb it — no thread pools to size, no queue to nurse.
Built for retries
Idempotency keys make resubmission safe, a retry endpoint reruns terminal jobs, and webhooks fire on every terminal state. The things a workflow needs at 3 a.m., built into the contract.
Built on FFmpeg
The formats your product already ships.
Common containers, codecs, audio, and image outputs — addressed with ordinary FFmpeg arguments.
Fair questions
The things you'd ask before trusting us with media.
- Where do files live?
- Wherever you decide. Outputs go straight to your R2 bucket, or sit in managed storage behind signed URLs with a Files API to list and delete. Inputs are read from your URL — staged for the job, not hoarded.
- What does it cost?
- Start free — 25 credits a month, no credit card. Paid plans start at $15. Jobs bill credits for what they actually consume — 1 credit per minute of processing, plus 10 per GB of output — rather than your video's length or a per-call fee. Downloads are unlimited. See pricing.
- What if the command is wrong?
-
Malformed jobs are
rejectedwith a structured error that says why — a state your code (or your agent) can branch on and fix, not a stack trace to screenshot. - Is running arbitrary FFmpeg safe?
- Args arrive as a JSON array, not a shell string — there is no shell to inject. Every job runs in an isolated worker that exists for that job.
Put FFmpeg one POST away.
Free to start, for you and for your agent. No credit card.
$ curl -s xora.sh/llms.txt # the whole API, one fetch