Very Good FFmpeg
PricingHow it worksCompareFAQDocs
Documentation

Very Good FFmpeg API

Getting started
  • Introduction
  • Quickstart
  • Authentication
API
  • Reference
  • Submit a job
  • Get a job
  • List jobs
  • Cancel a job
Working with jobs
  • Sync vs async
  • Webhooks
  • Polling
  • Output URLs
FFmpeg command
  • Template variables
  • Common recipes
  • Hardware acceleration
Operations
  • Errors
  • Pricing & billing
Code examples
  • Node.js
  • Python
Getting started

Introduction

Very Good FFmpeg is a REST API that runs your exact FFmpeg commands on dedicated cloud hardware and bills you per gigabyte processed.

The API is built around a single resource:

  • Jobs describe one FFmpeg invocation — input URLs, output filenames, and the command string.
  • Jobs run asynchronously by default. You receive a webhook on completion or poll GET /api/jobs/{id} until the status terminal-flips.
  • For short jobs you can pass ?wait=true to block on the response.
Base URL
All requests go to https://verygoodffmpeg.com/api.
Getting started

Quickstart

1. Get an API key

Sign up at verygoodffmpeg.com/sign-up and create a key from the dashboard.

2. Submit a transcode

POST /api/ffmpeg
bash
curl -X POST https://verygoodffmpeg.com/api/ffmpeg \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_files": {
      "input": "https://your-bucket.s3.amazonaws.com/input.mp4"
    },
    "output_files": ["output.mp4"],
    "ffmpeg_command": "-i {{input}} -c:v libx264 -crf 23 {{output.mp4}}",
    "webhook_url": "https://your-app.example.com/hooks/ffmpeg"
  }'

3. Read the response

200 OK — async response
json
{
  "data": {
    "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
    "status": "queued",
    "queued_at": "2026-05-06T18:42:01.114Z",
    "started_at": null,
    "finished_at": null,
    "created_at": "2026-05-06T18:42:01.103Z",
    "updated_at": "2026-05-06T18:42:01.103Z",
    "error_message": "",
    "ffmpeg_command": "-i {{input}} -c:v libx264 -crf 23 {{output.mp4}}",
    "input_files": {
      "input": "https://your-bucket.s3.amazonaws.com/input.mp4"
    },
    "output_files": {},
    "webhook_url": "https://your-app.example.com/hooks/ffmpeg",
    "total_input_bytes": null,
    "total_output_bytes": null
  }
}

The job starts in queued, transitions through running, and ends in succeeded, failed, or cancelled. Use the returned id to poll, or supply a webhook_url to be notified on completion.

Getting started

Authentication

All requests must include a Bearer token in the Authorization header.

Authorization header
http
Authorization: Bearer $API_KEY
  • Keys are scoped to a single organisation.
  • You can issue multiple keys per organisation — useful for separating staging and production, or per-service auditing.
  • Revoke a key from the dashboard; revocation is effective within seconds across all regions.
Treat keys as secrets
Never commit keys to source control or expose them in client-side code.
API

Reference

Four endpoints cover the full job lifecycle. All responses wrap the resource in a top-level data field.

NameTypeDescription
POST /api/ffmpegSubmitCreate and queue an FFmpeg job. Optionally wait for completion.
GET /api/jobsListList jobs for the authenticated organisation, newest first.
GET /api/jobs/{id}ReadFetch the current state of a single job.
POST /api/jobs/{id}/cancelCancelRequest cancellation. Best-effort for running jobs.
Endpoint

POST /api/ffmpeg

Submit a new FFmpeg job. The default is asynchronous — pass ?wait=true to block until the job finishes.

Request body

