メインコンテンツまでスキップ

Webhook通知

PDF生成完了時に自動的にWebhook通知を受け取ることができます。これにより、ポーリング不要でリアルタイムに完了を検知し、メール送信などのワークフローを自動化できます。

1つのワークスペースに複数のWebhookエンドポイントを登録できます。エンドポイントごとに「どのテンプレートの生成完了を通知するか」(対象デザインフィルタ)と、署名検証用のシークレットを独立して設定できるため、テンプレートごとに異なる送信先・異なるシークレットで通知を振り分けられます。

概要​

Webhook通知は、以下のすべてのPDF生成エンドポイントで自動的に送信されます:

  • POST /file/sync/single - 同期単一ファイル生成
  • POST /file/sync/multiple - 同期複数ファイル生成
  • POST /file/async/single - 非同期単一ファイル生成
  • POST /file/async/multiple - 非同期複数ファイル生成

生成リクエストで webhookUrl を指定しない場合、生成対象のテンプレートに対して対象デザインフィルタが一致するエンドポイントが複数あれば、それぞれに対して独立して(同じペイロードが個別に)POSTされます(webhookUrl を指定した場合は、そのエンドポイントだけに送られます)。1つのエンドポイントへの送信が失敗しても、他のエンドポイントへの送信には影響しません。

Webhookエンドポイントの設定​

Webhookエンドポイントを設定するには、ReportFlowのワークスペース設定画面から行います:

  1. ワークスペース設定を開く
  2. 「開発者向け」タブに移動
  3. 「エンドポイントを追加」をクリック
  4. 通知を受け取るHTTPS URLを入力
  5. 対象デザインフィルタを選択(下記参照)
  6. 「保存」をクリック

ワークスペースあたり登録できるエンドポイントは最大10件です。既存のエンドポイントは一覧から有効/無効の切り替え、編集、削除ができます。

手順 3 以降にはワークスペース管理者権限が必要です

追加・編集・削除と、シークレットの取得(「署名シークレットをコピー」)・再生成は管理者のみが行なえます(一覧の閲覧は閲覧者以上で可)。画面操作でも API でも同じです。詳しくは下記の「必要な権限(ワークスペース管理者)」を参照してください。

対象デザインフィルタ​

エンドポイントごとに、どのテンプレートの生成完了通知を受け取るかを選択します。

値説明
all(すべてのデザイン)ワークスペース内のすべてのテンプレートの生成完了を通知
selected(選択したデザインのみ)追加/編集モーダルで選択した特定のテンプレートの生成完了のみを通知

selected を選びつつ対象テンプレートを1件も指定しなかった場合は、「すべてのデザイン」(all)として登録されます。テンプレートを指定しない限り通知が届かなくなることはありません。

必要な権限(ワークスペース管理者)​

Webhookエンドポイントの追加・編集・削除と、署名シークレットの取得・再生成には、ワークスペースの管理者(ADMIN)権限が必要です。 一覧の閲覧は閲覧者(READ)以上で行えます。シークレットの取得は画面の「署名シークレットをコピー」から行う操作でもあるため、管理者以外のメンバーは一覧を見られてもコピーはできません(403)。

Webhookエンドポイントは、ワークスペース内で生成されたPDFの完了通知を任意のURLへ送り出す口です。他のメンバーが生成した PDF の情報も流れるため、APIキーの発行・OAuthクライアントの管理・通知設定と同じ「管理者のみ」の扱いにしています。

この制限は、画面から設定する場合もAPIから設定する場合も同じです。OAuth 2.0 連携(Zapier / Shopify アプリなど)が接続時にエンドポイントを自動登録する場合、認可したユーザーが管理者である必要があります。 管理者以外のアカウントで認可すると、連携側で 403(この操作には管理者権限が必要です)になります。

連携を接続するとき

Webhookを使う連携(Zapier の New PDF Generated トリガー、Shopify アプリの ReportFlow 接続)は、ワークスペース管理者のアカウントで接続してください。 認可画面には権限が表示されないため、同意し終えてから 403 に気付くことになります。

