ドキュメント

署名検証

エンドポイントに配信される Webhook には、Standard Webhooks 形式の署名が付いています。受信側は公式ライブラリで検証できます。

リクエストの形式

POST https://example.com/webhooks
content-type: application/json
webhook-id: msg_2Zq8…
webhook-timestamp: 1790000000
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
user-agent: Webhook Admin/1

{ "type": "invoice.paid", "timestamp": "2026-09-27T01:32:05.000Z", "data": { "invoice_id": "inv_88", "amount": 128000 } }
ヘッダー説明
webhook-idメッセージ ID(msg_…)。再送信でも変わりません
webhook-timestamp配信時刻(Unix 秒)
webhook-signaturev1,<base64>。署名シークレットのローテーション中は、空白区切りで 2 つ

ペイロードは { "type", "timestamp", "data" } です。data は POST /v1/messages の payload です。エンドポイントの body_format が raw の場合、ペイロードは data そのものです(署名は配信されたボディに対して付与されます)。

公式ライブラリ

言語パッケージ
JavaScript / TypeScriptnpm install standardwebhooks
Pythonpip install standardwebhooks
Gogo get github.com/standard-webhooks/standard-webhooks/libraries/go
Rubygem install standardwebhooks
Java / Kotlincom.standardwebhooks:standardwebhooks(Maven Central)
Rustcargo add standardwebhooks
C#dotnet add package StandardWebhooks.StandardWebhooks
PHP・Elixirgithub.com/standard-webhooks/standard-webhooks

署名シークレットは、エンドポイント作成時の secret(whsec_…)です。ライブラリにはそのまま渡します。

import express from 'express';
import { Webhook } from 'standardwebhooks';

const wh = new Webhook(process.env.WEBHOOK_SECRET); // whsec_…
const app = express();

// 署名は受信したボディそのもので検証するため、JSON に変換する前のボディを受け取る
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = wh.verify(req.body, req.headers);
    console.log(event.type, event.data);
    res.sendStatus(200);
  } catch {
    res.sendStatus(400);
  }
});

app.listen(3000);

公式ライブラリは、時刻が現在から 5 分以上ずれた署名を拒否します。

SDK

メッセージの送信・エンドポイントの管理・受信側での署名検証は、SDK からも実行できます。API キーを省略すると、環境変数 WEBHOOK_ADMIN_API_KEY を読み込みます。

言語インストール
Gogo get github.com/shannonllc/webhookadmin-go
PHPcomposer require shannonllc/webhookadmin
import webhookadmin "github.com/shannonllc/webhookadmin-go"

client, err := webhookadmin.NewClient(os.Getenv("WEBHOOK_ADMIN_API_KEY"))
message, err := client.Messages.Send(ctx, webhookadmin.SendMessageParams{
	Consumer:  "cus_1024",
	EventType: "invoice.paid",
	Payload:   map[string]any{"invoice_id": "inv_88", "amount": 128000},
}, webhookadmin.WithIdempotencyKey("inv_88-paid"))

wh, err := webhookadmin.NewWebhook(os.Getenv("WEBHOOK_SECRET"))
event, err := wh.Verify(body, r.Header)

署名の生成方法

  1. 署名対象の文字列は {webhook-id}.{webhook-timestamp}.{ボディ} です。ボディは受信したバイト列のまま使います
  2. 鍵は、whsec_ 以降を base64 デコードしたバイト列です
  3. HMAC-SHA256 の結果を base64 エンコードし、先頭に v1, を付けます
  4. webhook-signature のいずれか 1 つと一致すれば、正当なリクエストです
ライブラリを使用しない場合(Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(secret, headers, body) {
  const id = headers['webhook-id'];
  const ts = headers['webhook-timestamp'];
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest('base64');
  // 署名シークレットのローテーション中は、署名が空白区切りで 2 つ以上並ぶ
  return headers['webhook-signature'].split(' ').some((s) => {
    const [version, sig] = s.split(',');
    return version === 'v1' && sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
  });
}

レスポンスと再送信

  • 2xx を返すと成功です。それ以外のレスポンス、15 秒のタイムアウト、接続の失敗は再送信します。
  • 既定では、即時・5 秒後・5 分後・30 分後・2 時間後・5 時間後・10 時間後・10 時間後に配信します(初回配信を含めて最大 8 回、約 28 時間)。再送信回数と再送信間隔は、エンドポイントごとに設定できます(再送信ポリシー)。
  • リダイレクトには追従しません。
  • 同じ webhook-id が 2 回以上届くことがあります。処理済みの ID は、2 回目以降を破棄してください。
  • 失敗が 5 日間続いたエンドポイントは、自動で無効化します。

署名シークレットのローテーション

POST /v1/endpoints/{id}/rotate-secret または管理画面で署名シークレットをローテーションすると、24 時間は旧シークレットによる署名も付けて配信します。この間に、受信側の署名シークレットを差し替えてください。

互換署名

独自の署名を検証している受信側を移行する間、エンドポイントごとに署名ヘッダーを 1 つ追加できます。Standard Webhooks のヘッダーは常に付与します。設定は管理画面・API(compat_signature)・MCP で行い、顧客ポータルでは閲覧のみ可能です。

項目説明
headerヘッダー名(小文字、64 文字まで)。webhook-*・content-type・authorization など、配信に使うヘッダー名は使用できません
contentbody はボディのみ、timestamp_body は {webhook-timestamp}.{ボディ} に署名
encodinghex または base64
prefix任意。値の前に付ける文字列(例 sha256=)。空白を含まない ASCII で 32 文字まで
  • 方式は HMAC-SHA256 です。鍵には、署名シークレットの文字列(whsec_… 全体)を UTF-8 のまま使います。
  • 署名シークレットのローテーション中は、新しいシークレットのみで署名します。
  • テスト送信にも付与します。null を指定すると解除されます。
GitHub の X-Hub-Signature-256 と同じ形式
{ "compat_signature": { "header": "x-hub-signature-256", "content": "body", "encoding": "hex", "prefix": "sha256=" } }

x-hub-signature-256: sha256=<hex>
受信側での検証(Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyCompat(body, header, secret) {
  // シークレットは whsec_… の文字列のまま使う
  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
  return header.length === expected.length && timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}

body には時刻が含まれないため、同じリクエストをそのまま再送する攻撃(リプレイ攻撃)を防げません。できるだけ timestamp_body を使用し、受信側は Standard Webhooks の署名検証へ移行してください。