NameTypeDescription
input_filesobject<string,url>Required. Map of template names to publicly readable URLs. Reference each by name in ffmpeg_command as {{name}}.
output_filesstring[]Required. Filenames the command will produce. Reference each as {{<filename>}}. The API rejects jobs that don’t emit at least one declared output.
ffmpeg_commandstringRequired. FFmpeg arguments only — omit the leading ffmpeg binary name. Standard flags, filter graphs, and complex pipelines all work.
webhook_urlurlOptional. HTTPS URL we POST to with the terminal job payload.
machine"cpu" | "nvidia"Optional. Hardware target. Defaults to cpu. Set to nvidia to route the job to a GPU worker. Read Hardware acceleration before flipping this — GPU jobs usually slower unless your command actually uses GPU codecs and filters.

Query parameters

NameTypeDescription
waitbooleanOptional. When true, the request blocks for up to 60 seconds waiting for the job to finish, then returns the latest state. Default is false.

Example request

curl
bash
curl -X POST "https://verygoodffmpeg.com/api/ffmpeg?wait=false" \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_files": {
      "input": "https://example.com/source.mov"
    },
    "output_files": ["preview.mp4", "thumb.jpg"],
    "ffmpeg_command": "-i {{input}} -t 30 -c:v libx264 {{preview.mp4}} -ss 5 -frames:v 1 {{thumb.jpg}}"
  }'

Example response

200 OK
json
{
  "data": {
    "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
    "status": "queued",
    "queued_at": "2026-05-06T18:42:01.114Z",
    "started_at": null,
    "finished_at": null,
    "created_at": "2026-05-06T18:42:01.103Z",
    "updated_at": "2026-05-06T18:42:01.103Z",
    "error_message": "",
    "ffmpeg_command": "-i {{input}} -t 30 -c:v libx264 {{preview.mp4}} -ss 5 -frames:v 1 {{thumb.jpg}}",
    "input_files": { "input": "https://example.com/source.mov" },
    "output_files": {},
    "webhook_url": null,
    "total_input_bytes": null,
    "total_output_bytes": null
  }
}
Endpoint

GET /api/jobs/{id}

Fetch the latest state of a single job. The response shape matches the create endpoint.

Path parameters

NameTypeDescription
idstringJob identifier returned at submission.

Example request

curl
bash
curl https://verygoodffmpeg.com/api/jobs/8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c \
  -H "Authorization: Bearer $API_KEY"

Example response

200 OK
json
{
  "data": {
    "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
    "status": "succeeded",
    "queued_at": "2026-05-06T18:42:01.114Z",
    "started_at": "2026-05-06T18:42:03.812Z",
    "finished_at": "2026-05-06T18:43:11.402Z",
    "created_at": "2026-05-06T18:42:01.103Z",
    "updated_at": "2026-05-06T18:43:11.420Z",
    "error_message": "",
    "ffmpeg_command": "-i {{input}} -t 30 -c:v libx264 {{preview.mp4}}",
    "input_files": { "input": "https://example.com/source.mov" },
    "output_files": {
      "preview.mp4": "https://outputs.verygoodffmpeg.com/8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c/preview.mp4?sig=..."
    },
    "webhook_url": null,
    "total_input_bytes": 184729123,
    "total_output_bytes": 12873914
  }
}
Endpoint

GET /api/jobs

List jobs for your organisation, newest first. Supports cursor pagination.

Query parameters

NameTypeDescription
limitnumberOptional. Page size, 1–100. Default 25.
cursorstringOptional. Pagination cursor returned in the previous page.
statusstringOptional. Filter by status: queued | running | succeeded | failed | cancelled.

Example request

curl
bash
curl "https://verygoodffmpeg.com/api/jobs?limit=20&status=succeeded" \
  -H "Authorization: Bearer $API_KEY"

Example response

