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

PDF生成ガイド

Re:port Flow の PDF 生成には、その場で PDF バイナリを受け取る同期生成と、requestId だけを即座に受け取って後から取得する非同期生成の2モードがあります。即座に結果が必要なら同期、大量生成やバックグラウンド処理なら非同期を選びます。同期は 60 秒でタイムアウトします。このガイドでは両モードの呼び出し方、パラメータ構造、エラー処理までを通して説明します。

同期生成と非同期生成はどう使い分ける?​

即座に結果が必要なら同期、大量生成やバックグラウンド処理なら非同期です。同期は 60 秒でタイムアウトするため、60 秒を超えうる処理は非同期に寄せてください(制限事項)。

PDF生成には2つのモードがあります:

モードエンドポイントレスポンス用途
同期生成/file/sync/singlePDFバイナリ即座に結果が必要な場合
非同期生成/file/async/singlerequestId / url / files 配列 (202 Accepted)大量生成やバックグラウンド処理

同期生成​

基本的な使い方​

curl -X POST https://api.re-port-flow.com/v1/file/sync/single \
-H "appkey: your-application-key" \
-H "Content-Type: application/json" \
-d '{
"designId": "0eUDdgAjNXrrItA2",
"version": 1,
"content": {
"fileName": "invoice.pdf",
"params": {
"customerName": "山田太郎",
"invoiceNumber": "INV-2024-001",
"items": [
{
"name": "商品A",
"price": 1000,
"quantity": 2
}
]
}
}
}' \
--output invoice.pdf

レスポンスヘッダー​

Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice.pdf"
Content-Length: 123456
Request-Id: kZ3mP9qL2xV7nR4t
File-URL: https://re-port-flow.com/{workspaceId}/templates/{designId}/outputs?requestId={requestId}
X-File-Mapping: %5B%7B%22fileId%22%3A...%7D%5D

X-File-Mapping は URL エンコード済みの JSON 配列 で、fileId / fileName / share 情報を含みます。クライアント側で decodeURIComponent してから JSON.parse してください。詳細は 単一PDF同期生成エンドポイント を参照。

JavaScript実装例​

import axios from 'axios';
import fs from 'fs';

async function generatePDF(params) {
try {
const response = await axios.post(
'https://api.re-port-flow.com/v1/file/sync/single',
{
designId: params.designId,
version: params.version,
content: {
fileName: params.fileName,
params: params.data
}
},
{
headers: {
'appkey': process.env.APP_KEY,
'Content-Type': 'application/json'
},
responseType: 'arraybuffer'
}
);

// ファイルに保存
fs.writeFileSync(params.fileName, response.data);

// レスポンスヘッダから情報を取得
const requestId = response.headers['request-id'];
const fileUrl = response.headers['file-url'];
const fileMapping = JSON.parse(
decodeURIComponent(response.headers['x-file-mapping'] || '%5B%5D'),
);

return { requestId, fileUrl, fileMapping };
} catch (error) {
console.error('PDF生成エラー:', error.response?.data || error.message);
throw error;
}
}

// 使用例
generatePDF({
designId: '0eUDdgAjNXrrItA2',
version: 1,
fileName: 'invoice.pdf',
data: {
customerName: '山田太郎',
invoiceNumber: 'INV-2024-001',
items: [
{ name: '商品A', price: 1000, quantity: 2 }
]
}
});

非同期生成​

大量のPDFを生成する場合や、タイムアウトを避けたい場合は非同期生成を使用します。

基本的な使い方​

# 1. 生成リクエスト
curl -X POST https://api.re-port-flow.com/v1/file/async/single \
-H "appkey: your-application-key" \
-H "Content-Type: application/json" \
-d '{
"designId": "0eUDdgAjNXrrItA2",
"version": 1,
"content": {
"fileName": "invoice.pdf",
"params": {...}
}
}'

# レスポンス例 (202 Accepted)
{
"requestId": "kZ3mP9qL2xV7nR4t",
"url": "https://re-port-flow.com/{workspaceId}/templates/{designId}/outputs?requestId={requestId}",
"files": [
{
"fileName": "invoice.pdf",
"fileId": "Hn4sT8wQ1cV6bX2e",
"share": {
"shareType": "workspace",
"url": "https://re-port-flow.com/file/{requestId}/{fileId}",
"passcodeEnabled": false
}
}
]
}

