Image Generation & Edit (Async)

Generate or edit images with TheRouter's async media pipeline. Submit a job, get back a polling_url, and poll until the image is ready in S3.

Why async? Image models take 30–180 seconds. That is longer than the 100s sync deadline at our edge, and longer than most reverse proxies allow. Submitting a job + polling is the only reliable path for production. Sync mode exists but should only be used for quick tests on fast models.

Supported Models

Image-output models on TheRouter (see the full list at /models):

Every model in the list supports POST /v1/images/generations. Models whose input_modalities include image also support POST /v1/images/edits.

Flow Overview

Client ─POST /v1/images/generations?async=true─▶ Gateway
                                                  │ (returns immediately)
Client ◀─── 202 + { id, polling_url } ─────────── Gateway
   │
   │ every 5–10 s
   ▼
Client ─GET /v1/jobs/:id ─▶ Gateway ─▶ status JSON
                                          │
   queued / in_progress ───────────────────┤ keep polling
   succeeded ──────────────────────────────┤ download
   failed / cancelled / expired ───────────┘ stop

# Download (two options):
GET unsigned_urls[0]                       # pre-signed S3 URL (7d TTL)
GET /v1/jobs/:id/content                   # gateway 302 to S3

Submit a Generation Job

cURL

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 minimal red apple on a white marble table, soft natural light",
    "n": 1,
    "size": "1024x1024",
    "quality": "high"
  }'

Response (HTTP 202):

{
  "id": "img_uxRZK4KwxkWaaHXRfwP0fWUDST",
  "object": "image_job",
  "status": "queued",
  "model": "openai/gpt-image-2",
  "created_at": 1778637249,
  "polling_url": "https://api.therouter.ai/v1/jobs/img_uxRZK4KwxkWaaHXRfwP0fWUDST",
  "expires_at": 1779242049
}

Request Fields

FieldRequiredNotes
modelyesAny image-output alias, e.g. openai/gpt-image-2
promptyesImage description. Bilingual prompts (中英文) supported.
nnoDefaults to 1. Some providers (OpenRouter) only support n=1.
sizeno1024x1024 / 1792x1024 / 1024x1792
qualitynolow / medium / high
backgroundnotransparent / opaque / auto
output_formatnopng / webp / jpeg
?async=truerecommendedQuery string. Forces the async branch. Without it, the request goes async automatically when the model's timeout exceeds 100s — but explicit ?async=true avoids any sync-path edge-timeout drift.

Poll for Status

curl "https://api.therouter.ai/v1/jobs/img_uxRZK4KwxkWaaHXRfwP0fWUDST" \
  -H "Authorization: Bearer $THEROUTER_API_KEY"

Terminal response (succeeded):

{
  "id": "img_uxRZK4KwxkWaaHXRfwP0fWUDST",
  "object": "image_job",
  "status": "succeeded",
  "model": "openai/gpt-image-2",
  "created_at": 1778637249,
  "completed_at": 1778637444,
  "polling_url": "https://api.therouter.ai/v1/jobs/...",
  "content_url": "https://api.therouter.ai/v1/jobs/.../content",
  "unsigned_urls": [
    "https://therouter-media-prod.s3.us-east-2.amazonaws.com/...&X-Amz-Signature=..."
  ],
  "image_count": 1,
  "usage": {
    "prompt_tokens": 0,
    "completion_tokens": 0,
    "cost_credits": 9
  },
  "error": null
}

Status Values

StatusMeaningClient Action
queuedPersisted to DB, waiting for a worker to pick upKeep polling
in_progressWorker is invoking the upstream modelKeep polling
succeededImage uploaded to S3Download via unsigned_urls[0] or content_url
failedUpstream / network / parse error. Credits refunded.Read error.message to decide on retry
cancelledCancelled via DELETE /v1/jobs/:id. Credits refunded.Terminal
expiredOlder than 7 days. S3 artifact purged.Re-submit

Polling Cadence

Download the Image

Option A — Direct S3 (recommended for static use)

unsigned_urls[0] is a pre-signed S3 URL with a 7-day TTL. No auth header needed — the signature is in the query string.

curl -L -o apple.png "<unsigned_urls[0]>"

Good for: CDN origins, browser <img src>, client-side caching.

Option B — Gateway redirect (recommended for shareable links)

curl -L -o apple.png \
  -H "Authorization: Bearer $THEROUTER_API_KEY" \
  "https://api.therouter.ai/v1/jobs/img_uxRZK4KwxkWaaHXRfwP0fWUDST/content"

The gateway validates tenant ownership and 302-redirects to a freshly signed S3 URL on every request. Good for: long-lived links, server-side audit, per-download access control.

Edit an Image

POST /v1/images/edits takes an existing image and a prompt describing what to change. Same async semantics as /v1/images/generations.

curl -X POST "https://api.therouter.ai/v1/images/edits?async=true" \
  -H "Authorization: Bearer $THEROUTER_API_KEY" \
  -F "model=openai/gpt-image-2" \
  -F "prompt=Turn this scene into a watercolor painting" \
  -F "size=1024x1024" \
  -F "image=@input.png"