200 OK
json
{
  "data": [
    {
      "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
      "status": "succeeded",
      "queued_at": "2026-05-06T18:42:01.114Z",
      "finished_at": "2026-05-06T18:43:11.402Z",
      "ffmpeg_command": "-i {{input}} -t 30 -c:v libx264 {{preview.mp4}}",
      "output_files": {
        "preview.mp4": "https://outputs.verygoodffmpeg.com/.../preview.mp4?sig=..."
      },
      "total_output_bytes": 12873914
    },
    {
      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
      "status": "running",
      "queued_at": "2026-05-06T18:40:22.001Z",
      "finished_at": null,
      "ffmpeg_command": "-i {{input}} -c:v libx264 {{out.mp4}}",
      "output_files": {},
      "total_output_bytes": null
    }
  ]
}
Endpoint

POST /api/jobs/{id}/cancel

Request cancellation of a queued or running job. Cancellation is best-effort; jobs that have already finished return their terminal state unchanged.

Example request

curl
bash
curl -X POST \
  https://verygoodffmpeg.com/api/jobs/8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c/cancel \
  -H "Authorization: Bearer $API_KEY"

Example response

200 OK
json
{
  "data": {
    "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
    "status": "cancelled",
    "queued_at": "2026-05-06T18:42:01.114Z",
    "started_at": "2026-05-06T18:42:03.812Z",
    "finished_at": "2026-05-06T18:42:31.997Z",
    "error_message": "Job cancelled",
    "output_files": {},
    "total_output_bytes": null
  }
}

Successful cancellation flips the job to cancelled and emits a terminal webhook if one was configured. Partial outputs are discarded and not billed.

  • If the job was already terminal, the response contains its existing state unchanged.
Working with jobs

Sync vs async

Jobs run async by default. Use ?wait=true only for short, latency-sensitive operations.

  • Async (default). The submit call returns immediately with a queued job. Combine with webhook_url or polling.
  • Sync. Pass ?wait=true to block on the response. The request waits up to 60 seconds for the job to finish; if it doesn’t, the job is cancelled and the terminal state is returned.
When to choose which
Choose sync for short jobs in user-interactive code paths (a thumbnail behind a loading spinner). For anything that might exceed the 60-second window, use async with a webhook or polling.
Working with jobs

Webhooks

If you supply webhook_url at submission, we POST the terminal job payload to it as soon as the job reaches a terminal state.

Delivery semantics

  • At-least-once delivery — design endpoints to be idempotent on job id.
  • Retried with exponential backoff for up to 24 hours on 5xx or network errors.
  • 4xx responses are treated as terminal — we won’t retry.
  • HTTPS only. Custom ports allowed; self-signed certs are not.

Payload

POST {webhook_url}
json
{
  "data": {
    "id": "8f3c2b6a-4d9e-4f0c-9b5a-2d3e4f5a6b7c",
    "status": "succeeded",
    "finished_at": "2026-05-06T18:43:11.402Z",
    "output_files": {
      "preview.mp4": "https://outputs.verygoodffmpeg.com/.../preview.mp4?sig=..."
    },
    "total_input_bytes": 184729123,
    "total_output_bytes": 12873914
  }
}

The body is identical to the response from GET /api/jobs/{id} at the moment the job reached its terminal state.

Working with jobs

Polling

If you can’t accept inbound webhooks, poll the job endpoint until status is terminal.

Polling loop
javascript
const terminal = ["succeeded", "failed", "cancelled"]