エンドポイント管理API​

UIの代わりにAPIからエンドポイントを管理することもできます。

メソッドパス必要な権限説明
GET/workspace/:workspaceId/webhook-endpointsREADエンドポイント一覧取得
POST/workspace/:workspaceId/webhook-endpointsADMINエンドポイント作成(url, designFilterType。designFilterType=selected でも targetDesignIds が空・未指定なら all に正規化)
PATCH/workspace/:workspaceId/webhook-endpoints/:endpointIdADMINエンドポイント更新(url, enabled, designFilterType, targetDesignIdsはいずれも省略可)
DELETE/workspace/:workspaceId/webhook-endpoints/:endpointIdADMINエンドポイント削除
POST/workspace/:workspaceId/webhook-endpoints/:endpointId/secret/regenerateADMIN署名シークレット再生成
GET/workspace/:workspaceId/webhook-endpoints/:endpointId/secretADMIN署名シークレット取得。この 1 本だけはログインセッション専用で、OAuth のアクセストークンからは呼べません(後述)

権限が足りない場合は 403 を返します。同じアカウントで再認可・再接続しても解消しません(権限が足りないのはトークンではなくアカウントなので、やり直しても同じ結果になります)。解決するには、接続に使うアカウントを変えてください — 管理者アカウントで接続し直すか、管理者に設定してもらってください。

移行前からある単一URL用のAPI(PATCH /workspace/webhook と PATCH /workspace/:workspaceId/webhook)も同じく管理者権限が必要です。この2本は OAuth 2.0 のアクセストークンからも呼べるため、まだ単一URL方式で接続している連携も管理者アカウントで認可する必要があります(Make.com は現在エンドポイント管理APIへ移行済み。詳細は Make 連携 を参照)。

OAuth 連携に必要なスコープ​

ロールとは別に、OAuth 2.0 のアクセストークンで呼ぶ場合は pdf:generate スコープが必要です。上記の表のうち OAuth から呼べる操作すべてが対象で、一覧取得も含みます(GET .../secret はスコープ以前にログインセッション専用なので、OAuth からは呼べません)。上に挙げた単一URL用の 2 本(PATCH /workspace/webhook と PATCH /workspace/:workspaceId/webhook)も同じスコープを要求します。エンドポイント管理 API は PDF 生成完了通知の設定なので、生成系と同じスコープで保護されています。

ロールとスコープは独立した条件で、両方を満たす必要があります。 管理者アカウントで認可してもトークンに pdf:generate が無ければ 403 になり、逆にスコープがあってもロールが足りなければ(一覧取得を除き)403 になります。連携アプリを接続するときは、管理者アカウントで、pdf:generate を含むスコープを許可してください。

Client Credentials グラントのトークンでは、一覧取得も含めてすべて 403 になります。 このグラントのトークンはエンドユーザーを持たず、サブジェクトがクライアント自身なので、ワークスペースのメンバー行を引けません(ロール判定より前の、ワークスペースへのアクセス検証で落ちます)。Webhook エンドポイントの管理には、Authorization Code フローで管理者ユーザーの認可を受けたトークンを使ってください。

OAuth 連携が操作できるエンドポイントの範囲​

OAuth 2.0 のアクセストークンで呼び出した場合、更新・削除・シークレット再生成は、その連携自身が作成したエンドポイントに限られます。 別の連携が作成したエンドポイントを指定すると 404 になります(存在の有無を応答から読み取れないようにするため 403 ではなく 404 です)。

画面(ワークスペース設定)からの操作にはこの制限はなく、連携が作成したものを含め、ワークスペース内のすべてのエンドポイントを管理できます。

なおこの紐付けが導入される前から存在するエンドポイントは「作成者なし」の状態で、どの連携からも操作できます。連携が更新(PATCH)を行った時点でその連携に紐付きます。

