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

# Webhook

> 在操作工作流到达检查点或完成时接收实时通知。

Webhook 让您可以在操作工作流到达检查点或完成时接收实时通知。`new_content` 操作所使用的 [执行工作流](/api-reference/actions#execute-action) 必须使用 Webhook。

## 注册 Webhook

```
POST /webhooks
```

注册一个新的 Webhook 端点以接收事件通知。

### 请求体

| 字段       | 类型        | 必填  | 描述                            |
| -------- | --------- | --- | ----------------------------- |
| `url`    | string    | Yes | 用于接收 Webhook 事件的 HTTPS 端点 URL |
| `events` | string\[] | Yes | 要订阅的事件类型数组                    |

### 有效事件

| 事件                  | 描述                      |
| ------------------- | ----------------------- |
| `action.checkpoint` | 工作流在审核检查点暂停（大纲、草稿或终稿审核） |
| `action.completed`  | 工作流成功完成                 |
| `action.failed`     | 工作流遇到错误                 |

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks" \
      -H "X-API-Key: tk_live_..." \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks/topify",
        "events": ["action.checkpoint", "action.completed", "action.failed"]
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    resp = client.post("/webhooks", json={
        "url": "https://example.com/webhooks/topify",
        "events": ["action.checkpoint", "action.completed", "action.failed"],
    })
    ```
  </Tab>

  <Tab title="Node.js（服务端）">
    ```javascript theme={null}
    const resp = await fetch(`${BASE}/webhooks`, {
      method: "POST",
      headers: { ...headers, "Content-Type": "application/json" },
      body: JSON.stringify({
        url: "https://example.com/webhooks/topify",
        events: ["action.checkpoint", "action.completed", "action.failed"],
      }),
    });
    ```
  </Tab>
</Tabs>

### 响应（201 Created）

```json theme={null}
{
  "success": true,
  "data": {
    "id": "wh_a1b2c3d4-...",
    "url": "https://example.com/webhooks/topify",
    "events": ["action.checkpoint", "action.completed", "action.failed"],
    "is_active": true,
    "secret": "whsec_k7m9p2...",
    "created_at": "2026-04-08T12:00:00Z"
  }
}
```

<Warning>
  `secret` 字段仅在创建时返回一次。请妥善保管 —— 您将需要它来验证 Webhook 签名。
</Warning>

### 错误响应

| 状态    | 原因                                          |
| ----- | ------------------------------------------- |
| `400` | 缺少 `url`、URL 不是 HTTPS、`events` 数组为空，或事件类型无效 |
| `403` | API 密钥具有只读权限                                |
| `409` | 已存在使用此 URL 的 Webhook                        |

***

## 列出 Webhook

```
GET /webhooks
```

返回您账户下注册的所有 Webhook。

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks" \
      -H "X-API-Key: tk_live_..."
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    resp = client.get("/webhooks")
    ```
  </Tab>

  <Tab title="Node.js（服务端）">
    ```javascript theme={null}
    const resp = await fetch(`${BASE}/webhooks`, { headers });
    ```
  </Tab>
</Tabs>

### 响应

```json theme={null}
{
  "success": true,
  "data": [
    {
      "id": "wh_a1b2c3d4-...",
      "url": "https://example.com/webhooks/topify",
      "events": ["action.checkpoint", "action.completed", "action.failed"],
      "is_active": true,
      "created_at": "2026-04-08T12:00:00Z"
    }
  ]
}
```

<Note>
  列表响应中不包含 `secret` 字段。它仅在 Webhook 首次创建时返回。
</Note>

***

## 删除 Webhook

```
DELETE /webhooks/{webhook_id}
```

永久删除一个 Webhook。事件将不再发送至该端点。

<Warning>
  删除前请确认 Webhook ID 和目标地址。删除会立即停止事件投递；重新注册同一 URL 会生成新的签名密钥。
</Warning>

### 路径参数

| 参数           | 类型     | 描述         |
| ------------ | ------ | ---------- |
| `webhook_id` | string | Webhook ID |

<Tabs>
  <Tab title="cURL">
    ```bash theme={null}
    curl -X DELETE "https://topify-customer-api-production.up.railway.app/api/public/v1/webhooks/{webhook_id}" \
      -H "X-API-Key: tk_live_..."
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    resp = client.delete(f"/webhooks/{webhook_id}")
    ```
  </Tab>

  <Tab title="Node.js（服务端）">
    ```javascript theme={null}
    const resp = await fetch(`${BASE}/webhooks/${webhookId}`, {
      method: "DELETE",
      headers,
    });
    ```
  </Tab>
</Tabs>

### 响应

```json theme={null}
{
  "success": true,
  "data": {
    "id": "wh_a1b2c3d4-...",
    "message": "Webhook deleted successfully"
  }
}
```

### 错误响应

| 状态    | 原因           |
| ----- | ------------ |
| `403` | API 密钥具有只读权限 |
| `404` | 未找到 Webhook  |

***

## Webhook 载荷格式

事件触发时，Topify 会向您注册的 URL 发送一个 POST 请求，JSON 请求体如下。

### `action.checkpoint` 载荷

```json theme={null}
{
  "event": "action.checkpoint",
  "timestamp": "2026-04-08T21:19:13.463Z",
  "data": {
    "action_id": "a1b2c3d4-...",
    "workflow_id": "w1x2y3z4-...",
    "checkpoint": "outline",
    "content": "## Article Outline\n\n1. Introduction to project management tools...",
    "options": [
      { "action": "approve", "label": "Approve & Write" },
      { "action": "edit", "label": "Edit Outline" },
      { "action": "regenerate", "label": "Regenerate" },
      { "action": "reject", "label": "Cancel" }
    ]
  }
}
```

### `action.completed` 载荷

```json theme={null}
{
  "event": "action.completed",
  "timestamp": "2026-04-08T21:25:00.000Z",
  "data": {
    "action_id": "a1b2c3d4-...",
    "workflow_id": "w1x2y3z4-...",
    "result": {
      "title": "Best Project Management Tools in 2026",
      "content": "..."
    }
  }
}
```

### `action.failed` 载荷

```json theme={null}
{
  "event": "action.failed",
  "timestamp": "2026-04-08T21:22:00.000Z",
  "data": {
    "action_id": "a1b2c3d4-...",
    "workflow_id": "w1x2y3z4-...",
    "error": "LLM generation timed out after 120 seconds"
  }
}
```

### 载荷字段

| 字段                 | 类型            | 描述                                                           |
| ------------------ | ------------- | ------------------------------------------------------------ |
| `event`            | string        | 事件类型（`action.checkpoint`、`action.completed`、`action.failed`） |
| `timestamp`        | string        | 事件发生的 ISO 8601 时间戳                                           |
| `data.action_id`   | string (UUID) | 触发事件的操作                                                      |
| `data.workflow_id` | string (UUID) | 当前活跃的工作流 ID                                                  |
| `data.checkpoint`  | string        | 检查点名称（仅 `action.checkpoint`）                                 |
| `data.content`     | string        | 待审核内容（仅 `action.checkpoint`）                                 |
| `data.options`     | object\[]     | 可用的响应操作（仅 `action.checkpoint`）                               |
| `data.result`      | object        | 最终输出（仅 `action.completed`）                                   |
| `data.error`       | string        | 错误消息（仅 `action.failed`）                                      |

***

## 验证签名

每个 Webhook 请求都包含一个 `X-Webhook-Signature` 请求头，其中是基于未经改动的原始请求体并使用创建时返回的 Webhook 密钥计算得出的 HMAC-SHA256 签名。

解析或处理载荷前，始终使用常量时间比较验证签名。缺少签名或签名无效时拒绝请求；投递可能重试，因此处理程序必须具备幂等性。

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import hmac
    import hashlib

    def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
        expected = hmac.new(
            secret.encode(),
            body,
            hashlib.sha256,
        ).hexdigest()
        return hmac.compare_digest(expected, signature)

    # In your webhook handler:
    signature = request.headers["X-Webhook-Signature"]
    is_valid = verify_webhook(request.body, signature, WEBHOOK_SECRET)
    ```
  </Tab>

  <Tab title="Node.js（服务端）">
    ```javascript theme={null}
    import { createHmac, timingSafeEqual } from "crypto";

    function verifyWebhook(body, signature, secret) {
      const expected = Buffer.from(createHmac("sha256", secret)
        .update(body)
        .digest("hex"), "utf8");
      const received = Buffer.from(signature || "", "utf8");
      return expected.length === received.length
        && timingSafeEqual(expected, received);
    }

    // In your webhook handler:
    const signature = req.headers["x-webhook-signature"];
    const isValid = verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET);
    ```
  </Tab>
</Tabs>

<Note>
  您的端点应在 10 秒内返回 `200` 状态码。如果端点未能响应或返回非 2xx 状态，Topify 将以指数退避策略最多重试 3 次。
</Note>
