Wiring PDF Generation Into a Real Pipeline, Not Just One Request | PDFly

A document pipeline, whether it produces invoices, certificates or reports, is four stages regardless of which PDF service sits behind stage three: pull the data, render it into content, generate the file, get it to whoever needs it. Stage three is where most tutorials stop. Here is what the other three actually look like end to end.

## Stage 1: the data

```javascript const invoices = await db.query( 'SELECT * FROM invoices WHERE status = $1', ['pending'] ); ```

Nothing PDF-specific here. Whatever your data layer already does.

## Stage 2: rendering it into HTML

```javascript function renderInvoiceHTML(invoice) { const rows = invoice.items .map(item => `<tr><td style="padding:10px">${item.name}</td>` + `<td style="padding:10px;text-align:right">${item.amount}</td></tr>`) .join('');

return ` <div style="font-family:Arial;padding:40px"> <h1>Invoice #${invoice.number}</h1> <p>Date: ${invoice.date} &nbsp; Client: ${invoice.clientName}</p> <table style="width:100%;border-collapse:collapse;margin-top:20px"> <tr style="background:#f5f5f5"> <th style="padding:10px;text-align:left">Item</th> <th style="padding:10px;text-align:right">Amount</th> </tr> ${rows} </table> <p style="font-size:20px;font-weight:bold;margin-top:20px"> Total: ${invoice.currency}${invoice.total} </p> </div>`; } ```

Plain template literals are enough for most document shapes. Reach for a real templating engine once the conditionals inside the HTML start getting hard to read, not before.

## Stage 3: batching the calls correctly

The batch cap is five documents per request, not the ten you may have seen in an older version of this post. Chunk accordingly:

```javascript async function generatePdfs(invoices, apiKey) { const documents = invoices.map(inv => ({ title: `Invoice_${inv.number}`, content: renderInvoiceHTML(inv), }));

const results = []; for (let i = 0; i < documents.length; i += 5) { const batch = documents.slice(i, i + 5); const res = await fetch('https://pdfly.3idhmind.in/api/pdf/generate', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ documents: batch, template: 'professional' }), });

if (!res.ok) { const err = await res.json(); throw new Error(`${err.error}: ${err.message}`); } const data = await res.json(); results.push(...data.documents); } return results; } ```

## Stage 3b: retrying on rate limits without guessing

A 429 response carries a `Retry-After` header set to the actual seconds remaining in the current window, computed server-side. Read it instead of picking an arbitrary backoff:

```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 1; attempt <= maxRetries; attempt++) { const res = await fetch(url, options); if (res.status !== 429) return res;

const retryAfter = Number(res.headers.get('Retry-After') ?? 5); if (attempt === maxRetries) return res; await new Promise(r => setTimeout(r, retryAfter * 1000)); } } ```

A fixed exponential backoff also works, but it either waits longer than necessary or retries before the window has actually reset. The header is the ground truth.

## Stage 4: what you get back, and what to do with it

Every successful document carries a `storage` block in the response. If object storage is not configured for that deployment, `storage.persisted` is false, the PDF exists only as the base64 in `pdf_base64`, and it is gone once you stop holding onto that response. Decode and write it to your own storage immediately if you need it later:

```javascript import { writeFile } from 'node:fs/promises';

for (const doc of results) { await writeFile(`${doc.title}.pdf`, Buffer.from(doc.pdf_base64, 'base64')); } ```

If storage is configured on our end, you also get `doc.download_url`, a link on our own domain valid for one hour. That is a convenience for a caller who wants to hand a link to someone else immediately, not a substitute for your own storage in a production pipeline: an hour is not a retention policy for records you need next quarter.

## The Python version of stage 3

```python import requests, time

def generate_pdf_batch(documents, api_key): url = "https://pdfly.3idhmind.in/api/pdf/generate" headers = {"Authorization": f"Bearer {api_key}"} results = []

for i in range(0, len(documents), 5): batch = documents[i:i + 5] res = requests.post(url, json={"documents": batch, "template": "professional"}, headers=headers)

if res.status_code == 429: time.sleep(int(res.headers.get("Retry-After", 5))) res = requests.post(url, json={"documents": batch, "template": "professional"}, headers=headers)

res.raise_for_status() results.extend(res.json()["documents"]) return results ```

## What we left out on purpose

No analytics dashboard link, because there isn't one yet: usage is visible through the `/api/account/keys` response, not a separate UI. No claim about specific throughput numbers, because we have not load-tested this at a scale worth quoting. Full field and error-code reference is on the [docs page](/docs), generated from the same source as the endpoints themselves rather than written separately, which is the part that used to drift.