Documentation Index
Fetch the complete documentation index at: https://developer.uphold.com/llms.txt
Use this file to discover all available pages before exploring further.
This guide covers how to build a document template from processed data and run the generation pipeline in a background worker to ensure end users aren’t blocked waiting for the result.
Choose a generation strategy
Choose the method that best fits your infrastructure and layout requirements:
- Render via headless browser — build an HTML/CSS template and use a browser engine to render it to PDF. Best when you want full control over layout and styling via HTML/CSS.
- Construct programmatically — use a PDF library like PDFKit to build the document step-by-step in code. Highly performant and ideal for simple, repetitive layouts without the overhead of a browser.
- Use an external service — send your structured data to a third-party API that handles rendering and hosting, offloading document assembly entirely.
The example below uses headless browser rendering with Puppeteer: an HTML template is built from the processed data, and Puppeteer renders it to a PDF. By the end of this step, you will have a PDF buffer ready to store and deliver.
Render with a headless browser
Install Puppeteer:
Puppeteer downloads a compatible version of Chromium automatically. In environments where you cannot install Chromium (e.g. some serverless platforms), use puppeteer-core with a pre-installed browser instead.
The template receives the processed data object and returns an HTML string. All values are pre-computed — the template is only responsible for layout:
import puppeteer from 'puppeteer';
function buildHtml(data) {
const { period, denomination, holdings, totalValue, transactions, totalFees } = data;
const holdingsRows = holdings
.map(({ asset, amount, value }) =>
`<tr><td>${asset}</td><td>${amount}</td><td>${value} ${denomination}</td></tr>`
)
.join('');
const txRows = transactions
.map(({ date, id, status, origin, destination, denominatedAmount, denominatedAsset, value, txFees }) =>
`<tr>
<td>${date}</td>
<td>${id}</td>
<td>${status}</td>
<td>${origin.amount} ${origin.asset}</td>
<td>${destination.amount} ${destination.asset}</td>
<td>${denominatedAmount} ${denominatedAsset}</td>
<td>${value} ${denomination}</td>
<td>${txFees} ${denomination}</td>
</tr>`
)
.join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; font-size: 12px; color: #111; }
h1 { font-size: 16px; margin-bottom: 4px; }
h2 { font-size: 13px; margin-top: 24px; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
p { margin: 2px 0; color: #555; font-size: 11px; }
table { border-collapse: collapse; width: 100%; margin-top: 8px; font-size: 11px; }
th, td { border: 1px solid #ccc; padding: 4px 8px; text-align: left; }
th { background: #f5f5f5; font-weight: 600; }
tfoot td { font-weight: 600; background: #f9f9f9; }
</style>
</head>
<body>
<h1>Compliance Report</h1>
<p>Period: ${period.from.slice(0, 10)} to ${period.to.slice(0, 10)}</p>
<p>Generated: ${new Date().toISOString().slice(0, 10)}</p>
<p>Denomination: ${denomination}</p>
<h2>Holdings at end of period</h2>
<table>
<thead>
<tr>
<th>Asset</th>
<th>Amount</th>
<th>Value (${denomination})</th>
</tr>
</thead>
<tbody>${holdingsRows}</tbody>
<tfoot>
<tr><td colspan="2">Total</td><td>${totalValue} ${denomination}</td></tr>
</tfoot>
</table>
<h2>Transactions (${transactions.length})</h2>
<table>
<thead>
<tr>
<th>Date</th>
<th>Transaction ID</th>
<th>Status</th>
<th>Origin</th>
<th>Destination</th>
<th>Amount</th>
<th>Value (${denomination})</th>
<th>Fees (${denomination})</th>
</tr>
</thead>
<tbody>${txRows}</tbody>
<tfoot>
<tr><td colspan="7">Total fees</td><td>${totalFees} ${denomination}</td></tr>
</tfoot>
</table>
</body>
</html>
`;
}
export async function generateStatementPdf(data) {
const html = buildHtml(data);
const browser = await puppeteer.launch({ args: ['--no-sandbox'] });
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
return await page.pdf({
format: 'A4',
margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' },
printBackground: true
});
} finally {
await browser.close();
}
}
generateStatementPdf returns a Buffer containing the PDF, ready to be stored or delivered.
Execute as a background task
Offload report generation to an asynchronous worker to keep long-running generation from blocking your API. By returning immediately, your application remains responsive while the worker handles data retrieval, formatting, and file storage in the background.
End users don’t wait for a report — they request one and get notified when it’s ready.
Process outline:
- The end user requests a report. Your backend responds immediately with a reference to the background job.
- A worker runs the full pipeline: fetch → process → generate.
- When the report is ready, your backend notifies the end user — via a retrieval link, email, or any other delivery method.
// Route — accepts the request and enqueues the job
// Adapt to your preferred job queue (BullMQ, Agenda, etc.)
app.post('/reports/monthly', async (req, res, next) => {
try {
const { userId, year, month, denomination = 'GBP' } = req.body;
const token = req.headers.authorization?.split(' ')[1];
const jobId = await reportQueue.add('generate-statement', {
token,
userId,
year: Number(year),
month: Number(month),
denomination
});
res.status(202).json({ jobId });
} catch (error) {
next(error);
}
});
// Worker — runs the full pipeline in the background
// Adapt to your queue library (BullMQ v2: new Worker('generate-statement', async job => {...}), Agenda, etc.)
async function runReportJob(job) {
const { token, userId, year, month, denomination } = job.data;
const [{ statement: portfolio }, transactions] = await Promise.all([
fetchPortfolioStatement({ token, userId, year, month, denomination }),
fetchAllTransactions({ token, userId, year, month, denomination })
]);
const data = processStatementData({ portfolio, transactions, denomination });
const pdf = await generateStatementPdf(data);
const url = await storeReport(pdf, { userId, year, month }); // e.g. upload to S3 or equivalent storage
await notifyUser(userId, { reportUrl: url }); // e.g. send email, webhook, or push notification
}
If any step throws, the error propagates to the queue, which handles job failure and retry according to its configuration.
For implementation details on fetchPortfolioStatement and fetchAllTransactions check Fetching data. For details on processStatementData check Preparing data.