targetDesignIds に指定したテンプレートが後から削除された場合でも、エンドポイント設定から自動的には除去されません(削除済み件数がUI上に警告表示されます)。意図せず通知が届かなくなることを避けるため、対象テンプレートの削除後は設定を見直してください。

既存の単一URL設定からの移行​

このマルチエンドポイント機能の導入前に設定していた単一のWebhook URL/シークレットは、自動的に「移行済み」エンドポイント(対象デザインフィルタ = all)として一覧に引き継がれています。追加の作業は不要です。そのまま有効/無効の切り替えや削除、URLの変更、シークレットの再生成が行なえます。

Webhook通知ペイロード​

PDF生成が完了すると、以下のペイロード(file.completed)が POST されます。送信先は、生成リクエストで webhookUrl を指定しなかった場合は対象デザインフィルタにマッチした各Webhookエンドポイント、指定した場合はそのエンドポイントだけです。

ペイロード形式​

{
"event": "file.completed",
"timestamp": "2026-02-15T10:30:45.123Z",
"workspaceId": "ws_abc123",
"requestId": "kZ3mP9qL2xV7nR4t",
"designId": "0eUDdgAjNXrrItA2",
"version": 5,
"files": [
{
"fileId": "Hn4sT8wQ1cV6bX2e",
"fileName": "請求書.pdf",
"passthrough": { "pageId": "abc123" },
"share": {
"shareType": "workspace",
"url": "https://re-port-flow.com/file/{requestId}/{fileId}",
"passcodeEnabled": false
}
}
]
}

フィールド説明​

フィールド型説明
eventstring固定値: "file.completed"
timestampstringイベント発生時刻 (ISO 8601形式)
workspaceIdstringワークスペースID(参照用)
requestIdstringリクエストID(ダウンロードエンドポイントで使用)
designIdstringテンプレート ID
versionnumberバージョン番号
filesarray生成した PDF の情報の配列
files[].fileIdstringファイルID(個別ダウンロードエンドポイントで使用)
files[].fileNamestring生成リクエストで指定したファイル名そのまま(.pdf は付け足されない)
files[].passthroughobjectリクエスト時に指定した passthrough の値(指定時のみ)
files[].share.shareTypestring共有タイプ(workspace / invited / public)
files[].share.urlstringファイル表示URL
files[].share.passcodeEnabledbooleanパスコード有効フラグ
files[].share.passcodestringサーバー生成パスコード(passcodeEnabled=true かつ生成直後のみ)
passthrough の使い所

ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に params(生成データ)を返しません。

受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に passthrough に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。

{
"fileName": "invoice.pdf",
"passthrough": { "invoiceId": "INV-001", "tenantId": "acme" },
"params": { "customerName": "山田太郎", "amount": 10000 }
}

Webhook 受信時に passthrough の値がそのまま返るため、invoiceId から DB を引いて該当レコードを更新する、といった処理が組めます。 params(顧客名や金額)は外部に送信されません。

さらに passthrough のトップレベルにある文字列・数値は、帳票検索用のメタデータ としても保存されます(生成PDF内の XMP メタデータと、Re:port Flow アプリの帳票検索 インデックス)。対象になる値の条件・上限と、個人情報を入れてはいけない理由は passthrough と帳票検索メタデータを参照してください。

リクエスト単位の通知先(webhookUrl)​

生成リクエスト(上記 4 エンドポイント)のボディに webhookUrl を指定すると、その生成の通知を指定した 1 件のエンドポイントだけに送ります。

{
"designId": "0eUDdgAjNXrrItA2",
"version": 1,
"content": { "fileName": "invoice", "params": { "amount": 10000 } },
"webhookUrl": "https://example.com/webhooks/report-flow"
}
  • https:// で始まる URL だけを指定できます(最大 2048 文字)。形式が合わない場合は 400 VALIDATION_FAILED になります。
  • ワークスペースに登録済みで有効なエンドポイントの URL と一致する必要があります。一致しない(未登録・無効化済み)場合は 400 WEBHOOK_URL_NOT_ALLOWED を返し、生成は行いません。任意の URL へは送れないので、先にエンドポイントとして登録してください。
  • 指定した場合、通知は対象デザインフィルタに関係なくそのエンドポイントだけに送られ、他のエンドポイントには送られません。署名には、そのエンドポイントのシークレットを使います。
  • 指定しない場合は従来どおり、対象デザインフィルタに一致する有効なエンドポイントすべてに file.completed が送られます。
  • 生成失敗の通知(file.failed)は、webhookUrl を指定したときだけ送られます。