async function waitForJob(id, apiKey) {
  for (let i = 0; i < 600; i++) {
    const res = await fetch(`https://verygoodffmpeg.com/api/jobs/${id}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    })
    const { data } = await res.json()
    if (terminal.includes(data.status)) return data
    await new Promise((r) => setTimeout(r, i < 10 ? 1000 : 5000))
  }
  throw new Error("Job did not finish within poll budget")
}
Working with jobs

Output URLs

Outputs land at signed S3-compatible URLs. They’re stable, time-limited, and safe to hand to end users.

  • Signed URLs are valid for 7 days from the moment the job is fetched. Re-fetch the job to refresh.
  • Output bytes are retained for 30 days, then garbage-collected. Copy them to your own storage if you need long-term retention.
  • Range requests are supported, so you can stream large outputs directly from the signed URL.
Don’t cache the URL itself
Cache the underlying bytes if you need them long-term — the URL’s signature expires. The job id is the durable handle.
FFmpeg command

Template variables

Inputs and outputs are interpolated into your FFmpeg command via {{...}} placeholders. We resolve them to local paths inside the worker before running.

  • {{<input-name>}} — resolves to the local path of the downloaded input file. Names come from the keys of input_files.
  • {{<output-filename>}} — resolves to a writable local path. Use the exact filename you listed in output_files.

Multi-input filter graph

Concat two clips
bash
// input_files: { intro: "...", main: "..." }
// output_files: ["combined.mp4"]
// ffmpeg_command:
-i {{intro}} -i {{main}} \
  -filter_complex "[0:v][0:a][1:v][1:a]concat=n=2:v=1:a=1[v][a]" \
  -map "[v]" -map "[a]" -c:v libx264 -c:a aac {{combined.mp4}}
FFmpeg command

Common recipes

Drop-in commands for the most common video and audio operations. All assume {{input}} is the source.

H.264 web preview (CRF 23, 720p)
bash
-i {{input}} -vf scale=-2:720 -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k {{preview.mp4}}
HLS adaptive ladder (480p / 720p / 1080p)
bash
-i {{input}} \
  -filter_complex "[0:v]split=3[v1][v2][v3]; \
    [v1]scale=854:480[v480]; \
    [v2]scale=1280:720[v720]; \
    [v3]scale=1920:1080[v1080]" \
  -map "[v480]" -c:v:0 libx264 -b:v:0 1200k \
  -map "[v720]" -c:v:1 libx264 -b:v:1 2800k \
  -map "[v1080]" -c:v:2 libx264 -b:v:2 5000k \
  -map a:0? -c:a aac -b:a 128k \
  -f hls -hls_time 4 -hls_playlist_type vod \
  -master_pl_name {{master.m3u8}} \
  -hls_segment_filename "stream_%v/seg_%03d.ts" \
  -var_stream_map "v:0,a:0 v:1,a:0 v:2,a:0" \
  stream_%v/playlist.m3u8
Audio normalisation (EBU R128, two-pass)
bash
-i {{input}} -af loudnorm=I=-16:TP=-1.5:LRA=11 -c:a aac -b:a 192k {{normalized.m4a}}
Thumbnail at 5s, 640px wide
bash
-i {{input}} -ss 5 -frames:v 1 -vf scale=640:-2 {{thumb.jpg}}
Webhook-friendly waveform PNG
bash
-i {{input}} -filter_complex showwavespic=s=1280x256:colors=#7c3aed -frames:v 1 {{waveform.png}}
FFmpeg command

Hardware acceleration

Optionally speed up your workloads by routing them to GPU workers — set machine: 'nvidia' on the submission body.

GPU is not automatically faster
FFmpeg uses CPU code paths unless you ask it not to. Sending a normal libx264 command to a GPU machine will be slower than the CPU machine, not faster — the GPU sits idle and the host CPU is weaker. To get a speed-up, your command has to use GPU decoders (*_cuvid, -hwaccel cuda), GPU encoders (*_nvenc), and ideally GPU filters (scale_npp, scale_cuda, overlay_cuda). If your pipeline isn’t written for the GPU, stay on the default CPU machine.

Supported codecs

CodecEncodeDecode
H.264h264_nvench264_cuvid
HEVC / H.265hevc_nvenchevc_cuvid
AV1av1_nvencav1_cuvid
VP9—vp9_cuvid

Submitting a GPU job

Set "machine": "nvidia" on the submission body and use NVENC / NVDEC codec names in your command. The default "machine": "cpu" targets our standard 16 vCPU instances.

GPU job submission
bash
curl -X POST https://verygoodffmpeg.com/api/ffmpeg \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input_files": { "input": "https://example.com/source.mov" },
    "output_files": ["encoded.mp4"],
    "machine": "nvidia",
    "ffmpeg_command": "-hwaccel cuda -hwaccel_output_format cuda -i {{input}} -c:v hevc_nvenc -preset p5 -rc vbr -b:v 6M {{encoded.mp4}}"
  }'
Pricing
GPU jobs are billed on the same per-GB model as CPU jobs — there’s no GPU surcharge.
Operations

Errors

Errors return a structured JSON body with an errorCode, human-readable message, and optional data field for validation details.

400 Bad Request
json
{
  "errorCode": "validationError",
  "message": "Invalid FFmpeg request",
  "data": [
    {
      "path": ["output_files"],
      "message": "Array must contain at least 1 element(s)"
    }
  ]
}

Error codes

StatusCodeMeaning
400validationErrorRequest body failed schema validation. The data field lists per-field issues.
400badRequestRequest was syntactically valid but couldn’t be acted on (e.g. job not retryable).
401badApiKeyMissing or invalid Authorization header.
400notFoundJob id doesn’t exist or belongs to another organisation.
402paymentRequiredOrganisation can’t submit jobs until billing is set up. data may include a billing URL.
500unknownErrorServer-side failure. Includes a trace id you can quote when contacting support.
Operations

Pricing & billing

Usage is metered as the sum of bytes downloaded for inputs and bytes produced for outputs. Volume discounts apply automatically per calendar month.

Tiers

VolumePrice per GBDiscount
0 – 10 GB$0.50—
10 – 100 GB$0.2550% off
100 – 500 GB$0.1570% off
500 GB – 2 TB$0.1080% off
2 TB+$0.0786% off
  • Cancelled jobs aren’t billed. Failed jobs aren’t billed.
Code examples

Node.js

Use the standard fetch API. The shape of the request body matches what you’d send via curl.

submit-and-wait.ts
typescript
const apiKey = process.env.VGF_API_KEY!
const base = "https://verygoodffmpeg.com"

async function submitAndWait() {
  const submit = await fetch(`${base}/api/ffmpeg`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      input_files: { input: "https://example.com/source.mov" },
      output_files: ["out.mp4"],
      ffmpeg_command: "-i {{input}} -c:v libx264 -crf 23 {{out.mp4}}",
    }),
  })
  const { data: queued } = await submit.json()

  while (true) {
    const res = await fetch(`${base}/api/jobs/${queued.id}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    })
    const { data } = await res.json()
    if (["succeeded", "failed", "cancelled"].includes(data.status)) {
      return data
    }
    await new Promise((r) => setTimeout(r, 2000))
  }
}
Code examples

