# Polling Async Jobs

Source: https://docs.prodia.com/guides/polling-async-jobs/

The synchronous [`/v2/job`](https://docs.prodia.com/reference/inference/) endpoint blocks until a job completes, which can be several minutes for video generation. The [async API](https://docs.prodia.com/reference/async/) 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](https://docs.prodia.com/models/wan-2-2/) text-to-video. The same pattern works for job types that support async execution. See the [async API reference](https://docs.prodia.com/reference/async/) for supported request formats and errors when a job type requires the synchronous API.

![What you'll build — a clip of a puppy running through wildflowers](https://docs.prodia.com/llms/assets/757e83bec300-async-poller-poster.jpg)

### Project Setup

```bash
# Create a project directory.
mkdir prodia-async-polling
cd prodia-async-polling
```

### prodia-js

Install Node (if not already installed):

### macOS

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

### Linux

```bash
apt install node
# Close the current terminal and open a new one so that node is available.
```

### Windows

```bash
winget install -e --id OpenJS.NodeJS.LTS
# Close the current terminal and open a new one so that node is available.
```

Create project skeleton:

```bash
# Requires node --version >= 18
# Initialize the project with npm.
npm init -y

# Install the prodia-js library.
npm install prodia --save
```

### requests

Install Python (if not already installed):

### macOS

```bash
brew install python
# Close the current terminal and open a new one so that python is available.
```

### Linux

```bash
apt install python3 python3-venv python-is-python3
# Close the current terminal and open a new one so that python is available.
```

### Windows

```bash
winget install -e --id Python.Python.3.12
# Close the current terminal and open a new one so that python is available.
```

```bash
# Requires python --version >= 3.12
python -m venv venv
source venv/bin/activate
pip install requests
```

### curl

Install curl (if not already installed):

### macOS

```bash
brew install curl
# Close the current terminal and open a new one so that curl is available.
```

### Linux

```bash
apt install curl
# Close the current terminal and open a new one so that curl is available.
```

### Windows

```bash
# 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.
```

```bash
# Export your token so it can be used by the main code.
export PRODIA_TOKEN=your-token-here
```

> note:
>
> 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:

### prodia-js

```js title="main.js"
const { createProdia } = require("prodia/v2");

const prodia = createProdia({
    token: process.env.PRODIA_TOKEN // get it from environment
});
```

### requests

Create the following `main.py`

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
import os
import requests
import 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}"})
```

### curl

```bash title="main.sh"
set -euo pipefail
```

You're now ready to make some API calls!

### The polling flow

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

> note:
>
> Async jobs use the state `processed` (not `completed`) when successful. Outputs expire **1 hour** after the job finishes, so download promptly.

### Submit, poll, and download

### prodia-js

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

```bash
npm install axios axios-retry --save
```

```javascript title="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();
```

```bash
node main.js
```

### requests

```python title="main.py"
from requests.adapters import HTTPAdapter, Retry
import os
import requests
import sys
import 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 job
job_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 error
if job_status != 'processed':
    res = session.get(f"{prodia_url}/{job_id}/job.json")
    print(res.json())
    sys.exit(1)

# 4. Download the output file
res = 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)')
```

```bash
python main.py
```

### curl

```bash title="main.sh"
set -euo pipefail

# 1. Submit the job
job=$(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" ] || break
done

# 3. On failure, fetch the full job metadata for the error
if [ "$state" != "processed" ]; then
  curl -sSf \
    -H "Authorization: Bearer $PRODIA_TOKEN" \
    "https://inference.prodia.com/v2/job/async/$job_id/job.json"
  exit 1
fi

# 4. Download the output file
curl -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
bash main.sh
```

Open the output video:

### macOS

```bash
open puppy.mp4
```

### Linux

```bash
xdg-open puppy.mp4
```

### Windows

```bash
start puppy.mp4
```

### 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:

```bash
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

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

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

```json title="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": "..." }
    ]
  }
}
```

### 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

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

### Related

[Async API Reference](https://docs.prodia.com/reference/async/) — Full endpoint reference for the async API.

[Generating Videos](https://docs.prodia.com/guides/generating-videos/) — Start here for video generation using the synchronous API.
