Cloudinary video transformation alternative: one job, not a URL

Your code doesn't call a video API today. It builds a string, something like /video/upload/w_1080,ar_9:16,c_fill,so_3,eo_18/clip.mp4, interpolated in maybe thirty places across a Blade template and an n8n Set node.
A Cloudinary video transformation alternative that hands the file back is a job API, and the swap is mechanical: each parameter in that path becomes a field in one POST, you poll a job id until it reads completed, and you download the MP4. To replace a Cloudinary video URL with FFmpeg Micro, you send inputs, outputFormat and an options array to POST https://api.ffmpeg-micro.com/v1/transcodes. Three classes of parameter don't port at all, and they're the ones no migration guide mentions.
From a URL path to a job id
The transformation you're migrating lives in the delivery path itself, between the resource type and the public id. So the work is an edit to the code that concatenates strings, not a change of base URL.
The job side is four requests. Ask for a presigned upload URL with POST /v1/upload/presigned-url, PUT the bytes to that URL, confirm with POST /v1/upload/confirm (both fileSize fields are JSON numbers, and a quoted "12345678" comes back as a 400), then submit the work. If the file already sits somewhere fetchable, skip the first three and pass its URL straight into inputs.
The vertical 9:16 trim from the intro, as one request:
curl -X POST https://api.ffmpeg-micro.com/v1/transcodes \
-H "Authorization: Bearer $FM_KEY" \
-H "Content-Type: application/json" \
-d '{
"inputs": [{ "url": "gs://your-bucket/1763000000-clip.mp4" }],
"outputFormat": "mp4",
"options": [
{ "option": "-ss", "argument": "3" },
{ "option": "-to", "argument": "18" },
{ "option": "-vf", "argument": "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920" },
{ "option": "-c:v", "argument": "libx264" },
{ "option": "-crf", "argument": "23" }
]
}'
That returns {"id": "...", "status": "pending", ...} immediately. Poll GET /v1/transcodes/:id until status is completed or failed, then call GET /v1/transcodes/:id/download for a signed URL. Two timings matter: the download URL expires after 10 minutes, so request a fresh one, and uploaded input links stay valid for 24 hours, so a job that waits in the queue still runs. Inputs over two minutes go to a separate long lane, so a 40-minute webinar encode can't sit in front of a 12-second clip.
That's the step FFmpeg Micro exists to be: one call, no encoder to install, no worker pool, and reachable from n8n and Make, where Cloudinary's video transformations never were. More than 300 builders already run it from one of those two tools. The full endpoint list is in the docs.
The parameter map: what replaces each Cloudinary video transformation
Most of what you have in those strings is ordinary FFmpeg work wearing a short name, so it ports one-to-one. Read the right-hand column as entries in the options array above, each one an { "option": ..., "argument": ... } pair.
| In your Cloudinary URL | What you're asking for | In the job request |
|---|---|---|
| `w_720,c_scale` | plain resize | `-vf scale=720:-2` |
| `w_1080,h_1920,c_fill` | fill the box, crop the overflow | `-vf scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920` |
| `ar_9:16,c_fill` | reframe to vertical | same scale plus crop at the 1080x1920 box |
| `w_1080,h_1920,c_pad` | letterbox, keep the whole frame | `-vf scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black` |
| `so_3,eo_18` | in point and out point | `-ss 3 -to 18` |
| `so_3,du_15` | in point plus a length | `-ss 3 -t 15` |
| `e_volume:-50` | halve the audio | `-af volume=0.5` |
| `e_volume:mute` | drop the audio track | `-an` |
| `vc_h264` | H.264 video | `-c:v libx264` |
| `vc_h264:baseline:3.1` | codec with profile and level | `-c:v libx264 -profile:v baseline -level 3.1` |
| `q_auto` | "pick a sane quality" | `-crf 23`, or `"preset": { "quality": "medium", "resolution": "1080p" }` |
| `f_auto` | format negotiated per browser | no equivalent: choose `outputFormat`, and run a second job if you need WebM too |
Text layers are the one row that changes shape rather than syntax. A l_text: component with its own fl_layer_apply and gravity becomes one structured option, because -filter_complex is refused by the API:
{ "option": "@text-overlay", "argument": {
"text": "3 things nobody tells you",
"style": { "fontSize": 60, "x": "0.15*w", "y": "(h-text_h)/2" }
} }
Logo overlays and stacked layers go the same route: @text-overlay, a -vf chain, or the filters array, never a hand-written filter_complex graph. Concatenation is the other structural change. inputs is an array, so a spliced clip stops being a layer parameter on one asset and becomes a second entry in that array. The JSON shape for multi-clip work is walked through in FFmpeg JSON video composition shipped. Not in your build..
In Python the whole thing is the four requests plus a sleep:
import time, requests
H = {"Authorization": f"Bearer {KEY}"}
BASE = "https://api.ffmpeg-micro.com/v1/transcodes"
job = requests.post(BASE, headers=H, json={
"inputs": [{"url": file_url}],
"outputFormat": "mp4",
"options": [
{"option": "-ss", "argument": "3"},
{"option": "-to", "argument": "18"},
{"option": "-vf", "argument": "scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920"},
],
}).json()
while True:
status = requests.get(f"{BASE}/{job['id']}", headers=H).json()
if status["status"] in ("completed", "failed"):
break
time.sleep(3)
if status["status"] == "completed":
out = requests.get(f"{BASE}/{job['id']}/download", headers=H).json()["url"]
The two official SDKs wrap these same calls: pip install ffmpeg-micro for Python 3.8+ and composer require ffmpeg-micro/ffmpeg-micro-php for PHP 8.1+, source under github.com/ffmpeg-micro. PHP shops replacing a transformation builder in a Laravel app will recognize the queue shape from PHP-FFMpeg vs laravel-ffmpeg vs exec(): which one breaks where.
Three kinds of parameter that have no job-API equivalent
Everything above ports. Three things in the model you're leaving don't, and pretending otherwise is how migrations get abandoned in week two.
First, generation at first request. In a URL-transform product the derived file is produced when someone first asks for that URL, then cached for every request after. A job API has no first viewer. You decide when to render and you store the result, so you own the cache that used to be free. Serve a repeat request from your own bucket or CDN, because re-submitting the job is billed again.
Second, named and eager transformation definitions. A saved preset you reference by name in a path, and pre-generation at upload time, are both features of the thing holding your library. The name becomes your own code: a function or an n8n sub-workflow that returns the options array, versioned in your repo instead of a vendor dashboard. Eager becomes "submit the job when the upload lands."
Third, anything that assumes the asset already lives there. Public-id addressing, folder and tag scoped operations, signed delivery URLs, the player, the asset manager. FFmpeg Micro takes an input, processes it, and hands back a file. It isn't a library and won't pretend to be one. If your whole workload is one master delivered as many variants to many viewers on demand, a URL-transform CDN with a derived-asset cache is the right shape and a job API is the wrong one. Which products do which job is sorted in Cloudinary alternative for video: which ones hand the file back.
One more client-side habit doesn't survive: there is no completion webhook. A webhookUrl on a request is ignored, so the poll loop is the contract.
Mistakes people make in this migration, and how to avoid each
The failures are predictable, and four of the five are habits carried over from URL transforms rather than problems with the API.
- Keeping the string builder. People wrap the job in a function that still returns a URL-shaped recipe, then discover they've rebuilt the old coupling with extra latency. Delete the builder. Store a job id and an output URL on the row that used to hold a public id.
- Rendering inside a web request. A transform used to happen behind a CDN edge, so the handler never waited. Submit the job from a queued worker and poll there. In n8n, guard the trigger too: a workflow that fires twice renders twice and bills twice, the exact failure in Your n8n Workflow Runs Twice and Renders Every Video Twice.
- Forgetting that the derived-asset cache is gone. Key your stored outputs on input plus options hash before you submit. Without that you'll re-encode the same 9:16 crop every time a page is rebuilt.
- Treating
failedas a retry. Poll for the terminal state and read the reason before resubmitting, because a corrupt input or a malformed option fails identically on attempt two. Why your transcode job failed (and what to do in the poll loop) covers the branches worth writing. - Migrating the images too. Nothing requires it. Leave image transforms and the library where they are and replace only the video step.
One input limit to plan around: on-the-fly video transforms are capped by file size in the product you're leaving, roughly 40 MB on a free plan and 100 MB on paid ones (estimates, so check your own plan), which is why anything long already runs through a pre-generation path. FFmpeg Micro's caps are per plan too: 100 MB on Free, 1,024 MB on Starter, 2,048 MB on Pro.
What changes in the bill
The unit changes from transformations counted against the delivered video to seconds of input processed, and that flips which workloads are cheap. One FFmpeg Micro token buys 10 seconds of input video, metered per second with no rounding up to the minute. Trim 30 seconds out of a one-hour recording and you're billed for 30 seconds, not the hour. Failed jobs are never billed, and checking status, listing jobs and downloading outputs are free.
Work a real month: 500 one-minute clips is 30,000 input seconds, so 3,000 tokens, which is Starter at $19.99 for 2,500 tokens plus a 500-token top-up at $5. Call it $24.99, with purchased tokens that don't expire while the account is open. On the credit model you're leaving, HD seconds bill at four transformations each and a credit covers roughly 250 HD seconds, so those same 30,000 seconds land near 120 credits. That arithmetic is this blog's reading of the published meter, not a quote, and it's walked through in Cloudinary pricing in 2026: what a credit actually buys for video.
Be honest about the direction of the trade. A cached derived asset is paid for once no matter how many people watch it, which beats per-input-second billing for a catalog watched repeatedly. Per-input-second wins where each output is produced once and consumed downstream: clips cut for scheduling, UGC normalized to a house spec, ads built in three variants.
FAQ about replacing Cloudinary video transformations
Can I migrate only the video transformations and leave my images and library alone?
Yes, and that's the cheapest version of this migration. FFmpeg Micro processes an input and returns the output file, so you can keep images, storage and delivery where they are and point only the video step at a job request.
Is there a callback when the job finishes?
No. FFmpeg Micro has no completion webhooks for callers, and a webhookUrl on a request is ignored, so the poll loop against GET /v1/transcodes/:id is the only completion signal. In practice that's a three-second sleep in a worker or a Wait node in n8n.
What replaces f_auto and q_auto when nobody negotiates format per browser?
Format and quality become explicit job fields instead of delivery-time decisions: outputFormat plus either -crf 23 or "preset": { "quality": "medium" }. If you need both MP4 and WebM, that's two jobs, and you're billed for the input seconds twice.
Can I run these jobs from n8n or Make instead of writing code?
Yes. The presigned upload, the PUT, the confirm and the transcode all work from n8n, Make and Zapier, over the community node or plain HTTP requests. More than 300 builders have run FFmpeg Micro from Make or n8n, and the n8n community node has passed 1,800 downloads.
Mid-migration, with half your transformation strings ported and a du_ branch still commented out, what you need is a working key in the next five minutes. The quickstart runs it for you after sign-up: a temporary key, the presigned upload, the transcode, the poll and the download, every request and response shown. Sign up free gets you 200 tokens, about 33 minutes of video and no card, callable from your code, from n8n, Make and Zapier, or from your AI agents.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Cloudinary alternative for video: which ones hand the file back
Most Cloudinary alternative lists rank image CDNs and hosting platforms. Sort the field into four kinds and see which ones hand the output file back to you.

Cloudinary pricing in 2026: what a credit actually buys for video
Cloudinary pricing in 2026, priced in videos instead of credits: the live plans, what a credit buys, and one 90-second 1080p job through all three meters.

PHP-FFMpeg vs laravel-ffmpeg vs exec(): which one breaks where
php-ffmpeg vs laravel-ffmpeg vs exec(): the error each throws, where each breaks (disable_functions, 504s, missing binaries), and when to use an API instead.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free