# Webhooks

Webhook 在事情发生时主动通知你自己的系统，不用反复来查。在控制台添加一个 https 地址并选好事件；每当其中一件事发生，SocialToAI 就向它推送一条签名的 JSON 消息。消息里只有数字、编号和链接，没有任何人写的文字。投递至少一次，失败会在大约一天内重试；Webhook 不收费。

- 页面：https://socialtoai.com/zh/docs/webhooks/
- 最近更新：2026-10-25

## 添加端点

- 在控制台打开设置 → Webhook (https://socialtoai.com/console/settings/)，添加一个端点：你自己的 https 地址，以及它要接收的事件。端点只在控制台管理，没有用 Key 调用的路由。
- 地址必须是 https，并且指向公网。保存前我们会先给它推送一条签名的检查消息（`type` 是 `endpoint.check`），它必须回 2xx。
- 然后显示签名密钥，只显示这一次。它以 `whsec_` 开头，请当场复制到你的接收端配置里。之后控制台只显示它的最后四位。
- 每个账户最多 10 个端点。端点属于账户，不属于某把 Key：撤销 Key 不影响它。

## 事件

每条消息都是一个 JSON 对象：`id`（事件编号）、`type`、`created_at`，然后是这个事件自己的字段。`summary` 里按名字放着这次运行的数字。

| type | 什么时候发 | 字段 |
| --- | --- | --- |
| `tool_run.finished` | 现成工具的一次运行以 `ok` 或 `empty` 结束。 | tool, handle, status, summary, cost_credits, quoted_credits, result_url |
| `tool_run.failed` | 现成工具的一次运行以 `partial` 或 `failed` 结束。 | tool, handle, status, summary, cost_credits, quoted_credits, result_url |
| `batch.finished` | 一个批量任务结束，不论什么状态。 | tool, batch_id, summary, cost_credits, download_url |
| `schedule_run.finished` | 定期运行的一次运行完成。`summary.new_items` 是上一次没有的条数，`new_links` 是其中前几条的链接。 | tool, schedule_id, handle, summary, cost_credits, new_links, report_url |
| `schedule.alert` | 定期运行的主要数字变化超过 `alert_percent`，或从 0 变成非 0。 | tool, schedule_id, handle, summary, cost_credits, report_url |
| `balance.low` | 余额跌破提醒线，和余额提醒邮件同时发出。 | balance_credits, threshold_credits, billing_url |
| `daily_cap.reached` | 当天（UTC）账户每日上限第一次拒绝调用。 | daily_limit_credits, resets_at, settings_url |

**一条消息**

```
{
  "id": "evt_0123456789abcdef01234567",
  "type": "schedule_run.finished",
  "created_at": "2026-10-25T09:00:12.000Z",
  "tool": "search",
  "schedule_id": "00000000-0000-4000-8000-000000000021",
  "handle": "res_00000000000040008000000000000003",
  "summary": {
    "new_items": 12
  },
  "cost_credits": 2,
  "report_url": "https://socialtoai.com/console/tools/"
}
```

_示例：编号和数字是占位。_

## 校验签名

每条消息带两个请求头。`SocialToAI-Signature` 是 `t=<秒>,v1=<十六进制>`：`t` 是签名时的 Unix 时间，`v1` 是用你的签名密钥对 `<t>.<正文>` 做的 HMAC-SHA256（十六进制），正文是原始请求体。`SocialToAI-Webhook-Id` 是这次投递的编号，重试时不变。

- 用收到的原始正文计算 HMAC，不要先解析 JSON；比较时用常数时间比较。
- `t` 和你的时钟相差超过 5 分钟的消息直接拒绝，防止重放。
- 同一条消息可能到达不止一次。已经处理过的 `id` 直接跳过；在控制台手动重发的消息 `id` 不变，投递编号是新的。

**Node.js**

```
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as it arrived, before any JSON parsing.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const seconds = Number(parts.t);
  if (!Number.isInteger(seconds) || Math.abs(Date.now() / 1000 - seconds) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${seconds}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}
```

**Python**

```
import hashlib, hmac, time

# raw_body: the request body as bytes, exactly as it arrived.
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    seconds = int(parts.get("t", "0"))
    if abs(time.time() - seconds) > 300:
        return False
    signed = f"{seconds}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))
```

## 投递与重试

- 在 8 秒内回 2xx。不跟随重定向；每次推送前都会重新检查地址。
- 其他状态码、没有应答，或 2 分钟内没有结果，都算一次失败的尝试。之后依次在 1 分钟、5 分钟、30 分钟、2 小时、6 小时、24 小时后重试，连首次共 7 次。
- 连续 10 次尝试失败，只暂停这一个端点，并给你发一封邮件；其他照常，定期运行也照常跑。在控制台里可以恢复。
- 控制台保留最近 30 天的投递记录，可以发送测试事件，也可以重发任意一条。
