> ## Documentation Index
> Fetch the complete documentation index at: https://docs.madiad.com/llms.txt
> Use this file to discover all available pages before exploring further.

# FFmpeg

> Run server-side video and audio processing jobs through the MADIAD Hub API.

The FFmpeg API lets you transcode, trim, convert, and otherwise process video and audio files entirely on the server. You provide a source URL and an FFmpeg command; MADIAD Hub fetches the file, runs the job, and makes the output available to download.

There is no binary upload — input files must be reachable over HTTPS. The API returns a `job_id` immediately; processing is asynchronous, so you poll for status and then download the result.

<Warning>
  **There is no FFmpeg webhook.** Polling is the ONLY way to learn that a job finished.
  [Webhooks](/concepts/webhooks) emit events for publishing and for connection changes only — there
  is no event for FFmpeg jobs. Do not design around an FFmpeg callback.
</Warning>

<Note>
  FFmpeg jobs require a paid plan. An account with no active plan has **0 FFmpeg minutes**. Activate Starter or higher to unlock the feature.
</Note>

## How a job works

<Steps>
  <Step title="Submit">
    POST to `/v1/ffmpeg/jobs` with one or more source URLs and an FFmpeg command. The API validates the command, reserves quota, and queues the job. You receive a `job_id`.
  </Step>

  <Step title="Poll">
    GET `/v1/ffmpeg/jobs/:id` until `status` is `finished`.
  </Step>

  <Step title="Download">
    GET `/v1/ffmpeg/jobs/:id/result` to stream the processed file.
  </Step>
</Steps>

## Submit a job

```bash theme={null}
POST /v1/ffmpeg/jobs
Authorization: Bearer $MADIAD_API_KEY
Content-Type: application/json
```

### Request body