生成失敗の通知(file.failed)​

非同期生成(POST /file/async/single・POST /file/async/multiple)が失敗したときに送られるイベントです。生成リクエストで webhookUrl を指定した場合だけ、その宛先 1 件にだけ送られます。webhookUrl を指定しない生成では、登録済みのどのエンドポイントにも送られません(既存の受信側が event を見ずに files[] を完了として扱うことがあるため)。同期生成の失敗は HTTP の応答で返るため、通知されません。

状況error.codefiles
単一生成が失敗した失敗の理由(例: PARAMS_TYPE_MISMATCH、INTERNAL_ERROR)そのファイル 1 件
複数生成ですべてのファイルが失敗したFILE_GENERATION_FAILED失敗したすべてのファイル(理由はファイルごと)
複数生成で一部のファイルだけ失敗したPARTIAL_FAILURE失敗したファイルだけ(成功したファイルは file.completed で届く)
受付後に生成処理全体が例外で止まった失敗の理由リクエストのすべてのファイル

ペイロード形式​

{
"event": "file.failed",
"timestamp": "2026-02-15T10:30:46.456Z",
"workspaceId": "ws_abc123",
"requestId": "kZ3mP9qL2xV7nR4t",
"designId": "0eUDdgAjNXrrItA2",
"version": 5,
"error": {
"code": "PARTIAL_FAILURE",
"message": "1 of 3 files failed to generate."
},
"files": [
{
"fileId": "Rt7uY2iO9pA4sD6f",
"fileName": "invoice_002",
"passthrough": { "invoiceId": "INV-002" },
"error": {
"code": "PARAMS_TYPE_MISMATCH",
"message": "Parameter validation failed: \"amount\" must be a number"
}
}
]
}
フィールド型説明
eventstring固定値: "file.failed"
timestampstringイベント発生時刻 (ISO 8601形式)
workspaceId / requestId / designId / versionfile.completed と同じ
error.codestringリクエスト全体の失敗理由(エラーコード一覧)。一部だけ失敗した場合は PARTIAL_FAILURE
error.messagestring失敗理由の説明(英語)
files[].fileIdstring202 応答で返したファイル ID
files[].fileNamestring生成リクエストで指定したファイル名
files[].passthroughobjectリクエスト時に指定した passthrough の値(指定時のみ)。どの入力が失敗したかの突き合わせに使う
files[].errorobjectそのファイルの失敗理由(code / message。英語)

生成に失敗したファイルには share はありません。署名ヘッダー・リトライの扱いは file.completed と同じなので、受信側は file.failed にも 2xx を返してください(2xx 以外はリトライされます)。同じ失敗理由は 状態 API の error / failedFiles でも確認できます。

PDFのダウンロード​

Webhook通知のペイロードから、以下の情報を使ってPDFをダウンロードできます:

  • requestId: ペイロードの requestId(ZIP一括ダウンロード用)
  • fileId: ペイロードの files[].fileId(個別ダウンロード用)
# ZIP一括ダウンロード
GET /v1/file/download/{requestId}

# 個別PDFダウンロード
GET /v1/file/download/{requestId}/{fileId}

詳細はPDF のダウンロードを参照してください。

実装例​

Node.js (Express)​

import express from 'express';
import axios from 'axios';
const app = express();

app.use(express.json());

