ドキュメント

署名検証

送信先に届く 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 です。

公式ライブラリ

言語パッケージ
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 分以上ずれた署名を受け付けません。

署名の作り方

  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 の署名の検証へ移行してください。