> ## Documentation Index
> Fetch the complete documentation index at: https://docs.widerouter.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Async task API

> Submit a generation task, get a task id back in under a second, then poll or receive a callback when the result is ready.

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.

<Info>
  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](#choosing-sync-or-async) before migrating.
</Info>

## Endpoints

| Method | Path                 | Purpose                                                 |
| ------ | -------------------- | ------------------------------------------------------- |
| `POST` | `/v1/task/submit`    | Create a task. Returns a task id immediately.           |
| `GET`  | `/v1/task/{task_id}` | Read the task's status and, once finished, its outputs. |

There is no list, cancel, or delete endpoint.

<Note>
  The plural spelling — `/v1/tasks/submit` and `/v1/tasks/{task_id}` — is also
  accepted, and is kept for integrations that were written against it. It is an
  alias, not a second API: same handler, same behaviour. Write new code against
  the singular form used everywhere in these docs.
</Note>

## Create a task

The request body is always the same three-key envelope:
`model`, `input`, and an optional `callback_url`.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.widerouter.com/v1/task/submit \
    -H "Authorization: Bearer $WIDEROUTER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "model": "gemini-3-pro-image",
      "input": {
        "prompt": "a small ceramic teapot on a light-grey studio backdrop"
      }
    }'
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.post(
      "https://api.widerouter.com/v1/task/submit",
      headers={"Authorization": f"Bearer {os.environ['WIDEROUTER_API_KEY']}"},
      json={
          "model": "gemini-3-pro-image",
          "input": {
              "prompt": "a small ceramic teapot on a light-grey studio backdrop",
          },
      },
      timeout=30,
  )
  task_id = resp.json()["id"]
  ```

  ```javascript Node theme={null}
  const resp = await fetch("https://api.widerouter.com/v1/task/submit", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.WIDEROUTER_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      model: "gemini-3-pro-image",
      input: {
        prompt: "a small ceramic teapot on a light-grey studio backdrop",
      },
    }),
  });
  const { id } = await resp.json();
  ```
</CodeGroup>

The response is three fields and arrives in well under a second:

```json theme={null}
{
  "id": "task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI",
  "status": "queued",
  "created_at": 1788104112
}
```

### Envelope fields

| Field          | Type   | Required | Notes                                                    |
| -------------- | ------ | -------- | -------------------------------------------------------- |
| `model`        | string | yes      | See [which models work here](#which-models-work-here).   |
| `input`        | object | yes      | Must be a JSON object. A string or an array is rejected. |
| `callback_url` | string | no       | Must be an `https` URL. See [Callbacks](#callbacks).     |

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:

```json theme={null}
{ "error": { "code": "invalid_params", "message": "unknown field", "param": "input.negative_prompt" } }
```

<CardGroup cols={3}>
  <Card title="Nano Banana series" icon="layers" href="/models/nano-banana/overview">
    Google's image models: field list, resolution tiers and aspect ratios.
  </Card>

  <Card title="Grok Imagine image" icon="image" href="/models/grok-imagine/image/overview">
    xAI's image models: resolution and quality tiers, reference images.
  </Card>

  <Card title="Grok Imagine video" icon="film" href="/models/grok-imagine/video/overview">
    xAI's video models: duration, modes, reference images and voices.
  </Card>
</CardGroup>

## Poll for the result

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.widerouter.com/v1/task/task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI \
    -H "Authorization: Bearer $WIDEROUTER_API_KEY"
  ```

  ```python Python theme={null}
  import os, time, requests

  def wait(task_id, timeout=600):
      headers = {"Authorization": f"Bearer {os.environ['WIDEROUTER_API_KEY']}"}
      deadline = time.time() + timeout
      while time.time() < deadline:
          task = requests.get(
              f"https://api.widerouter.com/v1/task/{task_id}",
              headers=headers, timeout=30,
          ).json()
          if task["status"] in ("completed", "failed"):
              return task
          time.sleep(3)
      raise TimeoutError(task_id)
  ```
</CodeGroup>

A finished task looks like this:

```json theme={null}
{
  "id": "task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI",
  "model": "gemini-3-pro-image",
  "status": "completed",
  "created_at": 1788104112,
  "started_at": 1788104113,
  "completed_at": 1788104131,
  "expires_at": 1788190531,
  "outputs": [
    "https://r2cdn.agisuitepro.com/o/2026/08/30/task_dksUgWLYX4jIVueCACD8KwJzjR5JhRsI_0.jpg"
  ],
  "counts": { "requested": 1, "succeeded": 1, "failed": 0 }
}
```

`outputs` holds whatever the model produces. A video task looks the same, with an
`.mp4` in place of the `.jpg`:

```json theme={null}
{
  "id": "task_IIISyUlKs0fJDptkSCLbcJvXWZLpECKM",
  "model": "grok-imagine-video",
  "status": "completed",
  "created_at": 1788189046,
  "started_at": 1788189046,
  "completed_at": 1788189065,
  "expires_at": 1788275465,
  "outputs": [
    "https://r2cdn.agisuitepro.com/o/2026/08/31/task_IIISyUlKs0fJDptkSCLbcJvXWZLpECKM_0.mp4"
  ],
  "counts": { "requested": 1, "succeeded": 1, "failed": 0 }
}
```

### Task fields

