コンテンツにスキップ
HaneDOCSEN

アプリを作る

webhook

Hane のイベントを自分のシステムに webhook で届ける購読の登録、署名の確かめ方、送り直しと一時停止を説明します。

このページでは、案件の完了や Task の作成などのイベントを、自分のシステムへ webhook で受け取る方法を説明します。Hane はイベントを購読ごとの配信として記録し、署名を付けて送り、届くまで送り直します。

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 から読み直します。

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.1
content-type: application/cloudevents+json
webhook-id: <配信の ID>
webhook-timestamp: 1793952000
webhook-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":"..."}}

署名は 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 を読み直して並べ替えた文字列で確かめないでください。

  • 2xx を返したら届いたとみなします。3xx はリダイレクトをたどらずに失敗とし、10 秒で応答がなければ時間切れにします。
  • 失敗したら、10 秒・30 秒・2 分・10 分・30 分・1 時間・3 時間・6 時間の間をあけて、最大 8 回送り直します(およそ 10.6 時間)。使い切った配信は失敗になり、/v1/audit/dead-letters から入れ直せます。
  • 少なくとも 1 回届けるので、同じ配信が 2 回届くことがあります。受け手は webhook-id の値で重複を捨ててください。
  • 受け手の応答の本文は読まず、残しません。

受け手に 5 日以上届かず、続けて 20 回以上失敗した購読は一時停止(paused)になります。一時停止の間もイベントは配信として残り、捨てません。受け手を直したら再開します。再開すると、届かなかった配信を同じ webhook-id で少しずつ送り直します。

ターミナルウィンドウ
hanectl subscriptions list
hanectl subscriptions deliveries <id> --limit 20
hanectl subscriptions delivery <id> <delivery_id> # 送るたびの結果(状態コード・失敗の理由・時間)
hanectl subscriptions resume <id>
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 つの値で署名するので、その間に受け手を新しい値へ移します。