Skip to content

Polling Async Jobs

The synchronous /v2/job endpoint blocks until a job completes, which can be several minutes for video generation. The async API returns immediately with a job ID that you poll for progress and download outputs when ready.

Use async when:

  • Generating video (Veo, Wan, Kling, Seedance) — runs commonly exceed typical HTTP timeouts
  • Running from a serverless platform with short request limits (Vercel, Cloudflare Workers, AWS Lambda)
  • You want to survive client disconnects and resume from a job ID later

This guide shows the full polling flow using Wan 2.2 Lightning text-to-video. The same pattern works for any job type that has "async": true in its permits (see Job Types).

What you'll build — a clip of a puppy running through wildflowers

Terminal window
# Create a project directory.
mkdir prodia-async-polling
cd prodia-async-polling

Install Node (if not already installed):

Terminal window
brew install node
# Close the current terminal and open a new one so that node is available.

Create project skeleton:

Terminal window
# Requires node --version >= 18
# Initialize the project with npm.
npm init -y
# Install the prodia-js library.
npm install prodia --save
Terminal window
# Export your token so it can be used by the main code.
export PRODIA_TOKEN=your-token-here

Your token is exported to an environment variable. If you close or switch your shell you’ll need to run export PRODIA_TOKEN=your-token-here again.

Create a main file for your project:

main.js
const { createProdia } = require("prodia/v2");
const prodia = createProdia({
token: process.env.PRODIA_TOKEN // get it from environment
});

You’re now ready to make some API calls!

The async API uses four endpoints:

  1. POST /v2/job/async — create the job, returns a job ID
  2. GET /v2/job/async/:id/job.state.current — cheap plain-text state check (processing, processed, failed)
  3. GET /v2/job/async/:id/output — list the output filenames once processed
  4. GET /v2/job/async/:id/output/:filename — download an output file

The prodia SDK wraps the synchronous endpoint only, so we use axios directly for the async endpoints.

Terminal window
npm install axios axios-retry --save
main.js
const axios = require('axios');
const axiosRetry = require('axios-retry').default;
const fs = require('node:fs');
async function main() {
axiosRetry(axios, {
retries: 3,
retryCondition: (error) => {
return [413, 429, 503].includes(error.response.status);
},
});
const headers = {
'Authorization': `Bearer ${process.env.PRODIA_TOKEN}`,
};
// 1. Submit the job
let res = await axios({
method: 'POST',
url: 'https://inference.prodia.com/v2/job/async',
headers,
data: {
type: 'inference.wan2-2.lightning.txt2vid.v0',
config: {
prompt: 'a golden retriever puppy running through a field of wildflowers',
},
},
});
const jobId = res.data.id;
let jobStatus = res.data.state.current;
console.log(`Job ID: ${jobId}`);
console.log(`Status: ${jobStatus}`);
// 2. Poll until the job leaves `processing`
while (jobStatus === 'processing') {
await new Promise(resolve => setTimeout(resolve, 2000));
res = await axios({
method: 'GET',
url: `https://inference.prodia.com/v2/job/async/${jobId}/job.state.current`,
headers,
});
jobStatus = res.data;
console.log(`Status: ${jobStatus}`);
}
// 3. On failure, fetch the full job metadata for the error
if (jobStatus !== 'processed') {
res = await axios({
method: 'GET',
url: `https://inference.prodia.com/v2/job/async/${jobId}/job.json`,
headers,
});
console.error(res.data);
process.exit(1);
}
// 4. Download the output file
res = await axios({
method: 'GET',
url: `https://inference.prodia.com/v2/job/async/${jobId}/output/video.mp4`,
headers,
responseType: 'arraybuffer',
});
fs.writeFileSync('puppy.mp4', res.data);
console.log(`Saved puppy.mp4 (${res.data.byteLength} bytes)`);
}
main();
Terminal window
node main.js

Open the output video:

Terminal window
open puppy.mp4

Some job types produce multiple files (for example, a video plus a thumbnail). Call the output-list endpoint first to discover what’s available:

Terminal window
curl -sSf \
-H "Authorization: Bearer $PRODIA_TOKEN" \
"https://inference.prodia.com/v2/job/async/$job_id/output"
# → ["video.mp4"]

Then download each filename with the /output/:filename endpoint.

Keep the polling loop cheap. The job.state.current endpoint returns a tiny plain-text body (processing, processed, or failed), so a 1–2 second interval is fine. Avoid polling job.json in a tight loop — it returns the full job object including state history, which is larger.

A reasonable pattern for long-running jobs:

  • Poll every 1–2 seconds for the first ~30 seconds
  • Back off to 5 seconds after that
  • Give up after a reasonable timeout (e.g. the job type’s ETA × 3) and surface the last status to your caller

When a job fails, the state becomes failed. Fetch job.json for diagnostic detail — the response includes the state history and error messages:

GET /v2/job/async/:id/job.json (failed)
{
"type": "inference.wan2-2.lightning.txt2vid.v0",
"id": "...",
"state": {
"current": "failed",
"history": [
{ "from": "created", "to": "processing", "at": "..." },
{ "from": "processing", "to": "failed", "at": "...", "message": "..." }
]
}
}

Because the job runs server-side, you don’t need to hold the HTTP connection open. Save the id to your database the moment you receive the 201 response — any worker with a valid token can resume polling later, and outputs stay available for an hour after completion.

This makes async a good fit for webhook-style architectures: submit, return the ID to your caller, and poll (or re-enter the polling loop on a scheduled worker) until ready.

CodeWhen
201Job created (POST)
200Poll / download succeeded (GET)
400Invalid config, or job type doesn’t permit async
401Missing or invalid token
404Job not found — either a bad ID, or the result expired (1 hour after completion)
429Too many in-flight async jobs, or no worker capacity right now