# Webhooks

> リソース書き込み、同意変更、および最終化されたドキュメントに関する署名付き、リトライ可能、軽量なイベント。TypeScript、Dart、Pythonでの署名検証に対応。

Source: https://developers.anpheros.com/ja/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` は `["*"]` または以下のいずれかを受け付けます。

| type | when | `data` |
|---|---|---|
| `resource.created` / `resource.updated` / `resource.deleted` | プロジェクトが FHIR リソース (REST v1, FHIR API またはバンドル) を作成した場合 — **または、アクセス権のあるレコードに別のプロジェクトが書き込んだ場合** (患者を作成した、またはアクティブなグラントを保持している場合)。この場合、`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` を返してください。それ以外（またはタイムアウト）は失敗とみなし、
最大24時間（10回試行）バックオフしてリトライし、その後 `exhausted` とマークされます。
デリバリーは少なくとも1回行われ、順不同で到着する可能性があります。重複排除には `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` — 新しいシークレット（古いシークレットは即座に無効になります。ギャップを許容できない場合は、まずサーバーを更新してください。2番目のエンドポイントを登録してから、古いものを削除します）。
- `PATCH /v1/webhooks/{id}` と `{"active": false}` は配信を一時停止します。保留中のものは、エンドポイントを削除すると破棄されます。

両方の SDK は、`webhooks`（`create`、`list`、`get`、`update`、`delete`、`rotateSecret`、`ping`、`deliveries`、`redeliver`）の下でこれを公開しています。
