ドキュメント
署名検証
エンドポイントに
リクエストの形式
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 | メッセージ IDmsg_…) |
webhook-timestamp | 配信時刻 |
webhook-signature | v1,<base64>。 |
ペイロードは{ "type", "timestamp", "data" } です。data はPOST /v1/messages のpayload です。body_format がraw のdata
公式ライブラリ
| 言語 | パッケージ |
|---|---|
| JavaScript / TypeScript | npm install standardwebhooks |
| Python | pip install standardwebhooks |
| Go | go get github.com/ |
| Ruby | gem install standardwebhooks |
| Java / Kotlin | com.standardwebhooks:standardwebhooks |
| Rust | cargo add standardwebhooks |
| C# | dotnet add package StandardWebhooks.StandardWebhooks |
| PHP・Elixir | github.com/standard-webhooks/standard-webhooks |
署名シークレットは、secretwhsec_…)
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);import os
from flask import Flask, request
from standardwebhooks import Webhook
wh = Webhook(os.environ["WEBHOOK_SECRET"]) # whsec_…
app = Flask(__name__)
@app.post("/webhooks")
def webhooks():
try:
event = wh.verify(request.get_data(), dict(request.headers))
except Exception:
return "", 400
print(event["type"], event["data"])
return "", 200公式ライブラリは、
SDK
メッセージのWEBHOOK_ADMIN_API_KEY を
| 言語 | インストール |
|---|---|
| Go | go get github.com/ |
| PHP | composer require shannonllc/ |
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)use WebhookAdmin\Client;
use WebhookAdmin\Webhook;
$wha = new Client(getenv('WEBHOOK_ADMIN_API_KEY'));
$message = $wha->messages->send(
['consumer' => 'cus_1024', 'event_type' => 'invoice.paid', 'payload' => ['invoice_id' => 'inv_88', 'amount' => 128000]],
['idempotency_key' => 'inv_88-paid'],
);
$wh = new Webhook(getenv('WEBHOOK_SECRET'));
$event = $wh->verify(file_get_contents('php://input'), $_SERVER);署名の生成方法
- 署名対象の
文字列は {webhook-id}.{webhook-timestamp}.{ボディ}です。ボディは 受信した バイト列の まま 使います - 鍵は、
whsec_以降をbase64 デコードした バイト列です - HMAC-SHA256 の
結果を base64 エンコードし、 先頭に v1,を付けます webhook-signatureのいずれか 1 つと 一致すれば、 正当な リクエストです
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 または
互換署名
独自のcompat_signature)
| 項目 | 説明 |
|---|---|
header | ヘッダー名webhook-*・content-type・authorization など、 |
content | body はtimestamp_body は{webhook-timestamp}.{ボディ} に |
encoding | hex またはbase64 |
prefix | 任意。sha256=) |
- 方式は
HMAC-SHA256 です。 鍵には、 署名シークレットの 文字列 ( whsec_…全体)を UTF-8 の まま 使います。 - 署名シークレットの
ローテーション中は、 新しい シークレットのみで 署名します。 - テスト送信にも
付与します。 nullを指定すると 解除されます。
{ "compat_signature": { "header": "x-hub-signature-256", "content": "body", "encoding": "hex", "prefix": "sha256=" } }
x-hub-signature-256: sha256=<hex>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 を