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).

Project Setup
Section titled “Project Setup”# Create a project directory.mkdir prodia-async-pollingcd prodia-async-pollingInstall Node (if not already installed):
brew install node# Close the current terminal and open a new one so that node is available.apt install node# Close the current terminal and open a new one so that node is available.winget install -e --id OpenJS.NodeJS.LTS# Close the current terminal and open a new one so that node is available.Create project skeleton:
# Requires node --version >= 18# Initialize the project with npm.npm init -y
# Install the prodia-js library.npm install prodia --saveInstall Python (if not already installed):
brew install python# Close the current terminal and open a new one so that python is available.apt install python3 python3-venv python-is-python3# Close the current terminal and open a new one so that python is available.winget install -e --id Python.Python.3.12# Close the current terminal and open a new one so that python is available.# Requires python --version >= 3.12python -m venv venvsource venv/bin/activatepip install requestsInstall curl (if not already installed):
brew install curl# Close the current terminal and open a new one so that curl is available.apt install curl# Close the current terminal and open a new one so that curl is available.# NOTE: Windows 10 and up have curl installed by default and this can be# skipped.winget install -e --id cURL.cURL# Close the current terminal and open a new one so that curl is available.# Export your token so it can be used by the main code.export PRODIA_TOKEN=your-token-hereYour 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:
const { createProdia } = require("prodia/v2");
const prodia = createProdia({ token: process.env.PRODIA_TOKEN // get it from environment});Create the following main.py
from requests.adapters import HTTPAdapter, Retryimport osimport requestsimport sys
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})set -euo pipefailYou’re now ready to make some API calls!
The polling flow
Section titled “The polling flow”The async API uses four endpoints:
POST /v2/job/async— create the job, returns a job IDGET /v2/job/async/:id/job.state.current— cheap plain-text state check (processing,processed,failed)GET /v2/job/async/:id/output— list the output filenames onceprocessedGET /v2/job/async/:id/output/:filename— download an output file
Submit, poll, and download
Section titled “Submit, poll, and download”The prodia SDK wraps the synchronous endpoint only, so we use axios directly for the async endpoints.
npm install axios axios-retry --saveconst 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();node main.jsfrom requests.adapters import HTTPAdapter, Retryimport osimport requestsimport sysimport time
prodia_token = os.getenv('PRODIA_TOKEN')prodia_url = 'https://inference.prodia.com/v2/job/async'
session = requests.Session()retries = Retry(allowed_methods=None, status_forcelist=Retry.RETRY_AFTER_STATUS_CODES)session.mount('http://', HTTPAdapter(max_retries=retries))session.mount('https://', HTTPAdapter(max_retries=retries))session.headers.update({'Authorization': f"Bearer {prodia_token}"})
# 1. Submit the jobjob_request = { 'type': 'inference.wan2-2.lightning.txt2vid.v0', 'config': { 'prompt': 'a golden retriever puppy running through a field of wildflowers', },}
res = session.post(prodia_url, json=job_request)print(f"Request ID: {res.headers.get('x-request-id')}")print(f"Status: {res.status_code}")
if res.status_code != 201: print(res.text) sys.exit(1)
job = res.json()job_id = job['id']job_status = job['state']['current']print(f"Job ID: {job_id}")print(f"Status: {job_status}")
# 2. Poll until the job leaves `processing`while job_status == 'processing': time.sleep(2) res = session.get(f"{prodia_url}/{job_id}/job.state.current") if res.status_code != 200: print(res.text) sys.exit(1) job_status = res.text print(f"Status: {job_status}")
# 3. On failure, fetch the full job metadata for the errorif job_status != 'processed': res = session.get(f"{prodia_url}/{job_id}/job.json") print(res.json()) sys.exit(1)
# 4. Download the output fileres = session.get(f"{prodia_url}/{job_id}/output/video.mp4")if res.status_code != 200: print(res.text) sys.exit(1)
with open('puppy.mp4', 'wb') as f: f.write(res.content)
print(f'Saved puppy.mp4 ({len(res.content)} bytes)')python main.pyset -euo pipefail
# 1. Submit the jobjob=$(cat <<JOB{ "type": "inference.wan2-2.lightning.txt2vid.v0", "config": { "prompt": "a golden retriever puppy running through a field of wildflowers" }}JOB)
response=$(curl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -H "Accept: application/json" \ --json "$job" \ --retry 3 \ https://inference.prodia.com/v2/job/async)
job_id=$(echo "$response" | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')echo "Job ID: $job_id"
# 2. Poll until the job leaves `processing`while true; do sleep 2 state=$(curl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ "https://inference.prodia.com/v2/job/async/$job_id/job.state.current") echo "Status: $state" [ "$state" = "processing" ] || breakdone
# 3. On failure, fetch the full job metadata for the errorif [ "$state" != "processed" ]; then curl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ "https://inference.prodia.com/v2/job/async/$job_id/job.json" exit 1fi
# 4. Download the output filecurl -sSf \ -H "Authorization: Bearer $PRODIA_TOKEN" \ -o puppy.mp4 \ "https://inference.prodia.com/v2/job/async/$job_id/output/video.mp4"echo "Saved puppy.mp4"bash main.shOpen the output video:
open puppy.mp4xdg-open puppy.mp4start puppy.mp4Listing outputs before downloading
Section titled “Listing outputs before downloading”Some job types produce multiple files (for example, a video plus a thumbnail). Call the output-list endpoint first to discover what’s available:
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.
Polling cadence
Section titled “Polling cadence”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
Handling failures
Section titled “Handling failures”When a job fails, the state becomes failed. Fetch job.json for diagnostic detail — the response includes the state history and error messages:
{ "type": "inference.wan2-2.lightning.txt2vid.v0", "id": "...", "state": { "current": "failed", "history": [ { "from": "created", "to": "processing", "at": "..." }, { "from": "processing", "to": "failed", "at": "...", "message": "..." } ] }}Persisting the job ID
Section titled “Persisting the job ID”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.
Status codes
Section titled “Status codes”| Code | When |
|---|---|
201 | Job created (POST) |
200 | Poll / download succeeded (GET) |
400 | Invalid config, or job type doesn’t permit async |
401 | Missing or invalid token |
404 | Job not found — either a bad ID, or the result expired (1 hour after completion) |
429 | Too many in-flight async jobs, or no worker capacity right now |