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

# Webhook 管理 API

> 通过 Dashboard、API 或 MCP 配置任务通知，验证签名并排查投递失败。

Webhook 在异步任务完成、失败或超时时通知你的应用。可在 [Dashboard → API → Webhooks](https://tokenlab.sh/dashboard/api?tab=webhooks)、管理 API 或 TokenLab MCP 的 `full` 模式配置；三者使用同一工作区的端点和投递记录。

## 用哪个 Key

| 凭据                | 用途                            | 使用位置                                                      |
| ----------------- | ----------------------------- | --------------------------------------------------------- |
| 管理令牌 `mt-…`       | 创建、读取、更新、删除、测试 Webhook，轮换签名密钥 | `/v1/management/webhooks*` 的 `Authorization: Bearer mt-…` |
| 模型 API Key `sk-…` | 调用模型、查询任务                     | 模型 API 和 `/v1/tasks/{id}`                                 |
| 签名密钥 `whsec_…`    | 验证收到的通知来自 TokenLab            | 你的回调接收服务，不作为 API Bearer Token                             |

在 [Dashboard → API → Management Tokens](https://tokenlab.sh/dashboard/api?tab=tokens) 创建管理令牌，选择与提交模型任务的 API Key 相同的工作区。令牌只能访问所属工作区，不能跨工作区读取或修改 Webhook。`sk-…` 和 `whsec_…` 不能代替 `mt-…`。Dashboard 配置使用已登录的工作区管理员身份。

管理令牌与签名密钥只保存在后端，不放入前端代码、提示词、代码仓库或 URL。Management Token 还可调用其他工作区管理接口，并不是只拥有 Webhook 权限的专用令牌。

## 创建与管理

```bash theme={null}
export TOKENLAB_MANAGEMENT_TOKEN="mt-your-management-token"
curl https://api.tokenlab.sh/v1/management/webhooks \
  -H "Authorization: Bearer $TOKENLAB_MANAGEMENT_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://your-app.example/webhooks/tokenlab","events":["task.completed","task.failed","task.timeout"],"description":"生产任务通知"}'
```

成功返回 `201`，包含端点 `id` 和仅显示一次的 `secret`（`whsec_…`）。立即安全保存；查询、列表、更新响应不包含签名密钥。每个工作区最多 10 个端点。URL 必须是公网 HTTPS，不能包含账号密码、查询参数或 fragment；不跟随重定向。

| 操作                                                                                                                                                     | 接口                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| [列表](/zh/api-reference/management/list-webhooks) / [创建](/zh/api-reference/management/create-webhook)                                                   | `GET` / `POST /v1/management/webhooks`                               |
| [查询](/zh/api-reference/management/get-webhook) / [更新](/zh/api-reference/management/update-webhook) / [删除](/zh/api-reference/management/delete-webhook) | `GET` / `PATCH` / `DELETE /v1/management/webhooks/{webhookId}`       |
| [轮换签名密钥](/zh/api-reference/management/rotate-webhook-secret)                                                                                           | `POST /v1/management/webhooks/{webhookId}/rotate-secret`             |
| [发送测试](/zh/api-reference/management/test-webhook)                                                                                                      | `POST /v1/management/webhooks/{webhookId}/test`                      |
| [投递记录](/zh/api-reference/management/list-webhook-deliveries)                                                                                           | `GET /v1/management/webhooks/{webhookId}/deliveries?page=1&limit=50` |

`PATCH {"is_active":false}` 暂停，`PATCH {"is_active":true}` 恢复并重置连续失败计数。轮换只返回一次新密钥，接收端应立即更新；正在发送的请求可能仍使用旧密钥，切换时可短暂同时接受新旧两把密钥。轮换不可撤销。

## 事件与载荷

订阅的是之后产生的异步任务终态事件，不补发历史事件，也不针对同步结果发送通知。同一工作区内符合事件类型的任务都会触发通知，用 `data.taskId` 关联创建响应的任务 ID。通知字段使用 camelCase，管理 API 响应使用 snake\_case。

```json theme={null}
{
  "id": "event-unique-id",
  "type": "task.completed",
  "created": 1790000000,
  "data": {
    "taskType": "video",
    "taskId": "ldtask_0123456789abcdef0123456789abcdef",
    "model": "your-selected-model",
    "durationMs": 42000,
    "resultUrls": ["https://your-result-url.example/video.mp4"],
    "settledCost": 0.12
  }
}
```

| 事件               | data 字段                                                                |
| ---------------- | ---------------------------------------------------------------------- |
| `task.completed` | `taskType`、`taskId`，可选 `model`、`durationMs`、`resultUrls`、`settledCost` |
| `task.failed`    | `taskType`、`taskId`、`error`、`errorCode`、`retryable`、`refundOutcome`    |
| `task.timeout`   | `taskType`、`taskId`、`durationMs`、`refundOutcome`                       |
| `webhook.test`   | 测试消息和订阅类型，仅测试操作发送                                                      |

字段依任务而定，可能缺省。使用原工作区的 `sk-…` 查询 `GET /v1/tasks/{id}`，获取权威结果和计费状态。失败事件的 `retryable` 描述生成失败是否可重试，不表示应持续轮询终态任务，也不授权自动创建新的付费任务。

## 先验签，再处理

请求头包含 `X-Webhook-ID`、`X-Webhook-Timestamp`（Unix 秒）和 `X-Webhook-Signature`（`sha256=<hex>`）。使用完整 `whsec_…` 作为密钥，对时间戳原字符串、一个英文句点和**原始请求体字节**计算 HMAC-SHA256。不能先解析 JSON 再序列化验签。

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyWebhook(rawBody, headers, secrets) {
  const timestamp = headers['x-webhook-timestamp'];
  const signature = headers['x-webhook-signature'];
  if (typeof timestamp !== 'string' || !/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  if (typeof signature !== 'string' || !/^sha256=[a-f0-9]{64}$/.test(signature)) return false;
  const received = Buffer.from(signature.slice(7), 'hex');
  return secrets.some(secret => {
    const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
    return timingSafeEqual(expected, received);
  });
}
```

验签后检查请求体 `id` 与 `X-Webhook-ID` 相同，把事件 ID 与处理任务原子保存，尽快返回 `2xx`，耗时业务由自己的队列处理。通知可能重复、乱序，应按接收端点和事件 ID 去重。五分钟时间窗口限制过旧请求，事件 ID 去重阻止窗口内重复处理。

## 重试、记录与恢复

每轮最多尝试 3 次，间隔为 1 秒、4 秒；单次 HTTP 超时 10 秒，每次重算时间戳和签名。网络失败、`429`、`5xx` 可重试；其他 `4xx`、重定向、非法网络地址停止本轮投递。暂时失败可能触发后续事件重试，继续使用同一投递 ID。连续 10 轮失败会自动暂停端点。

通过测试接口和投递记录查看 `outcome`、`http_status`、累计 `attempts` 和 `delivered_at`。测试接口返回 `200` 只表示尝试已记录，必须检查 `outcome == "delivered"` 才算接收成功。修复接收服务、恢复端点，再发送测试。历史记录只保存投递元数据，不保存完整载荷，暂不提供手工重放历史事件；接收服务暂停或不可用期间，应保留任务 ID 并用任务查询补偿核对。

## MCP 配置

Webhook 工具位于 `full` 模式。管理令牌与模型 Key 分开配置；仅使用目录和 Webhook 管理时不需要模型 Key。

```json theme={null}
{
  "mcpServers": {
    "tokenlab": {
      "command": "npx",
      "args": ["-y", "@tokenlabai/mcp-server@latest"],
      "env": {
        "TOKENLAB_MCP_TOOL_PROFILE": "full",
        "TOKENLAB_MANAGEMENT_TOKEN": "mt-your-management-token",
        "TOKENLAB_API_KEY": "sk-your-inference-key"
      }
    }
  }
}
```

工具包括 `list_webhooks`、`create_webhook`、`get_webhook`、`update_webhook`、`delete_webhook`、`rotate_webhook_secret`、`test_webhook`、`list_webhook_deliveries`。凭据不作为工具参数填写，只向可信 MCP 客户端授予管理访问权。

## 轮询兜底

Webhook 不取消查询能力，也不新增轮询限额。轮询时使用创建响应里的 `poll_url`，处理中逐步增加间隔，终态后停止。遇到 `401`、`403`、`404` 或 `error.retryable == false` 停止；`503 async_task_owner_unavailable` 属于临时故障，可退避重试。不存在或过期的任务返回 `404 async_task_not_found`，重复查询不能恢复或创建任务。

[Seedance 兼容 API 的 `callback_url`](/zh/api-reference/video/create-volc-compatible-seedance-task) 是独立的单任务回调，在创建请求中配置；它不使用这里的工作区事件载荷或 HMAC 请求头。
