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

# 异步任务 API

> 提交生成任务，一秒内拿到任务 ID，之后轮询或等回调取结果。

异步任务 API 只有两个端点和一个请求信封。提交后立刻拿到 ID，生成过程在 WideRouter
这一侧进行，模型算多久都不需要你的请求保持连接。

<Info>
  如果你现在的做法是把一个 HTTP 连接挂住 20–50 秒，就该换成这套接口。已经跑通的同步集成
  不必急着迁移，先看[怎么在同步和异步之间选](#怎么在同步和异步之间选)。
</Info>

## 端点

| 方法     | 路径                   | 作用              |
| ------ | -------------------- | --------------- |
| `POST` | `/v1/tasks/submit`   | 创建任务，立刻返回任务 ID。 |
| `GET`  | `/v1/task/{task_id}` | 读取任务状态，完成后读取产物。 |

注意创建用复数 `tasks`，查询用**单数** `task`。没有列表、取消、删除端点。

## 创建任务

请求体永远是这三个键的信封：`model`、`input`，以及可选的 `callback_url`。

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.widerouter.com/v1/tasks/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",
        "aspect_ratio": "16:9",
        "image_size": "2K"
      }
    }'
  ```

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

  resp = requests.post(
      "https://api.widerouter.com/v1/tasks/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",
              "aspect_ratio": "16:9",
              "image_size": "2K",
          },
      },
      timeout=30,
  )
  task_id = resp.json()["id"]
  ```

  ```javascript Node theme={null}
  const resp = await fetch("https://api.widerouter.com/v1/tasks/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",
        aspect_ratio: "16:9",
        image_size: "2K",
      },
    }),
  });
  const { id } = await resp.json();
  ```
</CodeGroup>

响应只有三个字段，一秒之内就能拿到：

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

### 信封字段

