Skip to main content

Async Workflows

The async API hands back a requestId without waiting for generation to finish, and you collect the result afterwards via webhook or polling. Because it sidesteps the 60-second timeout on the sync endpoints, it suits bulk generation and background processing. This guide covers three patterns — parallel, queue-based, and batch — plus retries and partial failures.

What the async API is​

A subset of the Re:port Flow API endpoints support asynchronous processing:

OperationSyncAsync
Single PDF/file/sync/single/file/async/single
Multiple PDFs/file/sync/multiple/file/async/multiple

How do I wait for completion?​

When the async API returns 202 Accepted and a requestId, generation is still in progress. Find out when it finishes in one of two ways:

MethodGood for
Poll the status API (GET /v1/file/status/{requestId})You cannot expose a public URL to receive callbacks, or you want to wait inside a batch job
Webhook notifications (file.completed; file.failed too, sent to the URL you pass as webhookUrl in the generation request)Your server can receive requests and you don't want to waste time waiting

Polling the status API​

  1. Take the requestId from the generation response
  2. Call GET /v1/file/status/{requestId}
  3. If status is queued / processing, wait for the Retry-After header (seconds) and go back to step 2
  4. If it is completed, download from files[].downloadPath. A multiple request in which only some files failed is also completed, with the failed files in failedFiles, so record those too. If it is failed, check the reason in error.code
const API_BASE = 'https://api.re-port-flow.com';

async function waitAndDownload(requestId) {
let notFoundRetries = 0;
for (;;) {
let res;
try {
res = await axios.get(`${API_BASE}/v1/file/status/${requestId}`, {
headers: { 'appkey': process.env.APP_KEY },
});
} 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, 3000));
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, waitMs));
continue;
}
throw e;
}

if (res.data.status === 'completed') {
// Fetch one file at a time (downloads allow 100 req/min; a multiple request can hold up to 100 files, so sending them all at once can hit 429)
const pdfs = [];
for (const file of res.data.files) {
for (;;) {
try {
const pdf = await axios.get(`${API_BASE}${file.downloadPath}`, {
headers: { 'appkey': process.env.APP_KEY },
responseType: 'arraybuffer',
});
pdfs.push(pdf);
break;
} catch (e) {
// On 429, wait Retry-After seconds and fetch the same file again
if (e.response?.status !== 429) throw e;
const retrySec = Number(e.response.headers['retry-after'] ?? 60);
await new Promise(resolve => setTimeout(resolve, retrySec * 1000));
}
}
}
// A multiple request in which only some files failed is still completed; the failed ones are in failedFiles, so hand them back
// (the reasons are no longer available 24 hours after completion, so record them here)
return { pdfs, failedFiles: res.data.failedFiles ?? [] };
}
if (res.data.status === 'failed') {
throw new Error(`${res.data.error.code}: ${res.data.error.message}`);
}

const waitSec = Number(res.headers['retry-after'] ?? 2);
await new Promise(resolve => setTimeout(resolve, waitSec * 1000));
}
}
  • The status API's rate limit is 300 req/min (per workspace), separate from the download API's 100 req/min.
  • To receive all files of a multiple request at once, you can also call the ZIP download (GET /v1/file/download/{requestId}) once instead of fetching them one by one (see Download PDF).
  • The status of an async generation is kept for 24 hours after it completes or fails. After 24 hours, a failed single request returns 404 REQUEST_NOT_FOUND, and a multiple request in which every file failed returns only status: "failed" (error.code FILE_GENERATION_FAILED) without the per-file reasons (failedFiles). Record the reason before then (see How long status is kept).
  • In production, add an overall timeout to the example above. See the PDF Generation Guide for an implementation.
Don't infer completion from download 404s

Previously, downloading a file that was still generating returned 404, so you had to poll and treat "404" as "still generating". The download API now returns 409 FILE_NOT_READY (with Retry-After) while generating and 422 FILE_GENERATION_FAILED when generation failed. A 404 means the ID is wrong, belongs to another workspace, is a single generation that failed more than 24 hours ago, or the output was deleted (a request that is still generating also returns 404 when its job record is missing). Use the status API to wait for completion.

Why use async​

1. Avoid timeouts​

Sync endpoints time out at 60 seconds. Async endpoints have no such cap, so they handle long-running jobs gracefully.

// ❌ Sync: may exceed 60s on large payloads
const pdf = await generateSyncPDF(largeData);

// ✅ Async: no client-side wait
const { url } = await generateAsyncPDF(largeData);

2. Background processing​

You can return a response to the user immediately and finish the work behind the scenes.

