Create a Job and Poll for Results
The core lifecycle: submit a transcode job, poll until it completes, and fetch a signed download URL for the output.
Follow along with a live API key. 100 free minutes, no credit card.
Get free API keyProcessing is asynchronous
Transcoding takes time, so FFmpeg Micro doesn't make you wait on a single request. You submit a job and get back an id immediately, then you poll that job until it finishes. Once it's completed, you ask for a signed download URL and pull the file. Three steps: create, poll, download.
Step 1: Create a job
POST /v1/transcodes with the inputs you want to process and the output format. The inputs array holds 1 to 10 items, each a { url }. That url is either a gs:// URL from the upload flow in Lesson 2 or a public http(s) URL. outputFormat is required; preset is optional.
const res = await fetch('https://api.ffmpeg-micro.com/v1/transcodes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ url: 'gs://your-bucket/input.mp4' }],
outputFormat: 'mp4',
preset: { quality: 'medium', resolution: '720p' },
}),
})
const job = await res.json()
// { id: '9193c4b4-a12d-4e35-8df0-46847a552b7b', status: 'pending', ... }
console.log(job.id)preset.quality is one of high, medium, or low, and preset.resolution is one of 480p, 720p, 1080p, or 4k. Save the returned id. Everything else keys off it.
Step 2: Poll for the result
GET /v1/transcodes/<id> returns the current state of the job. The response fields are snake_case. A freshly created job reports pending first, then moves through queued and processing to either completed or failed. Keep requesting on an interval until it settles.
const res = await fetch(`https://api.ffmpeg-micro.com/v1/transcodes/${job.id}`, {
headers: { Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}` },
})
const status = await res.json()
// {
// id: '...',
// status: 'completed',
// output_url: 'gs://ffmpeg-micro-user-...-output/<id>.mp4',
// output_format: 'mp4',
// duration_seconds: 5.02,
// processing_time_seconds: 1,
// error_message: null,
// created_at: '...',
// completed_at: '...',
// }Note that output_url is a gs:// path. It is not directly downloadable. It's the storage location of the result. To actually fetch the bytes, you use the download endpoint next. If the status comes back failed, the reason is in error_message.
Step 3: Get a download URL
GET /v1/transcodes/<id>/download returns a signed https URL you can fetch directly. That URL expires after 10 minutes, so request it right before you download and don't cache it. If it expires, just call /download again for a fresh one.
const res = await fetch(`https://api.ffmpeg-micro.com/v1/transcodes/${job.id}/download`, {
headers: { Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}` },
})
const { url } = await res.json()
// url: 'https://storage.googleapis.com/...?X-Goog-Signature=...'Three reusable helpers
Wrap each step in a small function so the rest of your code stays readable. waitForJob polls on an interval, returns the job when it's completed, throws on failed, and throws if it never settles within maxAttempts.
// client.mjs
const API_URL = 'https://api.ffmpeg-micro.com'
const API_KEY = process.env.FFMPEG_MICRO_API_KEY
const authHeaders = {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
}
// Create a transcode job
export async function createTranscode(body) {
const res = await fetch(`${API_URL}/v1/transcodes`, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify(body),
})
const json = await res.json()
if (!res.ok) throw new Error(`transcode ${res.status}: ${JSON.stringify(json)}`)
return json
}
// Poll until the job is completed or failed
export async function waitForJob(jobId, { intervalMs = 3000, maxAttempts = 100 } = {}) {
for (let i = 0; i < maxAttempts; i++) {
const res = await fetch(`${API_URL}/v1/transcodes/${jobId}`, {
headers: { Authorization: `Bearer ${API_KEY}` },
})
const job = await res.json()
if (job.status === 'completed') return job
if (job.status === 'failed') throw new Error(`Job ${jobId} failed: ${job.error_message || 'unknown'}`)
await new Promise((r) => setTimeout(r, intervalMs))
}
throw new Error(`Job ${jobId} timed out`)
}
// Get a signed download URL for a completed job
export async function getDownloadUrl(jobId) {
const res = await fetch(`${API_URL}/v1/transcodes/${jobId}/download`, {
headers: { Authorization: `Bearer ${API_KEY}` },
})
const json = await res.json()
if (!res.ok) throw new Error(`download ${res.status}: ${JSON.stringify(json)}`)
return json.url
}The full flow
Now put the three helpers together: create a job from a gs:// input, wait for it, fetch the download URL, and write the file to disk. This is a complete, runnable script on Node 18+.
// full-flow.mjs
import { writeFileSync } from 'node:fs'
import { createTranscode, waitForJob, getDownloadUrl } from './client.mjs'
const job = await createTranscode({
inputs: [{ url: 'gs://your-bucket/input.mp4' }],
outputFormat: 'mp4',
preset: { quality: 'medium', resolution: '720p' },
})
console.log('1. job created ->', { id: job.id, status: job.status })
const done = await waitForJob(job.id)
console.log('2. completed ->', { status: done.status, output_url: done.output_url })
// Fetch the signed URL right before downloading. It expires in 10 minutes
const url = await getDownloadUrl(job.id)
const out = await fetch(url)
const buf = Buffer.from(await out.arrayBuffer())
writeFileSync('out-720p.mp4', buf)
console.log('3. saved out-720p.mp4', buf.length, 'bytes')Run it:
node --env-file=.env full-flow.mjsWhat could go wrong: A long video can outlast a fixed poll timeout. If waitForJob throws "timed out" while the job is still processing, raise maxAttempts (or intervalMs) rather than assuming the job died. A failed status carries the reason in error_message (a bad input URL, an unsupported option). Read it before retrying. And the download URL expires 10 minutes after you request it, so fetch it right before downloading; don't store it and reuse it later.
Key takeaways
- The lifecycle is
create, thenpoll, thendownload. - The poll
GETreturnsoutput_url, but it's ags://path, so you download via the/downloadendpoint, which returns a signedhttpsURL. - Statuses move through
queued,processing,completed, andfailed(a fresh job reportspendingfirst). - Download URLs expire after 10 minutes, so request one right before you fetch the file.
Next up: change outputFormat to convert between MP4, WebM, and MOV, plus the codec gotcha that trips up WebM jobs.
Build this into your app. No FFmpeg install
One call kicks off a job. No local FFmpeg, no servers, no worker queue to run.
const res = await fetch('https://api.ffmpeg-micro.com/v1/transcodes', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FFMPEG_MICRO_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: [{ url: 'gs://your-bucket/input.mp4' }],
outputFormat: 'mp4',
preset: { quality: 'medium', resolution: '1080p' },
}),
})
const job = await res.json() // { id, status: 'pending' }