Skip to main content
The async task API is two endpoints and one request envelope. You submit a task, you get an id immediately, and the generation happens on WideRouter’s side. Nothing about your request has to stay connected while the model works.
Use this API when you would otherwise be holding an HTTP connection open for 20–50 seconds. If you already have a synchronous integration that works, see Choosing sync or async before migrating.

Endpoints

Note the path is plural on create and singular on read. There is no list, cancel, or delete endpoint.

Create a task

The request body is always the same three-key envelope: model, input, and an optional callback_url.
The response is three fields and arrives in well under a second:

Envelope fields

Unknown keys at the envelope level are currently accepted and ignored — do not rely on that, and do not put generation parameters there. They belong in input.

The input object

input carries the generation parameters, and which fields it accepts depends on the model. The envelope around it is fixed; the contents are not. Validation inside input is strict — any key the model does not recognize is rejected outright, which makes a typo loud instead of silent:

Nano Banana series

The full field list, resolution tiers, aspect ratios and image editing.

Poll for the result

A finished task looks like this:

Task fields

The response grows as the task advances — outputs and expires_at simply are not there while the task is still running. Read fields defensively rather than assuming a fixed shape, and branch on status first.

Status flow

Both terminal states are final and the read endpoint is idempotent: repeated reads of a finished task return byte-identical JSON.

How long to wait

Submitting is sub-second and stays that way under load: measured p50 0.88 s, p95 0.92 s across 30 tasks at concurrency 12. The generation itself is where the time goes — roughly 12–50 seconds depending on model and resolution, with 0–11 seconds of that spent queued. Per-model figures live on the model’s own page.
Poll every 2–3 seconds, not in a tight loop. A read costs about 0.9 s of round-trip on its own, so polling faster than that buys you nothing and only spends rate limit.

Downloading outputs

outputs holds plain https URLs with no signature or query string, served from WideRouter’s delivery CDN — a different hostname from the API. Two things follow from that:
1

They are unauthenticated

Do not send your API key to them, and treat the URL itself as the secret. Anyone holding the link can fetch the image for as long as it lives.
2

They expire after 24 hours

expires_at is always created_at plus 86400. Copy anything you need to keep into your own storage — do not store output URLs as permanent references.
Send a User-Agent header when you download. The CDN sits behind a WAF that answers a bare 403 to requests with no User-Agent and to the default Python-urllib/3.x one. It looks exactly like an expired link but is not. Tested and fine: browsers, curl, requests, axios, okhttp, Java, Go, Postman. Not fine: a raw urllib.request.urlopen(url) with no headers.
Images come back as JPEG carrying a C2PA content-credentials manifest. Measured file sizes: about 0.4–0.7 MB at 1K, 2.4–3.0 MB at 2K, and 7.5–8.2 MB at 4K. Read the Content-Type from the response instead of assuming a file extension.

Callbacks

Set callback_url on create and WideRouter posts the finished task to it, so you can skip polling entirely.
The URL must be https. Anything else — http, a bare hostname, a non-string — is rejected at submit time with invalid_callback_url, so a typo fails fast instead of silently never delivering. WideRouter posts the task object as application/json, with headers you can route on before parsing the body: The body is byte-identical to what GET /v1/task/{task_id} returns at that moment — same fields, same values — so one handler can serve both paths. Delivery was immediate in testing: callbacks for three tasks all arrived within a second of the task reaching completed. If your endpoint answers with a 5xx, WideRouter retries; observed attempts were at roughly 0, 10 and 70 seconds.
Callbacks are not signed. There is no HMAC header, and the URL is the only thing proving the request came from WideRouter. Treat the payload as a hint, not as authority: use a long unguessable path in your callback_url, and have the handler re-read GET /v1/task/{task_id} before acting on anything that matters.
A callback is a latency optimization, not a delivery guarantee. Keep a polling fallback for tasks whose callback never arrives, and make your handler idempotent — key it on the task id, since retries mean the same task can arrive more than once.

When a task fails

A failed task is still a 200 on the read endpoint. The failure is in the body, not the HTTP status:
Note there is no outputs and no expires_at. Input-fetch failures are fast — under a second — because they happen before any model work.

Errors on submit

Validation happens before anything is queued, so a 400 here costs nothing. Errors point at one field at a time, and param uses full paths including array indices (input.images[0]), so you can map a failure straight onto your request.

Which models work here

Model ids are matched exactly. There are no aliases, and -preview suffixed names are not accepted. Sending an id the async API does not serve returns model_not_supported at submit time, before anything is queued.

Nano Banana series

Google’s image models — availability on each surface, parameters, and measured latency.

Choosing sync or async

Both surfaces exist and neither is deprecated. Async is the better default for anything running behind a serverless function, a reverse proxy, or a mobile client, because none of those reliably survive a 50-second request. Synchronous calls remain simpler for a script that just wants bytes back.

Next steps

Nano Banana series

Parameter matrix, resolution tiers, and what differs between the two models.

Quickstart

The same flow end to end in about five minutes.