Xora

FFmpeg in the cloud

FFmpeg in the cloud,
for agents & humans.

The args you run locally, executed on cloud workers. Job state a machine can act on, recipes a person can reach for, and every output delivered to storage you control.

$ curl -s xora.sh/llms.txt # onboard your agent

  • First job in under 2 min
  • No credit card
  • OpenAPI + llms.txt
POST /v1/jobs
Select preset
Preview a completed job
Job status: Idle 0%
> Click "Run example" to preview a job...

Why this exists

The model already knows FFmpeg.

Every LLM has read thirty years of FFmpeg answers. Ask an agent to crop, concat, or transcode and it writes a working command on the first try — then hits the wall every agent hits: no binary, no CPU, nowhere for a 2 GB file to live.

Xora is the missing half. Sandboxed workers run the args, report state back as JSON, and put the output exactly where you said — whether the caller is a deploy script, an n8n node, or an agent in a loop.

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 JSON

    A tool call, not a shell string. Nothing to quote, nothing to inject.

  • deterministic state

    queued → transcoding → completed, progress in percent, idempotency keys, safe retries.

  • errors worth parsing

    Failures come back as structured JSON to branch on — not stderr soup.

  • a readable surface

    OpenAPI spec, llms.txt, plain REST. No SDK required.

Give your agent FFmpeg — paste into its system prompt
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.

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.

request.sh
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?;
1

POST the job

A recipe or raw FFmpeg args, plus an input URL. You get a job ID back immediately.

2

Watch the state

Poll the job or take a webhook. Every transition is explicit — queued, transcoding, completed, failed — with progress while it runs.

3

Collect the output

A signed download URL — or nothing to collect at all, because it's already in your bucket.

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.

-vf "scale=1280:-1" -crf 23
720p 1080p HQ 30 fps, mute Rotate 90°

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.

job 71a
job 92b
job 34c

Parallel by default

Submit the batch and let workers absorb it — no thread pools to size, no queue to nurse.

job_9f31 · queued idempotency-key: batch-042
job_9f31 · transcoding progress: 64%
job_9f31 · completed webhook → 200 OK

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.

MP4 WebM MKV MOV GIF JPG PNG MP4 WebM MKV MOV GIF JPG PNG
H.264 VP8 VP9 MP3 AAC WAV H.264 VP8 VP9 MP3 AAC WAV

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?
Right now: nothing. Xora is in private preview — free to build with, no credit card. Published rates meter compute minutes and delivery GB — what your job actually consumes, not your file sizes or your video's length. See preview pricing.
What if the command is wrong?
Malformed jobs are rejected with 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 while in private preview — for you, and for your agent.

$ curl -s xora.sh/llms.txt # the whole API, one fetch