# 回调
> 任务终态通知：签名验证、Header、重试策略与 payload 结构。

创建任务时传 `callback_url`（必须 HTTPS），任务到达终态后 Beatra 主动 POST 通知你。回调与轮询互不排斥；任务状态始终以 `GET /v1/tasks/{task_id}` 为准。

## 你会收到什么

`POST {callback_url}`，**10 秒内返回 2xx** 视为送达。

| Header | 说明 |
| --- | --- |
| `X-Event-Id` | 事件唯一 ID，重试间保持不变 —— **用它做幂等去重** |
| `X-Delivery-Id` | 每次投递尝试唯一 |
| `X-Delivery-Attempt` | 第几次尝试（从 1 开始） |
| `X-Task-Id` | 任务 ID |
| `X-Event-Type` | `task.succeeded` / `task.failed` / `task.canceled` |
| `X-Timestamp` | Unix 秒级时间戳 |
| `X-Signature-Key-Id` | 所选签名密钥 ID；仅签名投递携带 |
| `X-Signature` | 签名；仅签名投递携带，见下文 |

```json
{
  "event_id": "evt_01JX...",
  "event_type": "task.succeeded",
  "event_time": "2026-06-11T10:30:00.123456Z",
  "task": { "task_id": "task_01JX...", "status": "succeeded", "output": { "...": "..." } }
}
```

`task` 即完整 [Task envelope](https://docs.beatra.ai/zh/docs/mechanisms/async-tasks)。

## 选择签名或无签名投递

回调签名密钥与 API Key 完全独立。在 **Console → 开发者 → Webhook**
中管理，也可调用 `/account/callback-signing-keys` 接口创建、轮换和停用。
明文密钥只在创建或轮换签名密钥时展示一次。

- 只传 `callback_url`：无签名投递，不携带两个签名 Header；
- 同时传 `callback_url` 和有效的 `callback_signing_key_id`：签名投递；
- 所选密钥不可用时投递失败，Beatra 绝不会静默降级为无签名投递。

## 验证签名投递

```text
X-Signature: t=<unix_ts>,v1=<hex>
v1 = HMAC-SHA256( secret, "<unix_ts>." + raw_body )
```

```python
import hmac, hashlib, time

def verify(signature_header: str, raw_body: bytes, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    ts, sig = int(parts["t"]), parts["v1"]
    if abs(time.time() - ts) > 300:          # 拒绝 5 分钟以外的时间戳
        return False
    expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)
```

注意：必须对**原始请求体字节**计算，先解析再序列化会导致验签失败。

## 重试策略

投递失败（非 2xx 或超时）后，按以下间隔重试；总计 6 次投递（1 次首投 + 5 次重试）：

```text
1 分钟 → 5 分钟 → 30 分钟 → 2 小时 → 12 小时
```

- 重试期间 `X-Event-Id` 不变，请按它去重；
- 投递失败**不影响任务本身**——任务已是终态，随时可轮询兜底；
- 每次投递的状态可在 envelope 的 `callback` 字段（`pending / dispatching / delivered / retrying / failed`、`attempt_count`、`last_error`）和控制台回调日志中查看。
