WebhookペイロードJSONスキーマ完全リファレンス|HMAC検証・リトライパターン
PDF API Webhook連携ガイドでは通知の実装方法を解説しました。この記事では、Webhookペイロードの全フィールド定義・HMAC-SHA256署名検証の詳細・リトライパターンに絞ったリファレンスとしてまとめます。実装時のデバッグや仕様確認にご利用ください。
ペイロードのトップレベル構造
すべてのWebhookリクエストは、以下のトップレベルフィールドを持ちます。
{
"event": "pdf.generated",
"timestamp": "2026-05-12T10:00:00Z",
"webhook_id": "wh_01HZ4K9MXPQ3R8VW2N5T7BCJD",
"api_version": "2026-03-01",
"data": { ... }
}
フィールド定義
| フィールド | 型 | 説明 |
|---|---|---|
event |
string | イベント種別(後述) |
timestamp |
string (ISO 8601) | イベント発生日時(UTC) |
webhook_id |
string | ウェブフック固有ID(冪等性チェックに使用) |
api_version |
string | APIバージョン |
data |
object | イベント別のペイロード(後述) |
イベント種別とペイロードスキーマ
pdf.generated — PDF生成完了
{
"event": "pdf.generated",
"timestamp": "2026-05-12T10:00:00Z",
"webhook_id": "wh_01HZ4K9MXPQ3R8VW2N5T7BCJD",
"api_version": "2026-03-01",
"data": {
"id": "pdf_abc123",
"filename": "invoice-42.pdf",
"file_size": 48210,
"page_count": 2,
"download_url": "https://api.pdf.funbrew.cloud/dl/abc123?expires=1748736000&sig=...",
"download_url_expires_at": "2026-05-13T10:00:00Z",
"engine": "quality",
"generation_time_ms": 1250,
"metadata": {
"user_ref": "invoice-42",
"customer_id": "cust_987"
}
}
}
| フィールド | 型 | 説明 |
|---|---|---|
id |
string | PDF固有ID |
filename |
string | ダウンロード時のファイル名 |
file_size |
integer | ファイルサイズ(バイト) |
page_count |
integer | ページ数 |
download_url |
string | 署名付きダウンロードURL(有効期限あり) |
download_url_expires_at |
string (ISO 8601) | ダウンロードURLの有効期限 |
engine |
string | 使用エンジン(quality / speed) |
generation_time_ms |
integer | 生成にかかった時間(ミリ秒) |
metadata |
object | リクエスト時に渡した任意のメタデータ |
pdf.failed — PDF生成失敗
{
"event": "pdf.failed",
"timestamp": "2026-05-12T10:00:05Z",
"webhook_id": "wh_01HZ4KA1PNBR2C8XM4S6Y0DHEV",
"api_version": "2026-03-01",
"data": {
"id": "pdf_abc124",
"filename": "report-5.pdf",
"error_code": "TEMPLATE_RENDER_ERROR",
"error_message": "Template variable 'customer.name' is undefined",
"metadata": {
"user_ref": "report-5"
}
}
}
error_code |
説明 |
|---|---|
TEMPLATE_RENDER_ERROR |
テンプレート変数の未定義・構文エラー |
RESOURCE_FETCH_ERROR |
外部画像・CSSの取得失敗 |
TIMEOUT_ERROR |
エンジン処理タイムアウト(60秒超) |
INVALID_INPUT |
HTMLが不正または空 |
INTERNAL_ERROR |
サーバー内部エラー(自動リトライ対象) |
pdf.emailed — メール送信完了
{
"event": "pdf.emailed",
"data": {
"id": "pdf_abc123",
"filename": "invoice-42.pdf",
"email_to": "customer@example.com",
"email_subject": "請求書が届きました",
"email_sent_at": "2026-05-12T10:00:10Z"
}
}
請求書PDF自動生成でメール自動送信を設定している場合、このイベントを使ってCRM側の送信ステータスを更新できます。
batch.completed — バッチ処理完了
{
"event": "batch.completed",
"data": {
"batch_id": "batch_xyz789",
"total": 150,
"succeeded": 148,
"failed": 2,
"completed_at": "2026-05-12T10:05:00Z",
"failed_ids": ["pdf_err1", "pdf_err2"]
}
}
| フィールド | 説明 |
|---|---|
total |
バッチに含まれるPDF数 |
succeeded |
生成成功数 |
failed |
生成失敗数 |
failed_ids |
失敗したPDFのID一覧 |
HMAC-SHA256署名検証
署名の仕組み
リクエストヘッダー X-Funbrew-Signature には、リクエストボディ全体をHMAC-SHA256でハッシュした値が含まれます。
X-Funbrew-Signature: sha256=<hex-encoded HMAC>
署名は以下の手順で生成されます:
- リクエストボディ(UTF-8エンコードされたJSONバイト列)をメッセージとして使用
- ダッシュボードで設定したWebhookシークレットをキーとして使用
- HMAC-SHA256でハッシュ値を計算し、16進数文字列に変換
検証実装(言語別)
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(body, signature, secret) {
// bodyは Buffer またはバイト列のままであること(JSON.stringifyで再構築しない)
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body) // req.rawBodyまたはBuffer
.digest('hex');
// タイミング攻撃を防ぐために timingSafeEqual を使用
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express での利用例
app.use('/webhooks/funbrew-pdf', express.raw({ type: 'application/json' }));
app.post('/webhooks/funbrew-pdf', (req, res) => {
const sig = req.headers['x-funbrew-signature'] || '';
if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString());
// ...以降の処理
res.json({ received: true });
});
注意:
express.json()を使うとreq.bodyがオブジェクトになり生のバイト列が失われます。署名検証にはexpress.raw()を使ってください。
Python (FastAPI)
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
def verify_signature(body: bytes, signature: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
# compare_digest でタイミング攻撃を防ぐ
return hmac.compare_digest(expected, signature)
@app.post("/webhooks/funbrew-pdf")
async def handle_webhook(request: Request):
body = await request.body() # 生のバイト列を取得
sig = request.headers.get("x-funbrew-signature", "")
if not verify_signature(body, sig, os.environ["WEBHOOK_SECRET"]):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = await request.json()
# ...以降の処理
return {"received": True}
PHP (Laravel)
class WebhookController extends Controller
{
public function handle(Request $request)
{
$signature = $request->header('X-Funbrew-Signature', '');
$expected = 'sha256=' . hash_hmac(
'sha256',
$request->getContent(), // 生のリクエストボディ
config('services.funbrew.webhook_secret')
);
// hash_equals でタイミング攻撃を防ぐ
if (!hash_equals($expected, $signature)) {
abort(401, 'Invalid signature');
}
$payload = $request->json()->all();
// ...以降の処理
return response()->json(['received' => true]);
}
}
タイムスタンプ検証(リプレイ攻撃対策)
function isTimestampValid(timestamp, toleranceMs = 5 * 60 * 1000) {
const eventTime = new Date(timestamp).getTime();
const now = Date.now();
return Math.abs(now - eventTime) < toleranceMs;
}
app.post('/webhooks/funbrew-pdf', (req, res) => {
const payload = JSON.parse(req.body.toString());
if (!isTimestampValid(payload.timestamp)) {
return res.status(401).json({ error: 'Timestamp out of range' });
}
// ...
});
リトライパターン
FUNBREW PDF 側のリトライ動作
Webhookエンドポイントが5秒以内に 2xx を返さない場合、または接続エラーの場合、FUNBREW PDFは以下のスケジュールで自動リトライを行います。
| リトライ回数 | 待機時間 |
|---|---|
| 1回目 | 30秒後 |
| 2回目 | 5分後 |
| 3回目 | 30分後 |
| 4回目 | 2時間後 |
| 5回目(最終) | 6時間後 |
5回すべて失敗した場合、ダッシュボードの「Webhookエラー」セクションに記録されます。
受信側での重複処理防止(冪等性)
リトライにより同一イベントが複数回届く場合があります。webhook_id を使って重複を検出します。
const { createClient } = require('redis');
const redis = createClient({ url: process.env.REDIS_URL });
async function processOnce(webhookId, handler) {
const key = `webhook:processed:${webhookId}`;
// SET ... NX EX: キーが存在しない場合のみセット(TTL 24時間)
const isNew = await redis.set(key, '1', { NX: true, EX: 86400 });
if (!isNew) {
console.log(`Duplicate webhook ignored: ${webhookId}`);
return;
}
await handler();
}
app.post('/webhooks/funbrew-pdf', async (req, res) => {
// シグネチャ検証後...
res.json({ received: true }); // 先に200を返す
const { webhook_id, event, data } = JSON.parse(req.body.toString());
setImmediate(() =>
processOnce(webhook_id, () => handleEvent(event, data))
);
});
失敗時の再配信リクエスト
ダッシュボードから手動再配信も可能です。また、APIで再配信をトリガーできます。
# ダッシュボードAPIで特定のWebhookイベントを再配信
curl -X POST https://api.pdf.funbrew.cloud/v1/webhooks/deliveries/wh_01HZ4K9MXPQ3R8VW2N5T7BCJD/retry \
-H "Authorization: Bearer $API_KEY"
ペイロードサイズと制限
| 項目 | 制限 |
|---|---|
| 最大ペイロードサイズ | 1 MB |
download_url 有効期間 |
24時間 |
metadata 最大キー数 |
20 |
metadata 値の最大長 |
256文字 |
ローカル開発でのペイロードテスト
# ngrok でローカルを公開
ngrok http 3000
# テスト用ペイロードを手動で送信して検証
curl -X POST http://localhost:3000/webhooks/funbrew-pdf \
-H "Content-Type: application/json" \
-H "X-Funbrew-Signature: sha256=$(echo -n '{"event":"pdf.generated","timestamp":"2026-05-12T10:00:00Z","webhook_id":"wh_test","api_version":"2026-03-01","data":{"id":"pdf_test"}}' | openssl dgst -sha256 -hmac "your_secret" | cut -d' ' -f2)" \
-d '{"event":"pdf.generated","timestamp":"2026-05-12T10:00:00Z","webhook_id":"wh_test","api_version":"2026-03-01","data":{"id":"pdf_test"}}'
PlaygroundからPDFを生成すると、登録したWebhookエンドポイントに実際のペイロードが届きます。テスト前にngrokのURLをダッシュボードのWebhook設定に登録してください。
まとめ
Webhookペイロードの主要ポイント:
webhook_idで冪等性を確保: 同一IDが届いた場合はスキップする- 署名は生バイト列で検証: JSON再パース前のリクエストボディに対してHMACを計算する
- タイムスタンプ検証で5分以内かチェック: リプレイ攻撃を防ぐ
- 5秒以内に200を返す: 重い処理はキューに委譲してからレスポンスを送る
- リトライは最大5回: 最終的な失敗はダッシュボードで確認できる
実際の統合実装はPDF API Webhook連携ガイドを、本番環境での運用全体はPDF API本番運用ガイドを参照してください。Webhookを組み合わせた請求書自動化については請求書PDFの自動生成もご覧ください。
関連リンク
- PDF API Webhook連携ガイド — Slack・メール通知の実装
- PDF API本番運用ガイド — 本番環境でのベストプラクティス
- 請求書PDFの自動生成 — Webhookを活用した請求書ワークフロー
- PDF APIドキュメント — APIリファレンス全般
- Playground — Webhookのテスト生成