Returns the same { id, polling_url, … } envelope. Poll thepolling_url like a generation job.

Cancel a Job

curl -X DELETE "https://api.therouter.ai/v1/jobs/img_xxx" \
  -H "Authorization: Bearer $THEROUTER_API_KEY"

End-to-End Examples

Python

import os, time, requests

API = "https://api.therouter.ai"
KEY = os.environ["THEROUTER_API_KEY"]
H = {"Authorization": f"Bearer {KEY}"}

# submit
r = requests.post(
    f"{API}/v1/images/generations?async=true",
    headers={**H, "Content-Type": "application/json"},
    json={
        "model": "openai/gpt-image-2",
        "prompt": "a minimal red apple on a white marble table",
    },
    timeout=30,
)
r.raise_for_status()
job = r.json()
print("submitted:", job["id"])

# poll
deadline = time.time() + 300
while time.time() < deadline:
    s = requests.get(job["polling_url"], headers=H, timeout=15).json()
    print("status:", s["status"])
    if s["status"] == "succeeded":
        url = s["unsigned_urls"][0]
        with open("out.png", "wb") as f:
            f.write(requests.get(url, timeout=60).content)
        print("saved out.png")
        break
    if s["status"] in ("failed", "cancelled", "expired"):
        raise SystemExit(s.get("error") or s["status"])
    time.sleep(5)
else:
    raise SystemExit("polling timeout")

TypeScript (Bun / Node 20+)

const API = "https://api.therouter.ai";
const KEY = process.env.THEROUTER_API_KEY!;
const H = { Authorization: `Bearer ${KEY}` };

const submit = await fetch(`${API}/v1/images/generations?async=true`, {
  method: "POST",
  headers: { ...H, "Content-Type": "application/json" },
  body: JSON.stringify({
    model: "openai/gpt-image-2",
    prompt: "a minimal red apple on a white marble table",
  }),
});
if (!submit.ok) throw new Error(`submit failed: ${submit.status}`);
const job = await submit.json();
console.log("submitted:", job.id);

const deadline = Date.now() + 300_000;
while (Date.now() < deadline) {
  const s = await (await fetch(job.polling_url, { headers: H })).json();
  console.log("status:", s.status);
  if (s.status === "succeeded") {
    const buf = await (await fetch(s.unsigned_urls[0])).arrayBuffer();
    await Bun.write("out.png", buf);
    break;
  }
  if (["failed", "cancelled", "expired"].includes(s.status)) {
    throw new Error(s.error?.message ?? s.status);
  }
  await new Promise((r) => setTimeout(r, 5_000));
}

Bash

#!/usr/bin/env bash
set -euo pipefail
: "${THEROUTER_API_KEY:?set THEROUTER_API_KEY}"
BASE=https://api.therouter.ai

RESP=$(curl -sS -X POST "$BASE/v1/images/generations?async=true" \
  -H "Authorization: Bearer $THEROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"openai/gpt-image-2","prompt":"a minimal red apple"}')
JOB=$(echo "$RESP" | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
echo "submitted: $JOB"

for i in $(seq 1 60); do
  R=$(curl -sS "$BASE/v1/jobs/$JOB" -H "Authorization: Bearer $THEROUTER_API_KEY")
  S=$(echo "$R" | python3 -c "import sys,json;print(json.load(sys.stdin)['status'])")
  printf "[%02d t=%03ds] %s\n" "$i" $((i*5)) "$S"
  case "$S" in
    succeeded) URL=$(echo "$R" | python3 -c "import sys,json;print(json.load(sys.stdin)['unsigned_urls'][0])")
               curl -sSL -o out.png "$URL"; echo "saved out.png"; exit 0 ;;
    failed|cancelled|expired) echo "$R" | python3 -m json.tool; exit 1 ;;
  esac
  sleep 5
done
echo "timeout"; exit 1

Pricing

Image jobs are billed in credits. The gateway reserves the estimated cost at submit time and settles to the upstream-reported actual cost at succeeded. failed and cancelled get a full refund; expired settles at the already-reserved amount.

ModelApprox. credits/image (high quality)
openai/gpt-image-2~9 credits
openai/gpt-image-1~7 credits
google/imagen-4~4 credits

Live pricing is exposed at GET /v1/models and on each model's detail page under /models.

Error Cheat Sheet

SymptomHTTPCauseFix
OpenRouter image gen supports n=1 only400n > 1 for an OpenRouter-routed modelSet n: 1
insufficient_credits_error402Reservation rejectedTop up balance
AsyncJobCreditReservationUnavailableError503Redis reservation transiently unavailableBack off and retry
Terminal failed + Network error: fetch failed200Gateway → provider service network issueFile a support ticket; do not auto-retry
Terminal failed + missing image data200Prompt was too vague; model replied with textTighten the prompt; avoid demonstrative pronouns ("this image")
queued for > 30s with no transition200Worker not draining (rare)Cancel and re-submit; report to support

Best Practices

Related