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:
| 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": "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.
name as the key, unchangedname 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
| Type | Description | Example |
|---|---|---|
text | Text string | "John Doe" |
number | Numeric value | 1000 |
date | Date (ISO 8601) | "2024-02-12" |
boolean | Boolean | true |
collection | Nested object defined by spec | { "name": "value" } |
array | Array 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.
- Template Capabilities — what is and isn't supported
- Formula Reference — every operator and function
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 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.
| 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,
"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
- 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