/v1/account/webhook-endpoints
account/webhook-endpoints
Webhook:モニタリングが新しいシグナルを送る先と、その検証方法。
Webhookエンドポイントは、モニターの新しいシグナルを私たちが送る先となる、あなたのサーバーのHTTPSアドレスです。すべてのメッセージは署名されているため、私たちから届いたものかを確認できます。
コストと制限
| 内容 | クレジット |
|---|---|
| Webhookエンドポイントと配信の管理 | 無料 |
制限:1分あたり 30 リクエスト。 作成、更新、削除:1分あたり 10 回。 チェック、テスト、新しいシークレット:1分あたり 6 回。
各アカウントは最大 10 個のWebhookエンドポイントを持てます。アーカイブ済みのものはカウントされません。各Webhookエンドポイントには最大 10 個のカスタムヘッダーを付けられます。
管理者トークンまたはAPIキー
署名シークレットの作成と、Webhookエンドポイントの有効化、無効化、アーカイブには管理者トークンが必要です。アカウント › APIキーの「開発者向け:APIでキーを管理」の領域で作成します。有効期間は1年で、新しいものが古いものを置き換えます。
管理者トークンはAuthorization: Bearer YOUR_ADMIN_TOKENとして送ります。その他の呼び出しはすべてAPIキーでも動きます。
account/monitorsと同じく、すべての書き込み呼び出しにはヘッダーIdempotency-Keyが必要です。
エンドポイント
| メソッド | パス | 用途 | アクセス |
|---|---|---|---|
| GET |
/v1/account/webhook-endpoints |
すべてのWebhookエンドポイント(アーカイブ済みを除く)。 | APIキーまたは管理者トークン |
| POST |
/v1/account/webhook-endpoints |
Webhookエンドポイントを作成。 | 管理者トークン |
| POST |
/v1/account/webhook-endpoints/{id}/verify |
検証。 | APIキーまたは管理者トークン |
| POST |
/v1/account/webhook-endpoints/{id}/test |
テストメッセージを送信。 | APIキーまたは管理者トークン |
| POST |
/v1/account/webhook-endpoints/{id}/rotate-secret |
新しい署名シークレット。 | 管理者トークン |
| POST |
/v1/account/webhook-endpoints/{id}/enable |
有効化。 | 管理者トークン |
| POST |
/v1/account/webhook-endpoints/{id}/disable |
無効化。 | 管理者トークン |
| DELETE |
/v1/account/webhook-endpoints/{id} |
アーカイブ。 | 管理者トークン |
| GET |
/v1/account/webhook-deliveries |
直近 50 件の配信。 | APIキーまたは管理者トークン |
| POST |
/v1/account/webhook-deliveries/{id}/retry |
失敗した配信を再送。 | APIキーまたは管理者トークン |
| GET |
/v1/account/webhook-events |
直近 50 件のイベント。 | APIキーまたは管理者トークン |
Webhookエンドポイントを作成する
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }'
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints", headers={ "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints", { method: "POST", headers: { "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), "Content-Type": "application/json", }, body: JSON.stringify({ "name": "CRM-Sync", "url": "https://hooks.example.com/handelsregister", "headers": { "X-Receiver-Token": "YOUR_RECEIVER_TOKEN" } }), }); const data = await response.json();
| 名前 | 型 | 説明 | 例 |
|---|---|---|---|
| Idempotency-Keyヘッダー | 文字列必須 | 操作ごとに一意の値。できればUUID。Idempotency-Keyを参照。制限: 1~128 文字 |
req-20260802-01 |
| nameボディ | 文字列必須 | 自分で参照するための名前。制限: 1~120 文字 |
CRM-Sync |
| urlボディ | 文字列 (uri)必須 |
あなたのサーバーのアドレス:公開されていて、ポート443のhttps、IPアドレスではなくホスト名。制限: 1~2,048 文字 |
https://hooks.example.com/handelsregister |
| headersボディ | オブジェクト任意 | すべてのメッセージとともに送られるカスタムヘッダー。例えばあなたのサーバー用のトークン。制限: 10 件以下 |
{"X-Receiver-Token":"YOUR_RECEIVER_TOKEN"} |
ヘッダー名はwebhook-で始まってはならず、Content-TypeやHostなどの標準ヘッダーを上書きしてはいけません。値は書き込み専用で、後からAPIが表示するのは名前だけです。
レスポンスには署名シークレットwhsec_…が含まれます。表示されるのはこの一度だけです。すぐに保存してください。
1{2 "endpoint": {3 "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",4 "name": "CRM-Sync",5 "url": "hooks.example.com/…",6 "status": "pending_verification",7 "verified_at": null,8 "header_names": [9 "X-Receiver-Token"10 ],11 "consecutive_failures": 0,12 "created_at": "2026-11-01T08:28:01+00:00"13 },14 "signing_secret": "whsec_…",15 "meta": {16 "message": "Endpoint created. Store the signing secret now — it is shown only once.",17 "request_credit_cost": 0,18 "credits_remaining": 14519 }20}
新しいWebhookエンドポイントのステータスはpending_verificationで、検証されて初めてメッセージを受け取ります。名前、URL、ヘッダーは後から変更できません。新しいアドレスには、新しいWebhookエンドポイントを作成してください。
レスポンスでは、urlはホストのみを表示し、パスは/…に短縮します。
検証
POST …/{id}/verifyで、endpoint.verificationタイプの署名付きメッセージをあなたのURLに送ります。data.challengeにはランダムな値が入ります。
あなたのサーバーは 15 秒以内に2xxステータスで応答し、その値をレスポンスヘッダーwebhook-verificationで返します。するとWebhookエンドポイントはactiveになります。
1{2 "id": "msg_01m6b3jsqq7ntwzkdj099ayraz",3 "type": "endpoint.verification",4 "timestamp": "2026-11-01T08:28:26.743Z",5 "schema_version": 1,6 "data": {7 "challenge": "189fb34b2af819bb13cb47d9806dbf30"8 }9}
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify" \ -H "x-api-key: YOUR_API_KEY" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", headers={ "x-api-key": "YOUR_API_KEY", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/verify", { method: "POST", headers: { "x-api-key": "YOUR_API_KEY", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
成功すると、APIは"verified": trueを返し、失敗すると422と"verified": falseを返します。新しい試行には新しいIdempotency-Keyが必要です。
各Webhookエンドポイントで、1時間あたり 6 回試行できます。
1{2 "verified": true,3 "endpoint": {4 "id": "wep_01m6b3j1g0cdz98hw8zdefbf52",5 "name": "CRM-Sync",6 "url": "hooks.example.com/…",7 "status": "active",8 "verified_at": "2026-11-01T08:28:26+00:00",9 "header_names": [10 "X-Receiver-Token"11 ],12 "consecutive_failures": 0,13 "created_at": "2026-11-01T08:28:01+00:00"14 },15 "meta": {16 "request_credit_cost": 0,17 "credits_remaining": 14518 }19}
その後は、POST …/{id}/testでendpoint.testタイプの署名付きテストメッセージをいつでも送れます。レスポンスに配信の結果が表示されます。
1{2 "delivery": {3 "id": "del_01m6b3jsyrnc3sz1mt31dq6107",4 "message_id": "msg_01m6b3jsynxzhvagm7n2nq7mpv",5 "event_id": "evt_01m6b3jsynxzhvagm7n2nq7mpt",6 "event_type": "endpoint.test",7 "endpoint_id": "wep_01m6b3j1g0cdz98hw8zdefbf52",8 "status": "succeeded",9 "attempts": 1,10 "last_status_code": 200,11 "last_error_category": null,12 "next_attempt_at": "2026-11-01T08:28:26+00:00",13 "succeeded_at": "2026-11-01T08:28:26+00:00",14 "created_at": "2026-11-01T08:28:26+00:00"15 },16 "meta": {17 "request_credit_cost": 0,18 "credits_remaining": 14519 }20}
署名を検証する
すべてのメッセージは、オープン標準Standard Webhooksに従って署名されています。メッセージを処理する前に署名を検証してください。
| ヘッダー | 意味 |
|---|---|
| webhook-id |
メッセージのID(msg_…)。再試行でも同じです。重複の検出に使ってください。
|
| webhook-timestamp | この配信試行の時刻(Unix秒)。 |
| webhook-signature |
v1,<Base64>形式の1つ以上の署名(スペース区切り)。
|
メッセージの検証方法:
- シークレットの
whsec_以降の部分を取り出し、Base64からデコードします。これがキーです。 - 2つのヘッダーと変更されていないボディから、テキスト
webhook-id.webhook-timestamp.Bodyを組み立てます。 - キーを使ってそのHMAC-SHA256を計算し、結果をBase64でエンコードします。
- 結果を、ヘッダー内のすべての
v1署名と定数時間で比較します。1つでも一致すれば、メッセージは本物です。
ボディは届いたとおりに使い、JSONを再シリアライズしたものは使わないでください。タイムスタンプが5分以上ずれているメッセージは拒否してください。
import base64, hashlib, hmac, time def verify_webhook(secret: str, headers: dict, body: bytes) -> bool: msg_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] # 5分より古いメッセージを拒否する if abs(time.time() - int(timestamp)) > 300: return False key = base64.b64decode(secret.removeprefix("whsec_")) signed = f"{msg_id}.{timestamp}.".encode() + body expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode() # シークレットのローテーション中は、ヘッダーに署名が2つ入ります for part in headers["webhook-signature"].split(" "): version, _, signature = part.partition(",") if version == "v1" and hmac.compare_digest(signature, expected): return True return False
import crypto from "node:crypto"; export function verifyWebhook(secret, headers, rawBody) { const msgId = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; // 5分より古いメッセージを拒否する if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = crypto .createHmac("sha256", key) .update(`${msgId}.${timestamp}.${rawBody}`) .digest(); // シークレットのローテーション中は、ヘッダーに署名が2つ入ります return headers["webhook-signature"].split(" ").some((part) => { const [version, signature] = part.split(","); const given = Buffer.from(signature ?? "", "base64"); return version === "v1" && given.length === expected.length && crypto.timingSafeEqual(given, expected); }); }
独自のコードを書く代わりに、PythonとJavaScript用のパッケージstandardwebhooksなど、Standard Webhooksの公式ライブラリを使えます。
POST …/{id}/rotate-secretで新しいシークレットを受け取ります。レスポンスに一度だけ表示されます。7 日間、メッセージには新旧それぞれのシークレットによる2つの署名が付きます。そのため、停止時間なしで切り替えられます。
ペイロード
すべてのメッセージは、同じエンベロープを持つJSONオブジェクトです。イベントの内容はdataにあります。
| フィールド | 意味 |
|---|---|
| id |
メッセージのID。ヘッダーwebhook-idと同じ。
|
| event_id |
イベントのID(evt_…)。
|
| type | イベントの種類(下記参照)。 |
| timestamp | 私たちがイベントを検出した時刻(UTC)。 |
| schema_version |
構造のバージョン。現在は1。
|
| data | イベントのデータ。 |
| イベントの種類 | 意味 |
|---|---|
| organization.signal.detected |
モニターが新しいシグナルを見つけた。data.signalはsignalsのシグナルと同じ構造で、さらにdata.monitorと、data.links.signalにAPI内のシグナルへのリンクが付きます。
|
| organization.signal.sample |
アカウント › モニタリングから送られる、企業の実際の履歴からの例。構造は上記と同じで、data.sample: trueが付きます。
|
| endpoint.test |
/testによるテストメッセージ。
|
| endpoint.verification | 検証中のチャレンジ。 |
1{2 "id": "msg_01m65pqg9x0176cteqvtxw68sa",3 "event_id": "evt_01m65pqg9sx7mwmw0edc1mfbbe",4 "type": "organization.signal.sample",5 "timestamp": "2026-10-30T06:07:37.000Z",6 "schema_version": 1,7 "data": {8 "monitor": {9 "id": "mon_01m65pn3vm11sjjp1j076cbvec",10 "type": "organization.signals"11 },12 "sample": true,13 "signal": {14 "event": {15 "id": "36c7c8fddf99bb0299d6815bcaa01013",16 "topic": "TRANSFORMATIONS",17 "topic_name": {18 "de": "Umwandlungen & M&A",19 "en": "Transformations & M&A"20 },21 "occurred_on": "2022-12-22",22 "announced_on": "2023-08-10",23 "date_basis": "SOURCE_EVENT"24 },25 "organization": {26 "type": "ORGANIZATION",27 "entity_id": "94eb910fef13f94585c48c263fa446d4",28 "current_profile": {29 "name": "Hetzner Online GmbH",30 "registration": {31 "register_type": "HRB",32 "register_number": "6089",33 "court": "Ansbach",34 "register_locality": "Ansbach",35 "registration_authority_id": "D3201",36 "register_entity_name": "Ansbach HRB 6089"37 },38 "legal_form": {39 "name": {40 "de": "Gesellschaft mit beschränkter Haftung"41 },42 "short": {43 "de": "GmbH"44 }45 },46 "seat": {47 "city": "Gunzenhausen"48 },49 "status": {50 "code": "ACTIVE"51 }52 }53 },54 "parties": {55 "counterparty": {56 "entity": {57 "type": "ORGANIZATION",58 "entity_id": "dd4d2d6854b2dbd1ffce74e669f3852e",59 "current_profile": {60 "name": "Headtrip GmbH",61 "registration": {62 "register_type": "HRB",63 "register_number": "33432",64 "court": "Nürnberg",65 "register_locality": "Nuremberg",66 "registration_authority_id": "D3310",67 "register_entity_name": "Nürnberg HRB 33432"68 },69 "legal_form": {70 "name": {71 "de": "Gesellschaft mit beschränkter Haftung"72 },73 "short": {74 "de": "GmbH"75 }76 },77 "seat": {78 "city": "Nürnberg"79 },80 "status": {81 "code": "INACTIVE"82 }83 }84 },85 "relationship": "TRANSFERRING_COMPANY"86 }87 },88 "register_entry": {89 "entry_number": "3",90 "entry_date": "2023-08-10",91 "description_text": "Verschmelzung zur Aufnahme der Headtrip GmbH",92 "context": "PRIMARY",93 "flags": []94 },95 "source": {96 "kind": "COMMERCIAL_REGISTER"97 },98 "details": {99 "schema": "TRANSFORMATION",100 "kind": "MERGER",101 "specifics": {102 "merger_target": "ABSORPTION"103 },104 "direction": "INCOMING",105 "is_cross_border": false,106 "resolution_date": "2022-12-22",107 "agreement_date": "2022-12-22"108 }109 },110 "links": {111 "signal": "https://handelsregister.ai/api/v1/signals/36c7c8fddf99bb0299d6815bcaa01013"112 }113 }114}
メッセージは最大 20 KBです。シグナルがそれより大きくなる場合、detailsとregister_entryが省かれ、data.truncatedがtrueになります。その場合はリンクからシグナル全体を取得してください。
知らないイベントの種類は無視し、それでも2xxで応答してください。そうすれば、新しい種類が加わってもサーバーは安定して動きます。
配信と再試行
サーバーが 15 秒以内に2xxステータスで応答すると、配信は成功とみなされます。リダイレクトはたどりません。
それ以外の場合は再試行します:最大 10 回、約3日間にわたって間隔を広げながら。サーバーがRetry-Afterを送れば、それに従います。
サーバーが410を返すと、配信を止め、Webhookエンドポイントを無効化します。
5 件の配信が連続して最終的に失敗すると、Webhookエンドポイントはdisabled_failuresで無効化されます。POST …/{id}/enableで再び有効にできます。
webhook-idで検出してください。すぐに応答し、メッセージの処理はその後に行ってください。
GET /v1/account/webhook-deliveriesは直近 50 件の配信を表示します。?endpoint=wep_…で1つのWebhookエンドポイントに絞れます。GET /v1/account/webhook-eventsは直近 50 件のイベントを表示します。
最終的に失敗した配信は、POST /v1/account/webhook-deliveries/{id}/retryで、同じwebhook-idと同じボディで再送できます。これは 30 日間可能です。メッセージを保管するのはその期間です。
| ステータス | 意味 |
|---|---|
| withheld | チェックの課金を待っている。 |
| pending | 送信を待っている。 |
| in_flight | 現在送信中。 |
| retry_wait | 次の試行を待っている。 |
| succeeded | 配信済み。 |
| exhausted | すべての試行が失敗した。 |
| blocked_endpoint | Webhookエンドポイントが現在何も受け取れない。 |
| cancelled | キャンセル済み。例えばメッセージの期限が切れた場合。 |
有効化、無効化、アーカイブ
POST …/{id}/disableは配信を一時的に止めます。他のWebhookエンドポイントのないモニターは、paused_configurationで一時停止します。
POST …/{id}/enableは検証済みのWebhookエンドポイントを再び有効にします。その後、一時停止中のモニターをresumeで再開してください。
DELETE …/{id}はWebhookエンドポイントを完全にアーカイブします。しばらく止めるだけなら、代わりに無効化してください。
curl -X POST "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -H "Idempotency-Key: $(uuidgen)"
import uuid import requests response = requests.post( "https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", headers={ "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": str(uuid.uuid4()), }, ) data = response.json()
const response = await fetch("https://handelsregister.ai/api/v1/account/webhook-endpoints/wep_01m6b3j1g0cdz98hw8zdefbf52/disable", { method: "POST", headers: { "Authorization": "Bearer YOUR_ADMIN_TOKEN", "Idempotency-Key": crypto.randomUUID(), }, }); const data = await response.json();
エラー
エラーの構造はaccount/monitorsと同じです:errorにコード、meta.messageに説明。エラーに料金はかかりません。
| ステータス | 原因 | 対処法 |
|---|---|---|
| 400 |
api_keyがURLにある。 |
キーをヘッダーx-api-keyで送る。 |
| 400 |
URLが送信先として許可されていない、ヘッダーが許可されていない、最大数に達している、またはWebhookエンドポイントが有効でない。meta.messageに理由がある。 |
値を修正するか、先に検証する。 |
| 400 |
Idempotency-Keyの形式が無効。 |
形式を修正する。例えばUUIDを使う。 |
| 401 | APIキーもトークンもないか、無効。 |
キーをヘッダーx-api-keyで送る。 |
| 403 |
Bearerトークンに権限account:readがない。 |
APIキーか、account:read付きのトークンを使う。 |
| 403 | アカウントのメールアドレスがまだ確認されていないか、アカウントがロックされている。 | 確認メールのリンクを開く。アカウントがロックされている場合はサポートが対応できる。 |
| 403 | この呼び出しには管理者トークンが必要。APIキーでも、他のトークンでも足りない。 |
Authorization: Bearerと管理者トークンで送る。 |
| 403 |
Bearerトークンに権限monitoring:manageがない。 |
APIキーか管理者トークンを使う。 |
| 404 | このIDのWebhookエンドポイントまたは配信がない。 | IDを確認する。 |
| 409 |
Idempotency-Keyがすでに別のデータで使われている、24時間より古い、またはまだ処理中。 |
新しいキーを使うか、少し待つ。 |
| 422 |
nameやurlなど、値がないか無効。errorsにそのフィールドが示される。 |
値を修正し、新しいキーで再試行する。 |
| 422 | 検証に失敗した。 | チャレンジヘッダーと2xxステータスを確認し、新しいキーで再試行する。 |
| 428 |
ヘッダーIdempotency-Keyがない。 |
ヘッダーを送る。 |
| 429 | リクエストが多すぎる。 |
Retry-Afterの後に再試行する。 |
| 503 | 検証、テスト、再送が一時的に利用できない。 | 少し後に再試行する。 |
1{2 "error": "invalid_request",3 "meta": {4 "message": "The endpoint is not active.",5 "request_credit_cost": 0,6 "credits_remaining": 1457 }8}