# 任务完成回调（Webhook）
> 任务完成或失败时，由 OAIRoute 主动推送结果
> 免去轮询，长耗时任务尤其划算
> 推送失败会按退避策略重试
## 认证
- `Authorization` (string / 必填) — 所有接口均使用 Bearer Token 进行认证。 创建任务时同样需要在请求头里带上 Key。 Seedance 系列的接口需要在 [ark 站](https://ark.oairoute.com/) 单独获取 Key，与 api 站的 Key 不通用。 请求时在请求头中添加： ```http Authorization: Bearer YOUR_ARK_KEY ```
## 配置方式
在创建任务时传入 `callback_url`，任务进入终态后就会向这个地址发起 POST 请求。
```json
{
  "model": "seedance-2.5",
  "prompt": "海面日落，镜头缓慢前推",
  "callback_url": "https://your-server.com/hooks/oairoute"
}
```
> **Note:** 
> `callback_url` 必须是公网可达的 HTTP(S) 地址。本地开发可以先用内网穿透工具暴露端口，或改用轮询。
## 回调请求参数
- `id` (string) — 任务 ID，与轮询接口里的 `id` 一致。
- `status` (string) — 任务状态。`completed` 成功完成，`failed` 失败，`cancelled` 用户取消。
- `cost` (number) — 本次任务扣费金额。
- `credits_cost` (number) — 本次任务扣费积分。
- `result` (object) — 任务结果。仅 `completed` 时出现。
- `result.images` (array) — 图像任务结果数组，元素含 `url` 与 `expires_at`。
- `result.videos` (array) — 视频任务结果数组。
- `error` (object) — 错误详情。仅 `failed` 时出现。
- `created` (integer) — 任务创建时间戳。
- `completed` (integer) — 任务完成时间戳。
## 响应约定
接收端返回 2xx 即视为推送成功。返回其他状态码，或 10 秒内没有响应，会触发重试。
> **Note:** 
> 接收端需要具备幂等能力：同一个 `task_id` 可能被推送多次。建议按 `task_id` 做去重，处理过就直接返回 200。
## 重试策略
| 次数 | 间隔 | 说明 |
|---|---|---|
| 第 1 次 | 立即 | 初始推送 |
| 第 2 次 | 30 秒后 | 上次失败 |
| 第 3 次 | 2 分钟后 | 上次失败 |
| 第 4 次 | 10 分钟后 | 上次失败 |
| 第 5 次 | 30 分钟后 | 最后一次，仍失败则放弃 |
重试期间任务状态已经落库，随时可以用任务状态接口补查。
## 回调示例
```json
{
  "id": "task_01KA040M0HP1GJWBJYZMKX1XS1",
  "status": "completed",
  "cost": 0.48,
  "credits_cost": 4.8,
  "result": {
    "videos": [
      {
        "url": "https://cdn.oairoute.com/v/task_01KA040M0HP1GJWBJYZMKX1XS1.mp4",
        "expires_at": 1763174708
      }
    ]
  },
  "created": 1763088289,
  "completed": 1763088332
}
```
## 一个最小接收端
```python
from flask import Flask, request
app = Flask(__name__)
seen = set()
@app.post("/hooks/oairoute")
def hook():
    payload = request.get_json(force=True)
    task_id = payload.get("id")
    if task_id in seen:          # 幂等去重
        return {"ok": True}, 200
    seen.add(task_id)
    if payload.get("status") == "completed":
        for item in payload["result"].get("videos", []):
            download(item["url"])  # 立刻落盘，别依赖 CDN 长期保存
    else:
        log_error(payload.get("error"))
    return {"ok": True}, 200
```
## 使用示例
### 提交任务时带上回调地址
在生成请求里传 `callback_url`，任务状态变化时平台会 POST 到这个地址。比轮询省请求，也更及时。
### 处理完成回调
回调体里的 `status` 决定走哪个分支。产物 URL 会过期，收到就下载并落盘，不要只存链接。
```python
@app.post("/hooks/oairoute")
def hook():
    payload = request.get_json(force=True)
    if payload.get("status") != "completed":
        log_error(payload.get("error"))
        return {"ok": True}, 200
    for item in payload["result"].get("videos", []):
        save_locally(item["url"])
    return {"ok": True}, 200
```
### 用任务 ID 做幂等
网络抖动会导致同一条回调被投递多次。用 `id` 去重，避免重复下载或重复入库。
```python
import redis
seen = redis.Redis()
@app.post("/hooks/oairoute")
def hook():
    payload = request.get_json(force=True)
    task_id = payload.get("id")
    # 只在第一次见到时写入，返回 True 说明是新的
    if not seen.set("hook:" + task_id, "1", nx=True, ex=86400):
        return {"ok": True}, 200
    handle(payload)
    return {"ok": True}, 200
```
### 本地联调
开发阶段用 ngrok 之类把本地端口暴露出去，直接把公网地址填进 `callback_url`，不用先部署。
```bash
# 终端一：暴露本地 8000 端口
ngrok http 8000
# 终端二：把打印出的公网地址填进回调
curl --request POST https://oairoute-docs.ngrok-free.app/hooks/oairoute
```
**回调体{Callback payload}**
```
{
  "id": "task_01KA040M0HP1GJWBJYZMKX1XS1",
  "status": "completed",
  "cost": 0.48,
  "credits_cost": 4.8,
  "result": {
    "videos": [
      {
        "url": "https://cdn.oairoute.com/v/task_01KA040M0HP1GJWBJYZMKX1XS1.mp4",
        "expires_at": 1763174708
      }
    ]
  },
  "created": 1763088289,
  "completed": 1763088332
}
```
**失败回调{Failure callback}**
```
{
  "id": "task_01KA040M0HP1GJWBJYZMKX1XS1",
  "status": "failed",
  "error": {
    "code": 400,
    "message": "提示词触发内容审核",
    "type": "content_policy_violation"
  },
  "created": 1763088289,
  "completed": 1763088301
}
```
**创建任务时指定{Set at creation}**
```
curl --request POST \
  --url 'https://ark.oairoute.com/v1/videos' \
  --header 'Authorization: Bearer YOUR_ARK_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "model": "seedance-2.5",
    "prompt": "海面日落",
    "callback_url": "https://your-server.com/hooks/oairoute"
  }'
```
**200 提交任务时带上回调地址{Set at creation}**
```
{
  "id": "task_01KA040M0HP1GJWBJYZMKX1XS1",
  "status": "completed",
  "cost": 0.48,
  "credits_cost": 4.8,
  "result": {
    "videos": [
      {
        "url": "https://cdn.oairoute.com/v/task_01KA040M0HP1GJWBJYZMKX1XS1.mp4",
        "expires_at": 1763174708
      }
    ]
  },
  "created": 1763088289,
  "completed": 1763088332
}
```
