> ## 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-Hant/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-Hant/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-Hant/models/nano-banana/overview">
    參數對照矩陣、解析度檔位，以及兩個模型的差異。
  </Card>

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