| Field          | Present when       | Notes                                                                                |
| -------------- | ------------------ | ------------------------------------------------------------------------------------ |
| `id`           | always             | The task id.                                                                         |
| `model`        | always on read     | Echoed back; absent from the create response.                                        |
| `status`       | always             | `queued`, `in_progress`, `completed`, `failed`.                                      |
| `created_at`   | always             | Unix seconds, UTC.                                                                   |
| `started_at`   | from `in_progress` | When a worker picked the task up.                                                    |
| `completed_at` | terminal states    | Includes `failed`.                                                                   |
| `expires_at`   | `completed`        | `completed_at` plus 24 hours — not `created_at`.                                     |
| `outputs`      | `completed`        | Array of artifact URLs, one per requested artifact. Images are JPEG, videos are MP4. |
| `counts`       | terminal states    | `requested`, `succeeded`, `failed` — counted in artifacts, not in prompts.           |
| `error`        | `failed`           | Object with `code` and `message`.                                                    |

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

```
queued ──► in_progress ──► completed
                       └─► failed
```

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. Images land in roughly 6–50 seconds depending on model and resolution;
video is slower and scales with the clip length. Per-model figures live on the
model's own page — size your polling timeout from those, not from this range.

<Warning>
  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.
</Warning>

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

<Steps>
  <Step title="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 file for as long as it lives.
  </Step>

  <Step title="They expire 24 hours after the task finishes">
    `expires_at` is `completed_at` plus 86400, so a slow task keeps its window
    slightly longer than the submit time would suggest. Copy anything you need
    to keep into your own storage — do not store output URLs as permanent
    references.
  </Step>
</Steps>

**Read the `Content-Type` from the response rather than assuming a file
extension.** What comes back depends entirely on the model: images are JPEG,
video is MP4 served inline. File sizes and format details are on each model's
own page.

## Callbacks

Set `callback_url` on create and WideRouter posts the finished task to it, so
you can skip polling entirely.

```json theme={null}
{
  "model": "gemini-3-pro-image",
  "input": { "prompt": "a paper crane" },
  "callback_url": "https://your-app.example/hooks/widerouter"
}
```

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:

| Header           | Value              |
| ---------------- | ------------------ |
| `X-Wide-Event`   | `task.completed`   |
| `X-Wide-Task-Id` | the task id        |
| `Content-Type`   | `application/json` |

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.

<Warning>
  **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.
</Warning>

<Info>
  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.
</Info>

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

```json theme={null}
{
  "id": "task_jodb26qdNoG5CuwzA23OukOgjeH5ktnh",
  "model": "gemini-3-pro-image",
  "status": "failed",
  "created_at": 1788104830,
  "started_at": 1788104831,
  "completed_at": 1788104831,
  "counts": { "requested": 1, "succeeded": 0, "failed": 1 },
  "error": { "code": "input_fetch_error", "message": "fetch input image failed: ..." }
}
```

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.

| HTTP | `code`                 | Meaning                                                         |
| ---- | ---------------------- | --------------------------------------------------------------- |
| 400  | `invalid_params`       | Bad field. `param` names it exactly, e.g. `input.aspect_ratio`. |
| 400  | `invalid_callback_url` | `callback_url` is not an `https` URL.                           |
| 400  | `model_not_supported`  | Real model, but not available on the async API.                 |
| 401  | —                      | Missing or invalid `Authorization` header.                      |
| 404  | `task_not_found`       | No such task id, or it does not belong to your key.             |
| 503  | `model_not_found`      | The model name does not exist on the platform.                  |

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.

<CardGroup cols={3}>
  <Card title="Nano Banana series" icon="layers" href="/models/nano-banana/overview">
    Google's image models — availability on each surface, parameters, and measured latency.
  </Card>

  <Card title="Grok Imagine image" icon="image" href="/models/grok-imagine/image/overview">
    xAI's image models — three variants, resolution and quality tiers, reference images.
  </Card>

  <Card title="Grok Imagine video" icon="film" href="/models/grok-imagine/video/overview">
    xAI's video models — generate, edit and extend, with duration and voice controls.
  </Card>
</CardGroup>

## Choosing sync or async

Both surfaces exist and neither is deprecated.

Not every model has both. **Video is async-only** — there is no synchronous
video endpoint. The comparison below is therefore about image models.

|                               | Async task API                   | Synchronous image APIs                      |
| ----------------------------- | -------------------------------- | ------------------------------------------- |
| Client holds the connection   | no, \~0.9 s                      | yes, 12–50 s                                |
| Result delivery               | WideRouter CDN URL, 24 h         | base64, or a URL on the provider's own host |
| Client disconnects mid-flight | task still completes             | result is lost, request still billed        |
| Multiple outputs per call     | `n`, where the model supports it | varies by model; often accepted and ignored |
| Callback                      | yes                              | no                                          |

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 — and are the only option if you need the artifact inline rather than
as a link.

## Next steps

<CardGroup cols={2}>
  <Card title="Quickstart" icon="rocket" href="/quickstart">
    The same flow end to end in about five minutes.
  </Card>

  <Card title="Nano Banana series" icon="layers" href="/models/nano-banana/overview">
    Parameter matrix, resolution tiers, and what differs between the two models.
  </Card>

  <Card title="Grok Imagine image" icon="image" href="/models/grok-imagine/image/overview">
    Three image variants, the resolution and quality grid, and reference images.
  </Card>

  <Card title="Grok Imagine video" icon="film" href="/models/grok-imagine/video/overview">
    Text to video, image to video, editing and extending an existing clip.
  </Card>
</CardGroup>