Python

Stdlib urllib works fine. Use requests if you’re already pulling it in.

submit_and_wait.py
python
import json, os, time
from urllib import request

api_key = os.environ["VGF_API_KEY"]
base = "https://verygoodffmpeg.com"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}

body = json.dumps({
    "input_files": {"input": "https://example.com/source.mov"},
    "output_files": ["out.mp4"],
    "ffmpeg_command": "-i {{input}} -c:v libx264 -crf 23 {{out.mp4}}",
}).encode()

queued = json.load(request.urlopen(
    request.Request(f"{base}/api/ffmpeg", data=body, headers=headers, method="POST")
))["data"]

terminal = {"succeeded", "failed", "cancelled"}
while True:
    poll = request.Request(f"{base}/api/jobs/{queued['id']}", headers=headers)
    data = json.load(request.urlopen(poll))["data"]
    if data["status"] in terminal:
        print(data["output_files"])
        break
    time.sleep(2)
Very Good FFmpeg

The FFmpeg API with usage-based pricing.

Product
  • Pricing
  • How it works
  • Comparison
  • FAQ
Developers
  • Docs
  • API reference
  • Hardware acceleration
Company
  • Contact
  • Sign in
  • Sign up
Legal
  • Terms
  • Privacy
© 2026 Very Good FFmpeg