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

# 非同步任務 Webhook

> 接收 ToAPIs 簽名的最終成功與失敗事件

> [!NOTE]
>
> **中國大陸用戶請注意：** 中國大陸用戶請使用 `https://toapis.cn` 作為接口地址（Base URL）。文檔示例中的 `https://toapis.com` 請替換為 `https://toapis.cn`。

ToAPIs 對非同步圖片與影片發送 `generation.completed`、`generation.failed`，測試事件為 `endpoint.test`。投遞至少一次；重試保留相同 event ID，請以 `id` 冪等去重。

在控制台編輯 Token，設定 HTTPS 預設 URL、產生僅顯示一次的 32-byte 密鑰、啟用並測試。請求未帶 `callback_url` 時使用預設 URL；覆蓋 URL 只能更改同協議、同網域、同連接埠的路徑與 query。未設定時返回 `400 webhook_not_configured`。

```json theme={null}
{"id":"evt_xxx","type":"generation.completed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_xxx","client_business_id":"order_123","task_type":"video","model":"model-name","status":"SUCCESS","progress":"100%","result":{"type":"video","data":[{"url":"https://cdn.example/result.mp4","format":"mp4"}]},"usage":{"charge_type":"per_request","requests":1},"billing":{"status":"settled","credits":"100","cost_usd":"0.5"},"error":null,"created_at":1783953000,"finished_at":1783953300,"completed_at":1783953300,"expires_at":1784039700}}
```

最終失敗只會在內部重試結束、退款或計費完成後發送：

