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.

Sync endpoints (chat completions, embeddings) are unaffected. The ?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.

bash
# 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"
  }'
json
{
  "id": "img_01J9Z7QP8BT4EWHF6V3KDMSR99",
  "status": "queued",
  "model": "openai/gpt-image-2"
}
bash
# Step 2: Poll until done
curl https://api.therouter.ai/v1/jobs/img_01J9Z7QP8BT4EWHF6V3KDMSR99 \
  -H "Authorization: Bearer $THEROUTER_API_KEY"
json
{
  "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.

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

bash
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

StatusMeaning
queuedJob accepted, waiting for a worker slot
in_progressWorker is actively processing the request
succeededComplete — result available, artifact URL valid for 1 hour
failedProcessing error — credits refunded automatically
cancelledCancelled via DELETE — credits refunded automatically
expiredArtifact TTL elapsed — job is terminal, result gone

Polling best practices

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

Use the Jobs tab in the dashboard to view all your async jobs, filter by modality and status, and inspect lifecycle details and costs.

Python example (end-to-end)

python
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}")