{
  "openapi": "3.0.3",
  "info": {
    "title": "FFmpeGo API",
    "version": "2.0.0",
    "description": "FFmpeGo runs FFmpeg jobs in the cloud over a single HTTP request. Send public file URLs (or base64 bytes) and the FFmpeg arguments you already use, and get the encoded file back — or have it PUT directly to a signed storage URL.\n\n**Authentication** — every request needs `Authorization: Bearer ffc_live_...`. Create and revoke keys in the dashboard at https://ffmpego.com/dashboard/keys.\n\n**Billing** — successful 2xx responses include `X-Compute-Seconds`, the wall-clock time FFmpeg ran, with a minimum of 1 second. Failed jobs are never billed. Monthly plan allowances and pay-as-you-go credits are checked before the job starts; exhausted accounts receive `402`.\n\n**Inputs** — `inputs[i]` maps to FFmpeg input `i` (`[0:v]`, `-map 1:a`, …). Arguments must not contain `-i`; inputs are prepended automatically in array order. Placeholders such as `{input0}` are not supported on these endpoints.\n\nFull documentation, examples, and error codes: https://ffmpego.com/docs",
    "termsOfService": "https://ffmpego.com/terms",
    "contact": {
      "name": "FFmpeGo",
      "url": "https://ffmpego.com/docs"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://ffmpego.com/terms"
    }
  },
  "externalDocs": {
    "description": "FFmpeg API reference, examples, and error codes",
    "url": "https://ffmpego.com/docs"
  },
  "servers": [
    {
      "url": "https://ffmpego.com",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Encode",
      "description": "Run arbitrary FFmpeg commands with one or more inputs."
    },
    {
      "name": "YouTube",
      "description": "Download a time range of a YouTube video and encode the clip."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v2/run": {
      "post": {
        "tags": [
          "Encode"
        ],
        "operationId": "runFFmpeg",
        "summary": "Run FFmpeg",
        "description": "Runs FFmpeg with the supplied arguments and returns the encoded file, or uploads it to a signed URL when `upload` is present.\n\nInputs are downloaded (or decoded from base64) before FFmpeg starts, so `inputs[i]` lines up with FFmpeg input `i`. Jobs are bounded by the runtime cap; long encodes should be sent with a generous client timeout.\n\nOnly successful responses are billed. `X-Compute-Seconds` is present on 200 responses.\n\n**Response body** — by default the response is the encoded file with a `Content-Type` inferred from `outputFilename` (or set explicitly with `outputContentType`). When `upload` is supplied, the response is a JSON receipt instead.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RunRequest"
              },
              "examples": {
                "transcode": {
                  "summary": "Transcode to MP4",
                  "value": {
                    "inputs": [
                      {
                        "url": "https://cdn.pixabay.com/video/2025/11/07/314643_tiny.mp4"
                      }
                    ],
                    "args": [
                      "-c:v",
                      "libx264",
                      "-c:a",
                      "aac",
                      "-f",
                      "mp4"
                    ],
                    "outputFilename": "out.mp4",
                    "outputContentType": "video/mp4"
                  }
                },
                "videoToGif": {
                  "summary": "Video to GIF",
                  "value": {
                    "inputs": [
                      {
                        "url": "https://cdn.pixabay.com/video/2025/11/07/314643_tiny.mp4"
                      }
                    ],
                    "args": [
                      "-vf",
                      "fps=10,scale=320:-1:flags=lanczos",
                      "-c:v",
                      "gif"
                    ],
                    "outputFilename": "output.gif"
                  }
                },
                "concat": {
                  "summary": "Concatenate two inputs with filter_complex",
                  "value": {
                    "inputs": [
                      {
                        "url": "https://cdn.pixabay.com/video/2025/06/24/287510_tiny.mp4",
                        "filename": "video1.mp4"
                      },
                      {
                        "url": "https://cdn.pixabay.com/video/2025/11/07/314643_tiny.mp4",
                        "filename": "video2.mp4"
                      }
                    ],
                    "args": [
                      "-filter_complex",
                      "[0:v][1:v]concat=n=2:v=1:a=0[outv]",
                      "-map",
                      "[outv]"
                    ],
                    "outputFilename": "concatenated.mp4"
                  }
                },
                "upload": {
                  "summary": "Upload the result to a signed storage URL",
                  "value": {
                    "inputs": [
                      {
                        "url": "https://cdn.pixabay.com/video/2025/11/07/314643_tiny.mp4"
                      }
                    ],
                    "args": [
                      "-c:v",
                      "libx264",
                      "-f",
                      "mp4"
                    ],
                    "outputFilename": "export.mp4",
                    "upload": {
                      "url": "https://your-bucket.example.com/upload/sign/export.mp4?token=...",
                      "method": "PUT",
                      "headers": {
                        "Content-Type": "video/mp4"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The encoded file (default), or a JSON receipt when `upload` was used.",
            "headers": {
              "X-Compute-Seconds": {
                "$ref": "#/components/headers/XComputeSeconds"
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                },
                "example": "<encoded file bytes>"
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadReceipt"
                },
                "example": {
                  "ok": true,
                  "uploaded": true,
                  "bytes": 21375689,
                  "contentType": "video/mp4",
                  "outputFilename": "export.mp4"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "cURL",
            "label": "cURL",
            "source": "curl -X POST https://ffmpego.com/v2/run \\\n  -H \"Authorization: Bearer $FFMPEGO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"inputs\": [{ \"url\": \"https://example.com/video.mp4\" }],\n    \"args\": [\"-c:v\", \"libx264\", \"-c:a\", \"aac\", \"-f\", \"mp4\"],\n    \"outputFilename\": \"out.mp4\"\n  }' \\\n  --output out.mp4"
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript (fetch)",
            "source": "const res = await fetch(\"https://ffmpego.com/v2/run\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.FFMPEGO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    inputs: [{ url: \"https://example.com/video.mp4\" }],\n    args: [\"-c:v\", \"libx264\", \"-c:a\", \"aac\", \"-f\", \"mp4\"],\n    outputFilename: \"out.mp4\",\n  }),\n});\n\nif (!res.ok) throw new Error(await res.text());\nconst bytes = Buffer.from(await res.arrayBuffer());\nconsole.log(\"compute seconds:\", res.headers.get(\"X-Compute-Seconds\"));"
          },
          {
            "lang": "Python",
            "label": "Python (requests)",
            "source": "import os\nimport requests\n\nres = requests.post(\n    \"https://ffmpego.com/v2/run\",\n    headers={\"Authorization\": f\"Bearer {os.environ['FFMPEGO_API_KEY']}\"},\n    json={\n        \"inputs\": [{\"url\": \"https://example.com/video.mp4\"}],\n        \"args\": [\"-c:v\", \"libx264\", \"-c:a\", \"aac\", \"-f\", \"mp4\"],\n        \"outputFilename\": \"out.mp4\",\n    },\n    timeout=600,\n)\nres.raise_for_status()\nopen(\"out.mp4\", \"wb\").write(res.content)\nprint(\"compute seconds:\", res.headers.get(\"X-Compute-Seconds\"))"
          }
        ]
      }
    },
    "/v2/youtube-clip": {
      "post": {
        "tags": [
          "YouTube"
        ],
        "operationId": "clipYouTubeVideo",
        "summary": "Clip a YouTube video",
        "description": "Downloads only the requested time range of a public YouTube video with yt-dlp, then encodes it with the same FFmpeg pipeline as `/v2/run`. The returned file is the clip, or a JSON receipt when `upload` is present.\n\n`X-Compute-Seconds` covers the whole job — YouTube download plus encode. Only successful responses are billed.\n\n`502` means the YouTube download failed (bot check, private or age-restricted video, download timeout) or the signed upload was rejected; those jobs are not billed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/YoutubeClipRequest"
              },
              "examples": {
                "clip": {
                  "summary": "Clip 1:30–2:00 as a 720p MP4",
                  "value": {
                    "url": "https://www.youtube.com/watch?v=VIDEO_ID",
                    "start": "1:30",
                    "end": "2:00",
                    "args": [
                      "-c:v",
                      "libx264",
                      "-preset",
                      "veryfast",
                      "-crf",
                      "24",
                      "-c:a",
                      "aac",
                      "-vf",
                      "scale=-2:720",
                      "-f",
                      "mp4"
                    ],
                    "outputFilename": "clip.mp4"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The encoded clip (default), or a JSON receipt when `upload` was used.",
            "headers": {
              "X-Compute-Seconds": {
                "$ref": "#/components/headers/XComputeSeconds"
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                },
                "example": "<encoded clip bytes>"
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UploadReceipt"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "x-codeSamples": [
          {
            "lang": "cURL",
            "label": "cURL",
            "source": "curl -X POST https://ffmpego.com/v2/youtube-clip \\\n  -H \"Authorization: Bearer $FFMPEGO_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://www.youtube.com/watch?v=VIDEO_ID\",\n    \"start\": \"1:30\",\n    \"end\": \"2:00\",\n    \"outputFilename\": \"clip.mp4\"\n  }' \\\n  --output clip.mp4"
          },
          {
            "lang": "JavaScript",
            "label": "JavaScript (fetch)",
            "source": "const res = await fetch(\"https://ffmpego.com/v2/youtube-clip\", {\n  method: \"POST\",\n  headers: {\n    Authorization: `Bearer ${process.env.FFMPEGO_API_KEY}`,\n    \"Content-Type\": \"application/json\",\n  },\n  body: JSON.stringify({\n    url: \"https://www.youtube.com/watch?v=VIDEO_ID\",\n    start: \"1:30\",\n    end: \"2:00\",\n    outputFilename: \"clip.mp4\",\n  }),\n});\n\nif (!res.ok) throw new Error(await res.text());\nconst bytes = Buffer.from(await res.arrayBuffer());"
          },
          {
            "lang": "Python",
            "label": "Python (requests)",
            "source": "import os\nimport requests\n\nres = requests.post(\n    \"https://ffmpego.com/v2/youtube-clip\",\n    headers={\"Authorization\": f\"Bearer {os.environ['FFMPEGO_API_KEY']}\"},\n    json={\n        \"url\": \"https://www.youtube.com/watch?v=VIDEO_ID\",\n        \"start\": \"1:30\",\n        \"end\": \"2:00\",\n        \"outputFilename\": \"clip.mp4\",\n    },\n    timeout=600,\n)\nres.raise_for_status()\nopen(\"clip.mp4\", \"wb\").write(res.content)"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ffc_live_...",
        "description": "API key from https://ffmpego.com/dashboard/keys. Send it as `Authorization: Bearer ffc_live_...`."
      }
    },
    "headers": {
      "XComputeSeconds": {
        "description": "Wall-clock compute seconds billed for a successful job, rounded up to a minimum of 1. Present on 200 responses only. Failed jobs are not billed.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "example": 3
        }
      }
    },
    "schemas": {
      "RunRequest": {
        "type": "object",
        "required": [
          "inputs",
          "args"
        ],
        "properties": {
          "inputs": {
            "type": "array",
            "minItems": 1,
            "description": "Input files in FFmpeg order. `inputs[i]` is FFmpeg input `i`.",
            "items": {
              "$ref": "#/components/schemas/JobInput"
            }
          },
          "args": {
            "type": "array",
            "minItems": 1,
            "description": "FFmpeg flags and values, in order. Do not include `-i`; inputs are prepended automatically. Placeholders such as `{input0}` and `{output}` are rejected.",
            "items": {
              "type": "string"
            },
            "example": [
              "-c:v",
              "libx264",
              "-preset",
              "veryfast",
              "-crf",
              "23",
              "-c:a",
              "aac",
              "-movflags",
              "+faststart",
              "-f",
              "mp4"
            ]
          },
          "outputFilename": {
            "type": "string",
            "default": "output",
            "description": "Output filename including extension. The extension also determines the response `Content-Type` unless `outputContentType` is set.",
            "example": "out.mp4"
          },
          "outputContentType": {
            "type": "string",
            "description": "Overrides the response `Content-Type`. Ignored when `upload` is used, because the file is not returned in the response.",
            "example": "video/mp4"
          },
          "upload": {
            "$ref": "#/components/schemas/UploadTarget"
          }
        }
      },
      "YoutubeClipRequest": {
        "type": "object",
        "required": [
          "url",
          "start",
          "end"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public YouTube link. Supports `youtube.com`, `www.youtube.com`, `m.youtube.com`, `music.youtube.com`, and `youtu.be`. Playlists, live streams, private, and age-restricted videos fail with `502`.",
            "example": "https://www.youtube.com/watch?v=VIDEO_ID"
          },
          "start": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              }
            ],
            "description": "Start of the clip in seconds (`90`), `MM:SS` (`1:30`), or `HH:MM:SS` (`0:01:30`). Decimals are allowed.",
            "example": "1:30"
          },
          "end": {
            "oneOf": [
              {
                "type": "number"
              },
              {
                "type": "string"
              }
            ],
            "description": "End of the clip. Must be greater than `start`. Only this range is downloaded.",
            "example": "2:00"
          },
          "args": {
            "type": "array",
            "description": "FFmpeg flags for the encode step. Same rules as `RunRequest.args` — no `-i`, no placeholders. Defaults to H.264 + AAC in an MP4 with `+faststart`.",
            "items": {
              "type": "string"
            },
            "default": [
              "-c:v",
              "libx264",
              "-c:a",
              "aac",
              "-movflags",
              "+faststart",
              "-f",
              "mp4"
            ]
          },
          "outputFilename": {
            "type": "string",
            "default": "clip.mp4",
            "description": "Plain filename without path separators.",
            "example": "clip.mp4"
          },
          "outputContentType": {
            "type": "string",
            "description": "Overrides the response `Content-Type`.",
            "example": "video/mp4"
          },
          "upload": {
            "$ref": "#/components/schemas/UploadTarget"
          }
        }
      },
      "JobInput": {
        "type": "object",
        "description": "One input file. Provide `url` or `data`. When both are present, `url` is used.",
        "anyOf": [
          {
            "required": [
              "url"
            ]
          },
          {
            "required": [
              "data"
            ]
          }
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public HTTP(S) URL of the input file. The file is downloaded before FFmpeg runs.",
            "example": "https://cdn.example.com/input.mp4"
          },
          "data": {
            "type": "string",
            "format": "byte",
            "description": "Base64-encoded file bytes. Use this for private or local files that are not publicly reachable. Keep the encoded payload under the request body limit.",
            "example": "AAAAIGZ0eXBpc29tAAACAGlzb21pc28y"
          },
          "filename": {
            "type": "string",
            "description": "Optional filename. The extension helps FFmpeg detect the format; otherwise the extension is taken from `url`.",
            "example": "input.mp4"
          }
        }
      },
      "UploadTarget": {
        "type": "object",
        "description": "After a successful encode, PUT the output to a signed URL instead of returning it in the response. S3 presigned PUT, Supabase signed upload URLs, and similar one-shot URLs work. Resumable and TUS uploads are not supported.",
        "required": [
          "url",
          "headers"
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Signed upload URL, used as-is. Do not rewrite the path.",
            "example": "https://your-bucket.example.com/upload/sign/export.mp4?token=..."
          },
          "method": {
            "type": "string",
            "enum": [
              "PUT"
            ],
            "default": "PUT",
            "description": "Only `PUT` is supported."
          },
          "headers": {
            "type": "object",
            "description": "Headers sent with the upload. Must include `Content-Type`. `Content-Length` is added from the file size.",
            "additionalProperties": {
              "type": "string"
            },
            "example": {
              "Content-Type": "video/mp4"
            }
          }
        }
      },
      "UploadReceipt": {
        "type": "object",
        "description": "Returned as JSON when `upload` is present and the signed PUT succeeded.",
        "required": [
          "ok",
          "uploaded",
          "bytes",
          "contentType",
          "outputFilename"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "example": true
          },
          "uploaded": {
            "type": "boolean",
            "example": true
          },
          "bytes": {
            "type": "integer",
            "description": "Size of the uploaded file in bytes.",
            "example": 21375689
          },
          "contentType": {
            "type": "string",
            "description": "`Content-Type` taken from `upload.headers`.",
            "example": "video/mp4"
          },
          "outputFilename": {
            "type": "string",
            "example": "export.mp4"
          }
        }
      },
      "ApiError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable error message.",
            "example": "Monthly compute limit reached (75/75 seconds) and no credits remaining."
          },
          "code": {
            "type": "string",
            "enum": [
              "quota_exceeded",
              "rate_limited"
            ],
            "description": "Machine-readable code when the gateway, rather than FFmpeg, produced the error."
          },
          "pricingUrl": {
            "type": "string",
            "description": "Relative URL to the pricing page, present on quota errors.",
            "example": "/pricing"
          },
          "creditsUrl": {
            "type": "string",
            "description": "Relative URL to the credits page, present on quota errors.",
            "example": "/dashboard/credits"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Invalid JSON or request body — for example empty `inputs` or `args`, `-i` inside `args`, a placeholder in `args`, an input without `url` or `data`, an out-of-range time, or an invalid `upload` object. Nothing is billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "args must not include -i; all inputs are added automatically in inputs[] order. Use /run for manual -i placement."
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, malformed, revoked, or unknown API key. Nothing is billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": "Missing Authorization: Bearer <api_key>"
                }
              },
              "badFormat": {
                "value": {
                  "error": "Invalid API key format"
                }
              },
              "revoked": {
                "value": {
                  "error": "Invalid or revoked API key"
                }
              }
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "The monthly allowance is exhausted and no credits remain. Buy credits or enable auto top-up, then retry. Nothing is billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Monthly compute limit reached (75/75 seconds) and no credits remaining.",
              "code": "quota_exceeded",
              "creditsUrl": "/dashboard/credits"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Per-plan rate limit exceeded. Retry after the current window resets. Nothing is billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Rate limit exceeded",
              "code": "rate_limited"
            }
          }
        }
      },
      "ServerError": {
        "description": "FFmpeg failed, the output was missing, or the job could not run. The response includes FFmpeg's error message. Failed jobs are not billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "FFmpeg failed [runv2:1234567890]: ..."
            }
          }
        }
      },
      "BadGateway": {
        "description": "On `/v2/run`: the encode succeeded but the signed upload URL rejected the file. On `/v2/youtube-clip`: the YouTube download failed (bot check, private or age-restricted video, timeout) or the upload was rejected. Not billed.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ApiError"
            },
            "example": {
              "error": "Upload failed: HTTP 403 Forbidden"
            }
          }
        }
      },
      "ServiceUnavailable": {
        "description": "No encoder capacity is available. Retry the job later. Nothing is billed.",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string"
            },
            "example": "No container instance available at this time (all 3 instances busy for 600s)"
          }
        }
      }
    }
  }
}
