アプリを作る
webhook
Hane のイベントを自分のシステムに webhook で届ける購読の登録、署名の確かめ方、送り直しと一時停止を説明します。
このページでは、案件の完了や Task の作成などのイベントを、自分のシステムへ webhook で受け取る方法を説明します。Hane はイベントを購読ごとの配信として記録し、署名を付けて送り、届くまで送り直します。
購読できるイベント
Section titled “購読できるイベント”| type | data に入る項目 |
|---|---|
workflows.case.completed |
case_id・workflow・outcome |
workflows.case.failed |
step |
workflows.case.cancelled |
— |
approvals.task.created・approvals.task.completed |
case_id(完了は outcome も) |
data.record.created・data.record.updated |
type・version |
data.import.completed |
type・rows・created・updated・failed |
documents.version.created |
document_id・version |
integrations.sync.succeeded・integrations.sync.failed |
id・connection |
integrations.export.completed・integrations.export.failed |
id・connection・type など |
agents.run.succeeded・agents.run.failed・agents.run.cancelled・agents.run.limit_exceeded |
run_id・agent・status・case_id |
data には、外へ出してよい項目だけが入ります。業務の値を含みうる項目は入らないので、受け手は ID で公開 API から読み直します。
購読を登録する
Section titled “購読を登録する”workflows:event-subscriptions.manage のスコープで登録します。宛先は、公開された DNS の名前の HTTPS の URL だけです。
const sub = await unwrap( hane.POST("/v1/workflows/event-subscriptions", { body: { name: "erp-sync", url: "https://erp.example.com/hooks/hane", event_types: ["workflows.case.completed", "approvals.task.created"], }, }),);// sub.signing_secret(whsec_...)はこの応答でしか返らない。秘密値の保管場所に置く- 1 つのプロジェクトで持てる購読は 20 までです。同じ名前の購読があれば 409 になります。
- 宛先と type は
PATCH /v1/workflows/event-subscriptions/{id}(If-Match が要る)で変えます。 - 消すと、終わっていない配信を取り消し、署名の秘密値も消します。配信の記録は 30 日読めます。
本文は CloudEvents の JSON です。
POST /hooks/hane HTTP/1.1content-type: application/cloudevents+jsonwebhook-id: <配信の ID>webhook-timestamp: 1793952000webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{"specversion":"1.0","id":"...","source":"...","type":"workflows.case.completed","time":"...","datacontenttype":"application/json","tenantid":"...","projectid":"...","environment":"dev","data":{"case_id":"...","workflow":"lease-expiry","outcome":"..."}}署名を確かめる
Section titled “署名を確かめる”署名は Standard Webhooks の形なので、既製のライブラリで確かめられます。自分で確かめるときは、<webhook-id>.<webhook-timestamp>.<本文> の HMAC-SHA256 を、秘密値の whsec_ の後ろを base64 で読んだ鍵で計算し、v1, の後ろの base64 と比べます。
import { createHmac, timingSafeEqual } from "node:crypto";
export function verify(secret: string, headers: Headers, body: string): boolean { const id = headers.get("webhook-id") ?? ""; const ts = headers.get("webhook-timestamp") ?? ""; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // 古すぎる要求を断る const key = Buffer.from(secret.slice("whsec_".length), "base64"); const expected = createHmac("sha256", key).update(`${id}.${ts}.${body}`).digest(); // 秘密値を回している間は、空白で区切って 2 つ並ぶ。どれか 1 つが合えば受け取る return (headers.get("webhook-signature") ?? "").split(" ").some((sig) => { const given = Buffer.from(sig.replace(/^v1,/, ""), "base64"); return given.length === expected.length && timingSafeEqual(given, expected); });}本文は受け取ったバイト列のまま使い、JSON を読み直して並べ替えた文字列で確かめないでください。
送り直しと重複
Section titled “送り直しと重複”- 2xx を返したら届いたとみなします。3xx はリダイレクトをたどらずに失敗とし、10 秒で応答がなければ時間切れにします。
- 失敗したら、10 秒・30 秒・2 分・10 分・30 分・1 時間・3 時間・6 時間の間をあけて、最大 8 回送り直します(およそ 10.6 時間)。使い切った配信は失敗になり、
/v1/audit/dead-lettersから入れ直せます。 - 少なくとも 1 回届けるので、同じ配信が 2 回届くことがあります。受け手は
webhook-idの値で重複を捨ててください。 - 受け手の応答の本文は読まず、残しません。
一時停止と再開
Section titled “一時停止と再開”受け手に 5 日以上届かず、続けて 20 回以上失敗した購読は一時停止(paused)になります。一時停止の間もイベントは配信として残り、捨てません。受け手を直したら再開します。再開すると、届かなかった配信を同じ webhook-id で少しずつ送り直します。
hanectl subscriptions listhanectl subscriptions deliveries <id> --limit 20hanectl subscriptions delivery <id> <delivery_id> # 送るたびの結果(状態コード・失敗の理由・時間)hanectl subscriptions resume <id>秘密値を回す
Section titled “秘密値を回す”const r = await unwrap(hane.POST("/v1/workflows/event-subscriptions/{id}/rotate-secret", { params: { path: { id } }, body: {} }));// r.signing_secret が新しい値。r.previous_secret_expires_at までは新旧 2 つで署名する24 時間は新旧の 2 つの値で署名するので、その間に受け手を新しい値へ移します。