app.post('/webhooks/pdf-completed', async (req, res) => {
const payload = req.body;

// 生成失敗(webhookUrl を指定した生成だけに届く)。2xx を返さないとリトライされる
if (payload.event === 'file.failed') {
console.error(`PDF生成失敗: ${payload.error.code}`, payload.files);
return res.status(200).json({ received: true });
}

// イベント検証
if (payload.event !== 'file.completed') {
return res.status(400).json({ error: 'Unknown event' });
}

console.log(`PDF生成完了: ${payload.files.length}件`);

// 各ファイルをダウンロード
for (const file of payload.files) {
const downloadUrl = `https://api.re-port-flow.com/v1/file/download/${payload.requestId}/${file.fileId}`;

try {
// PDFダウンロード
const pdfResponse = await axios.get(downloadUrl, {
headers: {
'appkey': process.env.APP_KEY
},
responseType: 'arraybuffer'
});

const pdfBuffer = Buffer.from(pdfResponse.data);

// 送信先などの業務メタデータは、リクエスト時に content.passthrough に
// 入れて Webhook で受け取る。Webhook ペイロードに params (顧客名や金額)
// は返らないため、params から引くサンプルは動かない。
// 例: リクエスト content.passthrough = { recipientEmail, invoiceId }
await sendEmailWithAttachment({
to: file.passthrough?.recipientEmail,
subject: `PDFファイル: ${file.fileName}`,
attachments: [{
filename: file.fileName,
content: pdfBuffer
}]
});

console.log(`メール送信完了: ${file.fileName}`);
} catch (error) {
console.error(`ファイルダウンロードエラー: ${file.fileName}`, error.message);
}
}

// 200 OKを返す
res.status(200).json({ received: true });
});

app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});

Python (Flask)​

from flask import Flask, request, jsonify
import requests
import os

app = Flask(__name__)

@app.route('/webhooks/pdf-completed', methods=['POST'])
def webhook_handler():
payload = request.json

# 生成失敗(webhookUrl を指定した生成だけに届く)。2xx を返さないとリトライされる
if payload.get('event') == 'file.failed':
print(f"PDF生成失敗: {payload['error']['code']}", payload.get('files'))
return jsonify({'received': True}), 200

# イベント検証
if payload.get('event') != 'file.completed':
return jsonify({'error': 'Unknown event'}), 400

print(f"PDF生成完了: {len(payload['files'])}件")

# 各ファイルをダウンロード
request_id = payload['requestId']

for file_info in payload['files']:
download_url = f"https://api.re-port-flow.com/v1/file/download/{request_id}/{file_info['fileId']}"

try:
# PDFダウンロード
pdf_response = requests.get(
download_url,
headers={
'appkey': os.getenv('APP_KEY')
}
)
pdf_response.raise_for_status()

# ファイルに保存
with open(file_info['fileName'], 'wb') as f:
f.write(pdf_response.content)

print(f"ダウンロード完了: {file_info['fileName']}")

except Exception as e:
print(f"エラー: {file_info['fileName']}, {str(e)}")

# 200 OKを返す
return jsonify({'received': True}), 200

if __name__ == '__main__':
app.run(port=3000)

セキュリティベストプラクティス​

1. HTTPS URLを使用​

Webhook URLは必ずHTTPSを使用してください。HTTP URLは拒否されます。

2. 署名検証の実装(HMAC-SHA256)​

ReportFlow は Webhook ペイロードに HMAC-SHA256 署名を付与します。受信側で署名を検証することで、第三者によるなりすましリクエストを拒否できます。

署名鍵(Webhook Secret)の取得​

署名鍵はワークスペース単位ではなく、Webhookエンドポイントごとに whsec_ プレフィックス付きの秘密鍵が発行されます。同じワークスペース内でも、エンドポイントが異なればシークレットも異なります。

  • UI から取得: ワークスペース設定 > 開発者向けタブ > 対象エンドポイントの「署名シークレットをコピー」
  • API から再生成: POST /workspace/:workspaceId/webhook-endpoints/:endpointId/secret/regenerate(管理者のみ)
  • API から取得: GET /workspace/:workspaceId/webhook-endpoints/:endpointId/secret(管理者のみ・ログインセッション専用)