識別碼含義與處理方式請見[錯誤碼速查](https://docs.toapis.com/docs/zh-Hant/api-reference/errors.md). Webhook 的 `data.error` 不保證包含部分任務查詢中的 `reason` 欄位.

```json theme={null}
{"id":"evt_failure","type":"generation.failed","api_version":"v1","created_at":1783953302,"data":{"task_id":"tsk_failure","client_business_id":"order_456","task_type":"video","model":"model-name","status":"FAILURE","progress":"100%","result":null,"billing":{"status":"refunded","credits":"0","cost_usd":"0"},"error":{"code":"generation_failed","message":"Generation failed after retries"},"created_at":1783953000,"finished_at":1783953300}}
```

控制台測試使用相同簽名協議：

```json theme={null}
{"id":"evt_test","type":"endpoint.test","api_version":"v1","created_at":1783953302,"data":{"task_id":"endpoint_test","task_type":"image","status":"SUCCESS","progress":"100%","result":{"message":"ToAPIs webhook endpoint test"},"error":null,"created_at":0}}
```

`task_type` 為 `image|video`。結果與公開任務查詢一致；不返回渠道 ID、上游 task ID/密鑰、憑證或 PrivateData。

## 用量, 計費與查詢差異

圖片和影片成功事件都可包含 `data.usage`, 包括計費量, 可用的模型 token 明細及工具次數. 欄位定義與任務查詢一致, 見[任務用量欄位](https://docs.toapis.com/docs/zh-Hant/api-reference/account/pricing.md). 影片事件也可包含 `data.generation`, Seedance 的欄位及相容用量見[影片任務查詢](https://docs.toapis.com/docs/zh-Hant/api-reference/tasks/video-status.md).

`data.billing` 可包含 `status`, `credits`, `cost_usd`. 金額為十進位字串; `pending` 時省略金額, `refunded` 表示已確認淨費用為零. 無法確認時省略物件, 不代表免費. Webhook 與任務查詢使用同一確認條件: 已確認扣費的任務, 包括提交時一次扣清的固定計費任務, 兩者均回傳 `settled` 和實際金額; `data.usage` 回傳 `charge_type` 等計費量時, 一定同時回傳 `settled` 金額.

### `data.billing` 欄位取值

| 欄位 | 類型 | 取值 |
| - | - | - |
| `status` | string | `pending`: 待結算或退款尚未確認, 省略金額. `settled`: 已確認最終扣費, 回傳實際金額, 免費任務金額為 `"0"`. `refunded`: 已退款或失敗後確認無淨扣費, 金額均為 `"0"` |
| `credits` | string | 實際扣除的 ToAPIs Credits, 十進位字串, 例如 `"100"`. 僅 `settled` 和 `refunded` 時回傳 |
| `cost_usd` | string | 同一筆費用的美元金額, 十進位字串, 例如 `"0.5"`. 回傳條件同 `credits`, 已包含折扣和倍率, 不代表供應商成本 |

成功事件為 `settled`, 結算退回全部預扣時為 `refunded`; 失敗事件在退款完成後發送, 為 `refunded`. 無法確認金額時省略整個 `data.billing`.

### `data.usage` 欄位取值

| 欄位 | 類型 | 取值 |
| - | - | - |
| `charge_type` | string | 計費方式: `per_request` 按次, `per_second` 按秒, `per_token` 按 token |
| `requests` | integer | 計費次數, `per_request` 時回傳. 圖片請求的 `n` 不直接乘算次數 |
| `billed_tokens` | integer | 計價 token 數, `per_token` 時回傳 |
| `output_seconds` | integer | 參與計費的輸出影片時長, 整數秒, `per_second` 時回傳 |
| `input_seconds` | integer | 參與計費的輸入影片時長, 整數秒, 為零時省略 |
| `input_images` | integer | 扣除免費額度後參與計費的輸入圖片數, 為零時省略 |
| `input_tokens`, `output_tokens`, `total_tokens` | integer | 模型回報的輸入, 輸出和總 token 數 |
| `input_tokens_details` | object | 圖片輸入明細: `text_tokens`, `image_tokens`, `cached_tokens` 及 `cached_tokens_details.text_tokens`, `cached_tokens_details.image_tokens`. 快取 token 已包含在輸入總數中 |
| `output_tokens_details` | object | 圖片輸出明細: `text_tokens`, `image_tokens`; 文字與思考同價且未細分時另回傳 `text_or_reasoning_tokens`, 不計入 `text_tokens` |
| `prompt_tokens`, `completion_tokens` | integer | 影片模型回報的 token 數 |
| `tool_usage` | object | 工具次數: `web_search`, `image_search`, `grounding_queries`, 依實際發生回傳 |

`charge_type` 到 `input_images` 是計費量, 只與 `data.billing.status=settled` 同時回傳. token 明細和工具次數依供應商實際提供情況獨立回傳. 未回傳的欄位直接省略, 不補零.

Webhook 是事件建立時的快照, 任務查詢讀取最新已確認狀態. 缺少用量或金額不會等待補齊, 不延遲投遞或改變重試; 已建立事件不回填, 重試沿用原始事件內容.

事件結構與 `api_version: "v1"` 保持不變. Webhook 使用 `data.task_id`, `SUCCESS` / `FAILURE` 和字串進度 `"100%"`; 查詢使用 `id`, 小寫狀態和整數進度. 時間欄位與公開 `generation.task` 查詢同名同口徑, 均為 Unix 秒: `data.created_at` 為任務提交時間, `data.finished_at` 為結束時間, 成功時另回傳 `data.completed_at` 和 `data.expires_at` (結果過期時間, 結束時間加 24 小時), 缺少值的欄位直接省略. 原 `data.submitted_at` 已由 `data.created_at` 取代, 不再回傳. 事件頂層 `created_at` 是事件建立時間, 不是任務提交時間. 兩者的媒體 URL 可能因存取策略不同而不同.

### Seedance 影片成功事件範例

以下範例用於展示欄位, 不代表模型目前的價格或固定計費方式. 實際欄位與金額以本次任務回傳為準.

Seedance 成功事件的 `data.result.data[]` 也可包含 `fallback_url` 和 `fallback_last_frame_url`. 優先使用 `url` 和 `last_frame_url`, 原始位址無法存取時才使用對應備用位址. 備用欄位省略不表示任務失敗. 事件保留建立時的位址快照, 後續副本變化不會改寫已建立事件或其重試負載. 欄位定義與完整使用規則見[影片任務查詢](https://docs.toapis.com/docs/zh-Hant/api-reference/tasks/video-status.md#seedance-原始位址與備用位址).

```json theme={null}
{
  "id": "evt_seedance_example",
  "type": "generation.completed",
  "api_version": "v1",
  "created_at": 1792400061,
  "data": {
    "task_id": "task_seedance_example",
    "client_business_id": "order_123",
    "task_type": "video",
    "model": "seedance-2-global",
    "status": "SUCCESS",
    "progress": "100%",
    "result": {
      "type": "video",
      "data": [
        {
          "url": "https://cdn.example/result.mp4",
          "fallback_url": "https://files.toapis.com/videos/task_seedance_example/result.mp4",
          "format": "mp4",
          "last_frame_url": "https://cdn.example/last-frame.jpg",
          "fallback_last_frame_url": "https://files.toapis.com/videos/task_seedance_example/last-frame.jpg"
        }
      ]
    },
    "usage": {
      "charge_type": "per_token",
      "billed_tokens": 87300,
      "completion_tokens": 87300,
      "total_tokens": 87300
    },
    "billing": {
      "status": "settled",
      "credits": "87.3",
      "cost_usd": "0.4365"
    },
    "generation": {
      "duration": 5,
      "resolution": "720p",
      "ratio": "16:9",
      "generate_audio": true,
      "usage": {
        "completion_tokens": 87300,
        "total_tokens": 87300
      }
    },
    "error": null,
    "created_at": 1792400000,
    "finished_at": 1792400060,
    "completed_at": 1792400060,
    "expires_at": 1792486460
  }
}
```

```text theme={null}
User-Agent: ToAPIs-Webhook/1.0
X-ToAPIs-Webhook-Id
X-ToAPIs-Webhook-Timestamp
X-ToAPIs-Webhook-Signature: v1=<current>[,v1=<previous>]
```

簽名原文：`event_id + "." + timestamp + "." + raw_request_body`；HMAC-SHA256。首次投遞拒絕超過 5 分鐘的時間戳。輪換後 24 小時同時驗證新舊密鑰。

```python Python theme={null}
def verify(headers, raw_body, secrets):
    event_id=headers["X-ToAPIs-Webhook-Id"]
    timestamp=int(headers["X-ToAPIs-Webhook-Timestamp"])
    if abs(int(time.time())-timestamp)>300: return False
    signed=f"{event_id}.{timestamp}.".encode()+raw_body
    received=[v.strip()[3:] for v in headers["X-ToAPIs-Webhook-Signature"].split(",") if v.strip().startswith("v1=")]
    return any(hmac.compare_digest(hmac.new(s.encode(),signed,hashlib.sha256).hexdigest(),r) for s in secrets for r in received)
```

```javascript Node.js theme={null}
function verify(headers, rawBody, secrets) {
  const id=headers['x-toapis-webhook-id']; const ts=Number(headers['x-toapis-webhook-timestamp']);
  if (!Number.isFinite(ts) || Math.abs(Date.now()/1000-ts)>300) return false;
  const signed=Buffer.concat([Buffer.from(`${id}.${ts}.`),rawBody]);
  const received=headers['x-toapis-webhook-signature'].split(',').filter(v=>v.trim().startsWith('v1=')).map(v=>v.trim().slice(3));
  return secrets.some(secret=>received.some(sig=>{const expected=crypto.createHmac('sha256',secret).update(signed).digest('hex'); return expected.length===sig.length&&crypto.timingSafeEqual(Buffer.from(expected),Buffer.from(sig));}));
}
```

```go Go theme={null}
func Verify(id, ts, sig string, body []byte, secrets []string) bool {
    unix, err := strconv.ParseInt(ts,10,64); now:=time.Now().Unix()
    if err!=nil || now-unix>300 || unix-now>300 { return false }
    signed:=append([]byte(id+"."+ts+"."),body...)
    for _,secret:=range secrets { mac:=hmac.New(sha256.New,[]byte(secret)); mac.Write(signed); expected:=hex.EncodeToString(mac.Sum(nil)); for _,v:=range strings.Split(sig,",") { if hmac.Equal([]byte(expected),[]byte(strings.TrimPrefix(strings.TrimSpace(v),"v1="))) { return true } } }
    return false
}
```

首次投遞立即執行；失敗後約於 10秒、30秒、2分鐘、10分鐘、1小時、6小時、24小時重試。僅 `2xx` 成功；`3xx` 不跟隨，`4xx/5xx` 會重試，HTTP 逾時為 10 秒。HTTPS、userinfo、fragment、非法 port、SSRF、DNS/IP 與 DNS rebinding 防護會在每次投遞重新驗證。回調失敗以任務查詢兜底，結果 URL 可能過期，請及時保存。真人認證、支付與 OAuth callback 不受影響。參閱 [限流](https://docs.toapis.com/docs/zh-Hant/api-reference/rate-limits/async-tasks.md)。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.