PDF Generation Guide
Re:port Flow generates PDFs in two modes: sync, which returns the PDF binary in the response, and async, which returns a requestId immediately so you can collect the file later. Use sync when you need the result immediately, and async for bulk or background work; sync calls time out after 120 seconds. This guide walks through both modes, the parameter structure, and error handling.
When should I use sync vs async generation?
Use sync when you need the result immediately, and async for bulk or background work. Sync calls time out after 120 seconds, so anything that might run longer belongs on the async endpoints.
There are two modes for PDF generation:
| Mode | Endpoint | Response | Use Case |
|---|---|---|---|
| Sync Generation | /file/sync/single | PDF binary | When immediate results are needed |
| Async Generation | /file/async/single | requestId / url / files array (202 Accepted) | For batch processing or background tasks |
Sync Generation
Basic Usage
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": "John Doe",
"invoiceNumber": "INV-2024-001",
"items": [
{
"name": "Product A",
"price": 1000,
"quantity": 2
}
]
}
}
}' \
--output invoice.pdf
Response Headers
Content-Type: application/pdf
Content-Disposition: attachment; filename="invoice.pdf"
Content-Length: 123456
Request-Id: 550e8400-e29b-41d4-a716-446655440000
File-URL: https://re-port-flow.com/{workspaceId}/design/{designId}/outputs?requestId={requestId}
X-File-Mapping: %5B%7B%22fileId%22%3A...%7D%5D
X-File-Mapping is a URL-encoded JSON array containing fileId / fileName / share for each generated file. Clients should decodeURIComponent and then JSON.parse the value. See the single sync PDF endpoint for the full schema.
JavaScript Implementation Example
const axios = require('axios');
const fs = require('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'
}
);
// Save to file
fs.writeFileSync(params.fileName, response.data);
// Get file URL
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 generation error:', error.response?.data || error.message);
throw error;
}
}
// Usage example
generatePDF({
designId: '550e8400-e29b-41d4-a716-446655440000',
version: 1,
fileName: 'invoice.pdf',
data: {
customerName: 'John Doe',
invoiceNumber: 'INV-2024-001',
items: [
{ name: 'Product A', price: 1000, quantity: 2 }
]
}
});
Async Generation
Use async generation for large PDF batches or to avoid timeouts.
Basic Usage
# 1. Generation request
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": {...}
}
}'
# Response example (202 Accepted)
{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://re-port-flow.com/{workspaceId}/design/{designId}/outputs?requestId={requestId}",
"files": [
{
"fileName": "invoice.pdf",
"fileId": "7f3d1a2b-4c5e-6f7a-8b9c-0d1e2f3a4b5c",
"share": {
"shareType": "workspace",
"url": "https://re-port-flow.com/file/{requestId}/{fileId}",
"passcodeEnabled": false
}
}
]
}
See the single async PDF endpoint for the full response schema.
JavaScript Implementation Example (Polling)
// Poll the download API until generation finishes.
// A 404 means "still generating"; anything else is thrown immediately.
async function pollDownload(downloadUrl, { intervalMs = 3000, timeoutMs = 300000 } = {}) {
const deadline = Date.now() + timeoutMs;
for (;;) {
// Compute the remaining time once. Calling Date.now() separately for the
// loop condition and the timeout can yield `timeout: 0`, which axios reads
// as "no timeout" — a stalled request would then hang forever.
const remainingMs = deadline - Date.now();
if (remainingMs <= 0) break;
try {
return await axios.get(downloadUrl, {
headers: { 'appkey': process.env.APP_KEY },
responseType: 'arraybuffer',
timeout: remainingMs,
});
} catch (error) {
// 404 means "still generating". Timeouts (ECONNABORTED) and everything
// else are rethrown so the helper terminates.
if (error.response?.status !== 404) throw error;
// Cap the wait at the remaining time. Sleeping a full intervalMs would
// overshoot timeoutMs on a 404 near the deadline, or when
// intervalMs > timeoutMs.
await new Promise(resolve =>
setTimeout(resolve, Math.min(intervalMs, deadline - Date.now())),
);
}
}
throw new Error(`PDF generation timed out: ${downloadUrl}`);
}
async function generatePDFAsync(params) {
// 1. Async generation request
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. Fetch the file from the download API.
// The `url` in the response points to the app screen (output history) and
// requires a login, so it cannot be used for programmatic downloads.
// Whole request as ZIP: GET /v1/file/download/{requestId}
// Single file: GET /v1/file/download/{requestId}/{fileId}
//
const downloadUrl =
`https://api.re-port-flow.com/v1/file/download/${requestId}/${files[0].fileId}`;
// 3. Poll until generation finishes — fetching too early returns 404.
// Polling wastes wall-clock time, so prefer the webhook notification in
// production (see "Webhook notifications" below).
const pdfResponse = await pollDownload(downloadUrl);
return {
data: pdfResponse.data,
requestId,
// Link to the app screen (output history) — not a download URL
outputsUrl: url,
files,
};
}
Multiple PDF Generation (ZIP)
Generate multiple PDFs at once and receive them as a ZIP file.
Sync Generation
async function generateMultiplePDFs(designId, contents) {
const response = await axios.post(
'https://api.re-port-flow.com/v1/file/sync/multiple',
{
designId,
version: 1,
contents // Array of ContentDto
},
{
headers: {
'appkey': process.env.APP_KEY
},
responseType: 'arraybuffer'
}
);
// Get file mapping from X-File-Mapping header
const fileMapping = JSON.parse(response.headers['x-file-mapping']);
console.log('Generated files:', fileMapping);
return response.data; // ZIP binary
}
// Usage example
const contents = [
{
fileName: 'invoice_001.pdf',
params: { customerName: 'John Doe', invoiceNumber: 'INV-001' }
},
{
fileName: 'invoice_002.pdf',
params: { customerName: 'Jane Smith', invoiceNumber: 'INV-002' }
}
];
const zipData = await generateMultiplePDFs('550e8400-...', contents);
fs.writeFileSync('invoices.zip', zipData);
Parameter Structure
Retrieving Design Parameters
Before generation, you can check the available parameter structure for a design:
curl -X GET https://api.re-port-flow.com/v1/file/design/parameter/{designId}?version=1 \
-H "appkey: your-application-key"
Response Example:
{
"customerName": "string",
"invoiceNumber": "string",
"amount": "number",
"items": [
{
"name": "string",
"price": "number",
"quantity": "number"
}
],
"issueDate": "date"
}
Parameter Type Mapping
| Type | Description | Example |
|---|---|---|
string | Text string | "John Doe" |
number | Numeric value | 1000 |
date | Date (ISO 8601) | "2024-02-12" |
object | Nested object | { "name": "value" } |
array | Array | [{ "item": 1 }] |
passthrough and report-search metadata
content.passthrough is arbitrary metadata that is echoed back unchanged in the
response and in webhooks. Since params (the render data) is never returned in the
response or in webhooks, use passthrough when the receiving side needs to identify
which business record a PDF corresponds to.
{
"fileName": "invoice.pdf",
"passthrough": { "invoiceId": "INV-001", "customerName": "Acme Corp" },
"params": { "customerName": "Acme Corp", "amount": 10000 }
}
In addition, passthrough values are also stored as search metadata, in two places:
| Storage | Content | How it's read |
|---|---|---|
| XMP metadata inside the generated PDF | Embedded as rf:params (key=value) | The PDF recipient's OS search (Spotlight / Windows Search, etc.) |
| Re:port Flow's search index | Used by the workspace report-search screen | The report-search screen in the Re:port Flow app |
This means that if you pass customerName or invoiceId at generation time, you can
later find that report from the app's report-search screen using that value.
Which values get indexed
Each passthrough value (a string or a number) is indexed under the following conditions.
| Condition | Behavior |
|---|---|
String (string) | Leading/trailing whitespace trimmed and stored. Empty strings are excluded |
Number (number) | Only finite numbers, stored as their base-10 string representation |
| Value length | Truncated beyond 256 characters |
| Key count | Maximum 64 per file; entries beyond the limit are dropped |
Values that don't make it into the index (empty strings, non-finite numbers, entries beyond the 64-key cap) are still fully included in the response/webhook echo-back as before (the echo-back behavior itself is unchanged).
passthroughpassthrough values are embedded in the generated PDF's metadata. Anyone who
receives the PDF can view that metadata, and it is also picked up by their OS's
search index.
Do not put sensitive information (personal numbers, bank account numbers, health
information, etc.) or any personal data you don't want disclosed into passthrough.
If you only need a value printed on the PDF itself, use params instead — through
this mechanism, params values are not stored in the PDF's metadata or in the
search index. This may not hold if a specific parameter has been separately marked
as searchable on the template itself; check with your template's designer if that's
a concern.
Error Handling
Common Errors
400 Bad Request - Validation Error
{
"statusCode": 400,
"message": [
"designId must be a UUID",
"fileName can only contain alphanumeric characters, Japanese characters, hyphens, underscores, and dots"
],
"error": "Bad Request"
}
Solution:
- Check request body
- Verify file name format (regex:
^[a-zA-Z0-9\u3000-\uFFEF_.\-]+$)
500 Internal Server Error
{
"statusCode": 500,
"message": "Internal server error",
"error": "Internal Server Error"
}
Solution:
- Possible temporary server error
- Implement retry logic
- Contact support if it persists
Retry Strategy
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) {
// Retry with exponential backoff
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
continue;
}
throw error;
}
}
}
Best Practices
1. Timeout Settings
Sync generation times out after 30 seconds. Use async generation for large PDFs.
// Set timeout with axios
const response = await axios.post(url, data, {
timeout: 30000 // 30 seconds
});
2. File Name Sanitization
function sanitizeFileName(fileName) {
return fileName.replace(/[^a-zA-Z0-9\u3000-\uFFEF_.\-]/g, '_');
}
3. Parameter Validation
function validateParams(params, schema) {
// Validate against design parameter schema
for (const [key, type] of Object.entries(schema)) {
if (!(key in params)) {
throw new Error(`Required parameter ${key} is missing`);
}
// Type checking, etc.
}
}
Next Steps
- Async Workflow Guide
- Error Handling
- Zapier Integration — generate PDFs from spreadsheet, form or CRM data
- MCP Server — generate PDFs from Claude / Cursor / VS Code
- Coze Integration — generate PDFs from a Coze bot
- n8n Integration — no-code workflow integration