ドキュメント
署名検証
送信先に
届く形
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 | メッセージのmsg_…) |
webhook-timestamp | 送った |
webhook-signature | v1,<base64>。 |
本文は{ "type", "timestamp", "data" }。data はPOST /v1/messages のpayload です。
公式ライブラリ
| 言語 | パッケージ |
|---|---|
| JavaScript / TypeScript | npm install standardwebhooks |
| Python | pip install standardwebhooks |
| Go | go get github.com/standard-webhooks/standard-webhooks/libraries/go |
| 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公式ライブラリは、
署名の作り方
- 署名する
文字列は {webhook-id}.{webhook-timestamp}.{本文}。本文は 届いた バイト列の まま 使う - 鍵は
whsec_の後ろを base64 で 戻した バイト列 - HMAC-SHA256 の
結果を base64 にし、 先頭に v1,を付ける 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 時間後 (1 回目を 含めて 最大 8 回) 。 - リダイレクトは
追いません。 - 同じ
webhook-idが2 回以上 届く ことがあります。 処理済みの ID は 2 回目以降を 捨てます。 - 失敗が
5 日続いた 送信先は 自動で 止めます。
鍵の切り替え
POST /v1/endpoints/{id}/rotate-secret か