// Accept the user's request right away
app.post('/api/generate-report', async (req, res) => {
// Kick off PDF generation in the background
const { requestId, url, fileId } = await startPDFGeneration(req.body);

// Respond immediately
res.json({
message: 'PDF generation started',
requestId,
fileId,
// `url` is an app-screen (output history) link. It requires a login and is
// not a direct download URL. Fetch the file from the download API using
// requestId / fileId (see the authentication guide for credentials).
outputsUrl: url
});

// Notify by email when finished (background)
notifyWhenComplete(fileId, req.user.email);
});

3. Parallel bulk generation​

You can fan out and generate many PDFs in parallel.

// Generate 100 PDFs in parallel
const requests = invoices.map(invoice =>
generateAsyncPDF({
designId: 'invoice-template',
content: invoice
})
);

const results = await Promise.all(requests);
console.log(`Generated ${results.length} PDFs`);

Bulk-processing patterns​

Pattern 1: Parallel execution (small/medium scale)​

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} done`);
}

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

// Usage: 100 items, 5 in parallel
const invoices = [...]; // 100 items
const results = await generateBulkPDFs(invoices, 5);

Pattern 2: Queue-based (large scale)​

import Queue from 'bull';

// Create a Redis-backed queue
const pdfQueue = new Queue('pdf-generation', {
redis: {
host: 'localhost',
port: 6379
}
});

// Worker
pdfQueue.process(async (job) => {
const { designId, content } = job.data;

// Call the PDF generation API
const result = await generateAsyncPDF({ designId, content });

// Update progress
await job.progress(100);

return result;
});

// Enqueue jobs
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);
}

// Listen for events
pdfQueue.on('completed', (job, result) => {
console.log(`Job ${job.id} completed:`, result);
});

pdfQueue.on('failed', (job, err) => {
console.error(`Job ${job.id} failed:`, err);
});

Pattern 3: Batch processing (very large scale)​

// For 1000+ items, prefer the multi-file generation endpoint
async function generateMassiveBulk(items) {
const batchSize = 100; // align with the API limit
const batches = chunkArray(items, batchSize);

const results = [];

for (const [index, batch] of batches.entries()) {
console.log(`Processing batch ${index + 1}/${batches.length}…`);

// Multi-file async generation
const { url, files } = await generateMultiplePDFsAsync({
designId: 'template-id',
contents: batch.map(item => ({
fileName: `${item.id}.pdf`,
params: item
}))
});

results.push({ batchIndex: index, url, files });

// Throttle for rate limits
if (index < batches.length - 1) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}

return results;
}

Error handling​

Retry logic​

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(`Retry ${attempt + 1}/${maxRetries} (after ${delay}ms)`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

throw error;
}
}
}

// Resend only 500 / 502 / 503. A 504 / 524 and other 5xx may come after the job was accepted, so a resend can create it twice
function isServerError(error) {
return [500, 502, 503].includes(error.response?.status);
}

Partial-failure handling​

If only some files of a /file/async/multiple request fail, the status API returns status: "completed" with the failed files listed in failedFiles. Over webhooks, the successful files arrive as file.completed. The failed ones arrive as file.failed with error.code: "PARTIAL_FAILURE" only when the generation request specified webhookUrl, and only at that URL (see Webhook Notifications).

When you fan out many single-generation requests, track success and failure per request:

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(`Failed to generate item ${item.id}:`, error);
errors.push({ item, error, status: 'failed' });
}
}

return {
results,
errors,
summary: {
total: items.length,
success: results.length,
failed: errors.length
}
};
}

Python example​

import asyncio
import aiohttp
from typing import List, Dict, Any

async def generate_pdf_async(session: aiohttp.ClientSession, params: Dict[str, Any]) -> Dict:
"""Generate a PDF asynchronously."""
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,
):
"""Generate multiple PDFs in parallel, sharing one ClientSession so connection pooling is effective."""
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)

# Usage
items = [...] # data to generate from
results = asyncio.run(generate_bulk_pdfs(
items,
app_key='your-app-key',
design_id='template-id',
version=1,
concurrency=10,
))

Best practices​

  1. Pick a sensible concurrency level: balance server load and throughput.
  2. Follow Retry-After: poll the status API (and retry 429 / 409) at the interval in the header; use exponential backoff when retrying 500 / 502 / 503 (do not resend automatically on a 504 / 524 or other 5xx, since the job may already have been accepted).
  3. Set timeouts: don't wait forever.
  4. Log errors: keep track of failed items.
  5. Show progress: keep users informed.

Next steps​