# Webhooks

> 签名、重试、精简的事件，用于资源写入、同意变更和已完成文档，支持 TypeScript、Dart 和 Python 的签名验证。

Source: https://developers.anpheros.com/zh/guides/webhooks

当您的项目中发生某些事件时，Anpheros Platform 可以调用您的服务器：资源被写入、某人授予或撤销访问权限、文档已完成。事件是**精简的**：仅包含 ID、类型、版本 — 从不包含临床内容。如果需要，请通过 API 读取资源。每次传递都会进行签名；在信任之前请验证签名。

## 注册端点

```bash
curl -X POST $BASE/v1/webhooks -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://your.app/anpheros/hook", "events": ["resource.created", "consent.revoked"], "description": "prod"}'
```

响应包含 `secret` (`whsec_…`) **一次**。请妥善存储，您需要它来验证签名。
`events` 接受 `["*"]` 或以下任意一项：

| 类型 | 何时 | `data` |
|---|---|---|
| `resource.created` / `resource.updated` / `resource.deleted` | 您的项目写入了 FHIR 资源（REST v1、FHIR API 或 bundle）—— **或另一个项目写入了您有权访问的记录**（您创建了患者，或您持有有效的授权）；然后 `data.external` 是 `true`，`data.source` 指明了写入者：`author_type`（patient / practitioner / device / import / ai）、`organization`（写入者代表的显示名称）、`practitioner`（已知时，来自目录的名称）、`source_system` | `resource_type`、`id`、`patient`（您看到的 ID）、`version`、`last_updated`，以及对于外部写入 `external`、`source` |
| `consent.granted` | 某人通过同意页面授予了您的应用访问权限（或受信任的应用链接了其记录） | `grant_id`、`patient`、`application_id`、`scopes`、`purpose`、`expires_at` |
| `consent.revoked` | 某人（或平台）终止了授权 | 与上面相同，外加 `revoked_at`、`reason` |
| `document.finalized` | 文档上传已完成并进行了校验和检查 | `id`、`patient`、`content_type`、`size`、`sha256` |
| `proposal.created` | AI 代理建议写入您可以看到的记录；等待人员批准 | `id`、`patient`、`action`、`resource_type` |
| `ping` | 您请求了测试交付 | `endpoint` |

端点是按项目**和环境**划分的：`sk_test_` 密钥管理沙盒端点，
`sk_live_` 密钥管理生产端点。URL 必须是公共 `https://` 端点。注册（以及之后任何 `url` 的更改）会拒绝本地、私有、链路本地、元数据和其他保留地址——`localhost`、`127.0.0.1`、`::1`、`10.0.0.0/8`、`192.168.0.0/16`、`fd00::/8` 等——以及解析到这些地址的名称，在 `400` `invalid` 字段 `url` 中；这在沙盒中也适用。每次交付都会再次检查地址，并且仅连接到它刚刚检查过的 IP 地址，因此一个后来指向私有网络的名称（DNS 重新绑定）将无法收到任何内容。要在开发过程中在您自己的机器上接收事件，请通过公共 HTTPS 隧道公开它，并注册隧道的地址。
每个项目和环境最多 10 个端点。

## 交付

```
POST https://your.app/anpheros/hook
Content-Type: application/json
User-Agent: Anpheros-Webhooks/1.0
Anpheros-Event: resource.created
Anpheros-Event-Id: evt_…
Anpheros-Delivery: whd_…
Anpheros-Signature: t=1789800000,v1=5f3a…

{"id":"evt_…","type":"resource.created","created_at":"2026-09-19T12:00:00.000Z","environment":"production",
 "project":"proj_…","data":{"resource_type":"Observation","id":"…","patient":"…","version":1,"last_updated":"…"}}
```

在 10 秒内响应任何 `2xx`。其他任何情况（或超时）都视为失败，交付将以指数退避（backoff）重试，最多重试 24 小时（10 次），然后标记为 `exhausted`。
交付是至少一次（at-least-once）的，并且可能会乱序到达：使用 `Anpheros-Event-Id` 进行去重，使用 `data.version` 忽略过时的更新。

## 验证签名

`v1` 是使用您的密钥对 `<t>.<raw body>` 进行 HMAC-SHA256 计算的结果；如果 `t` 的时间戳早于 5 分钟，则拒绝。

```ts
import { verifyWebhookSignature } from '@anpheros/sdk';
app.post('/anpheros/hook', express.raw({ type: 'application/json' }), async (req, res) => {
  const ok = await verifyWebhookSignature({ secret: process.env.ANPHEROS_WEBHOOK_SECRET!, header: req.header('anpheros-signature'), body: req.body });
  if (!ok) return res.status(400).end();
  const event = JSON.parse(req.body.toString());
  // … enqueue and return quickly
  res.status(204).end();
});
```

```dart
import 'package:anpheros_sdk/anpheros_sdk.dart';
final ok = verifyWebhookSignature(secret: secret, header: request.headers['anpheros-signature'], body: rawBodyBytes);
```

```python
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

## 操作

- `GET /v1/webhooks/{id}/deliveries?status=failed` — 日志，最新在前，包含状态码和错误信息。
- `POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver` — 立即重新发送一次。
- `POST /v1/webhooks/{id}/ping` — 测试事件。
- `POST /v1/webhooks/{id}/rotate` — 新密钥（旧密钥立即失效；如果不能容忍中断，请先更新您的服务器：注册第二个端点，然后删除旧的）。
- `PATCH /v1/webhooks/{id}` 和 `{"active": false}` 会暂停发送；当您删除端点时，待处理的发送将被丢弃。

两个 SDK 都在 `webhooks` 下暴露了这些功能（`create`、`list`、`get`、`update`、`delete`、`rotateSecret`、`ping`、`deliveries`、`redeliver`）。
