2026/05/12

WebhookペイロードJSONスキーマ完全リファレンス|HMAC検証・リトライパターン

WebhookAPIセキュリティPDF生成自動化

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>

署名は以下の手順で生成されます:

  1. リクエストボディ(UTF-8エンコードされたJSONバイト列)をメッセージとして使用
  2. ダッシュボードで設定したWebhookシークレットをキーとして使用
  3. 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の自動生成もご覧ください。

関連リンク

Powered by FUNBREW PDF