任务完成回调(Webhook)
- 任务完成或失败时,由 OAIRoute 主动推送结果
- 免去轮询,长耗时任务尤其划算
- 推送失败会按退避策略重试
认证
所有接口均使用 Bearer Token 进行认证。
创建任务时同样需要在请求头里带上 Key。
Seedance 系列的接口需要在 ark 站 单独获取 Key,与 api 站的 Key 不通用。
请求时在请求头中添加:
Authorization: Bearer YOUR_ARK_KEYAll endpoints authenticate with a Bearer token.
Key is also required in the request header when creating a task.
Seedance series endpoints require a key obtained separately from the ark site; it isn't shared with the api site's key.
Add it to the request header:
Authorization: Bearer YOUR_ARK_KEY配置方式
在创建任务时传入 callback_url,任务进入终态后就会向这个地址发起 POST 请求。
{
"model": "seedance-2.5",
"prompt": "海面日落,镜头缓慢前推",
"callback_url": "https://your-server.com/hooks/oairoute"
}callback_url 必须是公网可达的 HTTP(S) 地址。本地开发可以先用内网穿透工具暴露端口,或改用轮询。
Pass callback_url when creating a task; once the task reaches a terminal state, a POST request is sent to that address.
{
"model": "seedance-2.5",
"prompt": "Sunset over the sea, camera slowly pushing in",
"callback_url": "https://your-server.com/hooks/oairoute"
}callback_url must be a publicly reachable HTTP(S) address. For local development, expose a port with a tunneling tool first, or poll instead.
回调请求参数
任务 ID,与轮询接口里的 id 一致。
任务状态。completed 成功完成,failed 失败,cancelled 用户取消。
本次任务扣费金额。
本次任务扣费积分。
任务结果。仅 completed 时出现。
图像任务结果数组,元素含 url 与 expires_at。
视频任务结果数组。
错误详情。仅 failed 时出现。
任务创建时间戳。
任务完成时间戳。
The task ID, matching id in the polling endpoint.
The task status. completed succeeded, failed failed, cancelled cancelled by the user.
The amount charged for this task.
The credits charged for this task.
The task result. Appears only when completed.
The result array for image tasks; elements have url and expires_at.
The result array for video tasks.
Error details. Appears only when failed.
Task creation timestamp.
Task completion timestamp.
响应约定
接收端返回 2xx 即视为推送成功。返回其他状态码,或 10 秒内没有响应,会触发重试。
接收端需要具备幂等能力:同一个 task_id 可能被推送多次。建议按 task_id 做去重,处理过就直接返回 200。
Returning 2xx from the receiving end counts as a successful delivery. Any other status code, or no response within 10 seconds, triggers a retry.
The receiving end must be idempotent: the same task_id may be pushed more than once. Deduplicate by task_id and return 200 immediately if you've already processed it.
重试策略
| 次数 | 间隔 | 说明 |
|---|---|---|
| 第 1 次 | 立即 | 初始推送 |
| 第 2 次 | 30 秒后 | 上次失败 |
| 第 3 次 | 2 分钟后 | 上次失败 |
| 第 4 次 | 10 分钟后 | 上次失败 |
| 第 5 次 | 30 分钟后 | 最后一次,仍失败则放弃 |
重试期间任务状态已经落库,随时可以用任务状态接口补查。
| Attempt | Interval | Notes |
|---|---|---|
| 1st | Immediately | Initial push |
| 2nd | After 30 s | Previous attempt failed |
| 3rd | After 2 min | Previous attempt failed |
| 4th | After 10 min | Previous attempt failed |
| 5th | After 30 min | Last attempt; give up if it fails |
The task state is already persisted during retries, so you can always check it with the task status endpoint.
回调示例
{
"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
}{
"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
}一个最小接收端
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}, 200from 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: # idempotent dedupe
return {"ok": True}, 200
seen.add(task_id)
if payload.get("status") == "completed":
for item in payload["result"].get("videos", []):
download(item["url"]) # save to disk immediately; don't rely on the CDN
else:
log_error(payload.get("error"))
return {"ok": True}, 200使用示例
提交任务时带上回调地址
在生成请求里传 callback_url,任务状态变化时平台会 POST 到这个地址。比轮询省请求,也更及时。
处理完成回调
回调体里的 status 决定走哪个分支。产物 URL 会过期,收到就下载并落盘,不要只存链接。
@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 去重,避免重复下载或重复入库。
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,不用先部署。
# 终端一:暴露本地 8000 端口
ngrok http 8000
# 终端二:把打印出的公网地址填进回调
curl --request POST https://oairoute-docs.ngrok-free.app/hooks/oairouteInclude a callback URL when submitting
Pass callback_url in the generation request, and the platform POSTs to that address as the task status changes. It saves requests compared with polling and is more timely.
Handle the completion callback
The status in the callback body decides which branch to take. Artifact URLs expire, so download on receipt rather than storing only the link.
@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}, 200Deduplicate by task ID
Network jitter can deliver the same callback more than once. Deduplicate by id to avoid double downloads or duplicate records.
import redis
seen = redis.Redis()
@app.post("/hooks/oairoute")
def hook():
payload = request.get_json(force=True)
task_id = payload.get("id")
# write only on first sight; True means it's new
if not seen.set("hook:" + task_id, "1", nx=True, ex=86400):
return {"ok": True}, 200
handle(payload)
return {"ok": True}, 200Local debugging
During development, expose your local port with something like ngrok and put the public URL into callback_url; no deployment needed.
# terminal 1: expose local port 8000
ngrok http 8000
# terminal 2: use the printed public URL in the callback
curl --request POST https://oairoute-docs.ngrok-free.app/hooks/oairoute