詳細は 単一PDF非同期生成エンドポイント を参照。

JavaScript実装例(ポーリング)​

const API_BASE = 'https://api.re-port-flow.com';

// 状態API(GET /v1/file/status/{requestId})で完了・失敗を待つ。
// 生成中は Retry-After ヘッダー(秒)が付くので、その間隔で問い合わせる。
async function waitForCompletion(requestId, { timeoutMs = 300000 } = {}) {
const deadline = Date.now() + timeoutMs;
let notFoundRetries = 0;

for (;;) {
// 残り時間は 1 度だけ計算する。axios は timeout: 0 を「タイムアウト無効」と
// 解釈するため、期限切れのまま 0 を渡すとハングしたリクエストで永久に待つ
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) break;

let res;
try {
res = await axios.get(`${API_BASE}/v1/file/status/${requestId}`, {
headers: { 'appkey': process.env.APP_KEY },
timeout: remainingMs,
});
} catch (e) {
// 生成直後の 404 は、ジョブの記録を保存できなかった生成中のリクエストのことがある。2 回まで確かめ直す
if (e.response?.status === 404 && notFoundRetries < 2) {
notFoundRetries += 1;
await new Promise(resolve => setTimeout(resolve, Math.min(3000, Math.max(0, deadline - Date.now()))));
continue;
}
// 429(状態 API は 300 req/min)は Retry-After 秒待って確かめ直す
if (e.response?.status === 429) {
const waitMs = Number(e.response.headers['retry-after'] ?? 60) * 1000;
await new Promise(resolve => setTimeout(resolve, Math.min(waitMs, Math.max(0, deadline - Date.now()))));
continue;
}
throw e;
}
const { status } = res.data;

if (status === 'completed') return res.data; // files[].downloadPath で取得できる
if (status === 'failed') {
// error.code で分岐できる(例: PARAMS_TYPE_MISMATCH / GENERATION_INTERRUPTED)
throw new Error(`PDF生成に失敗しました: ${res.data.error.code} ${res.data.error.message}`);
}

// queued / processing。Retry-After(秒)に従い、残り時間で頭打ちにする
const retryAfterMs = Number(res.headers['retry-after'] ?? 2) * 1000;
await new Promise(resolve =>
setTimeout(resolve, Math.min(retryAfterMs, deadline - Date.now())),
);
}

throw new Error(`PDF生成がタイムアウトしました: ${requestId}`);
}

async function generatePDFAsync(params) {
// 1. 非同期生成リクエスト
const response = await axios.post(
'https://api.re-port-flow.com/v1/file/async/single',
{
designId: params.designId,
version: params.version,
content: {
fileName: params.fileName,
params: params.data
}
},
{
headers: {
'appkey': process.env.APP_KEY,
'Content-Type': 'application/json'
}
}
);

const { requestId, url, files } = response.data;

// 2. 状態APIで完了を待つ。ポーリングの代わりに Webhook 通知(file.completed)を
// 受け取る方法もある。失敗の通知 file.failed は、生成リクエストで webhookUrl を
// 指定した場合だけ、その宛先に届く。
const status = await waitForCompletion(requestId);

// 3. ファイルはダウンロードAPIから取得する。
// レスポンスの `url` はアプリ画面(出力履歴)へのリンクで、ログインが必要なため
// プログラムからのダウンロードには使えない。
// downloadPath は /v1/file/download/{requestId}/{fileId}
const pdfResponse = await axios.get(`${API_BASE}${status.files[0].downloadPath}`, {
headers: { 'appkey': process.env.APP_KEY },
responseType: 'arraybuffer',
});

return {
data: pdfResponse.data,
requestId,
// アプリ画面(出力履歴)へのリンク。ダウンロードURLではない
outputsUrl: url,
files,
};
}

複数PDF生成(ZIP)​

複数のPDFを一度に生成してZIPで取得できます。

同期生成​

