Skip to main content

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 60 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 60 seconds, so anything that might run longer than that belongs on the async endpoints (see Limitations).

There are two modes for PDF generation:

ModeEndpointResponseUse Case
Sync Generation/file/sync/singlePDF binaryWhen immediate results are needed
Async Generation/file/async/singlerequestId / 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": "0eUDdgAjNXrrItA2",
"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: 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 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: '0eUDdgAjNXrrItA2',
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": "0eUDdgAjNXrrItA2",
"version": 1,
"content": {
"fileName": "invoice.pdf",
"params": {...}
}
}'

# Response example (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
}
}
]
}

See the single async PDF endpoint for the full response schema.

JavaScript Implementation Example (Polling)​

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

// Wait for completion or failure with the status API (GET /v1/file/status/{requestId}).
// While generating, the response carries a Retry-After header (seconds); poll at that interval.
async function waitForCompletion(requestId, { timeoutMs = 300000 } = {}) {
const deadline = Date.now() + timeoutMs;
let notFoundRetries = 0;

for (;;) {
// Compute the remaining time once. axios reads `timeout: 0` as "no timeout",
// so passing 0 after the deadline would let a stalled request hang forever.
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) {
// A 404 right after generating may be a request still in progress whose job record could not be saved. Re-check up to twice
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;
}
// On 429 (the status API allows 300 req/min), wait Retry-After seconds and check again
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; // download via files[].downloadPath
if (status === 'failed') {
// Branch on error.code (e.g. PARAMS_TYPE_MISMATCH / GENERATION_INTERRUPTED)
throw new Error(`PDF generation failed: ${res.data.error.code} ${res.data.error.message}`);
}

// queued / processing: follow Retry-After (seconds), capped at the remaining time
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 generation timed out: ${requestId}`);
}

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. Wait for completion with the status API. Instead of polling you can also
// receive the file.completed webhook. The failure event file.failed is sent only
// when the generation request specified webhookUrl, and only to that URL.
const status = await waitForCompletion(requestId);

// 3. 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.
// downloadPath is /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,
// 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 the X-File-Mapping header (URL-encoded JSON)
const fileMapping = JSON.parse(
decodeURIComponent(response.headers['x-file-mapping'] || '%5B%5D'),
);
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('0eUDdgAjNXrrItA2', contents);
fs.writeFileSync('invoices.zip', zipData);

Parameter Structure​

Retrieving Template Parameters​

Before generation, you can check the available parameter structure for a template:

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

Response Example:

[
{ "name": "customerName", "type": "text", "label": "Customer name" },
{ "name": "invoiceNumber", "type": "text", "label": "Invoice number" },
{ "name": "amount", "type": "number", "label": "Amount" },
{
"name": "items",
"type": "array",
"label": "Line items",
"spec": [
{ "name": "name", "type": "text", "label": "Item" },
{ "name": "price", "type": "number", "label": "Unit price" },
{ "name": "quantity", "type": "number", "label": "Quantity" }
]
},
{ "name": "issueDate", "type": "date", "label": "Issue date" }
]

The response is an array of parameter definitions. Each element's name is the key you send in params when generating; label is the display name (falls back to name when unset). array and collection carry their child fields in spec.

Use name as the key, unchanged

name is whatever the template author chose, and it can contain non-ASCII characters such as Japanese (for example "顧客名"). Always use the name value as-is as the key in params (using label as the key will not match). When you design a template meant to be driven from the API, we recommend ASCII keys (for example customerName) and putting the human-readable name in label.

Parameter Type Mapping​

TypeDescriptionExample
textText string"John Doe"
numberNumeric value1000
dateDate (ISO 8601)"2024-02-12"
booleanBooleantrue
collectionNested object defined by spec{ "name": "value" }
arrayArray whose row structure is defined by spec[{ "item": 1 }]

See Get Template Parameters for details on each type.

What the template can calculate​

Thousands separators, the Japanese era, date formats, per-row calculations (unit price × quantity) and switching text or text color on a condition can all be handled by formulas in the template. Check whether the template can do it before you format values on the API side. Column totals and per-tax-rate subtotals, on the other hand, cannot be calculated in the template, so compute them on the calling side and pass them in params.

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:

StorageContentHow it's read
XMP metadata inside the generated PDFEmbedded as rf:params (key=value)The PDF recipient's OS search (Spotlight / Windows Search, etc.)
Re:port Flow's search indexUsed by the workspace report-search screenThe 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 PDF 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.

ConditionBehavior
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 lengthTruncated beyond 256 characters
Key countMaximum 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).

Do not put personal or sensitive information in passthrough

passthrough 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,
"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"
}

Solution:

  • Check the fields pointed to by details[].field
  • Verify the file name: / \ : * ? " < > | and control characters (0x00–0x1F) are not allowed; spaces, Japanese and other characters are fine

For nested fields, each element of message is prefixed with the parent path (for example content.fileName ..., or contents.0.fileName ... for a multiple request). details[].message has no prefix.

Branch on code, not on message (see Error Codes).

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"
}

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 60 seconds (the load balancer in front of the API returns 504). Use async generation for large PDFs. Set the client-side timeout a little above 60 seconds so the server's 504 can arrive.

// Set timeout with axios
const response = await axios.post(url, data, {
timeout: 70000 // 70 seconds (the server returns 504 at 60 seconds)
});

2. File Name Sanitization​

function sanitizeFileName(fileName) {
// Replace / \ : * ? " < > | and control characters
return fileName.replace(/[\/\\:*?"<>|\x00-\x1F]/g, '_');
}

3. Parameter Validation​

function validateParams(params, schema) {
// schema is the array from GET /v1/file/design/parameter/{designId}: [{ name, type, label, spec? }]
for (const field of schema) {
// The key is name, not label (it may be Japanese — do not transform it)
if (!(field.name in params)) {
console.warn(`${field.label} (${field.name}) is not set`);
continue;
}
// Type checks based on field.type (text / number / date / boolean / array / collection)
}
}

See Get Template Parameters > Validation for a per-type example.

Next Steps​