Webhooks or polling
Encoding is slow enough that you cannot hold an HTTP request open for it. Something has to tell your application that a job finished. There are two ways.
Which one to use
Use webhooks for anything running in production. You configure one URL on
your account and we call it when a job reaches completed or failed. No
timers, no wasted requests, and you hear about a result within seconds of it
existing.
Use polling for scripts, one-off jobs, local development, and anywhere you cannot expose a public HTTPS endpoint.
You can use both. Webhooks are the signal; polling is the reconciliation that catches anything your endpoint missed while it was down.
Polling
Read the job on an interval until its status is terminal. Ten to fifteen seconds is a sensible period; encoding takes minutes, so polling faster only costs you requests.
const TERMINAL = new Set(['completed', 'failed', 'cancelled'])
async function waitForJob(jobId, { intervalMs = 12000, timeoutMs = 45 * 60_000 } = {}) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const res = await fetch(`https://videotranscode.cloud/api/v1/jobs/${jobId}`, {
headers: { Authorization: `Bearer ${process.env.VT_API_KEY}` }
})
if (res.status >= 500) {
await sleep(intervalMs)
continue
}
if (!res.ok) throw new Error(`Job read failed: ${res.status}`)
const job = await res.json()
if (TERMINAL.has(job.status)) return job
await sleep(intervalMs)
}
throw new Error(`Job ${jobId} did not finish within the timeout`)
}
const sleep = ms => new Promise(r => setTimeout(r, ms))Two details worth keeping:
- Give the loop a deadline. A job that never reaches a terminal state should surface as an error in your system, not as a loop that runs forever.
- Treat
5xxas retryable and other failures as fatal. A401will not fix itself by polling again.
Do not use the list endpoint to poll. It signs a download link for every completed job on the page, which is real work on our side and gives you nothing you did not already have from reading the one job you care about.
Webhooks
Configure the URL once in the dashboard. It applies to every job on the account; there is no per-job webhook field, and sending one in the job body has no effect.
A minimal handler:
app.post('/hooks/video', express.json(), (req, res) => {
// Acknowledge first. Slow handlers get retried.
res.status(200).end()
const { jobId, status, outputUrl, errorMessage } = req.body
if (status === 'completed') {
void archiveOutput(jobId, outputUrl)
} else if (status === 'failed') {
void recordFailure(jobId, errorMessage)
}
})Three rules make a webhook handler reliable:
Answer quickly. Return 2xx before doing the work. We wait 30 seconds and
then treat the delivery as failed.
Verify the signature. Set a secret alongside the URL, then check the
X-VideoTranscoder-Signature header before trusting the body. Without that
check, anyone who learns your URL can tell you a job succeeded.
Be idempotent. Deliveries can repeat. Key your processing on jobId and
status so a repeat is a no-op.
The payload, the headers, the signature scheme and the retry policy are all in Webhooks.
What webhooks do not tell you
You are notified for completed and failed only. There is no notification
when a job is accepted, when a worker picks it up, or when you cancel it. If
your interface shows "encoding now", derive it from the job's status when
somebody looks, rather than waiting for an event that never arrives.