async function generateMultiplePDFs(designId, contents) {
const response = await axios.post(
'https://api.re-port-flow.com/v1/file/sync/multiple',
{
designId,
version: 1,
contents // ContentDto の配列
},
{
headers: {
'appkey': process.env.APP_KEY
},
responseType: 'arraybuffer'
}
);

// X-File-Mapping ヘッダー(URL エンコード済み JSON)からファイルマッピングを取得
const fileMapping = JSON.parse(
decodeURIComponent(response.headers['x-file-mapping'] || '%5B%5D'),
);
console.log('生成されたファイル:', fileMapping);

return response.data; // ZIP binary
}

// 使用例
const contents = [
{
fileName: 'invoice_001.pdf',
params: { customerName: '山田太郎', invoiceNumber: 'INV-001' }
},
{
fileName: 'invoice_002.pdf',
params: { customerName: '佐藤花子', invoiceNumber: 'INV-002' }
}
];

const zipData = await generateMultiplePDFs('0eUDdgAjNXrrItA2', contents);
fs.writeFileSync('invoices.zip', zipData);

パラメータの構造​

テンプレートのパラメータの取得​

生成前に、テンプレートで使用可能なパラメータ構造を確認できます:

curl -X GET https://api.re-port-flow.com/v1/file/design/parameter/{designId}?version=1 \
-H "appkey: your-application-key"

レスポンス例:

[
{ "name": "customerName", "type": "text", "label": "顧客名" },
{ "name": "invoiceNumber", "type": "text", "label": "請求書番号" },
{ "name": "amount", "type": "number", "label": "金額" },
{
"name": "items",
"type": "array",
"label": "明細",
"spec": [
{ "name": "name", "type": "text", "label": "品目" },
{ "name": "price", "type": "number", "label": "単価" },
{ "name": "quantity", "type": "number", "label": "数量" }
]
},
{ "name": "issueDate", "type": "date", "label": "発行日" }
]

レスポンスはパラメータ定義の配列です。各要素の name が生成時に params へ渡すキー、label は画面表示用の名前(未設定なら name と同じ)です。array と collection は子フィールドを spec に持ちます。

キーは name をそのまま使う

name はテンプレート作成者が付けた名前で、日本語などの非 ASCII 文字(例: "顧客名")のこともあります。params のキーには name の値を変換せずそのまま使ってください(label をキーにすると一致しません)。API から使う前提のテンプレートを作るときは、プログラムで扱いやすい ASCII のキー(例: customerName)を付け、日本語の表示名は label 側に入れることを推奨します。

パラメータ型の対応​

型説明例
text文字列"山田太郎"
number数値1000
date日付(ISO 8601)"2024-02-12"
boolean真偽値true
collectionspec で定義されたネストオブジェクト{ "name": "値" }
arrayspec で行構造が定義された配列[{ "item": 1 }]

型ごとの詳細は テンプレートのパラメータ取得 を参照してください。

テンプレート側で計算できること​

3 桁区切り・和暦・日付の書式・明細行ごとの計算(単価 × 数量)・条件による文字や文字色の切り替えは、テンプレートの数式で処理できます。API 側で整形する前に、テンプレートに任せられるかを確認してください。一方、表の列の合計や税率ごとの集計はテンプレートでは計算できないため、呼び出し側で計算して params に渡します。

passthrough と帳票検索メタデータ​

content.passthrough は、レスポンスや Webhook にそのままエコーバックされる任意のメタデータです。 params(生成データ)はレスポンスにも Webhook にも返らないため、受信側で「どの業務レコードに 対する PDF か」を識別する用途に使います。

{
"fileName": "invoice.pdf",
"passthrough": { "invoiceId": "INV-001", "customerName": "株式会社サンプル" },
"params": { "customerName": "株式会社サンプル", "amount": 10000 }
}

加えて passthrough の値は検索用メタデータとしても保存されます。保存先は次の2か所です。

保存先内容参照方法
生成PDF内の XMP メタデータrf:params に key=value として埋め込みPDF 受領者の OS 検索(Spotlight / Windows Search)等
Re:port Flow の検索インデックスワークスペースの帳票検索に使用Re:port Flow アプリの帳票検索画面

これにより、生成時に customerName や invoiceId を渡しておくと、後から アプリの帳票検索画面でその値を使って PDF を探せます。

インデックス対象になる値​

