Async Media
Generate images, videos, and audio without blocking your request thread
TheRouter.ai supports async media generation for three modalities: image, video, and audio. Instead of waiting inline (which can take seconds to minutes), you submit a job, receive a job ID immediately, and poll for the result when it is ready.
?async=true flag selectively enables async mode on image and audio endpoints. Video generation is always async.Quick start: async image generation
Add ?async=true to your image generation request to get a 202 Accepted with a job ID instead of waiting for the result.
# Step 1: Submit the job
curl -X POST "https://api.therouter.ai/v1/images/generations?async=true" \
-H "Authorization: Bearer $THEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/gpt-image-2",
"prompt": "a serene mountain lake at sunset, photorealistic"
}'{
"id": "img_01J9Z7QP8BT4EWHF6V3KDMSR99",
"status": "queued",
"model": "openai/gpt-image-2"
}# Step 2: Poll until done
curl https://api.therouter.ai/v1/jobs/img_01J9Z7QP8BT4EWHF6V3KDMSR99 \
-H "Authorization: Bearer $THEROUTER_API_KEY"{
"id": "img_01J9Z7QP8BT4EWHF6V3KDMSR99",
"status": "succeeded",
"result": {
"data": [{ "url": "https://..." }]
},
"expires_at": 1746704490
}Video generation
Video is always async — there is no sync option. Submit to POST /v1/videos and poll using the returned vid_* job ID.
curl -X POST https://api.therouter.ai/v1/videos \
-H "Authorization: Bearer $THEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/sora-2",
"prompt": "a timelapse of clouds over a mountain range",
"duration": 5,
"aspect_ratio": "16:9"
}'Audio TTS (async)
For large TTS workloads or when you need non-blocking behavior, add ?async=true to the speech endpoint. Small TTS requests are usually faster sync.
curl -X POST "https://api.therouter.ai/v1/audio/speech?async=true" \
-H "Authorization: Bearer $THEROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/tts-1",
"input": "The quick brown fox jumps over the lazy dog.",
"voice": "alloy"
}'Job lifecycle and statuses
| Status | Meaning |
|---|---|
| queued | Job accepted, waiting for a worker slot |
| in_progress | Worker is actively processing the request |
| succeeded | Complete — result available, artifact URL valid for 1 hour |
| failed | Processing error — credits refunded automatically |
| cancelled | Cancelled via DELETE — credits refunded automatically |
| expired | Artifact TTL elapsed — job is terminal, result gone |
Polling best practices
async function pollJob(id: string, apiKey: string) {
let delay = 2_000;
const maxDelay = 30_000;
const maxWait = 10 * 60 * 1_000; // 10 minutes
const deadline = Date.now() + maxWait;
while (Date.now() < deadline) {
const res = await fetch(`https://api.therouter.ai/v1/jobs/${id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!res.ok) throw new Error(`Poll failed: ${res.status}`);
const job = await res.json();
if (job.status === 'succeeded') return job.result;
if (['failed', 'cancelled', 'expired'].includes(job.status)) {
throw new Error(job.error?.message ?? `Job ${job.status}`);
}
await new Promise((r) => setTimeout(r, delay));
delay = Math.min(delay * 1.5, maxDelay);
}
throw new Error('Job polling timeout');
}Billing and credit refunds
Credits are reserved at submission time. If the job fails, is cancelled, or expires before completion, the full reserved amount is automatically returned to your balance. You are only charged for successful jobs.
Python example (end-to-end)
import time
import httpx
API_KEY = "your-api-key"
BASE_URL = "https://api.therouter.ai"
HEADERS = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
def submit_image_job(prompt: str) -> str:
r = httpx.post(
f"{BASE_URL}/v1/images/generations?async=true",
headers=HEADERS,
json={"model": "openai/gpt-image-2", "prompt": prompt},
)
r.raise_for_status()
return r.json()["id"]
def poll_job(job_id: str, timeout: int = 300) -> dict:
deadline = time.time() + timeout
delay = 2.0
while time.time() < deadline:
r = httpx.get(f"{BASE_URL}/v1/jobs/{job_id}", headers=HEADERS)
r.raise_for_status()
job = r.json()
if job["status"] == "succeeded":
return job["result"]
if job["status"] in {"failed", "cancelled", "expired"}:
raise RuntimeError(f"Job {job['status']}: {job.get('error', {}).get('message')}")
time.sleep(delay)
delay = min(delay * 1.5, 30)
raise TimeoutError("Job timed out")
# Usage
job_id = submit_image_job("a cat wearing sunglasses on a beach")
result = poll_job(job_id)
image_url = result["data"][0]["url"]
print(f"Image ready: {image_url}")