Guides
Webhooks or polling

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 5xx as retryable and other failures as fatal. A 401 will 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.