Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: FFmpeg Micro Transcodes API
version: 1.0.0
description: 'The FFmpeg Micro API runs FFmpeg for you. Upload a file or point at a URL,
create a transcode or transcription job, poll it, and download the result
from a signed URL.
Getting started:
1. Sign up at https://www.ffmpeg-micro.com and create an API key on the dashboard.
2. Send the key on every request as `Authorization: Bearer `.
3. Create a job with `POST /v1/transcodes` (or `POST /v1/transcribe`), poll
`GET /v1/transcodes/{id}` until `status` is `completed`, then call
`GET /v1/transcodes/{id}/download` for a signed download URL.
Billing is metered per second of input duration. Free accounts start with a
one-time token grant and no card.
This file is the official contract. It is the same spec the API serves at
https://api.ffmpeg-micro.com/openapi.yaml, and the public Postman
workspace is generated from it.'
servers:
- url: https://api.ffmpeg-micro.com
description: Production
tags:
- name: Transcodes
paths:
/v1/transcodes:
post:
summary: Create a transcode or multi-video composition
description: 'Queues a new transcode job with support for single or multiple inputs. Supports simple presets (quality/resolution) or advanced mode (raw FFmpeg options). Billing is based on total input duration (sum of all inputs). Returns a short acknowledgement with `status=pending`; poll `GET /v1/transcodes/{id}` for the full resource.
Quota and plan limits (processing minutes, max input file size) are checked asynchronously after the 201: a job that exceeds them transitions to `failed` with an explanatory `error_message` and `error_category` (`quota` or `file_size`) instead of an HTTP error.'
security:
- bearerApiKey: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateTranscodeRequest'
examples:
singleVideoPreset:
summary: Single video with quality preset (simple mode)
value:
inputs:
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
outputFormat: mp4
preset:
quality: high
resolution: 1080p
multiVideoComposition:
summary: Stitch multiple videos together
value:
inputs:
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
outputFormat: mp4
preset:
quality: medium
advancedOptions:
summary: Advanced mode with raw FFmpeg options
value:
inputs:
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
outputFormat: mp4
options:
- option: -c:v
argument: libx265
- option: -crf
argument: '28'
- option: -preset
argument: slow
textOverlay:
summary: Add text overlay with auto word-wrap
value:
inputs:
- url: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
outputFormat: mp4
options:
- option: '@text-overlay'
argument:
text: I dont know who needs to hear this but building features without talking to users is just guessing
style:
charsPerLine: 18
fontSize: 60
lineSpacing: 15
x: 0.15*w
y: (h-text_h)/2
boxBorderW: 12
responses:
'201':
description: Created
content:
application/json:
schema:
type: object
required:
- id
- status
properties:
id:
type: string
format: uuid
status:
type: string
enum:
- pending
description: New jobs always start as `pending`
inputUrl:
type: string
description: First input URL (echo)
outputFormat:
type: string
downloadToken:
type: string
description: Token for anonymous "Try It" downloads
metadata:
type: object
description: Present only when the request carried `metadata`; echoed as `metadata.user`
properties:
user:
type: object
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'402':
$ref: '#/components/responses/QuotaWall'
'500':
$ref: '#/components/responses/ServerError'
tags:
- Transcodes
operationId: postV1Transcodes
x-operation-id-source: derived
get:
summary: List transcodes (for the authenticated user)
security:
- bearerApiKey: []
parameters:
- in: query
name: status
schema:
$ref: '#/components/schemas/TranscodeStatus'
description: 'Filter by status. The value is not validated against the enum — an unknown value returns an empty result set, not an error.
'
- in: query
name: page
schema:
type: integer
minimum: 1
default: 1
- in: query
name: limit
schema:
type: integer
minimum: 1
maximum: 100
default: 10
description: Values outside 1-100 are clamped; non-numeric values return 400
- in: query
name: order
schema:
type: string
enum:
- created_at.desc
- updated_at.asc
- updated_at.desc
default: created_at.desc
description: 'Result ordering. Use `updated_at.asc` for cursor-based polling so the sort matches the `since` filter (both key on `updated_at`); paging forward with the default `created_at.desc` skips backlog items. Use `updated_at.desc` for a newest-first view on the same cursor column (for example, choosing where a poller starts). Unknown values return 400.
'
- in: query
name: job_type
schema:
type: string
enum:
- transcode
- transcribe
description: 'Restrict to one job type. Without it this endpoint returns BOTH transcode and transcribe jobs (they share the resource shape).
'
- in: query
name: since
schema:
type: string
format: date-time
description: 'Return transcodes last updated at/after this timestamp (filters on `updated_at`, not `created_at` — a job created earlier but completed inside the window is included). Invalid dates are rejected with 400.
'
- in: query
name: until
schema:
type: string
format: date-time
description: Return transcodes last updated at/before this timestamp (filters on `updated_at`)
responses:
'200':
description: OK (total count is in the body `total` field)
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Transcode'
page:
type: integer
limit:
type: integer
total:
type: integer
'400':
description: Invalid page, limit, since, or until parameter
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
$ref: '#/components/responses/Unauthorized'
tags:
- Transcodes
operationId: getV1Transcodes
x-operation-id-source: derived
/v1/transcodes/{id}:
get:
summary: Get a transcode
security:
- bearerApiKey: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Transcode'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
tags:
- Transcodes
operationId: getV1TranscodesById
x-operation-id-source: derived
/v1/transcodes/{id}/cancel:
patch:
summary: Cancel a transcode job
description: Cancels a transcode job that is currently `pending` or `processing`. Completed, failed, or already-canceled jobs cannot be cancelled and return 400. If the job has been handed off to the toolkit service, a best-effort cancel is also sent to the toolkit — failures there are logged but do not fail this request.
security:
- bearerApiKey: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Job cancelled successfully
content:
application/json:
schema:
type: object
required:
- success
- message
- job
properties:
success:
type: boolean
example: true
message:
type: string
example: Job cancelled successfully
job:
$ref: '#/components/schemas/Transcode'
'400':
description: Job is not in a cancellable state
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: 'Cannot cancel job with status: completed'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/ServerError'
tags:
- Transcodes
operationId: patchV1TranscodesByIdCancel
x-operation-id-source: derived
/v1/transcodes/{id}/download:
get:
summary: Get a signed download URL for a completed transcode
description: 'Returns a short-lived (10 minute) signed HTTPS URL for the completed output file.
Because `output_url` on the transcode resource is a `gs://` URL that cannot be fetched directly by HTTP clients, use this endpoint to obtain a fetchable URL. The client is then responsible for downloading the file directly from the returned URL — the gateway does not proxy file bytes.
The job must be in `completed` status and have a non-null `output_url`.'
security:
- bearerApiKey: []
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
responses:
'200':
description: Signed HTTPS URL valid for 10 minutes
content:
application/json:
schema:
type: object
required:
- url
properties:
url:
type: string
format: uri
description: Signed HTTPS URL valid for 10 minutes
example: https://storage.googleapis.com/my-bucket/output.mp4?X-Goog-Algorithm=...
'400':
description: Transcode not completed or has no output
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
notCompleted:
value:
error: Transcode not completed
noOutput:
value:
error: No output available for this transcode
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'500':
$ref: '#/components/responses/ServerError'
tags:
- Transcodes
operationId: getV1TranscodesByIdDownload
x-operation-id-source: derived
components:
responses:
BadRequest:
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
Unauthorized:
description: Missing/invalid API key
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
example:
value:
error: unauthorized
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
ServerError:
description: Unexpected error
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
QuotaWall:
description: 'The account is fully capped: zero plan minutes remaining AND a zero token balance, so no job of any size can run until the plan renews, the plan is upgraded, or tokens are topped up. Returned synchronously instead of a `201` — no processing is attempted. A job record IS still created (visible in job listings with `status: failed`), and its id is returned so the blocked attempt can be inspected. Automations should treat `402` as terminal for the billing period and STOP retrying: the same request will keep returning `402` until the account''s quota state changes. Partially-capped accounts (some minutes or tokens remaining) still return `201`, with any quota failure reported asynchronously on the job''s `status`/`error_message`.
'
content:
application/json:
schema:
type: object
required:
- error
- job_id
- upgrade_url
properties:
error:
type: string
description: Human-readable refusal naming the remedy
job_id:
type: string
format: uuid
description: The failed job row recording this blocked attempt
upgrade_url:
type: string
description: Deep link to billing, carrying `job=<job_id>`
examples:
example:
value:
error: 'Quota exceeded: Video Processing Minutes. Available: 0.00 minutes and your token balance is 0. Upgrade or top up tokens to continue: https://www.ffmpeg-micro.com/dashboard/billing?utm_source=usage-alert&utm_medium=api&utm_campaign=quota-api-block&utm_content=error-message&job=6a1f0c9e-6a56-4a1a-9d2f-0d5d0f7b1c2d'
job_id: 6a1f0c9e-6a56-4a1a-9d2f-0d5d0f7b1c2d
upgrade_url: https://www.ffmpeg-micro.com/dashboard/billing?utm_source=usage-alert&utm_medium=api&utm_campaign=quota-api-block&utm_content=error-message&job=6a1f0c9e-6a56-4a1a-9d2f-0d5d0f7b1c2d
schemas:
CreateTranscodeRequest:
type: object
required:
- inputs
- outputFormat
properties:
inputs:
type: array
description: Array of input videos (single for transcode, multiple for composition)
minItems: 1
maxItems: 10
items:
type: object
required:
- url
properties:
url:
type: string
description: 'GCS URL (gs://bucket/object) from presigned upload, or public HTTP(S) URL. Prefer gs:// to avoid egress charges.
'
example: https://cdn.pixabay.com/video/2021/05/22/74779-554220620_large.mp4
outputFormat:
type: string
enum:
- mp4
- webm
- mov
- avi
- mp3
- m4a
- aac
- wav
- ogg
- opus
- flac
- jpeg
- jpg
- png
- gif
- webp
description: 'Output container format — video (mp4, webm, mov, avi), audio (mp3, m4a, aac, wav, ogg, opus, flac) or image (jpeg/jpg, png, gif, webp). Validation is case/alias-insensitive (`jpg` = `jpeg`), but the value is STORED VERBATIM and reads back exactly as submitted. webm output automatically uses VP9/Opus codecs in preset and default modes; you only need explicit `-c:v`/`-c:a` options when supplying raw options.
'
preset:
type: object
description: Simple mode - quality and resolution presets (ignored if options provided)
properties:
quality:
type: string
enum:
- high
- medium
- low
description: Maps to CRF values (high=18, medium=23, low=28)
resolution:
type: string
enum:
- 480p
- 720p
- 1080p
- 4k
description: Output resolution preset
options:
type: array
description: 'Advanced mode - FFmpeg options or virtual options (overrides preset if both provided).
**Virtual Options** (prefix with `@`): - `@text-overlay`: Add text overlay with auto word-wrap and styling
**FFmpeg Options**: Only allowlisted flags accepted for security.
'
items:
oneOf:
- $ref: '#/components/schemas/VirtualOption'
- $ref: '#/components/schemas/FFmpegOption'
metadata:
type: object
additionalProperties: true
description: 'Optional caller-supplied metadata for cross-referencing jobs with external systems (for example an original filename or a source record ID). Must be a JSON object; serialized size is capped at 8192 bytes. Stored under the `user` key of the job''s `metadata` field, so the submitted value is available at `metadata.user` when fetching or listing jobs, and is echoed as `metadata.user` in the create response.
'
example:
original_filename: clip.mp4
drive_file_id: 1a2b3c
VirtualOption:
type: object
required:
- option
- argument
properties:
option:
type: string
enum:
- '@text-overlay'
description: Virtual option name (prefixed with @)
argument:
oneOf:
- $ref: '#/components/schemas/TextOverlayConfig'
description: Configuration object for the virtual option
TranscodeStatus:
type: string
enum:
- pending
- processing
- completed
- failed
- canceled
description: 'Job lifecycle: `pending` (created, awaiting pickup) → `processing` → `completed` | `failed`; a user cancellation sets `canceled` (US spelling). Note there is no `queued` status — new jobs start as `pending`.
'
ErrorResponse:
type: object
required:
- error
properties:
error:
type: string
details:
type:
- string
- 'null'
FFmpegOption:
type: object
required:
- option
- argument
properties:
option:
type: string
description: FFmpeg flag (e.g., -c:v, -crf, -preset)
example: -c:v
argument:
type: string
description: Flag value
example: libx264
TextOverlayConfig:
type: object
required:
- text
properties:
text:
type: string
description: Text to display (use \n for manual line breaks, or use charsPerLine for auto word-wrap)
example: 'Hello
World'
maxLength: 10000
style:
type: object
properties:
charsPerLine:
type: integer
description: 'Auto word-wrap at this character width (default: no wrap)'
minimum: 5
maximum: 100
example: 18
font:
type: string
description: 'Font name (default: Arial)'
example: Arial
fontSize:
type: integer
minimum: 12
maximum: 200
description: 'Font size in pixels (default: 48)'
example: 60
lineSpacing:
type: integer
description: 'Space between lines in pixels (default: 10)'
minimum: 0
maximum: 100
example: 15
fontColor:
type: string
description: 'Font color (default: white)'
example: white
x:
type: string
description: 'X position FFmpeg expression (default: (w-text_w)/2 for center)'
example: 0.15*w
y:
type: string
description: 'Y position FFmpeg expression (default: (h-text_h)/2 for center)'
example: (h-text_h)/2
boxBorderW:
type: integer
description: 'Box padding/border width in pixels (default: 20)'
minimum: 0
maximum: 100
example: 12
Transcode:
type: object
required:
- id
- status
- output_format
- created_at
properties:
id:
type: string
format: uuid
example: b5f5a9c0-9e33-4e77-8a5b-6a0c2cd9c0b3
status:
$ref: '#/components/schemas/TranscodeStatus'
inputs:
type: array
description: 'Array of processed input metadata, populated at enqueue from the media probe.
'
items:
type: object
properties:
url:
type: string
description: GCS URL of the (possibly re-hosted) input
filename:
type: string
duration:
type: number
description: Seconds; 0 for still images
format:
type: string
size_bytes:
description: 'File size in bytes, as a decimal string the way the media probe reports it (`"991017"`; `"0"` when the probe gave no size). Older jobs may carry an integer. Parse it as a number before doing arithmetic.
'
oneOf:
- type: integer
- type: string
pattern: ^[0-9]+$
downloadUrl:
type: string
description: Short-lived signed HTTPS URL for the input
options:
type: array
items:
type: object
description: Per-input FFmpeg options as submitted
outputs:
type: array
description: Output file records, populated on completion
items:
type: object
properties:
url:
type: string
description: gs:// URL of the output file
filename:
type: string
size_bytes:
type: integer
format:
type: string
output_format:
type: string
description: 'Output format as submitted — the value is stored verbatim, so a job created with `jpg` or `MP4` reads back exactly that way (validation is case/alias-insensitive; canonical values are mp4, webm, mov, avi, mp3, m4a, aac, wav, ogg, opus, flac, jpeg, png, gif, webp). Rows with `job_type: transcribe` carry `srt`.
'
job_type:
type: string
enum:
- transcode
- transcribe
description: 'Discriminator — transcode and transcribe jobs share this resource shape and both appear in `GET /v1/transcodes` listings (there is no job_type filter). Transcribe rows additionally carry `media_url`.
'
billable_minutes:
type: number
description: 'Billable minutes, metered per second on input duration (a 19-second job bills 0.3167). Not an integer.
'
example: 2.25
duration_seconds:
type: number
description: 'While the job is pending/processing: the billable input span (sum of input durations after trim options such as `-ss`/`-t`). Once completed: replaced by the measured duration of the OUTPUT file, which can differ from the input span (e.g. loops run longer). Billing always uses the input-side value.
'
example: 135.0
processing_time_seconds:
type:
- number
- 'null'
description: Wall-clock processing time measured by the toolkit; null until completed
queue_position:
type:
- integer
- 'null'
description: 'Jobs waiting in the job''s toolkit lane at acceptance, this one included: the accepting worker''s queue length right after this job was enqueued, so 1 means it was next in line. Set once at acceptance and never updated: a rough "how far back was I", not a live counter. Once `started_at` is set the job is no longer waiting, whatever this says. Null on jobs accepted before the field existed, or when the toolkit''s accept response carried no queue length.
'
example: 3
lane:
type:
- string
- 'null'
enum:
- short
- long
- heavy
- null
description: 'The processing lane the job runs in. Jobs with many large inputs (more than five 4K frames'' worth of pixels in total) go to `heavy`, a service with more memory. Otherwise, inputs longer than two minutes (by probed length or requested output span) go to `long`; everything else is `short`. Each lane is a separate service, so nothing in one can delay a job in another. Null on jobs accepted before lanes existed.
'
started_at:
type:
- string
- 'null'
format: date-time
description: 'When the toolkit began executing the job. Set as soon as encoding starts, so null means the job is still waiting in its lane and non-null with status `processing` means it is encoding now. If that start signal was missed, it is filled from the completion or failure report. `started_at - created_at` is submission-to-start and includes the gateway''s own download and probe before dispatch, so it is an upper bound on queue wait, not the wait itself.
'
error_message:
type:
- string
- 'null'
description: 'Populated when status is `failed`, and also on user cancellation (`canceled` jobs carry "Job canceled by user") — do not treat non-null error_message alone as job failure.
'
error_category:
type: string
enum:
- processing_limit
- file_size
- quota
- upload
- storage
- validation
- processing
- transient
- unknown
description: 'Machine classification derived from error_message (generated column). Only meaningful when status is `failed`; jobs with no error carry `unknown`, never null. `processing_limit` is reserved for a job stopped at a per-job processing ceiling; no deployed release emits it yet (the ceiling ships separately).
'
completed_at:
type:
- string
- 'null'
format: date-time
description: Set on successful completion only — failed jobs keep this null
metadata:
type: object
description: 'Job metadata object. Caller-supplied metadata from the create request is echoed under `metadata.user` (JSON object, max 8 KB serialized). The object also carries internal keys written by the service (e.g. preset details, request origin, download token for the anonymous "Try It" flow) — treat keys other than `user` as opaque and unstable.
'
properties:
user:
type: object
ffmpeg_options:
type: object
description: Final FFmpeg options used (audit trail)
properties:
source:
type: string
enum:
- preset
- raw_options
- virtual_options
- defaults
description: 'How the options were derived. `raw_options`: the caller''s `options`, sanitized. `virtual_options`: the caller''s `options` included a virtual option (for example `@text-overlay`) that the gateway expanded. `preset`: a named preset. `defaults`: neither options nor a preset was given.
'
options:
type: array
description: The normalized option list handed to FFmpeg, in order.
items:
type: object
required:
- option
properties:
option:
type: string
description: FFmpeg flag (e.g. `-c:v`, `-crf`, `-f`)
example: -c:v
argument:
oneOf:
- type: string
- type: number
description: 'Flag value. Omitted (never null) for flag-only options (`-an`, `-vn`, `-shortest`) and whenever the caller sent null, so a bare flag serializes without one.
'
example: libx264
preset:
type:
- object
- 'null'
output_url:
type:
- string
- 'null'
description: Populated when status = completed
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
user_id:
type: string
description: Internal user identifier resolved from API key
input_url:
type: string
deprecated: true
description: '[DEPRECATED] Use inputs array instead'
securitySchemes:
bearerApiKey:
type: http
scheme: bearer
bearerFormat: opaque-api-key
externalDocs:
description: Guides and examples
url: https://www.ffmpeg-micro.com/docs