passthrough の各値(文字列または数値)は、次の条件でインデックス対象になります。

条件挙動
文字列(string)前後の空白を除去して保存。空文字列は対象外
数値(number)有限数のみ、10進表記の文字列として保存
値の長さ256 文字を超える分は切り詰めて保存
キーの件数1ファイルあたり最大 64 件。超過分は保存されない

インデックス対象外になった値(空文字列・非有限数・64件超過分)も、レスポンス・Webhook へのエコーバックには従来どおりすべて含まれます(エコーバックの挙動は変わりません)。

個人情報・機微情報は入れないでください

passthrough の値は生成したPDFのメタデータに埋め込まれます。PDF を受け取った相手が メタデータを閲覧でき、相手の OS の検索インデックスにも載ります。

マイナンバー・口座番号・健康情報などの機微情報や、開示したくない個人情報は passthrough に入れないでください。PDF 面に印字したいだけの値は params を使います (この仕組みでは params の値は PDF 内メタデータにも検索インデックスにも保存されません。 テンプレート側でパラメータが個別に検索対象として設定されている場合はこの限りではないため、 心配な場合はテンプレート作成者に確認してください)。

エラーハンドリング​

一般的なエラー​

400 Bad Request - バリデーションエラー​

{
"statusCode": 400,
"code": "VALIDATION_FAILED",
"message": [
"version must be an integer number",
"content.fileName contains characters that are not allowed (/ \\ : * ? \" < > | and control characters)."
],
"error": "Bad Request",
"details": [
{ "field": "version", "message": "version must be an integer number" },
{
"field": "content.fileName",
"code": "FILE_NAME_INVALID",
"message": "fileName contains characters that are not allowed (/ \\ : * ? \" < > | and control characters)."
}
],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#validation_failed"
}

対処法:

  • details[].field が指す項目を確認
  • ファイル名の形式を確認(/ \ : * ? " < > | および制御文字は使用不可)

message の各要素は、ネストした項目では親のパスが前に付きます(例: content.fileName ...、複数生成なら contents.0.fileName ...)。details[].message には付きません。

エラー本文の message は既定で英語です。Accept-Language: ja を送ると、日本語訳がある文言だけが日本語になります(version must be an integer number のような入力検証の標準の文言は英語のまま)。分岐には message ではなく code を使ってください(一覧: エラーコード一覧)。

500 Internal Server Error​

{
"statusCode": 500,
"code": "INTERNAL_ERROR",
"message": "Internal Server Error",
"error": "Internal Server Error",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#internal_error"
}

対処法:

  • 一時的なサーバーエラーの可能性
  • リトライロジックを実装
  • 継続する場合はサポートに連絡

リトライ戦略​

async function generatePDFWithRetry(params, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await generatePDF(params);
} catch (error) {
if (error.response?.status === 500 && i < maxRetries - 1) {
// 指数バックオフでリトライ
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
continue;
}
throw error;
}
}
}

ベストプラクティス​

1. タイムアウト設定​

同期生成は60秒でタイムアウトします(API の手前のロードバランサーが 504 を返します)。大きなPDFの場合は非同期生成を使用してください。クライアント側のタイムアウトは、サーバー側の 504 を受け取れるよう 60 秒より少し長くします。

// axios でタイムアウト設定
const response = await axios.post(url, data, {
timeout: 70000 // 70秒(サーバー側は 60 秒で 504 を返す)
});

2. ファイル名のサニタイゼーション​

function sanitizeFileName(fileName) {
// / \ : * ? " < > | および制御文字を除去
return fileName.replace(/[\/\\:*?"<>|\x00-\x1F]/g, '_');
}

3. パラメータの検証​

function validateParams(params, schema) {
// schema は GET /v1/file/design/parameter/{designId} の配列 [{ name, type, label, spec? }]
for (const field of schema) {
// キーは label ではなく name(日本語のこともあるので変換しない)
if (!(field.name in params)) {
console.warn(`${field.label}(${field.name})が指定されていません`);
continue;
}
// field.type(text / number / date / boolean / array / collection)で型チェック等
}
}

型ごとの検証例は テンプレートのパラメータ取得 > バリデーション を参照してください。

次のステップ​