| 字段             | 类型     | 必填 | 说明                          |
| -------------- | ------ | -- | --------------------------- |
| `model`        | string | 是  | 见[支持的模型](#支持的模型)。           |
| `input`        | object | 是  | 必须是 JSON 对象，传字符串或数组都会被拒。    |
| `callback_url` | string | 否  | 必须是 `https` URL，见[回调](#回调)。 |

信封层的未知字段目前会被接受并忽略。不要依赖这个行为，更不要把生成参数写在这一层——
它们属于 `input`。

### `input` 对象

`input` 装的是生成参数，而**它接受哪些字段取决于模型**。外层信封是固定的，
里面装什么不是。

`input` 内部的校验很严：模型不认识的键会被直接拒掉，所以拼错字段名会立刻报错，
不会被静默忽略：

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

<Card title="Nano Banana 系列" icon="layers" href="/zh/models/nano-banana/overview">
  完整字段清单、分辨率档位、画幅比例与图生图用法。
</Card>

## 轮询结果

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

完成的任务长这样：

```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 }
}
```

### 任务字段

| 字段             | 出现时机               | 说明                                           |
| -------------- | ------------------ | -------------------------------------------- |
| `id`           | 始终                 | 任务 ID。                                       |
| `model`        | 查询时始终              | 原样回显；创建响应里没有这个字段。                            |
| `status`       | 始终                 | `queued`、`in_progress`、`completed`、`failed`。 |
| `created_at`   | 始终                 | Unix 秒，UTC。                                  |
| `started_at`   | 进入 `in_progress` 后 | worker 取走任务的时刻。                              |
| `completed_at` | 终态                 | `failed` 也有。                                 |
| `expires_at`   | `completed`        | 等于 `created_at` 加 24 小时。                     |
| `outputs`      | `completed`        | 图片 URL 数组，请求几张就有几个。                          |
| `counts`       | 终态                 | `requested`、`succeeded`、`failed`。            |
| `error`        | `failed`           | 含 `code` 与 `message` 的对象。                    |

响应是随任务推进逐步长出来的——任务还在跑的时候，`outputs` 和 `expires_at` 根本不存在。
读字段时别假设固定结构，先按 `status` 分支。

### 状态流转

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

两个终态都不可逆，查询端点幂等：对已完成任务的重复查询返回逐字节相同的 JSON。

### 该等多久

提交是亚秒级的，压力下也稳：30 个任务、并发 12 实测 p50 0.88 秒、p95 0.92 秒。
时间都花在生成上——按模型和分辨率不同，大约 12–50 秒，其中 0–11 秒是排队。
分模型的具体数字在各自的模型页上。

<Warning>
  每 2–3 秒轮询一次，不要死循环。一次查询本身就要约 0.9 秒往返，比这更快地轮询
  什么都换不来，只会白白消耗限额。
</Warning>

## 下载产物

`outputs` 里是普通 `https` URL，没有签名也没有查询串，由 WideRouter 的分发 CDN
提供——域名和 API 不是同一个。由此有两件事要注意：

<Steps>
  <Step title="它们不鉴权">
    不要把 API Key 发给这些地址，同时把 URL 本身当成密钥看待：拿到链接的人在有效期内
    都能取到图。
  </Step>

  <Step title="24 小时后失效">
    `expires_at` 恒为 `created_at` 加 86400。需要长期保留的内容要转存到你自己的存储，
    不要把产物 URL 当成永久引用存下来。
  </Step>
</Steps>

<Warning>
  **下载时必须带 `User-Agent`。** CDN 前面有 WAF，对不带 `User-Agent` 的请求、以及
  默认的 `Python-urllib/3.x`，一律直接返回 `403`。这个现象和「链接过期」长得一模一样，
  但并不是过期。实测没问题的：浏览器、`curl`、`requests`、`axios`、`okhttp`、Java、Go、
  Postman。不行的：不带任何头的 `urllib.request.urlopen(url)`。
</Warning>

```python theme={null}
import urllib.request

req = urllib.request.Request(url, headers={"User-Agent": "my-app/1.0"})
with urllib.request.urlopen(req, timeout=120) as r:
    data = r.read()
```

产物是带 C2PA 内容凭证清单的 JPEG。实测体积：`1K` 约 0.4–0.7 MB，`2K` 约 2.4–3.0 MB，
`4K` 约 7.5–8.2 MB。文件类型请从响应的 `Content-Type` 读，不要靠扩展名猜。

## 回调

创建时带上 `callback_url`，WideRouter 会在任务结束后把任务对象 POST 过去，
你就不用轮询了。

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

URL 必须是 `https`。`http`、光秃秃的主机名、非字符串，都会在提交时就被
`invalid_callback_url` 拒掉，写错了当场报错，而不是投递不到也没人知道。

WideRouter 以 `application/json` 投递任务对象，并带上可以在解析正文之前先用来分流的头：

| 头                | 值                  |
| ---------------- | ------------------ |
| `X-Wide-Event`   | `task.completed`   |
| `X-Wide-Task-Id` | 任务 ID              |
| `Content-Type`   | `application/json` |

正文与同一时刻 `GET /v1/task/{task_id}` 的返回逐字节相同——字段一样、值一样——
所以一个 handler 可以同时服务两条路径。

实测投递很快：三个任务的回调都在任务进入 `completed` 后一秒内到达。如果你的接收端
返回 `5xx`，WideRouter 会重试，观测到的三次投递分别在约 0、10、70 秒。

<Warning>
  **回调没有签名。** 没有 HMAC 头，唯一能证明请求来自 WideRouter 的只有 URL 本身。
  把 payload 当线索而不是当权威：`callback_url` 用一段足够长、猜不到的路径，
  handler 在做任何要紧的事之前重新读一次 `GET /v1/task/{task_id}`。
</Warning>

<Info>
  回调是省掉延迟的优化，不是投递保证。要给收不到回调的任务留一条轮询兜底，
  并且让 handler 幂等——按任务 `id` 去重，因为重试意味着同一个任务可能到达多次。
</Info>

## 任务失败时

`failed` 的任务在查询端点上依然是 `200`。失败信息在正文里，不在 HTTP 状态码上：

```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: ..." }
}
```

注意这里没有 `outputs`，也没有 `expires_at`。取输入图失败会很快——不到一秒——
因为它发生在任何模型计算之前。

## 提交时的错误

校验发生在入队之前，所以这里的 `400` 不产生任何费用。

| HTTP | `code`                 | 含义                                             |
| ---- | ---------------------- | ---------------------------------------------- |
| 400  | `invalid_params`       | 字段有问题，`param` 精确指出是哪个，例如 `input.aspect_ratio`。 |
| 400  | `invalid_callback_url` | `callback_url` 不是 `https` URL。                 |
| 400  | `model_not_supported`  | 模型存在，但异步接口不支持它。                                |
| 401  | —                      | `Authorization` 头缺失或无效。                        |
| 404  | `task_not_found`       | 任务 ID 不存在，或不属于当前 Key。                          |
| 503  | `model_not_found`      | 平台上没有这个模型名。                                    |

错误一次只指一个字段，`param` 用的是包含数组下标的完整路径（`input.images[0]`），
可以直接映射回你的请求。

## 哪些模型能走这条链路

模型 ID **精确匹配**，没有别名互通，带 `-preview` 后缀的名字也不接受。
传一个异步接口不服务的 ID，会在提交时就返回 `model_not_supported`，不会入队。

<Card title="Nano Banana 系列" icon="layers" href="/zh/models/nano-banana/overview">
  Google 的图像模型——两条链路各自的可用性、参数，以及实测时延。
</Card>

## 怎么在同步和异步之间选

两套接口并存，都没有被废弃。

|        | 异步任务 API      | 同步图像接口                 |
| ------ | ------------- | ---------------------- |
| 客户端挂连接 | 不用，约 0.9 秒    | 要，12–50 秒              |
| 结果形式   | CDN URL，24 小时 | 响应体里的 base64           |
| 中途断线   | 任务照常完成        | 结果丢失，但照常计费             |
| 单次多图   | `n` 最多 4      | 没有——`n` 会被接受并忽略，始终只回一张 |
| 回调     | 有             | 无                      |

跑在 Serverless 函数、反向代理或移动端后面的场景应当默认用异步——这几类环境都不能
可靠地扛住一个 50 秒的请求。只想拿到图片字节的脚本，同步调用仍然更简单。

## 下一步

<CardGroup cols={2}>
  <Card title="Nano Banana 系列" icon="layers" href="/zh/models/nano-banana/overview">
    参数对照矩阵、分辨率档位，以及两个模型的差异。
  </Card>

  <Card title="快速开始" icon="rocket" href="/zh/quickstart">
    同一套流程，五分钟跑通。
  </Card>
</CardGroup>
