# Report Flow API Documentation > Developer documentation for Report Flow API Source: https://doc.re-port-flow.com Generated: 2026-05-30T09:43:07.007Z --- # docs/authentication/api-keys.md # APIキーの取得と管理 このページでは、Report Flow APIを使用するために必要なAPIキーの取得方法と管理方法を説明します。 ## APIキーの取得 ### 1. ワークスペースを作成 Report Flowの管理画面(`https://re-port-flow.com`)にログインし、新しいワークスペースを作成します。 ワークスペース作成時に、アプリケーションキーが自動的に生成されます。 ### 2. API連携情報を確認 ワークスペース詳細画面→「API連携」タブを開くと、以下の情報が表示されます: - **API URL**: `https://api.re-port-flow.com/`(コピーボタンあり) - **アプリケーションキー**: `ak_xxxxxxxxxxxxxxxx`(コピーボタンあり) 各項目の右側にあるコピーボタンをクリックすると、値がクリップボードにコピーされます。 :::info **1ワークスペースにつき1つのアプリケーションキー** 各ワークスペースには1つのアプリケーションキーが紐付いています。キーは定期的に再生成して更新できます。 ::: ## APIキーの管理 ### キーの確認 ワークスペース詳細画面の「API連携」タブで、現在のアプリケーションキー情報を確認できます: - **API URL**: `https://api.re-port-flow.com/`(コピー可能) - **アプリケーションキー**: `ak_xxxxxxxxxxxxxxxx`(コピー可能) - **最終ローテーション日**: `2024-02-14` ## セキュリティ推奨事項 ### 環境変数での管理 **推奨:** APIキーは環境変数で管理し、コードにハードコードしない ```bash # .env ファイル REPORT_FLOW_APP_KEY=ak_xxxxxxxxxxxxxxxx ``` ```javascript // 使用例 const appKey = process.env.REPORT_FLOW_APP_KEY; ``` ### .gitignoreへの追加 ```gitignore # APIキーを含むファイルをGit管理から除外 .env .env.local .env.production secrets.json ``` ### 定期的なローテーション セキュリティ向上のため、**3ヶ月ごと**にアプリケーションキーを再生成することを推奨します。 :::tip ワークスペース詳細画面の「API連携」セクションで、最終ローテーション日から90日以上経過している場合、警告が表示されます。 ::: **UIからの再生成手順:** 1. ワークスペース詳細画面→「API連携」タブを開く 2. 「アプリケーションキーを再生成」ボタンをクリック 3. 確認モーダルで「再生成」を実行 4. **表示された新しいキーをコピーし、アプリケーションに設定する** 5. 動作確認を実施 **APIからの再生成方法:** ```bash curl -X POST \ https://api.re-port-flow.com/v1/workspace/{workspaceId}/application-token/regenerate \ -H 'Authorization: Bearer {access_token}' \ -H 'Content-Type: application/json' ``` レスポンス例: ```json { "applicationKey": "ak_xxxxxxxxxxxxxxxx", "lastRotatedAt": "2024-02-14T10:30:00.000Z" } ``` :::warning アプリケーションキーを再生成すると、**古いキーは即座に無効**になります。アプリケーションのダウンタイムを避けるため、新しいキーを速やかに設定に反映してください。 ::: ## セキュリティ Report FlowのAPIキーは、業界標準のセキュリティ対策により安全に管理されています: - アプリケーションキーは暗号化されて保存されます - すべてのAPI操作は監査ログに記録されます - HTTPS通信のみをサポートし、平文での通信は受け付けません :::tip セキュリティを最大限に高めるため、定期的なキーローテーション(3ヶ月ごと)を推奨します。 ::: ### 本番環境のセキュリティ要件 HTTPS通信必須・TLSバージョン要件については[認証概要](./overview.md)を参照してください。 ### その他のセキュリティベストプラクティス #### APIキーの保管 - 環境変数で管理し、コードにハードコードしない - `.gitignore`に`.env`ファイルを追加 - シークレット管理サービス(AWS Secrets Manager等)の使用を推奨 #### アクセス制御 - 必要最小限のワークスペースにのみAPIキーを発行 - 不要になったAPIキーは即座に再生成 - 定期的なアクセスログの監査 詳細な制限事項については、[制限事項ページ](../limitations.md)を参照してください。 ## トラブルシューティング ### アプリケーションキーを再生成する場合 アプリケーションキーは管理画面の「API連携」タブから確認できます。確認できない場合は以下の手順で新しいキーを発行してください: 1. ワークスペース詳細画面→「API連携」タブを開く 2. 「アプリケーションキーを再生成」ボタンをクリック 3. 確認モーダルで再生成を実行 4. 表示された新しいキーをコピー 5. アプリケーションの設定を新しいキーで更新 ### キーが動作しない場合 以下を確認してください: - [ ] `appkey` ヘッダーが正しく設定されているか - [ ] アプリケーションキーが正しくコピーされているか - [ ] キーが有効化されているか(管理画面で確認) - [ ] ヘッダー名が `appkey`(小文字)になっているか ## 次のステップ - [認証方法の詳細](./overview) - [PDF生成ガイド](../guides/pdf-generation) --- # docs/authentication/oauth.md # OAuth 2.0 認証 Report Flow API は `appkey` ヘッダによる認証に加えて、OAuth 2.0 / OpenID Connect をサポートしています。サードパーティ統合(Make.com 等)や、独自アプリケーションからの認証フローに利用できます。 ## 利用シーンと選択指針 | 用途 | 推奨方式 | |------|---------| | 単一ワークスペースの自動化(cURL、社内バッチ) | `appkey` ヘッダ | | サーバー間連携(バックエンド → Report Flow) | OAuth 2.0 **Client Credentials** | | ユーザー個別の認可(Make.com、外部 SaaS、個人アプリ) | OAuth 2.0 **Authorization Code + PKCE** | ## エンドポイント > **重要**: OAuth フロー(Discovery / Authorization / Token / UserInfo)は **`re-port-flow.com/api/v1`** にホストされており、PDF 生成等の保護リソース API(`api.re-port-flow.com/v1/...`)とは **別ドメイン**です。発行された Access Token (`Bearer`) は保護リソース API 側で利用します。 OIDC Discovery ドキュメントから自動取得することを推奨します: ``` GET https://re-port-flow.com/api/v1/.well-known/openid-configuration ``` 主なエンドポイント: | 項目 | URL | |------|-----| | Issuer | `https://re-port-flow.com/api/v1` | | Authorization | `${issuer}/oauth/authorize` | | Token | `${issuer}/oauth/token` | | UserInfo | `${issuer}/oauth/userinfo` | | 保護リソース API(Bearer 利用先) | `https://api.re-port-flow.com/v1/...` | ## サポート仕様 | 項目 | 値 | |------|----| | `response_type` | `code` | | `grant_types` | `authorization_code`, `refresh_token`, `client_credentials` | | `code_challenge_method` | `S256`(PKCE 必須) | | `token_endpoint_auth_methods` | `none`, `client_secret_post`, `client_secret_basic` | | アクセストークン有効期限 | 3600 秒(1 時間) | ## スコープ | Scope | 説明 | |-------|------| | `openid` | ユーザーIDの確認(OIDC) | | `profile` | プロフィール情報の取得(本実装ではメールアドレスを含みます) | | `designs:read` | デザインの一覧・詳細を表示する(読み取りのみ) | | `designs:write` | デザインを作成・編集する | | `templates:read` | テンプレートの一覧・詳細を表示する(読み取りのみ) | | `templates:write` | テンプレートを作成・編集する | | `pdf:generate` | テンプレートから PDF を生成する | 複数スコープを指定する場合はスペース区切り(例: `pdf:generate designs:read`)。 ## クライアント種別 | 種別 | `is_public` | 認証方式 | 主な用途 | |------|------------|---------|---------| | Public Client | `true` | PKCE のみ(client_secret なし) | モバイル/SPA、Make.com 等の公開クライアント | | Confidential Client | `false` | client_secret 必須 | サーバー間連携、`client_credentials` グラント | `client_credentials` グラントは **Confidential Client 専用**です。Public Client では使用できません。 --- ## Authorization Code フロー(PKCE) ユーザーごとに認可を取得するフロー。Make.com 等の統合先や、ユーザーが自分のワークスペースに対して操作するアプリで利用します。 ### 1. Authorization リクエスト ``` GET https://re-port-flow.com/api/v1/oauth/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://your-app.example.com/callback &scope=openid%20profile%20pdf:generate &state=RANDOM_STATE &code_challenge=BASE64URL(SHA256(verifier)) &code_challenge_method=S256 ``` ユーザーがログインと同意を完了すると、`redirect_uri` に `code` と `state` が付与されてリダイレクトされます。 ### 2. Token リクエスト ```http POST https://re-port-flow.com/api/v1/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=AUTHORIZATION_CODE &redirect_uri=https://your-app.example.com/callback &client_id=YOUR_CLIENT_ID &code_verifier=ORIGINAL_VERIFIER ``` レスポンス: ```json { "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "rt_...", "scope": "openid profile pdf:generate" } ``` Confidential Client の場合は `client_secret` も同時に送信(`client_secret_post`)するか、Basic 認証ヘッダ(`client_secret_basic`)で送信します。 ### 3. Refresh Token ```http POST https://re-port-flow.com/api/v1/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=refresh_token &refresh_token=rt_... &client_id=YOUR_CLIENT_ID ``` --- ## Client Credentials フロー サーバー間通信用。ユーザー認可なしで、登録済み Confidential Client が自分のワークスペースに対してアクセストークンを取得します。 ### Token リクエスト ```http POST https://re-port-flow.com/api/v1/oauth/token Content-Type: application/x-www-form-urlencoded Authorization: Basic BASE64(client_id:client_secret) grant_type=client_credentials &scope=pdf:generate%20templates:read ``` または `client_secret_post` 形式: ```http POST https://re-port-flow.com/api/v1/oauth/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials &client_id=YOUR_CLIENT_ID &client_secret=YOUR_CLIENT_SECRET &scope=pdf:generate%20templates:read ``` レスポンス(Refresh Token は発行されません): ```json { "access_token": "eyJhbGc...", "token_type": "Bearer", "expires_in": 3600, "scope": "pdf:generate templates:read" } ``` `scope` を省略した場合は、登録時に許可されたスコープ全体(`allowed_scopes`)が付与されます。 --- ## アクセストークンの利用 発行されたアクセストークンは `Authorization: Bearer` ヘッダで送信します。`appkey` ヘッダの代わりに利用できます。 ```bash curl -X POST https://api.re-port-flow.com/v1/file/sync/single \ -H "Authorization: Bearer eyJhbGc..." \ -H "Content-Type: application/json" \ -d '{...}' ``` ## エラーレスポンス トークンエンドポイントは [RFC 6749 Section 5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) に従います: ```json { "error": "invalid_client", "error_description": "Invalid client_secret" } ``` 主なエラーコード: | `error` | 状況 | |---------|------| | `invalid_client` | `client_id` / `client_secret` が不正 | | `invalid_grant` | 認可コード/リフレッシュトークンが不正・期限切れ | | `invalid_scope` | 要求スコープが `allowed_scopes` を超えている | | `unauthorized_client` | Public Client が `client_credentials` を要求した等 | | `unsupported_grant_type` | 未対応の `grant_type` | ## 次のステップ - [APIキーの取得方法](./api-keys) — `appkey` ヘッダの利用方法 - [認証方法(概要)](./overview) — 認証方式の選択と共通事項 - [PDF生成ガイド](../guides/pdf-generation) --- # docs/authentication/overview.md # 認証方法 Report Flow API は **`appkey` ヘッダ認証** と **OAuth 2.0 / OpenID Connect** の 2 方式をサポートします。用途に応じて選択してください。 ## 用途別の選択指針 | 用途 | 推奨方式 | |------|---------| | 単一ワークスペースの自動化(cURL、社内バッチ) | `appkey` ヘッダ | | サーバー間連携(バックエンド → Report Flow) | OAuth 2.0 **Client Credentials** | | ユーザー個別の認可(Make.com、外部 SaaS、個人アプリ) | OAuth 2.0 **Authorization Code + PKCE** | OAuth 2.0 の詳細は [OAuth 2.0 認証](./oauth) を参照してください。本ページでは `appkey` 方式を中心に説明します。 ## `appkey` 方式の仕組み `appkey` ヘッダで認証するリクエストは、以下のヘッダーを付与します: ```http appkey: your-application-key ``` ## APIエンドポイント **Base URL**: `https://api.re-port-flow.com/v1` 例: - 単一PDF生成: `https://api.re-port-flow.com/v1/file/sync/single` - デザインパラメータ取得: `https://api.re-port-flow.com/v1/file/design/parameter/{designId}` ## 認証エラー 認証に失敗した場合、以下のエラーが返されます: ### 401 Unauthorized ```json { "statusCode": 401, "message": "認証情報が不正です", "error": "Unauthorized" } ``` **原因:** - `appkey` ヘッダーが不正または無効 ### 412 Precondition Failed ```json { "statusCode": 412, "message": "認証方式ヘッダーが存在しません", "error": "Precondition Failed" } ``` **原因:** - `appkey` ヘッダーが欠落 ## セキュリティのベストプラクティス ### 1. APIキーの安全な保管 ```javascript // ❌ 悪い例:ソースコードにハードコード const APP_KEY = 'hardcoded-key'; // ✅ 良い例:環境変数を使用 const APP_KEY = process.env.REPORT_FLOW_APP_KEY; ``` ### 2. HTTPS通信の使用 すべてのAPIリクエストは必ずHTTPSを使用してください。HTTPでの通信は受け付けられません。 ### 3. キーのローテーション 定期的にAPIキーを再生成し、古いキーを無効化することを推奨します。 ### 4. スコープの制限 本番環境と開発環境で異なるAPIキーを使用し、環境ごとにアクセス権を分離してください。 ## サンプルコード ### cURL ```bash curl -X POST https://api.re-port-flow.com/v1/file/sync/single \ -H "appkey: your-application-key" \ -H "Content-Type: application/json" \ -d '{...}' ``` ### JavaScript ```javascript const headers = { 'appkey': process.env.REPORT_FLOW_APP_KEY, 'Content-Type': 'application/json' }; ``` ### Python ```python import os headers = { 'appkey': os.environ['REPORT_FLOW_APP_KEY'], 'Content-Type': 'application/json' } ``` ## OAuth 2.0 認証 サーバー間連携や、ユーザー個別の認可が必要な場合は、`appkey` の代わりに OAuth 2.0 を利用できます。 - **Authorization Code + PKCE**: Make.com、独自アプリ等のユーザー認可フロー - **Client Credentials**: バックエンドから Report Flow へのサーバー間連携 詳細は [OAuth 2.0 認証](./oauth) を参照してください。 ## 次のステップ - [APIキーの取得方法](./api-keys) - [OAuth 2.0 認証](./oauth) - [PDF生成ガイド](../guides/pdf-generation) --- # docs/guides/async-workflows.md # 非同期ワークフロー 非同期APIの活用方法とベストプラクティスを説明します。 ## 非同期APIとは Report Flow APIの一部のエンドポイントは非同期処理をサポートしています: | API | 同期 | 非同期 | |-----|------|--------| | 単一PDF生成 | `/file/sync/single` | `/file/async/single` | | 複数PDF生成 | `/file/sync/multiple` | `/file/async/multiple` | ## 非同期処理のメリット ### 1. タイムアウト回避 同期APIは120秒でタイムアウトしますが、非同期APIは長時間の処理に対応できます。 ```javascript // ❌ 同期API: 大量データで120秒を超える可能性 const pdf = await generateSyncPDF(largeData); // ✅ 非同期API: タイムアウトなし const { url } = await generateAsyncPDF(largeData); ``` ### 2. バックグラウンド処理 ユーザーを待たせずに処理を実行できます。 ```javascript // ユーザーリクエストを即座に受け付け app.post('/api/generate-report', async (req, res) => { // 非同期でPDF生成をキック const { url, fileId } = await startPDFGeneration(req.body); // 即座にレスポンス res.json({ message: 'レポート生成を開始しました', downloadUrl: url, fileId }); // 完了後にメール通知(バックグラウンド) notifyWhenComplete(fileId, req.user.email); }); ``` ### 3. 大量処理の並列化 複数のPDF生成を並列実行できます。 ```javascript // 100件のPDFを並列生成 const requests = invoices.map(invoice => generateAsyncPDF({ designId: 'invoice-template', content: invoice }) ); const results = await Promise.all(requests); console.log(`${results.length}件のPDFを生成しました`); ``` ## 大量処理のパターン ### パターン1: 並列実行(小〜中規模) ```javascript async function generateBulkPDFs(items, concurrency = 5) { const chunks = chunkArray(items, concurrency); const results = []; for (const chunk of chunks) { const promises = chunk.map(item => generateAsyncPDF({ designId: 'template-id', content: item }) ); const chunkResults = await Promise.all(promises); results.push(...chunkResults); console.log(`${results.length}/${items.length} 完了`); } return results; } function chunkArray(array, size) { const chunks = []; for (let i = 0; i < array.length; i += size) { chunks.push(array.slice(i, i + size)); } return chunks; } // 使用例: 100件を5件ずつ並列処理 const invoices = [...]; // 100件 const results = await generateBulkPDFs(invoices, 5); ``` ### パターン2: キューベース(大規模) ```javascript import Queue from 'bull'; // Redisベースのキューを作成 const pdfQueue = new Queue('pdf-generation', { redis: { host: 'localhost', port: 6379 } }); // ワーカー設定 pdfQueue.process(async (job) => { const { designId, content } = job.data; // PDF生成API呼び出し const result = await generateAsyncPDF({ designId, content }); // 進捗更新 await job.progress(100); return result; }); // ジョブ追加 async function enqueuePDFGeneration(items) { const jobs = items.map(item => pdfQueue.add({ designId: 'template-id', content: item }, { attempts: 3, backoff: { type: 'exponential', delay: 2000 } }) ); return Promise.all(jobs); } // イベント監視 pdfQueue.on('completed', (job, result) => { console.log(`ジョブ ${job.id} 完了:`, result); }); pdfQueue.on('failed', (job, err) => { console.error(`ジョブ ${job.id} 失敗:`, err); }); ``` ### パターン3: バッチ処理(超大規模) ```javascript // 1000件以上の場合は複数PDFの一括生成を使用 async function generateMassiveBulk(items) { const batchSize = 100; // APIの上限に合わせる const batches = chunkArray(items, batchSize); const results = []; for (const [index, batch] of batches.entries()) { console.log(`バッチ ${index + 1}/${batches.length} 処理中...`); // 複数PDF一括生成API const { url, files } = await generateMultiplePDFsAsync({ designId: 'template-id', contents: batch.map(item => ({ fileName: `${item.id}.pdf`, params: item })) }); results.push({ batchIndex: index, url, files }); // レート制限対策で少し待機 if (index < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return results; } ``` ## エラーハンドリング ### リトライロジック ```javascript async function generateWithRetry(params, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await generateAsyncPDF(params); } catch (error) { const isLastAttempt = attempt === maxRetries - 1; if (isServerError(error) && !isLastAttempt) { const delay = 1000 * Math.pow(2, attempt); console.log(`リトライ ${attempt + 1}/${maxRetries} (${delay}ms後)`); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } } function isServerError(error) { return error.response?.status >= 500; } ``` ### 部分的な失敗の処理 ```javascript async function generateWithFallback(items) { const results = []; const errors = []; for (const item of items) { try { const result = await generateAsyncPDF({ designId: 'template-id', content: item }); results.push({ item, result, status: 'success' }); } catch (error) { console.error(`アイテム ${item.id} の生成失敗:`, error); errors.push({ item, error, status: 'failed' }); } } return { results, errors, summary: { total: items.length, success: results.length, failed: errors.length } }; } ``` ## Python実装例 ```python import asyncio import aiohttp from typing import List, Dict, Any async def generate_pdf_async(session: aiohttp.ClientSession, params: Dict[str, Any]) -> Dict: """非同期でPDFを生成""" url = "https://api.re-port-flow.com/v1/file/async/single" headers = { 'appkey': params['app_key'], 'Content-Type': 'application/json' } data = { 'designId': params['design_id'], 'version': params['version'], 'content': params['content'] } async with session.post(url, headers=headers, json=data) as response: response.raise_for_status() return await response.json() async def generate_bulk_pdfs( items: List[Dict], app_key: str, design_id: str, version: int = 1, concurrency: int = 5, ): """複数PDFを並列生成(Session を 1 つ共有して接続プールを効かせる)""" semaphore = asyncio.Semaphore(concurrency) async with aiohttp.ClientSession() as session: async def generate_with_limit(item): async with semaphore: return await generate_pdf_async(session, { 'app_key': app_key, 'design_id': design_id, 'version': version, 'content': item, }) tasks = [generate_with_limit(item) for item in items] return await asyncio.gather(*tasks) # 使用例 items = [...] # 生成対象データ results = asyncio.run(generate_bulk_pdfs( items, app_key='your-app-key', design_id='template-id', version=1, concurrency=10, )) ``` ## ベストプラクティス 1. **適切な並列度を設定**: サーバー負荷とレスポンス速度のバランスを取る 2. **指数バックオフを使用**: ポーリング間隔を徐々に増やす 3. **タイムアウトを設定**: 無限待機を防ぐ 4. **エラーをログ**: 失敗したアイテムを追跡 5. **進捗を表示**: ユーザーに状況を伝える ## 次のステップ - [PDF生成ガイド](./pdf-generation) - [エラーハンドリング](./error-handling) --- # docs/guides/endpoints/async-multiple.md # 複数PDF非同期生成 `POST /file/async/multiple` エンドポイントは、指定されたデザインと複数のパラメータから複数のPDFファイルを非同期的に生成します。 ## エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/async/multiple` - **メソッド**: `POST` - **認証**: `appkey` ヘッダーが必要 - **タイムアウト**: なし(非同期処理) - **リクエストサイズ上限**: 50MB(Base64エンコード後、実質約37MB相当) ## 使用例 ### cURL ```bash curl -X POST https://api.re-port-flow.com/v1/file/async/multiple \ -H "appkey: your-application-key" \ -H "Content-Type: application/json" \ -d '{ "designId": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "contents": [ { "fileName": "invoice_001.pdf", "shareType": "01", "passcodeEnabled": false, "params": { "customerName": "山田太郎", "invoiceNumber": "INV-001" } }, { "fileName": "invoice_002.pdf", "shareType": "01", "passcodeEnabled": false, "params": { "customerName": "佐藤花子", "invoiceNumber": "INV-002" } } ] }' ``` ## リクエストパラメータ | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `designId` | string (UUID) | ✓ | デザインID | | `version` | integer | ✓ | バージョン番号 | | `contents` | array | ✓ | ContentDtoの配列(最小1件) | | `contents[].fileName` | string | ✓ | ファイル名(`/ \\ : * ? " < > \|` および制御文字以外は使用可能。**配列内で一意であること**(大文字・小文字は区別しない)) | | `contents[].shareType` | string | - | 共有タイプ(リクエストは数値コード)。`"01"`=ワークスペース内共有(デフォルト) / `"02"`=招待者共有 / `"03"`=公開URL共有。**レスポンス**の `share.shareType` は `workspace` / `invited` / `public` の人間可読な名前で返ります | | `contents[].passcodeEnabled` | boolean | - | パスコード保護(デフォルト: `false`) | | `contents[].passthrough` | object | - | レスポンスの `files[].passthrough` に透過する任意のメタデータ(例: `{ "pageId": "abc123" }`) | | `contents[].params` | object | ✓ | パラメータ([デザインパラメータ取得API](./design-parameters.md)で構造を確認可能) | ## レスポンス ### 成功時 (202 Accepted) ```json { "requestId": "550e8400-e29b-41d4-a716-446655440000", "url": "https://api.re-port-flow.com/v1/file/download/{requestId}", "files": [ { "fileName": "invoice_001.pdf", "fileId": "aaa111", "passthrough": { "pageId": "abc123" }, "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/aaa111", "passcodeEnabled": false } }, { "fileName": "invoice_002.pdf", "fileId": "bbb222", "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/bbb222", "passcodeEnabled": false } } ] } ``` | フィールド | 型 | 説明 | |-----------|-----|------| | `requestId` | string (UUID) | リクエストID(ZIPダウンロードエンドポイントで使用) | | `url` | string (URI) | ZIPダウンロードURL | | `files` | array | 各PDFファイルの情報 | | `files[].fileName` | string | PDFファイル名 | | `files[].fileId` | string | 個別ファイルID(個別ダウンロードエンドポイントで使用) | | `files[].passthrough` | object | リクエスト時に指定した `contents[].passthrough` の値(指定時のみ) | | `files[].share.shareType` | string | 共有タイプ(`workspace` / `invited` / `public`) | | `files[].share.url` | string | ファイル表示URL | | `files[].share.passcodeEnabled` | boolean | パスコード有効フラグ | | `files[].share.passcode` | string | サーバー生成パスコード(`passcodeEnabled=true` かつ生成直後のみ) | :::info passthrough の使い所 ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に `params`(生成データ)を返しません。 受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に `passthrough` に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。 ```json { "fileName": "invoice.pdf", "passthrough": { "invoiceId": "INV-001", "tenantId": "acme" }, "params": { "customerName": "山田太郎", "amount": 10000 } } ``` Webhook 受信時に `passthrough` の値がそのまま返るため、`invoiceId` から DB を引いて該当レコードを更新する、といった処理が組めます。 `params`(顧客名や金額)は外部に送信されません。 ::: ## エラー時 [単一PDF同期生成](./sync-single.md#エラー時)と同様のエラーレスポンスに加え、以下のエラーが返される場合があります。 ### 400 Bad Request(fileName 重複) ```json { "statusCode": 400, "message": [ "contents 内の fileName が重複しています(大文字・小文字は区別されません)。各ファイルには一意の名前を指定してください。" ], "error": "Bad Request" } ``` **原因**: `contents` 配列内に同じ `fileName`(大文字・小文字の違いのみを含む)が複数存在する ## ユースケース ### 大量請求書の一括生成 ```javascript async function generateBulkInvoices(invoices) { const batchSize = 100; const batches = chunkArray(invoices, batchSize); const results = []; for (const [index, batch] of batches.entries()) { console.log(`バッチ ${index + 1}/${batches.length} 処理中...`); const contents = batch.map(invoice => ({ fileName: `invoice_${invoice.number}.pdf`, shareType: '01', passcodeEnabled: false, params: invoice })); const response = await axios.post( 'https://api.re-port-flow.com/v1/file/async/multiple', { designId: 'template-id', version: 1, contents }, { headers: { 'appkey': process.env.APP_KEY } } ); results.push(response.data); if (index < batches.length - 1) { await new Promise(resolve => setTimeout(resolve, 1000)); } } return results; } ``` ## 次のステップ - [ZIP一括ダウンロード](./download.md#zip一括ダウンロード) - 生成されたZIPファイルのダウンロード - [非同期ワークフロー](../async-workflows.md) - 大量生成のベストプラクティス --- # docs/guides/endpoints/async-single.md # 単一PDF非同期生成 `POST /file/async/single` エンドポイントは、指定されたデザインとパラメータから単一のPDFファイルを非同期的に生成します。即座に `requestId` とファイル情報を返すため、タイムアウトを避けることができます。 ## エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/async/single` - **メソッド**: `POST` - **認証**: `appkey` ヘッダーが必要 - **タイムアウト**: なし(非同期処理) - **リクエストサイズ上限**: 50MB(Base64エンコード後、実質約37MB相当) ## 使用例 ### cURL ```bash 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": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "content": { "fileName": "invoice.pdf", "shareType": "01", "passcodeEnabled": false, "params": { "customerName": "山田太郎", "invoiceNumber": "INV-2024-001", "amount": 10000 } } }' ``` ## リクエストパラメータ | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `designId` | string (UUID) | ✓ | デザインID | | `version` | integer | ✓ | バージョン番号 | | `content.fileName` | string | ✓ | ファイル名(`/ \\ : * ? " < > \|` および制御文字以外は使用可能) | | `content.shareType` | string | - | 共有タイプ(リクエストは数値コード)。`"01"`=ワークスペース内共有(デフォルト) / `"02"`=招待者共有 / `"03"`=公開URL共有。**レスポンス**の `share.shareType` は `workspace` / `invited` / `public` の人間可読な名前で返ります | | `content.passcodeEnabled` | boolean | - | パスコード保護(デフォルト: `false`) | | `content.passthrough` | object | - | レスポンスの `files[].passthrough` に透過する任意のメタデータ(例: `{ "pageId": "abc123" }`) | | `content.params` | object | ✓ | テンプレートに埋め込むパラメータ([デザインパラメータ取得API](./design-parameters.md)で構造を確認可能) | ## レスポンス ### 成功時 (202 Accepted) ```json { "requestId": "550e8400-e29b-41d4-a716-446655440000", "url": "https://api.re-port-flow.com/v1/file/download/{requestId}", "files": [ { "fileName": "invoice.pdf", "fileId": "7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c", "passthrough": { "pageId": "abc123" }, "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/{fileId}", "passcodeEnabled": false } } ] } ``` | フィールド | 型 | 説明 | |-----------|-----|------| | `requestId` | string (UUID) | リクエストID(ダウンロードエンドポイントで使用) | | `url` | string (URI) | ZIPダウンロードURL | | `files` | array | 生成されたファイルの情報 | | `files[].fileName` | string | ファイル名 | | `files[].fileId` | string | ファイルID(個別ダウンロードエンドポイントで使用) | | `files[].passthrough` | object | リクエスト時に指定した `content.passthrough` の値(指定時のみ) | | `files[].share.shareType` | string | 共有タイプ(`workspace` / `invited` / `public`) | | `files[].share.url` | string | ファイル表示URL | | `files[].share.passcodeEnabled` | boolean | パスコード有効フラグ | | `files[].share.passcode` | string | サーバー生成パスコード(`passcodeEnabled=true` かつ生成直後のみ) | :::info passthrough の使い所 ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に `params`(生成データ)を返しません。 受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に `passthrough` に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。 ```json { "fileName": "invoice.pdf", "passthrough": { "invoiceId": "INV-001", "tenantId": "acme" }, "params": { "customerName": "山田太郎", "amount": 10000 } } ``` Webhook 受信時に `passthrough` の値がそのまま返るため、`invoiceId` から DB を引いて該当レコードを更新する、といった処理が組めます。 `params`(顧客名や金額)は外部に送信されません。 ::: ### エラー時 同期生成エンドポイントと同様のエラーレスポンスを返します。詳細は[単一PDF同期生成](./sync-single.md#エラー時)を参照してください。 ## 非同期処理のフロー ``` 1. クライアント → API: 生成リクエスト送信 ↓ 2. API → クライアント: 即座に requestId・url・files を返す (202 Accepted) ↓ 3. API: バックグラウンドでPDF生成開始 ↓ 4. API: PDF生成完了後、S3にアップロード ↓ 5. クライアント: 返された url または /download/{requestId}/{fileId} からPDFをダウンロード ``` ## ユースケース ### ケース1: バックグラウンドでPDF生成 ユーザーのリクエストを即座に受け付け、バックグラウンドで処理します。 ```javascript app.post('/api/generate-report', async (req, res) => { const response = await axios.post( 'https://api.re-port-flow.com/v1/file/async/single', { designId: '...', version: 1, content: { fileName: 'report.pdf', params: req.body } }, { headers: { 'appkey': process.env.APP_KEY } } ); const { requestId, url, files } = response.data; // 即座にレスポンス res.status(202).json({ message: 'レポート生成を開始しました', requestId, downloadUrl: url, fileId: files[0].fileId }); }); ``` ### ケース2: Webhook通知の活用(推奨) ReportFlowの標準Webhook機能を使用すると、ポーリング不要でリアルタイムに完了通知を受け取れます。 詳細は[Webhook通知ガイド](../webhooks.md)を参照してください。 ## 次のステップ - [複数PDF非同期生成](./async-multiple.md) - 複数のPDFを非同期で一括生成 - [非同期ワークフロー](../async-workflows.md) - 大量生成のベストプラクティス - [ファイルダウンロード](./download.md) - 生成されたファイルのダウンロード方法 --- # docs/guides/endpoints/design-parameters.md # デザインパラメータ取得 `GET /file/design/parameter/{designId}` エンドポイントは、指定されたデザインIDのパラメータ構造を取得します。PDFやサムネイル生成時に必要なパラメータのスキーマを確認できます。 ## エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/design/parameter/{designId}` - **メソッド**: `GET` - **認証**: `appkey` ヘッダーが必要 ## 使用例 ### cURL ```bash # 最新バージョンのパラメータを取得 curl -X GET https://api.re-port-flow.com/v1/file/design/parameter/550e8400-e29b-41d4-a716-446655440000 \ -H "appkey: your-application-key" # 特定バージョンのパラメータを取得 curl -X GET "https://api.re-port-flow.com/v1/file/design/parameter/550e8400-e29b-41d4-a716-446655440000?version=1" \ -H "appkey: your-application-key" ``` ### JavaScript ```javascript async function getDesignParameters(designId, version = null) { const baseUrl = 'https://api.re-port-flow.com/v1'; const url = version ? `${baseUrl}/file/design/parameter/${designId}?version=${version}` : `${baseUrl}/file/design/parameter/${designId}`; const response = await axios.get(url, { headers: { 'appkey': process.env.APP_KEY } }); return response.data; } // 使用例 const schema = await getDesignParameters('550e8400-...'); console.log('パラメータスキーマ:', schema); ``` ### Python ```python def get_design_parameters(design_id, version=None): base_url = "https://api.re-port-flow.com/v1" url = f"{base_url}/file/design/parameter/{design_id}" if version: url += f"?version={version}" headers = { 'appkey': os.getenv('APP_KEY') } response = requests.get(url, headers=headers) response.raise_for_status() return response.json() # 使用例 schema = get_design_parameters('550e8400-...') print('パラメータスキーマ:', schema) ``` ## パラメータ | パラメータ | 型 | 必須 | 説明 | |-----------|-----|------|------| | `designId` | string (UUID) | ✓ | デザインID(パスパラメータ) | | `version` | integer | - | バージョン番号(クエリパラメータ、省略時は最新) | ## レスポンス ### 成功時 (200 OK) ```json { "customerName": "string", "invoiceNumber": "string", "amount": "number", "items": [ { "name": "string", "price": "number", "quantity": "number" } ], "issueDate": "date" } ``` **フィールドの型**: | 型 | 説明 | 例 | |----|------|-----| | `string` | 文字列 | `"山田太郎"` | | `number` | 数値 | `1000` | | `date` | 日付(ISO 8601) | `"2024-02-12"` | | `object` | ネストしたオブジェクト | `{ "name": "値" }` | | `array` | 配列 | `[{ "item": 1 }]` | ### エラー時 #### 404 Not Found ```json { "statusCode": 404, "message": "指定したデザインが存在しません", "error": "Not Found" } ``` **原因**: 指定されたデザインIDまたはバージョンが存在しない(特定バージョンが見つからない場合は `指定したバージョン(X)が存在しません` が返ります) ## ユースケース ### 動的フォーム生成 デザインパラメータから入力フォームを動的に生成する例。 ```javascript async function createDynamicForm(designId) { // パラメータスキーマを取得 const schema = await getDesignParameters(designId); // スキーマからフォームフィールドを生成 const formFields = Object.entries(schema).map(([key, type]) => { if (type === 'string') { return ``; } else if (type === 'number') { return ``; } else if (type === 'date') { return ``; } else if (Array.isArray(type)) { // 配列型の場合 return `
`; } }); return formFields.join('\n'); } ``` ### バリデーション ```javascript function validateParams(params, schema) { const errors = []; for (const [key, type] of Object.entries(schema)) { if (!(key in params)) { errors.push(`必須パラメータ ${key} が不足しています`); continue; } const value = params[key]; if (type === 'string' && typeof value !== 'string') { errors.push(`${key} は文字列である必要があります`); } else if (type === 'number' && typeof value !== 'number') { errors.push(`${key} は数値である必要があります`); } else if (type === 'date') { if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { errors.push(`${key} はISO 8601形式の日付である必要があります`); } } } return errors; } // 使用例 const schema = await getDesignParameters('550e8400-...'); const params = { customerName: '山田太郎', invoiceNumber: 'INV-001', amount: 10000 }; const errors = validateParams(params, schema); if (errors.length > 0) { console.error('バリデーションエラー:', errors); } ``` ## ベストプラクティス ### 1. パラメータスキーマのキャッシュ デザインのバージョンが変わらない限り、パラメータスキーマは変わりません。キャッシュして再利用することを推奨します。 ```javascript const schemaCache = new Map(); async function getDesignParametersCached(designId, version) { const cacheKey = `${designId}:${version}`; if (schemaCache.has(cacheKey)) { return schemaCache.get(cacheKey); } const schema = await getDesignParameters(designId, version); schemaCache.set(cacheKey, schema); return schema; } ``` ### 2. PDF生成前の検証 パラメータスキーマを事前に取得してバリデーションを行うことで、エラーを早期に検出できます。 ```javascript async function generatePDFSafely(params) { // 1. パラメータスキーマを取得 const schema = await getDesignParameters(params.designId, params.version); // 2. バリデーション const errors = validateParams(params.data, schema); if (errors.length > 0) { throw new Error(`パラメータエラー: ${errors.join(', ')}`); } // 3. PDF生成 return await generatePDF(params); } ``` ## 次のステップ - [単一PDF同期生成](./sync-single.md) - パラメータを使用したPDF生成 - [エラーハンドリング](../error-handling.md) - バリデーションエラーの処理 --- # docs/guides/endpoints/download.md # ファイルダウンロード 生成されたPDFファイルをダウンロードするためのエンドポイントです。 ## ZIP一括ダウンロード ### エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/download/{requestId}` - **メソッド**: `GET` - **認証**: アプリケーションキー(`appkey` ヘッダー)が必要 ### レスポンス (200 OK) **レスポンスボディ**: ZIPファイル(バイナリ) | ヘッダー | 説明 | 例 | |---------|------|-----| | `Content-Type` | コンテンツタイプ | `application/zip` | | `Content-Length` | ファイルサイズ(バイト) | `307200` | | `Content-Disposition` | ファイル名 | `attachment; filename="files.zip"` | ### 使用例 #### cURL ```bash curl -X GET https://api.re-port-flow.com/v1/file/download/550e8400-e29b-41d4-a716-446655440000 \\ -H "appkey: your-app-key" \\ --output files.zip ``` #### JavaScript ```javascript async function downloadZip(requestId) { const response = await axios.get( `https://api.re-port-flow.com/v1/file/download/${requestId}`, { headers: { 'appkey': process.env.APP_KEY }, responseType: 'arraybuffer' } ); fs.writeFileSync('files.zip', response.data); return response.data; } ``` ## 個別ファイルダウンロード ### エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/download/{requestId}/{fileId}` - **メソッド**: `GET` - **認証**: アプリケーションキー(`appkey` ヘッダー)が必要 ### レスポンス (200 OK) **レスポンスボディ**: PDFファイル(バイナリ) | ヘッダー | 説明 | 例 | |---------|------|-----| | `Content-Type` | コンテンツタイプ | `application/pdf` | | `Content-Length` | ファイルサイズ(バイト) | `102400` | | `Content-Disposition` | ファイル名 | `attachment; filename="invoice.pdf"` | | `File-ID` | ファイルID | `7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c` | ### 使用例 #### cURL ```bash curl -X GET https://api.re-port-flow.com/v1/file/download/550e8400-e29b-41d4-a716-446655440000/7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c \\ -H "appkey: your-app-key" \\ --output invoice.pdf ``` #### JavaScript ```javascript async function downloadFile(requestId, fileId) { const response = await axios.get( `https://api.re-port-flow.com/v1/file/download/${requestId}/${fileId}`, { headers: { 'appkey': process.env.APP_KEY }, responseType: 'arraybuffer' } ); const returnedFileId = response.headers['file-id']; console.log('ダウンロードしたファイルID:', returnedFileId); return response.data; } ``` ## エラーレスポンス ### 404 Not Found ```json { "statusCode": 404, "message": "File not found", "error": "Not Found" } ``` **原因**: - 指定された `requestId` または `fileId` が存在しない - ファイルの有効期限が切れた **対処法**: - `requestId` / `fileId` が正しいことを確認 - ファイル生成から時間が経過している場合は再生成 ## ユースケース ### ZIP内の特定ファイルのみダウンロード ```javascript // async/multiple のレスポンスから requestId と files を取得済みとする async function downloadSpecificFile(requestId, files) { const targetFile = files.find(f => f.fileName === 'invoice_001.pdf'); if (targetFile) { const pdfData = await downloadFile(requestId, targetFile.fileId); fs.writeFileSync('invoice_001.pdf', pdfData); } } ``` ## 次のステップ - [単一PDF生成](./sync-single.md) - PDFファイルの生成方法 - [複数PDF生成](./sync-multiple.md) - 複数PDFの一括生成 --- # docs/guides/endpoints/sync-multiple.md # 複数PDF同期生成(ZIP) `POST /file/sync/multiple` エンドポイントは、指定されたデザインと複数のパラメータから複数のPDFファイルを同期的に生成し、ZIPファイルで返します。 ## エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/sync/multiple` - **メソッド**: `POST` - **認証**: `appkey` ヘッダーが必要 - **タイムアウト**: 120秒 - **リクエストサイズ上限**: 50MB(Base64エンコード後、実質約37MB相当) ## 使用例 ### cURL ```bash curl -X POST https://api.re-port-flow.com/v1/file/sync/multiple \ -H "appkey: your-application-key" \ -H "Content-Type: application/json" \ -d '{ "designId": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "contents": [ { "fileName": "invoice_001.pdf", "shareType": "01", "passcodeEnabled": false, "params": { "customerName": "山田太郎", "invoiceNumber": "INV-001" } }, { "fileName": "invoice_002.pdf", "shareType": "02", "passcodeEnabled": false, "params": { "customerName": "佐藤花子", "invoiceNumber": "INV-002" } } ] }' \ --output invoices.zip ``` ### JavaScript ```javascript import axios from 'axios'; import fs from 'fs'; async function generateMultiplePDFs(designId, contents) { const response = await axios.post( 'https://api.re-port-flow.com/v1/file/sync/multiple', { designId, version: 1, contents }, { headers: { 'appkey': process.env.APP_KEY }, responseType: 'arraybuffer' } ); const requestId = response.headers['request-id']; const fileUrl = response.headers['file-url']; // X-File-Mapping は URL エンコード済みの JSON 配列。decodeURIComponent → JSON.parse の順で復号する。 const fileMapping = JSON.parse(decodeURIComponent(response.headers['x-file-mapping'])); console.log('生成されたファイル:', fileMapping.map(f => ({ fileId: f.fileId, fileName: f.fileName, share: f.share }))); return { data: response.data, requestId, fileUrl, fileMapping }; } // 使用例 const contents = [ { fileName: 'invoice_001.pdf', shareType: '01', passcodeEnabled: false, params: { customerName: '山田太郎', invoiceNumber: 'INV-001' } }, { fileName: 'invoice_002.pdf', shareType: '02', passcodeEnabled: false, params: { customerName: '佐藤花子', invoiceNumber: 'INV-002' } } ]; const result = await generateMultiplePDFs('550e8400-...', contents); fs.writeFileSync('invoices.zip', result.data); ``` ## リクエストパラメータ | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `designId` | string (UUID) | ✓ | デザインID | | `version` | integer | ✓ | バージョン番号 | | `contents` | array | ✓ | ContentDtoの配列(最小1件) | | `contents[].fileName` | string | ✓ | ファイル名(`/ \\ : * ? " < > \|` および制御文字以外は使用可能。**配列内で一意であること**(大文字・小文字は区別しない)) | | `contents[].shareType` | string | - | 共有タイプ(リクエストは数値コード)。`"01"`=ワークスペース内共有(デフォルト) / `"02"`=招待者共有 / `"03"`=公開URL共有。**レスポンス**の `share.shareType` は `workspace` / `invited` / `public` の人間可読な名前で返ります | | `contents[].passcodeEnabled` | boolean | - | パスコード保護(デフォルト: `false`) | | `contents[].passthrough` | object | - | レスポンスの `X-File-Mapping` に透過する任意のメタデータ(例: `{ "pageId": "abc123" }`) | | `contents[].params` | object | ✓ | パラメータ([デザインパラメータ取得API](./design-parameters.md)で構造を確認可能) | ## レスポンス ### 成功時 (200 OK) **レスポンスボディ**: ZIPファイル(バイナリ) **レスポンスヘッダー**: | ヘッダー | 説明 | 例 | |---------|------|-----| | `Content-Type` | コンテンツタイプ | `application/zip` | | `Content-Length` | ファイルサイズ(バイト) | `307200` | | `Content-Disposition` | ファイル名 | `attachment; filename="files.zip"` | | `File-URL` | ZIPダウンロードURL | `https://api.re-port-flow.com/v1/file/download/{requestId}` | | `Request-Id` | リクエストID | `550e8400-e29b-41d4-a716-446655440000` | | `X-File-Mapping` | ファイル情報と共有設定(JSON 配列)。**URL エンコード済み**で返るため、クライアントは `decodeURIComponent` してから `JSON.parse` すること | 下記参照 | **`X-File-Mapping` の構造**: ```json [ { "fileId": "aaa111", "fileName": "invoice_001.pdf", "passthrough": { "pageId": "abc123" }, "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/aaa111", "passcodeEnabled": false } }, { "fileId": "bbb222", "fileName": "invoice_002.pdf", "share": { "shareType": "invited", "url": "https://app.re-port-flow.com/file/{requestId}/bbb222", "passcodeEnabled": false } } ] ``` `passthrough` フィールドは `contents[].passthrough` を指定したエントリのみに含まれます。 :::info passthrough の使い所 ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に `params`(生成データ)を返しません。 受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に `passthrough` に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。 ```json { "fileName": "invoice.pdf", "passthrough": { "invoiceId": "INV-001", "tenantId": "acme" }, "params": { "customerName": "山田太郎", "amount": 10000 } } ``` Webhook 受信時に `passthrough` の値がそのまま返るため、`invoiceId` から DB を引いて該当レコードを更新する、といった処理が組めます。 `params`(顧客名や金額)は外部に送信されません。 ::: ### エラー時 [単一PDF同期生成](./sync-single.md#エラー時)と同様のエラーレスポンスに加え、以下のエラーが返される場合があります。 #### 400 Bad Request(fileName 重複) ```json { "statusCode": 400, "message": [ "contents 内の fileName が重複しています(大文字・小文字は区別されません)。各ファイルには一意の名前を指定してください。" ], "error": "Bad Request" } ``` **原因**: `contents` 配列内に同じ `fileName`(大文字・小文字の違いのみを含む)が複数存在する ## ユースケース ### 月次請求書の一括生成 ```javascript async function generateMonthlyInvoices(month) { const invoices = await getInvoicesByMonth(month); const contents = invoices.map(invoice => ({ fileName: `invoice_${invoice.number}.pdf`, shareType: '01', passcodeEnabled: false, params: invoice })); const result = await generateMultiplePDFs('invoice-template-id', contents); fs.writeFileSync(`invoices_${month}.zip`, result.data); } ``` ## 次のステップ - [複数PDF非同期生成](./async-multiple.md) - タイムアウトを避けたい場合 - [ファイルダウンロード](./download.md) - 生成されたZIPファイルのダウンロード --- # docs/guides/endpoints/sync-single.md # 単一PDF同期生成 `POST /file/sync/single` エンドポイントは、指定されたデザインとパラメータから単一のPDFファイルを同期的に生成します。 ## エンドポイント情報 - **URL**: `https://api.re-port-flow.com/v1/file/sync/single` - **メソッド**: `POST` - **認証**: `appkey` ヘッダーが必要 - **タイムアウト**: 120秒 - **リクエストサイズ上限**: 50MB(Base64エンコード後、実質約37MB相当) ## 使用例 ### cURL ```bash 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": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "content": { "fileName": "invoice.pdf", "shareType": "01", "passcodeEnabled": false, "params": { "customerName": "山田太郎", "invoiceNumber": "INV-2024-001", "amount": 10000 } } }' \ --output invoice.pdf ``` ### JavaScript (Node.js) ```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, shareType: params.shareType ?? '01', passcodeEnabled: params.passcodeEnabled ?? false, 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']; // X-File-Mapping は URL エンコード済みの JSON 配列。 // decodeURIComponent → JSON.parse の順で復号する。 const fileMapping = JSON.parse(decodeURIComponent(response.headers['x-file-mapping'])); console.log('PDF生成成功:', { requestId, fileUrl, fileMapping }); return { data: response.data, requestId, fileUrl, fileMapping }; } catch (error) { console.error('PDF生成エラー:', error.response?.data || error.message); throw error; } } // 使用例 generatePDF({ designId: '550e8400-e29b-41d4-a716-446655440000', version: 1, fileName: 'invoice.pdf', shareType: '01', passcodeEnabled: false, data: { customerName: '山田太郎', invoiceNumber: 'INV-2024-001', amount: 10000 } }); ``` ### Python ```python import requests import json from urllib.parse import unquote def generate_pdf(params): url = "https://api.re-port-flow.com/v1/file/sync/single" headers = { 'appkey': params['app_key'], 'Content-Type': 'application/json' } data = { 'designId': params['design_id'], 'version': params['version'], 'content': { 'fileName': params['file_name'], 'shareType': params.get('share_type', '01'), 'passcodeEnabled': params.get('passcode_enabled', False), 'params': params['data'] } } response = requests.post(url, headers=headers, json=data) response.raise_for_status() # ファイルに保存 with open(params['file_name'], 'wb') as f: f.write(response.content) # レスポンスヘッダーから情報取得 request_id = response.headers.get('request-id') file_url = response.headers.get('file-url') # X-File-Mapping は URL エンコード済みの JSON 配列。unquote → json.loads の順で復号する。 file_mapping = json.loads(unquote(response.headers.get('x-file-mapping', '[]'))) return {'request_id': request_id, 'file_url': file_url, 'file_mapping': file_mapping} # 使用例 result = generate_pdf({ 'app_key': 'your-application-key', 'design_id': '550e8400-e29b-41d4-a716-446655440000', 'version': 1, 'file_name': 'invoice.pdf', 'share_type': '01', 'passcode_enabled': False, 'data': { 'customerName': '山田太郎', 'invoiceNumber': 'INV-2024-001', 'amount': 10000 } }) ``` ## リクエストパラメータ | フィールド | 型 | 必須 | 説明 | |-----------|-----|------|------| | `designId` | string (UUID) | ✓ | デザインID | | `version` | integer | ✓ | バージョン番号 | | `content.fileName` | string | ✓ | ファイル名(`/ \\ : * ? " < > \|` および制御文字以外は使用可能) | | `content.shareType` | string | - | 共有タイプ(リクエストは数値コード)。`"01"`=ワークスペース内共有(デフォルト) / `"02"`=招待者共有 / `"03"`=公開URL共有。**レスポンス**の `share.shareType` は `workspace` / `invited` / `public` の人間可読な名前で返ります | | `content.passcodeEnabled` | boolean | - | パスコード保護(デフォルト: `false`) | | `content.passthrough` | object | - | レスポンスの `X-File-Mapping` に透過する任意のメタデータ(例: `{ "pageId": "abc123" }`) | | `content.params` | object | ✓ | テンプレートに埋め込むパラメータ([デザインパラメータ取得API](./design-parameters.md)で構造を確認可能) | ## レスポンス ### 成功時 (200 OK) **レスポンスボディ**: PDFファイル(バイナリ) **レスポンスヘッダー**: | ヘッダー | 説明 | 例 | |---------|------|-----| | `Content-Type` | コンテンツタイプ | `application/pdf` | | `Content-Length` | ファイルサイズ(バイト) | `102400` | | `Content-Disposition` | ファイル名 | `attachment; filename="invoice.pdf"` | | `File-URL` | ZIPダウンロードURL | `https://api.re-port-flow.com/v1/file/download/{requestId}` | | `Request-Id` | リクエストID | `550e8400-e29b-41d4-a716-446655440000` | | `X-File-Mapping` | ファイル情報と共有設定(JSON 配列)。**URL エンコード済み**で返るため、クライアントは `decodeURIComponent` してから `JSON.parse` すること | 下記参照 | **`X-File-Mapping` の構造**: ```json [ { "fileId": "7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c", "fileName": "invoice.pdf", "passthrough": { "pageId": "abc123" }, "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/{fileId}", "passcodeEnabled": false } } ] ``` `passthrough` フィールドはリクエスト時に `content.passthrough` を指定した場合のみ含まれます。 :::info passthrough の使い所 ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に `params`(生成データ)を返しません。 受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に `passthrough` に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。 ```json { "fileName": "invoice.pdf", "passthrough": { "invoiceId": "INV-001", "tenantId": "acme" }, "params": { "customerName": "山田太郎", "amount": 10000 } } ``` Webhook 受信時に `passthrough` の値がそのまま返るため、`invoiceId` から DB を引いて該当レコードを更新する、といった処理が組めます。 `params`(顧客名や金額)は外部に送信されません。 ::: ### エラー時 #### 412 Precondition Failed ```json { "statusCode": 412, "message": "認証方式ヘッダーが存在しません" } ``` **原因**: `appkey` ヘッダーが欠落 #### 400 Bad Request ```json { "statusCode": 400, "message": [ "designId must be a UUID", "ファイル名に使用できない文字が含まれています(/ \\ : * ? \" < > | および制御文字は使用不可)" ], "error": "Bad Request" } ``` **原因**: リクエストパラメータが不正 #### 401 Unauthorized ```json { "statusCode": 401, "message": "認証情報が不正です", "error": "Unauthorized" } ``` **原因**: 認証エラー **対処法**: - `appkey` ヘッダーの値が正しいことを確認 #### 500 Internal Server Error ```json { "statusCode": 500, "message": "Internal server error", "error": "Internal Server Error" } ``` ## ベストプラクティス ### 1. タイムアウト処理 同期APIは120秒でタイムアウトします。大きなPDFや複雑なデザインの場合は、非同期APIの使用を検討してください。 ```javascript const response = await axios.post(url, data, { timeout: 120000 // 120秒 }); ``` ### 2. リトライロジック 一時的なサーバーエラー(500系)の場合は、指数バックオフでリトライを実装します。 ```javascript 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) { const delay = 1000 * Math.pow(2, i); await new Promise(resolve => setTimeout(resolve, delay)); continue; } throw error; } } } ``` ### 3. ファイル名のサニタイゼーション ```javascript function sanitizeFileName(fileName) { // / \ : * ? " < > | および制御文字を除去 return fileName.replace(/[\/\\:*?"<>|\x00-\x1F]/g, '_'); } ``` ## ユースケース ### ケース1: 請求書の即時生成 ユーザーが「請求書ダウンロード」ボタンをクリックしたときに、即座にPDFを生成してダウンロードさせる。 ```javascript app.get('/api/invoices/:id/download', async (req, res) => { const invoice = await getInvoice(req.params.id); const pdf = await generatePDF({ designId: 'invoice-template-id', version: 1, fileName: `invoice_${invoice.number}.pdf`, data: invoice }); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `attachment; filename="invoice_${invoice.number}.pdf"`); res.send(pdf.data); }); ``` ### ケース2: プレビュー生成 デザインエディタでユーザーがプレビューボタンをクリックしたときに、リアルタイムでPDFを生成して表示する。 ```javascript async function showPreview(designId, params) { const pdf = await generatePDF({ designId, version: 1, fileName: 'preview.pdf', data: params }); // ブラウザでPDFを表示 const blob = new Blob([pdf.data], { type: 'application/pdf' }); const url = URL.createObjectURL(blob); window.open(url, '_blank'); } ``` ## 次のステップ - [単一PDF非同期生成](./async-single.md) - タイムアウトを避けたい場合 - [複数PDF同期生成](./sync-multiple.md) - 複数のPDFを一度に生成 - [非同期ワークフロー](../async-workflows.md) - 大量生成のベストプラクティス --- # docs/guides/error-handling.md # エラーハンドリング Report Flow APIのエラーハンドリング方法とベストプラクティスを説明します。 ## エラーレスポンスの形式 すべてのエラーは以下の形式で返されます: ```json { "statusCode": 400, "message": "エラーの詳細説明", "error": "エラータイプ" } ``` ## HTTPステータスコード一覧 ### 2xx 成功 | コード | 意味 | 説明 | |--------|------|------| | 200 OK | 成功 | リクエストが正常に処理されました | | 202 Accepted | 受付 | 非同期処理が受け付けられました | ### 4xx クライアントエラー #### 400 Bad Request **原因:** リクエストの形式や内容が不正 ```json { "statusCode": 400, "message": [ "designId must be a UUID", "version must be an integer number" ], "error": "Bad Request" } ``` **対処法:** - リクエストボディのバリデーション - 型の確認(UUID、integer等) - 必須フィールドの確認 **例:ファイル名バリデーションエラー** ```json { "statusCode": 400, "message": "ファイル名に使用できない文字が含まれています(/ \\ : * ? \" < > | および制御文字は使用不可)", "error": "Bad Request" } ``` #### 401 Unauthorized **原因:** 認証情報が不正または欠落 ```json { "statusCode": 401, "message": "認証情報が不正です", "error": "Unauthorized" } ``` **対処法:** - アプリケーションキー(`appkey` ヘッダー)を確認 - キーの有効期限・再生成状況を確認 #### 404 Not Found **原因:** リソースが見つからない ```json { "statusCode": 404, "message": "指定したデザインが存在しません", "error": "Not Found" } ``` **対処法:** - `designId` が正しいことを確認 - 指定したバージョンが存在するか確認(存在しない場合は `指定したバージョン(X)が存在しません` が返ります) #### 403 Forbidden **原因:** プラン制限超過またはアクセス権限不足 ```json { "statusCode": 403, "message": "プラン情報が見つからないため、この操作は許可されていません", "error": "Forbidden" } ``` **対処法:** - ワークスペースのプラン情報を確認(管理画面の **ワークスペース設定 → プラン**) - 月間ファイル作成回数の上限超過の場合は `今月のファイル作成回数上限(X)に達しました。プランをアップグレードするか、翌月までお待ちください。` メッセージが返るので、プランをアップグレードするか翌月まで待つ #### 412 Precondition Failed **原因:** 認証方式ヘッダー(`appkey`)が欠落 ```json { "statusCode": 412, "message": "認証方式ヘッダーが存在しません", "error": "Precondition Failed" } ``` **対処法:** - `appkey` ヘッダーが設定されているか確認 #### 413 Payload Too Large **原因:** リクエストサイズが50MBを超過 ```json { "statusCode": 413, "message": "Payload Too Large", "error": "Request Entity Too Large" } ``` **対処法:** - 画像を最適化する(解像度・圧縮率の調整) - リクエストを分割する - 詳細は[制限事項](../limitations.md#-リクエストサイズ制限)を参照 #### 429 Too Many Requests **原因:** Rate Limit超過(Workspace単位で適用) ```http HTTP/1.1 429 Too Many Requests Retry-After: 60 Content-Type: application/json { "statusCode": 429, "message": "Rate limit exceeded for this workspace. Please try again in 60 seconds.", "error": "Too Many Requests" } ``` **`Retry-After` ヘッダ:** RFC 9110 §10.2.3 に準拠し、再試行まで待つべき秒数を整数で返します。クライアントはエラーメッセージをパースする代わりにこのヘッダの値だけ待てば十分です。本文の数値もこのヘッダと同じ値です(人間可読版)。 **対処法:** - `Retry-After` ヘッダの秒数だけ待ってから再送する - 同期エンドポイント: 30 req/min、非同期エンドポイント: 100 req/min - バッチ処理の場合は非同期エンドポイントを使用 - 詳細は[制限事項](../limitations.md#-rate-limit)を参照 ### 5xx サーバーエラー #### 500 Internal Server Error **原因:** サーバー内部エラー ```json { "statusCode": 500, "message": "Internal server error", "error": "Internal Server Error" } ``` **対処法:** - リトライ処理を実装 - 継続する場合はサポートに連絡 ## エラーハンドリングのベストプラクティス ### 1. 適切なエラーキャッチ ```javascript async function generatePDF(params) { try { const response = await axios.post(url, data, config); return response.data; } catch (error) { if (error.response) { // サーバーがエラーレスポンスを返した handleApiError(error.response); } else if (error.request) { // リクエストは送信されたがレスポンスがない console.error('ネットワークエラー:', error.message); throw new Error('サーバーに接続できません'); } else { // リクエスト設定時のエラー console.error('リクエストエラー:', error.message); throw error; } } } function handleApiError(response) { const { status, data } = response; switch (status) { case 400: console.error('バリデーションエラー:', data.message); throw new Error(`リクエストが不正です: ${data.message}`); case 401: console.error('認証エラー'); throw new Error('APIキーを確認してください'); case 404: console.error('リソースが見つかりません'); throw new Error('指定されたリソースが存在しません'); case 500: console.error('サーバーエラー'); throw new Error('一時的なエラーです。しばらく待ってから再試行してください'); default: console.error('予期しないエラー:', data); throw new Error(`エラー (${status}): ${data.message}`); } } ``` ### 2. リトライロジック 一時的なサーバーエラー(5xx)に対してリトライを実装します。 ```javascript async function generatePDFWithRetry(params, options = {}) { const { maxRetries = 3, initialDelay = 1000, maxDelay = 10000, backoffFactor = 2 } = options; let lastError; for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await generatePDF(params); } catch (error) { lastError = error; // リトライ可能なエラーか判定 if (!isRetryableError(error)) { throw error; } // 最後の試行ではリトライしない if (attempt === maxRetries - 1) { break; } // 指数バックオフで待機 const delay = Math.min( initialDelay * Math.pow(backoffFactor, attempt), maxDelay ); console.log(`リトライ ${attempt + 1}/${maxRetries} (${delay}ms後)`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } function isRetryableError(error) { if (!error.response) { // ネットワークエラーはリトライ可能 return true; } const { status } = error.response; // 5xxエラーのみリトライ return status >= 500 && status < 600; } ``` ### 3. タイムアウト処理 ```javascript async function generatePDFWithTimeout(params, timeout = 120000) { return Promise.race([ generatePDF(params), new Promise((_, reject) => setTimeout(() => reject(new Error('タイムアウト')), timeout) ) ]); } ``` ### 4. エラーロギング ```javascript function logError(error, context) { const errorLog = { timestamp: new Date().toISOString(), context, error: { message: error.message, stack: error.stack, response: error.response ? { status: error.response.status, data: error.response.data } : null } }; console.error(JSON.stringify(errorLog)); // エラートラッキングサービスに送信(例: Sentry) // Sentry.captureException(error, { contexts: { custom: context } }); } // 使用例 try { await generatePDF(params); } catch (error) { logError(error, { operation: 'generatePDF', designId: params.designId, fileName: params.fileName }); throw error; } ``` ## Python実装例 ```python import requests import time from typing import Optional, Dict, Any class ReportFlowError(Exception): """Report Flow API エラーの基底クラス""" def __init__(self, status_code: int, message: str, error: str): self.status_code = status_code self.message = message self.error = error super().__init__(f"{error} ({status_code}): {message}") class ValidationError(ReportFlowError): """バリデーションエラー (400)""" pass class AuthenticationError(ReportFlowError): """認証エラー (401)""" pass class NotFoundError(ReportFlowError): """リソースが見つからない (404)""" pass class ServerError(ReportFlowError): """サーバーエラー (5xx)""" pass def generate_pdf_with_retry(params: Dict[str, Any], max_retries: int = 3) -> bytes: """リトライ付きPDF生成""" for attempt in range(max_retries): try: return generate_pdf(params) except ServerError as e: if attempt == max_retries - 1: raise delay = 1000 * (2 ** attempt) # 指数バックオフ time.sleep(delay / 1000) print(f"リトライ {attempt + 1}/{max_retries}") except ReportFlowError: # サーバーエラー以外はリトライしない raise def generate_pdf(params: Dict[str, Any]) -> bytes: """PDF生成""" url = "https://api.re-port-flow.com/v1/file/sync/single" headers = { 'appkey': params['app_key'], 'Content-Type': 'application/json' } data = { 'designId': params['design_id'], 'version': params['version'], 'content': { 'fileName': params['file_name'], 'params': params['data'] } } try: response = requests.post(url, headers=headers, json=data, timeout=120) response.raise_for_status() return response.content except requests.exceptions.HTTPError as e: handle_http_error(e.response) except requests.exceptions.Timeout: raise ServerError(504, 'リクエストがタイムアウトしました', 'Gateway Timeout') except requests.exceptions.RequestException as e: raise ServerError(500, str(e), 'Network Error') def handle_http_error(response): """HTTPエラーを適切な例外に変換""" try: error_data = response.json() status = error_data.get('statusCode', response.status_code) message = error_data.get('message', 'Unknown error') error = error_data.get('error', 'Error') except ValueError: status = response.status_code message = response.text error = 'Unknown Error' if status == 400: raise ValidationError(status, message, error) elif status == 401: raise AuthenticationError(status, message, error) elif status == 404: raise NotFoundError(status, message, error) elif 500 <= status < 600: raise ServerError(status, message, error) else: raise ReportFlowError(status, message, error) # 使用例 try: pdf_data = generate_pdf_with_retry({ 'app_key': 'your-app-key', 'design_id': '660e8400-e29b-41d4-a716-446655440000', 'version': 1, 'file_name': 'invoice.pdf', 'data': { 'customerName': '山田太郎', 'invoiceNumber': 'INV-2024-001' } }) with open('invoice.pdf', 'wb') as f: f.write(pdf_data) except ValidationError as e: print(f"バリデーションエラー: {e.message}") except AuthenticationError as e: print(f"認証エラー: APIキーを確認してください") except NotFoundError as e: print(f"リソースが見つかりません: {e.message}") except ServerError as e: print(f"サーバーエラー: しばらく待ってから再試行してください") except ReportFlowError as e: print(f"エラー: {e}") ``` ## トラブルシューティング ### よくあるエラーと対処法 | エラー | 原因 | 対処法 | |--------|------|--------| | `designId must be a UUID` | デザインIDの形式が不正 | UUID形式(例: `550e8400-e29b-41d4-a716-446655440000`)を確認 | | `ファイル名に使用できない文字が含まれています` | ファイル名に `/ \ : * ? " < > \|` または制御文字を含む | これらの文字を除去または置換 | | `認証情報が不正です` | 認証失敗 | アプリケーションキーを確認 | | `認証方式ヘッダーが存在しません` | `appkey` ヘッダー欠落 | リクエストに `appkey: <キー>` ヘッダーを追加 | ## 次のステップ - [PDF生成ガイド](./pdf-generation) - [非同期ワークフロー](./async-workflows) --- # docs/guides/pdf-generation.md # PDF生成ガイド Report Flow APIを使用したPDF生成の詳細ガイドです。 ## 概要 PDF生成には2つのモードがあります: | モード | エンドポイント | レスポンス | 用途 | |--------|--------------|-----------|------| | 同期生成 | `/file/sync/single` | PDFバイナリ | 即座に結果が必要な場合 | | 非同期生成 | `/file/async/single` | `requestId` / `url` / `files` 配列 (202 Accepted) | 大量生成やバックグラウンド処理 | ## 同期生成 ### 基本的な使い方 ```bash 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": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "content": { "fileName": "invoice.pdf", "params": { "customerName": "山田太郎", "invoiceNumber": "INV-2024-001", "items": [ { "name": "商品A", "price": 1000, "quantity": 2 } ] } } }' \ --output invoice.pdf ``` ### レスポンスヘッダー ```http Content-Type: application/pdf Content-Disposition: attachment; filename="invoice.pdf" Content-Length: 123456 Request-Id: 550e8400-e29b-41d4-a716-446655440000 File-URL: https://api.re-port-flow.com/v1/file/download/{requestId} X-File-Mapping: %5B%7B%22fileId%22%3A...%7D%5D ``` `X-File-Mapping` は **URL エンコード済みの JSON 配列** で、`fileId` / `fileName` / `share` 情報を含みます。クライアント側で `decodeURIComponent` してから `JSON.parse` してください。詳細は [単一PDF同期生成エンドポイント](./endpoints/sync-single.md) を参照。 ### JavaScript実装例 ```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: '550e8400-e29b-41d4-a716-446655440000', version: 1, fileName: 'invoice.pdf', data: { customerName: '山田太郎', invoiceNumber: 'INV-2024-001', items: [ { name: '商品A', price: 1000, quantity: 2 } ] } }); ``` ## 非同期生成 大量のPDFを生成する場合や、タイムアウトを避けたい場合は非同期生成を使用します。 ### 基本的な使い方 ```bash # 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": "550e8400-e29b-41d4-a716-446655440000", "version": 1, "content": { "fileName": "invoice.pdf", "params": {...} } }' # レスポンス例 (202 Accepted) { "requestId": "550e8400-e29b-41d4-a716-446655440000", "url": "https://api.re-port-flow.com/v1/file/download/{requestId}", "files": [ { "fileName": "invoice.pdf", "fileId": "7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c", "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/{fileId}", "passcodeEnabled": false } } ] } ``` 詳細は [単一PDF非同期生成エンドポイント](./endpoints/async-single.md) を参照。 ### JavaScript実装例(ポーリング) ```javascript 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. ZIP ダウンロードURL (url) からまとめて、または個別ファイルを取得 // 個別取得: GET /v1/file/download/{requestId}/{fileId} const pdfResponse = await axios.get(url, { headers: { 'appkey': process.env.APP_KEY }, responseType: 'arraybuffer', }); return { data: pdfResponse.data, requestId, url, files, }; } ``` ## 複数PDF生成(ZIP) 複数のPDFを一度に生成してZIPで取得できます。 ### 同期生成 ```javascript 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 ヘッダーからファイルマッピングを取得 const fileMapping = JSON.parse(response.headers['x-file-mapping']); 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('550e8400-...', contents); fs.writeFileSync('invoices.zip', zipData); ``` ## パラメータの構造 ### デザインパラメータの取得 生成前に、デザインで使用可能なパラメータ構造を確認できます: ```bash curl -X GET https://api.re-port-flow.com/v1/file/design/parameter/{designId}?version=1 \ -H "appkey: your-application-key" ``` **レスポンス例:** ```json { "customerName": "string", "invoiceNumber": "string", "amount": "number", "items": [ { "name": "string", "price": "number", "quantity": "number" } ], "issueDate": "date" } ``` ### パラメータ型の対応 | 型 | 説明 | 例 | |----|------|-----| | `string` | 文字列 | `"山田太郎"` | | `number` | 数値 | `1000` | | `date` | 日付(ISO 8601) | `"2024-02-12"` | | `object` | ネストしたオブジェクト | `{ "name": "値" }` | | `array` | 配列 | `[{ "item": 1 }]` | ## エラーハンドリング ### 一般的なエラー #### 400 Bad Request - バリデーションエラー ```json { "statusCode": 400, "message": [ "designId must be a UUID", "ファイル名に使用できない文字が含まれています(/ \\ : * ? \" < > | および制御文字は使用不可)" ], "error": "Bad Request" } ``` **対処法:** - リクエストボディを確認 - ファイル名の形式を確認(`/ \ : * ? " < > |` および制御文字は使用不可) #### 500 Internal Server Error ```json { "statusCode": 500, "message": "Internal server error", "error": "Internal Server Error" } ``` **対処法:** - 一時的なサーバーエラーの可能性 - リトライロジックを実装 - 継続する場合はサポートに連絡 ### リトライ戦略 ```javascript 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. タイムアウト設定 同期生成は120秒でタイムアウトします。大きなPDFの場合は非同期生成を使用してください。 ```javascript // axios でタイムアウト設定 const response = await axios.post(url, data, { timeout: 120000 // 120秒 }); ``` ### 2. ファイル名のサニタイゼーション ```javascript function sanitizeFileName(fileName) { // / \ : * ? " < > | および制御文字を除去 return fileName.replace(/[\/\\:*?"<>|\x00-\x1F]/g, '_'); } ``` ### 3. パラメータの検証 ```javascript function validateParams(params, schema) { // デザインパラメータスキーマと照合 for (const [key, type] of Object.entries(schema)) { if (!(key in params)) { throw new Error(`必須パラメータ ${key} が不足しています`); } // 型チェック等 } } ``` ## 次のステップ - [非同期ワークフローガイド](./async-workflows) - [エラーハンドリング](./error-handling) - [MCP Server](../integrations/mcp) — Claude / Cursor / VS Code から PDF 生成 - [n8n Integration](../integrations/n8n) — ノーコードワークフロー連携 --- # docs/guides/webhooks.md # Webhook通知 PDF生成完了時に自動的にWebhook通知を受け取ることができます。これにより、ポーリング不要でリアルタイムに完了を検知し、メール送信などのワークフローを自動化できます。 ## 概要 Webhook通知は、以下のすべてのPDF生成エンドポイントで自動的に送信されます: - `POST /file/sync/single` - 同期単一ファイル生成 - `POST /file/sync/multiple` - 同期複数ファイル生成 - `POST /file/async/single` - 非同期単一ファイル生成 - `POST /file/async/multiple` - 非同期複数ファイル生成 ## Webhook URLの設定 Webhook URLを設定するには、**ReportFlowのワークスペース設定画面**から行います: 1. ワークスペース設定を開く 2. 「開発者」タブに移動 3. 「Webhook URL」フィールドに、通知を受け取るHTTPS URLを入力 4. 「更新」をクリック ## Webhook通知ペイロード PDF生成が完了すると、設定されたWebhook URLに以下のペイロードがPOSTされます。 ### ペイロード形式 ```json { "event": "file.completed", "timestamp": "2026-02-15T10:30:45.123Z", "workspaceId": "ws_abc123", "requestId": "550e8400-e29b-41d4-a716-446655440000", "designId": "design_123", "version": 5, "files": [ { "fileId": "7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c", "fileName": "請求書.pdf", "passthrough": { "pageId": "abc123" }, "share": { "shareType": "workspace", "url": "https://app.re-port-flow.com/file/{requestId}/{fileId}", "passcodeEnabled": false } } ] } ``` ### フィールド説明 | フィールド | 型 | 説明 | |-----------|---|------| | `event` | string | 固定値: `"file.completed"` | | `timestamp` | string | イベント発生時刻 (ISO 8601形式) | | `workspaceId` | string | ワークスペースID(参照用) | | `requestId` | string | リクエストID(ダウンロードエンドポイントで使用) | | `designId` | string | デザインID | | `version` | number | バージョン番号 | | `files` | array | 生成ファイル情報の配列 | | `files[].fileId` | string | ファイルID(個別ダウンロードエンドポイントで使用) | | `files[].fileName` | string | ファイル名(拡張子付き) | | `files[].passthrough` | object | リクエスト時に指定した `passthrough` の値(指定時のみ) | | `files[].share.shareType` | string | 共有タイプ(`workspace` / `invited` / `public`) | | `files[].share.url` | string | ファイル表示URL | | `files[].share.passcodeEnabled` | boolean | パスコード有効フラグ | | `files[].share.passcode` | string | サーバー生成パスコード(`passcodeEnabled=true` かつ生成直後のみ) | :::info passthrough の使い所 ReportFlow はセキュリティとペイロードサイズの観点から、レスポンスや Webhook 通知に `params`(生成データ)を返しません。 受信側で「どの業務レコードに対する PDF か」を識別したい場合は、 リクエスト時に `passthrough` に自社DBのIDなどを入れてください。 レスポンスや Webhook でそのまま返却されます。 ```json { "fileName": "invoice.pdf", "passthrough": { "invoiceId": "INV-001", "tenantId": "acme" }, "params": { "customerName": "山田太郎", "amount": 10000 } } ``` Webhook 受信時に `passthrough` の値がそのまま返るため、`invoiceId` から DB を引いて該当レコードを更新する、といった処理が組めます。 `params`(顧客名や金額)は外部に送信されません。 ::: ## PDFのダウンロード Webhook通知のペイロードから、以下の情報を使ってPDFをダウンロードできます: - `requestId`: ペイロードの `requestId`(ZIP一括ダウンロード用) - `fileId`: ペイロードの `files[].fileId`(個別ダウンロード用) ``` # ZIP一括ダウンロード GET /v1/file/download/{requestId} # 個別ファイルダウンロード GET /v1/file/download/{requestId}/{fileId} ``` 詳細は[ファイルダウンロード](./endpoints/download.md)を参照してください。 ## 実装例 ### Node.js (Express) ```javascript 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; // イベント検証 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) ```python 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 # イベント検証 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)の取得 ワークスペースごとに発行される `whsec_` プレフィックス付きの秘密鍵を使用します。 - **UI から取得**: ワークスペース設定 > 開発者タブ - **API から再生成**: `POST /workspace/:workspaceId/webhook/secret/regenerate` - **API から取得**: `GET /workspace/:workspaceId/webhook/secret` 再生成すると既存のシークレットは即座に無効化されます。 #### 署名ヘッダの形式 ``` X-Report-Flow-Signature: t=1739610645,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd ``` - `t`: Unix タイムスタンプ(秒)。リプレイ攻撃対策として、現在時刻との差が 5 分以内であることを推奨。 - `v1`: HMAC-SHA256 署名。署名対象は `