シークレット取得 API は OAuth アクセストークンでは呼べません

GET .../webhook-endpoints/:endpointId/secret は第一者のログインセッション(rf_access_token Cookie、またはログインで発行されたアクセストークン)専用です。OAuth2 で連携アプリに発行したアクセストークンで呼ぶと、付与スコープに関係なく 403 を返します。

既存シークレットを読み出せると、そのエンドポイント宛の Webhook を受信側に気付かれないまま偽造できるためです。一方 POST .../secret/regenerate は旧シークレットを破棄するので、盗まれた場合は既存連携の署名検証が壊れて発覚します。このため regenerate は OAuth からも呼べるままにしてあります(ただし管理者ロールと pdf:generate スコープの両方が必要で、対象エンドポイントは上記「OAuth 連携が操作できるエンドポイントの範囲」の制限を受けます)。

連携アプリ側でシークレットが必要な場合は、UI からコピーして設定するか、secret/regenerate の応答で受け取ってください。

再生成すると、そのエンドポイントの既存のシークレットは即座に無効化されます(他のエンドポイントのシークレットには影響しません)。

シークレット未生成のエンドポイントには署名ヘッダは付与されません(後述)。移行済みエンドポイント(既存の単一URL設定から引き継いだもの)には、移行前に生成済みだったシークレットがそのまま引き継がれます。

署名ヘッダの形式​

X-Report-Flow-Signature: t=1739610645,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t: Unix タイムスタンプ(秒)。リプレイ攻撃対策として、現在時刻との差が 5 分以内であることを推奨。
  • v1: HMAC-SHA256 署名。署名対象は <t>.<rawBody>(タイムスタンプとリクエストボディを . で連結)。

重要: 署名対象は 生のリクエストボディ(パース前) です。JSON パース後にシリアライズし直すとキー順序や空白が変わって署名が一致しなくなります。

検証サンプル(Node.js)​

import crypto from 'crypto';
import express from 'express';

const app = express();
app.use(express.raw({ type: 'application/json' })); // 生のボディを保持

app.post('/webhook', (req, res) => {
const sigHeader = req.header('X-Report-Flow-Signature') || '';
const parts = Object.fromEntries(
sigHeader
.split(',')
.map((kv) => kv.split('='))
.filter(([, v]) => v !== undefined),
);
const timestamp = parts.t;
const signature = parts.v1;

if (!timestamp || !signature) {
return res.status(400).send('Missing signature components');
}

// 5分以内のタイムスタンプか確認
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.status(400).send('Stale timestamp');
}

const expected = crypto
.createHmac('sha256', process.env.REPORT_FLOW_WEBHOOK_SECRET)
.update(`${timestamp}.${req.body.toString()}`)
.digest('hex');

// timingSafeEqual は長さが違うと TypeError を投げるので先に長さを揃える
const expectedBuf = Buffer.from(expected);
const signatureBuf = Buffer.from(signature);
if (
expectedBuf.length !== signatureBuf.length ||
!crypto.timingSafeEqual(expectedBuf, signatureBuf)
) {
return res.status(400).send('Invalid signature');
}

const payload = JSON.parse(req.body.toString());
console.log('Verified webhook:', payload.event);
res.status(200).send('OK');
});

検証サンプル(Python / Flask)​

import hmac, hashlib, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['REPORT_FLOW_WEBHOOK_SECRET']

@app.post('/webhook')
def webhook():
raw = request.get_data() # 生バイト列
sig_header = request.headers.get('X-Report-Flow-Signature', '')
parts = dict(
kv.split('=', 1) for kv in sig_header.split(',') if '=' in kv
)
timestamp, signature = parts.get('t'), parts.get('v1')

if not timestamp or not signature:
abort(400, 'Missing signature components')

if abs(time.time() - int(timestamp)) > 300:
abort(400, 'Stale timestamp')

