# Webhook 回调

视频生成等异步任务可通过 `callback_url` 注册回调，任务进入终态（`succeeded` / `failed`）时 XRToken 会向你的 URL 推送一次 `POST` 请求，无需轮询。

## 启用回调

在 `POST /v1/videos/generations` 请求体中加 `callback_url`：

```json
{
  "model": "doubao-seedance-2-0-260128",
  "content": [{ "type": "text", "text": "..." }],
  "callback_url": "https://your.app/webhooks/xrtoken"
}
```

## 载荷格式

```json
{
  "task_id": "5e1f2c8a-...",
  "status": "succeeded",
  "model": "doubao-seedance-2-0-260128",
  "video_url": "https://tos.../video.mp4",
  "created_at": "2026-04-15T10:00:00Z",
  "finished_at": "2026-04-15T10:01:32Z"
}
```

失败时：

```json
{
  "task_id": "5e1f2c8a-...",
  "status": "failed",
  "model": "doubao-seedance-2-0-260128",
  "error": { "code": "task_failed", "message": "..." },
  "created_at": "...",
  "finished_at": "..."
}
```

## 请求头

| Header | 含义 |
|---|---|
| `Content-Type` | `application/json` |
| `X-XRToken-Timestamp` | 发送时间戳（Unix 秒）|
| `X-XRToken-Signature` | `sha256=<hex>`，HMAC-SHA256 签名 |

## 验证签名

签名计算：

```
HMAC_SHA256(secret, timestamp + "." + raw_body)
```

`secret` 是账号专属的 Webhook Secret（Dashboard → 设置 → Webhook 查看 / 轮换；以 `whsec_` 开头）。

Node.js 示例：

```js
import crypto from 'node:crypto'

function verify(req, secret) {
  const ts = req.headers['x-xrtoken-timestamp']
  const sig = req.headers['x-xrtoken-signature']  // 'sha256=...'
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(ts + '.' + req.rawBody)  // 注意：使用原始未解析的 body
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))
}
```

Python 示例：

```python
import hmac, hashlib

def verify(headers, raw_body, secret):
    ts = headers['x-xrtoken-timestamp']
    sig = headers['x-xrtoken-signature'].split('=', 1)[1]
    expected = hmac.new(
        secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig, expected)
```

校验完签名后，建议同时检查 `timestamp` 与当前时间相差不超过 5 分钟，防止重放。

## 响应与重试

- 接收方需在 **10 秒内**返回 `2xx`，否则视为失败
- 失败/超时最多重试 **3 次**，间隔 `5s / 30s / 5min`
- 4 次仍失败的回调会标记为 `dropped`，不再投递；任务结果仍可通过 `GET /v1/videos/generations/{taskId}` 查询

## 幂等

同一 `task_id` 的回调在重试期间可能重复抵达。接收方应当用 `task_id` 做幂等。