| Field              | Type      | Required                        | Description                                                                            |
| ------------------ | --------- | ------------------------------- | -------------------------------------------------------------------------------------- |
| `file_url`         | string    | One of `file_url` / `file_urls` | A single HTTPS source URL                                                              |
| `file_urls`        | string\[] | One of `file_url` / `file_urls` | Multiple HTTPS source URLs (e.g. for mixing tracks)                                    |
| `full_command`     | string    | Yes                             | The complete FFmpeg command string (see [Command rules](#command-rules))               |
| `output_extension` | string    | Yes                             | Container/format for the output (see [Allowed extensions](#allowed-output-extensions)) |

### Response

```json theme={null}
{
  "job_id": "ffj_01HZX9G4P6R8S0T2V4W6X8Y0Z2",
  "status": "queued"
}
```

## Poll status

```bash theme={null}
GET /v1/ffmpeg/jobs/:id
Authorization: Bearer $MADIAD_API_KEY
```

### Response

```json theme={null}
{
  "job_id": "ffj_01HZX9G4P6R8S0T2V4W6X8Y0Z2",
  "status": "processing",
  "duration_sec": null
}
```

`duration_sec` is `null` until the job finishes; it is populated with the actual output duration once complete.

| `status` value | Meaning                                    |
| -------------- | ------------------------------------------ |
| `queued`       | Job is waiting to run                      |
| `processing`   | Job is running                             |
| `finished`     | Output is ready to download                |
| `failed`       | Job failed; retry with a corrected command |

## Download the result

Once `status` is `finished`, stream the output file:

```bash theme={null}
GET /v1/ffmpeg/jobs/:id/result
Authorization: Bearer $MADIAD_API_KEY
```

The response is a binary stream with `Content-Disposition: attachment`. Pipe it to a file or buffer it in memory.

## Command rules

The `full_command` field must satisfy all of the following rules. Requests that fail validation return `400 invalid_request` before any quota is consumed.

| Rule                            | Detail                                                                               |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| Starts with `ffmpeg`            | The command must begin with the literal word `ffmpeg`                                |
| Contains `{input}` placeholder  | Use `{input}` for a single source, or `{input0}`, `{input1}`, … for multiple sources |
| Contains `{output}` placeholder | MADIAD Hub substitutes the output path at run time                                   |
| Maximum length                  | 4,000 characters                                                                     |
| No shell metacharacters         | Characters `;`, `\|`, `&`, `$`, `` ` ``, `<`, `>` are blocked                        |
| No destructive tokens           | `$(`, `rm`, `rmdir`, `mkfs`, `dd` are blocked                                        |

Newlines in the command are converted to spaces automatically, so you can paste multi-line commands directly.

## Allowed output extensions

`output_extension` must be one of the following values (without a leading dot):

| Category | Values                             |
| -------- | ---------------------------------- |
| Video    | `mp4`, `mov`, `webm`, `mkv`, `gif` |
| Audio    | `mp3`, `wav`, `m4a`, `aac`, `ogg`  |
| Image    | `jpg`, `png`                       |

Any other value returns `400 invalid_request`.

## Quota and billing

FFmpeg usage is measured in **FFmpeg minutes**. In v1, every job costs a flat **1 minute**, regardless of the actual output duration. The quota is reserved at submit time and refunded automatically if the job fails to queue.

Monthly limits by plan. The allowance is **account-wide** — it is not multiplied by the number of profiles, and all your brands draw from the same pool:

| Plan           | FFmpeg minutes / month |
| -------------- | ---------------------- |
| No active plan | 0 (not available)      |
| Starter        | 15                     |
| Growth         | 150                    |
| Business       | 500                    |
| Custom         | Unlimited              |

When you exhaust your monthly allowance, the API returns `429 quota_exceeded`. Your allowance resets on the same day of the month your plan was activated (Vietnam time, UTC+7) — not on the 1st. [Usage](/concepts/usage) reports the exact start and renewal date of your current cycle.

## Authentication and rate limits

All FFmpeg endpoints use the same API-key Bearer authentication as the rest of MADIAD Hub — see [Authentication](/authentication).

The rate limit is **120 requests per minute** per account across all FFmpeg endpoints. Exceeding this returns `429` with a `Retry-After` header indicating how long to wait.

## Job ownership

Jobs are private to the account that created them. Polling or downloading a job that belongs to another account returns `404 not_found` — there is no cross-account access.

## Errors

| HTTP status | Code              | When                                                                               |
| ----------- | ----------------- | ---------------------------------------------------------------------------------- |
| `400`       | `invalid_request` | Missing or non-HTTPS URL, command fails validation, unsupported `output_extension` |
| `404`       | `not_found`       | Unknown job ID, or the job belongs to a different account                          |
| `429`       | `quota_exceeded`  | Monthly FFmpeg minutes exhausted                                                   |
| `429`       | `rate_limited`    | More than 120 requests/minute; back off and retry after `Retry-After` seconds      |
| `502`       | `upstream_error`  | Result not available yet, or processing failed                                     |

## Worked example

### 1. Submit: convert an MP4 to a web-optimized WebM

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.madiad.com/v1/ffmpeg/jobs \
    -H "Authorization: Bearer $MADIAD_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "file_url": "https://cdn.example.com/raw/interview.mp4",
          "full_command": "ffmpeg -i {input} -c:v libvpx-vp9 -crf 33 -b:v 0 -c:a libopus {output}",
          "output_extension": "webm"
        }'
  ```

  ```js JavaScript theme={null}
  const res = await fetch("https://api.madiad.com/v1/ffmpeg/jobs", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MADIAD_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      file_url: "https://cdn.example.com/raw/interview.mp4",
      full_command:
        "ffmpeg -i {input} -c:v libvpx-vp9 -crf 33 -b:v 0 -c:a libopus {output}",
      output_extension: "webm",
    }),
  });
  const { job_id, status } = await res.json();
  // { job_id: "ffj_01HZX9G4P6R8S0T2V4W6X8Y0Z2", status: "queued" }
  ```
</CodeGroup>

### 2. Poll until finished

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.madiad.com/v1/ffmpeg/jobs/ffj_01HZX9G4P6R8S0T2V4W6X8Y0Z2 \
    -H "Authorization: Bearer $MADIAD_API_KEY"
  # Keep polling until "status": "finished"
  ```

  ```js JavaScript theme={null}
  let job;
  do {
    await new Promise((r) => setTimeout(r, 3000)); // wait 3 s between polls
    const r = await fetch(
      `https://api.madiad.com/v1/ffmpeg/jobs/${job_id}`,
      { headers: { Authorization: `Bearer ${process.env.MADIAD_API_KEY}` } }
    );
    job = await r.json();
  } while (job.status !== "finished" && job.status !== "failed");
  ```
</CodeGroup>

### 3. Download the result

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.madiad.com/v1/ffmpeg/jobs/ffj_01HZX9G4P6R8S0T2V4W6X8Y0Z2/result \
    -H "Authorization: Bearer $MADIAD_API_KEY" \
    --output interview.webm
  ```

  ```js JavaScript theme={null}
  const dl = await fetch(
    `https://api.madiad.com/v1/ffmpeg/jobs/${job_id}/result`,
    { headers: { Authorization: `Bearer ${process.env.MADIAD_API_KEY}` } }
  );
  const buffer = await dl.arrayBuffer();
  // write buffer to disk or stream it to the client
  ```
</CodeGroup>