# raw はバイナリのまま結合する (UTF-8 でない可能性に備えて decode しない)
signed_payload = f'{timestamp}.'.encode() + raw
expected = hmac.new(SECRET.encode(), signed_payload, hashlib.sha256).hexdigest()

if not hmac.compare_digest(expected, signature):
abort(400, 'Invalid signature')

return 'OK', 200

検証サンプル(Go)​

package main

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)

var secret = os.Getenv("REPORT_FLOW_WEBHOOK_SECRET")

func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
parts := map[string]string{}
for _, kv := range strings.Split(r.Header.Get("X-Report-Flow-Signature"), ",") {
if i := strings.Index(kv, "="); i > 0 {
parts[kv[:i]] = kv[i+1:]
}
}
if parts["t"] == "" || parts["v1"] == "" {
http.Error(w, "missing signature components", 400)
return
}
ts, err := strconv.ParseInt(parts["t"], 10, 64)
if err != nil {
http.Error(w, "invalid timestamp", 400)
return
}
if diff := time.Now().Unix() - ts; diff > 300 || diff < -300 {
http.Error(w, "stale timestamp", 400)
return
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(parts["t"] + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal は長さ不一致でも安全に false を返す
if !hmac.Equal([]byte(expected), []byte(parts["v1"])) {
http.Error(w, "invalid signature", 400)
return
}
w.WriteHeader(200)
}

後方互換について​

シークレット未生成のエンドポイントには X-Report-Flow-Signature ヘッダは付与されません。署名検証を導入する受信側は、まず開発者向けタブで対象エンドポイントのシークレットを生成してください。

3. タイムアウト設定​

Webhookエンドポイントは5秒以内に応答することを推奨します。重い処理(メール送信、データベース書き込みなど)は非同期ジョブキューで実行してください。

4. URLに認証情報を含めない​

Webhook URLのクエリパラメータに認証トークンやシークレットを含めないでください。認証が必要な場合は、Headerで送信するか、別途管理してください。

配信が失敗したときはリトライされる?​

されます。失敗した配信は指数バックオフで自動的に再送され、初回を含めて最大3回まで試行されます(=リトライは最大2回)。すべての試行が失敗しても、PDF生成処理自体は成功扱いのままです。

Webhook配信は、エンドポイントが2xx系のステータスコードを返した場合のみ成功として扱われます。2xx以外のレスポンス(4xx/5xxを問わず)、およびネットワークエラーやタイムアウトは、すべて失敗として扱われリトライ対象になります。

失敗した配信は指数バックオフ(1秒 → 2秒)で自動的に再送され、初回を含めて最大3回まで試行されます(=リトライは最大2回)。すべての試行が失敗しても、PDF生成処理自体は成功扱いとなります。

トラブルシューティング​

Webhook通知が届かないときは?​

原因の多くは「エンドポイントが無効」「対象デザインフィルタの不一致」「HTTPS/公開URLでない」のいずれかです。設定画面とエンドポイントのログを次の順に確認してください。

以下を確認してください:

  1. Webhookエンドポイントが設定・有効化されているか

    • ワークスペース設定 > 開発者向けタブで確認(無効化したエンドポイントには送信されません)
  2. 対象デザインフィルタが一致しているか

    • selected を選択しているエンドポイントの場合、生成に使ったテンプレートが targetDesignIds に含まれているか確認してください
    • 対象テンプレートを削除した場合、エンドポイント設定からは自動で除去されないため、一覧の警告バッジ(削除済み対象 X 件)を確認してください
  3. HTTPSを使用しているか

    • HTTP URLは拒否されます
  4. 公開URLを使用しているか

    • localhostやプライベートIPアドレスは拒否されます
    • テスト環境ではwebhook.siteなどのサービスを利用してください
  5. エンドポイントが2xxを返すか

    • 2xx以外のレスポンス(4xx/5xx)やタイムアウトはすべて失敗として扱われ、リトライされます(リトライ動作参照)
    • エンドポイントのログを確認してください

通知が届かない場合は、サポートにお問い合わせください。

次のステップ​