Single PDF Sync Generation
The POST /file/sync/single endpoint generates a single PDF file synchronously from the specified template and parameters.
Endpoint Information
- URL:
https://api.re-port-flow.com/v1/file/sync/single - Method:
POST - Authentication:
appkeyheader required - Timeout: 60 seconds
- Request Size Limit: 50MB (Base64-encoded; ~37MB raw equivalent)
Usage Examples
cURL
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",
"shareType": "01",
"passcodeEnabled": false,
"params": {
"customerName": "John Doe",
"invoiceNumber": "INV-2024-001",
"amount": 10000
}
}
}' \
--output invoice.pdf
JavaScript (Node.js)
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);
// X-File-Mapping is URL-encoded JSON. Decode with decodeURIComponent → JSON.parse.
const requestId = response.headers['request-id'];
const fileUrl = response.headers['file-url'];
const fileMapping = JSON.parse(decodeURIComponent(response.headers['x-file-mapping']));
console.log('PDF generated:', { requestId, fileUrl, fileMapping });
return { data: response.data, requestId, fileUrl, fileMapping };
} catch (error) {
console.error('PDF generation error:', error.response?.data || error.message);
throw error;
}
}
generatePDF({
designId: '0eUDdgAjNXrrItA2',
version: 1,
fileName: 'invoice.pdf',
shareType: '01',
passcodeEnabled: false,
data: {
customerName: 'John Doe',
invoiceNumber: 'INV-2024-001',
amount: 10000
}
});
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)
# X-File-Mapping is URL-encoded JSON; decode with unquote → json.loads.
request_id = response.headers.get('request-id')
file_url = response.headers.get('file-url')
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': '0eUDdgAjNXrrItA2',
'version': 1,
'file_name': 'invoice.pdf',
'share_type': '01',
'passcode_enabled': False,
'data': {
'customerName': 'John Doe',
'invoiceNumber': 'INV-2024-001',
'amount': 10000
}
})
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
designId | string | ✓ | Template ID (16-character alphanumeric, e.g. 0eUDdgAjNXrrItA2) |
version | integer | ✓ | Version number |
webhookUrl | string | - | Webhook destination for this request (https, up to 2048 characters). Must match the URL of an enabled webhook endpoint registered in the workspace, otherwise 400 WEBHOOK_URL_NOT_ALLOWED. When set, only that endpoint receives this request's events (details) |
content.fileName | string | ✓ | File name (anything except / \\ : * ? " < > | and control characters is allowed) |
content.shareType | string | - | Share type (request side uses numeric codes). "01" = workspace share (default) / "02" = invited-only / "03" = public URL share. The response share.shareType returns the human-readable name (workspace / invited / public). |
content.passcodeEnabled | boolean | - | Enable passcode protection (default: false). When true, the response share.passcode returns a server-generated passcode exactly once. |
content.passthrough | object | - | Arbitrary string KV that is echoed back in the response X-File-Mapping[].passthrough. Useful for tracking with webhooks (e.g. { "orderId": "...", "userId": "..." }). Top-level strings/numbers are also stored as report-search metadata. |
content.params | object | ✓ | Parameters embedded into the template (check structure with the Template Parameters API) |
Response
Success (200 OK)
Response Body: PDF file (binary)
Response Headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Content type | application/pdf |
Content-Length | File size in bytes | 102400 |
Content-Disposition | File name | attachment; filename="invoice.pdf" |
File-URL | URL of the app screen (output history) where the generated result can be reviewed. This is not a direct file download URL | https://re-port-flow.com/{workspaceId}/templates/{designId}/outputs?requestId={requestId} |
Request-Id | Request ID | kZ3mP9qL2xV7nR4t |
X-File-Mapping | File metadata and share info (JSON array). URL-encoded, so clients must call decodeURIComponent before JSON.parse. | See below |
File-URL is not a download URLFile-URL points to the app screen (output history) where the generated result can be reviewed, and opening it requires signing in to Re:port Flow. To fetch files programmatically, use the PDF download endpoints instead (requestId for a ZIP of the whole request, requestId + fileId for a single PDF).
This header was previously documented as a "ZIP download URL", which did not match what the implementation returns. The documentation has been corrected to match the implementation; the response behaviour itself is unchanged.
X-File-Mapping structure (after decoding):
[
{
"fileId": "Hn4sT8wQ1cV6bX2e",
"fileName": "invoice.pdf",
"passthrough": { "orderId": "ORD-2024-001" },
"share": {
"shareType": "workspace",
"url": "https://re-port-flow.com/file/{requestId}/{fileId}",
"passcodeEnabled": false
}
}
]
passthrough is included only when content.passthrough was specified in the request.
Getting identifiers as JSON (?response=json)
Add ?response=json to the URL and the identifiers are returned as JSON instead of the PDF body. This is for tools that cannot read response headers (Request-Id / File-URL / X-File-Mapping), such as Coze.
curl -X POST "https://api.re-port-flow.com/v1/file/sync/single?response=json" \
-H "appkey: YOUR_APP_KEY" \
-H "Content-Type: application/json" \
-d @request.json
{
"requestId": "Xk3pQ9vB2mN7aR1t",
"fileUrl": "https://re-port-flow.com/{workspaceId}/templates/{designId}/outputs?requestId=Xk3pQ9vB2mN7aR1t",
"files": [
{
"fileId": "a8Fh2LkQ9zT4wE6y",
"fileName": "invoice-001",
"pageCount": 1,
"downloadPath": "/v1/file/download/Xk3pQ9vB2mN7aR1t/a8Fh2LkQ9zT4wE6y",
"share": { "shareType": "workspace" }
}
]
}
- Only the
?response=jsonquery switches the format. TheAccept: application/jsonheader does not (so existing integrations do not start receiving JSON unexpectedly). fileUrlis the output-history page, not the file itself. Fetch the PDF by GETting the host (https://api.re-port-flow.com) +downloadPathwithappkey(or a Bearer token) — see PDF download.- Generation and page consumption are the same as when the PDF itself is returned.
Errors
See Error Codes for the error body shape (statusCode / code / message / error / details / docsUrl) and the full list of codes. message is in English by default.
412 Precondition Failed
{
"statusCode": 412,
"code": "AUTH_HEADER_MISSING",
"message": "Authentication header is missing. Send an `appkey` header or `Authorization: Bearer <token>`.",
"error": "Precondition Failed",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#auth_header_missing"
}
Cause: Neither an appkey header nor an Authorization: Bearer <token> header was sent, or the appkey header is malformed.
400 Bad Request
{
"statusCode": 400,
"code": "FILE_NAME_INVALID",
"message": [
"content.fileName contains characters that are not allowed (/ \\ : * ? \" < > | and control characters)."
],
"error": "Bad Request",
"details": [
{
"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#file_name_invalid"
}
Cause: Invalid request body (this example: forbidden characters in the file name). When different kinds of validation errors are mixed, code is VALIDATION_FAILED. A params type mismatch is PARAMS_TYPE_MISMATCH. designId is a 16-character alphanumeric string such as 0eUDdgAjNXrrItA2 and is not format-checked, so a wrong designId returns 404 DESIGN_VERSION_NOT_FOUND, not a 400.
401 Unauthorized
{
"statusCode": 401,
"code": "INVALID_CREDENTIALS",
"message": "Invalid API key or access token.",
"error": "Unauthorized",
"details": [],
"docsUrl": "https://doc.re-port-flow.com/en/docs/guides/error-codes#invalid_credentials"
}
Cause: Authentication failure.
Solution:
- Verify the value of the
appkeyheader (or the access token) is correct.
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"
}
Best Practices
1. Timeout Handling
The sync API times out after 60 seconds. For large PDFs or complex templates, consider using the async API. Set the client-side timeout a little above 60 seconds so the server's 504 can arrive.
const response = await axios.post(url, data, {
timeout: 70000 // 70 seconds (the server returns 504 at 60 seconds)
});
2. Retry Logic
For transient server errors (500 / 502 / 503), retry with exponential backoff (the example resends only a 500). Do not resend after a 504 / 524: generation may still be running and would be counted twice (see Error Handling).
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. File Name Sanitization
function sanitizeFileName(fileName) {
// Remove / \ : * ? " < > | and control characters
return fileName.replace(/[\/\\:*?"<>|\x00-\x1F]/g, '_');
}
Use Cases
Case 1: Instant Invoice Generation
Generate and download a PDF immediately when the user clicks the "Download Invoice" button.
app.get('/api/invoices/:id/download', requireLogin, async (req, res) => {
// Check that the signed-in user may see this invoice (never serve someone else's ID)
if (!(await canViewInvoice(req.user, req.params.id))) return res.status(404).end();
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);
});
Case 2: Preview Generation
Generate a PDF in real time when the user clicks the preview button in the template editor.
In this example the browser gets the PDF through your server.
// Your server (the appkey lives only here)
// Relaying designId / params without authentication hands the appkey's power to anyone.
// Always require your own login (requireLogin) and an allow-list of accepted designIds.
const PREVIEW_DESIGN_IDS = new Set(['0eUDdgAjNXrrItA2']);
app.post('/api/preview', requireLogin, async (req, res) => {
if (!PREVIEW_DESIGN_IDS.has(req.body.designId)) return res.status(400).end();
const pdf = await generatePDF({
designId: req.body.designId,
version: 1,
fileName: 'preview.pdf',
data: req.body.params
});
res.setHeader('Content-Type', 'application/pdf');
res.send(pdf.data);
});
// Browser: show the PDF returned by your server
async function showPreview(designId, params) {
const res = await fetch('/api/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ designId, params })
});
const url = URL.createObjectURL(await res.blob());
window.open(url, '_blank');
}
FAQ
Should I use sync or async generation?
Use the sync endpoint when you need to return a result to a user action on the spot. If generation may take more than 60 seconds (see Limitations), or you want to avoid the timeout entirely, use Single PDF Async Generation.
What happens if the request times out?
Sync endpoints time out after 60 seconds: the load balancer in front of the API returns
504 Gateway Timeout (the body is not necessarily JSON). For large PDFs
or complex templates, switch to an async endpoint or simplify the template — see
Limitations.
Next Steps
- Async Single PDF — to avoid timeout
- Sync Multiple PDFs — generate multiple PDFs in one call
- Async Workflows — best practices for bulk generation