MediaTofu

Compress, convert and trim video with one API call

Need full control? Run any FFmpeg command. Each job runs in its own sandbox, and you can pay only for the compute you use.

Get a free key5 minutes a month, no card

ffmpeg -i {{in}} -vf scale=1280:-2 -crf 26 {{out}}

Your flags stay as they are. {{in}} and {{out}} stand in for the files you declare.

Send the command, get a link to the file

With ?wait=25 a short job comes back finished in the same response. Leave it off and you get a job id to check on with GET /v1/jobs/:id.

Bring your own command

Name your files, then write the FFmpeg command you would run on your own machine.

POST /v1/run?wait=25
curl "https://api.mediatofu.com/v1/run?wait=25" \
  -H "Authorization: Bearer $MEDIATOFU_KEY" \
  -d '{
    "input_files": { "in": "https://example.com/clip.mov" },
    "output_files": { "out": "small.mp4" },
    "ffmpeg_command": "-i {{in}} -vf scale=1280:-2 -crf 26 {{out}}"
  }'

Inputs are URLs or uploaded file ids. A job can have up to 10 inputs and 10 outputs, and every one you declare has to appear in the command.

Response202 Accepted
{
  "id": "job_7c41e09ab2d35f68104bc7ae",
  "tool": "ffmpeg",
  "status": "succeeded",
  "compute_seconds": 7,
  "compute_ms": 6412,
  "outputs": [
    {
      "name": "small.mp4",
      "size": 18350412,
      "url": "https://api.mediatofu.com/o/e2a94c17d05b36f8a1c7904ed2b6735f/job_7c41e09ab2d35f68104bc7ae/small.mp4"
    }
  ],
  "exit_code": 0,
  "created_at": "2026-11-09T10:42:03Z",
  "finished_at": "2026-11-09T10:42:13Z"
}

Or skip the flags

The tools build the command for you and run on the same engine.

POST /v1/tools/compress?wait=25
curl "https://api.mediatofu.com/v1/tools/compress?wait=25" \
  -H "Authorization: Bearer $MEDIATOFU_KEY" \
  -d '{ "input": "https://example.com/clip.mov", "crf": 28 }'

A lower crf means higher quality and a larger file. Compress accepts 18 to 40 and uses 28 if you leave it out.

Response202 Accepted
{
  "id": "job_b80f3d16c94e27a5013fd86c",
  "tool": "compress",
  "status": "succeeded",
  "compute_seconds": 6,
  "compute_ms": 5238,
  "outputs": [
    {
      "name": "out.mp4",
      "size": 15204877,
      "url": "https://api.mediatofu.com/o/59d3b07ae18c4f26b7a0d31c86ef2490/job_b80f3d16c94e27a5013fd86c/out.mp4"
    }
  ],
  "exit_code": 0,
  "created_at": "2026-11-09T10:44:51Z",
  "finished_at": "2026-11-09T10:45:00Z"
}

Every job gets its own container, and only that job

  1. Step 1: A new container starts for this job.

  2. Step 2: Your input files are copied in.

  3. Step 3: FFmpeg runs with no internet and no secrets.

  4. Step 4: Your output files are saved.

  5. Step 5: The container is destroyed.

Commands stay inside the job

Your filters, codecs and flags work as usual, and there is never a shell. A command can only touch the files you declared. URLs inside the command and options that read or write other files are rejected before anything runs.

If a job fails on our side, you aren't charged.

What you get back if you try
  • -i {{in}} -i other.mp4 {{out}}

    Every -i must be an {{input}} placeholder, or follow -f lavfi.

  • -i {{in}} -vf movie=http://example.com/logo.png {{out}}

    URLs and protocols can't be used inside ffmpeg_command. Declare them in input_files.

  • -i {{in}} -vf movie=/etc/passwd {{out}}

    Paths outside the job aren't allowed: movie=/etc/passwd

  • -i {{in}} -filter_script filters.txt {{out}}

    The option -filter_script is not allowed.

Eight jobs you can run without learning FFmpeg

Post to /v1/tools/<name> with an input, which is a URL or a file id, plus any options below.

Hear back when it's done

On paid plans, add webhook_url to any job and stop polling. Useful for long jobs and automations.

When
One POST when the job succeeds or fails. The body is the same JSON as the job response.
Signature
Each POST carries an HMAC-SHA256 of the raw body in X-MediaTofu-Signature.
Delivery
At least once, with up to 5 attempts until your endpoint answers with a 2xx. Duplicates are possible, so key on the job id.
Check the signature in Node
import { createHash, createHmac } from "node:crypto";

const secret = createHash("sha256")
  .update(process.env.MEDIATOFU_KEY)
  .digest("hex");
const expected = "sha256=" + createHmac("sha256", secret)
  .update(rawBody)
  .digest("hex");

// Compare with the X-MediaTofu-Signature header.

Pay for compute, by the second

Compute is the time FFmpeg spends running your job, rounded up to a whole second. Waiting in the queue and moving files around don't count.

Free

$0

No card needed

Compute
5 minutes a month
Job length
Up to 1 minute
Input size
Up to 50 MB
Concurrency
1 job at a time
Webhooks
Not included
Outputs kept
24 hours
Get a free key

Pay per use

$0.024/ minute

No monthly fee

Compute
Billed by the second
Job length
Up to 5 minutes
Input size
Up to 2 GB
Concurrency
3 jobs at once
Webhooks
Included
Outputs kept
7 days
Choose Pay per use

Starter

$9/ month

Billed monthly

Compute
600 minutes a month, then $0.015 a minute
Job length
Up to 1 minute
Input size
Up to 500 MB
Concurrency
2 jobs at once
Webhooks
Included
Outputs kept
7 days
Choose Starter

Pro

$19/ month

Billed monthly

Compute
1,500 minutes a month, then $0.0127 a minute
Job length
Up to 10 minutes
Input size
Up to 2 GB
Concurrency
5 jobs at once
Webhooks
Included
Outputs kept
7 days
Choose Pro

Metered usage is capped at $25 per billing period, rising to $250 after your first paid renewal. Input size limits apply to files passed by URL. Direct uploads are limited to 100 MB, or 50 MB on Free.

Questions

Do I need to know FFmpeg?

No. The eight tools take plain options like a format or a width. If you do know FFmpeg, POST /v1/run takes your command with placeholders where the file names go.

How do I get a file in?

Pass a public http or https URL, or upload the file to POST /v1/files and use the file id it returns. URL inputs can be as large as your plan allows. Direct uploads stop at 100 MB, or 50 MB on Free.

How do I get the result?

Add ?wait=25 and a short job returns its result in the same response. 25 seconds is the longest wait. Otherwise you get a job id to check with GET /v1/jobs/:id, or on a paid plan you can pass webhook_url and we POST the result to you.

Do failed jobs cost anything?

A failure on our side is never charged, and neither is an input we couldn't download or one over your plan's size limit. If your command runs and FFmpeg exits with an error, or the job reaches your plan's length limit, the seconds it used count.

What happens when I reach a limit?

On Free, jobs stop after 5 compute minutes until the next calendar month or until you upgrade. On paid plans, usage past your allowance is billed by the second, and jobs stop if you reach your spending cap for the period.

Who can download my output files?

Anyone who has the output URL. Each URL contains a long random token, so treat it like a password. Outputs are kept for 24 hours on Free and 7 days on paid plans.

Try it with 5 free minutes

Get a free key