Embed QR Codes in PDF: Node.js, Python & API Guide (2026)
Adding a QR code to a PDF — whether for verification links, tracking, ticketing, or delivery confirmations — should take less than 20 lines of code. The key is generating the QR code as a Base64 data URI on the server, embedding it in an HTML template, and converting the HTML to PDF via an API.
This guide covers Node.js and Python implementations with copy-paste examples, plus patterns for multi-page PDFs, certificate generation, and batch processing. If you need a broader introduction to HTML-to-PDF conversion, see the HTML to PDF Complete Guide. For troubleshooting layout issues, the HTML to PDF CSS Tips guide covers @page, page breaks, and image rendering in detail.
Why Base64, Not a File Path
PDF APIs render HTML in a headless browser that cannot access your local filesystem. Referencing a QR code as /tmp/qr.png will produce a broken image in the output PDF. The reliable approach is to encode the QR image as a Base64 data URI and embed it directly in the HTML:
<img src="data:image/png;base64,iVBORw0KGgo..." alt="QR Code" />
This works regardless of where the PDF API runs — on your server, in a Lambda function, or in a Docker container.
Node.js: Generate QR Code and Convert to PDF
Install Dependencies
npm install qrcode axios
Basic Example
const QRCode = require('qrcode');
const axios = require('axios');
async function generatePdfWithQr(targetUrl, outputPath) {
// Step 1: Generate QR code as Base64 data URI
const qrDataUri = await QRCode.toDataURL(targetUrl, {
errorCorrectionLevel: 'M',
width: 200,
margin: 2,
});
// Step 2: Build HTML template with embedded QR code
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; padding: 40px; }
.qr-container {
display: flex;
align-items: center;
gap: 20px;
border: 1px solid #ddd;
padding: 20px;
border-radius: 8px;
}
.qr-container img { width: 120px; height: 120px; }
.qr-label { font-size: 12px; color: #555; margin-top: 8px; }
h1 { font-size: 24px; }
</style>
</head>
<body>
<h1>Document with QR Verification</h1>
<p>Scan the QR code to verify this document.</p>
<div class="qr-container">
<div>
<img src="${qrDataUri}" alt="Verification QR Code" />
<p class="qr-label">Scan to verify</p>
</div>
<div>
<strong>Document ID:</strong> DOC-2026-001<br>
<strong>Issued:</strong> 2026-04-28<br>
<strong>Verification URL:</strong><br>
<small>${targetUrl}</small>
</div>
</div>
</body>
</html>
`;
// Step 3: POST to FUNBREW PDF API
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html },
{
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}`,
'Content-Type': 'application/json',
},
responseType: 'arraybuffer',
}
);
// Step 4: Save PDF
require('fs').writeFileSync(outputPath, response.data);
console.log(`PDF saved to ${outputPath}`);
}
generatePdfWithQr(
'https://verify.example.com/doc/DOC-2026-001',
'./output.pdf'
);
Batch QR Code Generation
When generating multiple PDFs (tickets, certificates, invoices), pre-generate all QR codes in parallel before building templates:
const QRCode = require('qrcode');
const axios = require('axios');
const fs = require('fs');
const QR_OPTIONS = { errorCorrectionLevel: 'M', width: 180, margin: 2 };
async function batchGeneratePdfs(records) {
// Generate all QR codes in parallel
const qrDataUris = await Promise.all(
records.map(r => QRCode.toDataURL(r.verifyUrl, QR_OPTIONS))
);
// Generate PDFs with a concurrency limit of 5
const results = [];
for (let i = 0; i < records.length; i += 5) {
const batch = records.slice(i, i + 5);
const batchQrs = qrDataUris.slice(i, i + 5);
const batchResults = await Promise.all(
batch.map((record, idx) => generateOnePdf(record, batchQrs[idx]))
);
results.push(...batchResults);
}
return results;
}
async function generateOnePdf(record, qrDataUri) {
const html = buildTemplate(record, qrDataUri);
const response = await axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{ html },
{
headers: { 'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}` },
responseType: 'arraybuffer',
}
);
const filename = `./output/${record.id}.pdf`;
fs.writeFileSync(filename, response.data);
return filename;
}
function buildTemplate(record, qrDataUri) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page { size: A4; margin: 20mm; }
body { font-family: Arial, sans-serif; }
.qr { width: 100px; height: 100px; float: right; }
</style>
</head>
<body>
<img class="qr" src="${qrDataUri}" alt="QR" />
<h2>${record.title}</h2>
<p>ID: ${record.id}</p>
<p>Issued to: ${record.recipientName}</p>
</body>
</html>
`;
}
Python: Generate QR Code and Convert to PDF
Install Dependencies
pip install qrcode[pil] requests
Basic Example
import qrcode
import base64
import io
import requests
import os
def generate_pdf_with_qr(target_url: str, output_path: str) -> None:
"""Generate a PDF with an embedded QR code."""
# Step 1: Generate QR code
qr = qrcode.QRCode(
version=None,
error_correction=qrcode.constants.ERROR_CORRECT_M,
box_size=8,
border=2,
)
qr.add_data(target_url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# Step 2: Convert to Base64 data URI
buffer = io.BytesIO()
img.save(buffer, format="PNG")
qr_b64 = base64.b64encode(buffer.getvalue()).decode()
qr_data_uri = f"data:image/png;base64,{qr_b64}"
# Step 3: Build HTML template
html = f"""
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {{ font-family: Arial, sans-serif; padding: 40px; }}
.qr-section {{
display: flex;
align-items: center;
gap: 24px;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}}
.qr-section img {{ width: 120px; height: 120px; }}
p {{ margin: 4px 0; }}
</style>
</head>
<body>
<h1>Verified Document</h1>
<div class="qr-section">
<img src="{qr_data_uri}" alt="QR Code" />
<div>
<p><strong>Scan to verify this document</strong></p>
<p>URL: {target_url}</p>
<p>Generated: 2026-04-28</p>
</div>
</div>
</body>
</html>
"""
# Step 4: Convert to PDF via FUNBREW PDF API
response = requests.post(
"https://pdf.funbrew.cloud/api/v1/generate",
json={"html": html},
headers={
"Authorization": f"Bearer {os.environ['FUNBREW_API_KEY']}",
"Content-Type": "application/json",
},
)
response.raise_for_status()
with open(output_path, "wb") as f:
f.write(response.content)
print(f"PDF saved to {output_path}")
if __name__ == "__main__":
generate_pdf_with_qr(
"https://verify.example.com/doc/DOC-2026-001",
"output.pdf",
)
Async Batch Processing (Python)
import asyncio
import aiohttp
import qrcode
import base64
import io
import os
QR_CONFIG = {
"error_correction": qrcode.constants.ERROR_CORRECT_M,
"box_size": 7,
"border": 2,
}
def make_qr_data_uri(url: str) -> str:
qr = qrcode.QRCode(**QR_CONFIG)
qr.add_data(url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buf = io.BytesIO()
img.save(buf, format="PNG")
b64 = base64.b64encode(buf.getvalue()).decode()
return f"data:image/png;base64,{b64}"
async def generate_one(session: aiohttp.ClientSession, record: dict) -> str:
qr_uri = make_qr_data_uri(record["verify_url"])
html = f"""<!DOCTYPE html>
<html><head><meta charset="UTF-8"></head>
<body>
<img src="{qr_uri}" width="100" height="100" />
<h2>{record['title']}</h2>
<p>Recipient: {record['name']}</p>
</body></html>"""
async with session.post(
"https://pdf.funbrew.cloud/api/v1/generate",
json={"html": html},
headers={"Authorization": f"Bearer {os.environ['FUNBREW_API_KEY']}"},
) as resp:
resp.raise_for_status()
pdf_bytes = await resp.read()
path = f"./output/{record['id']}.pdf"
with open(path, "wb") as f:
f.write(pdf_bytes)
return path
async def batch_generate(records: list) -> list:
semaphore = asyncio.Semaphore(5) # max 5 concurrent requests
async def limited(session, record):
async with semaphore:
return await generate_one(session, record)
async with aiohttp.ClientSession() as session:
tasks = [limited(session, r) for r in records]
return await asyncio.gather(*tasks)
QR Codes in PDF Headers and Footers
To place a QR code on every page of a multi-page PDF, embed it in the headerTemplate or footerTemplate parameter. This is useful for compliance documents where each page must be individually verifiable.
const QRCode = require('qrcode');
const axios = require('axios');
async function generateWithQrFooter(html, verifyUrl) {
const qrDataUri = await QRCode.toDataURL(verifyUrl, {
errorCorrectionLevel: 'M',
width: 80,
margin: 1,
});
const footerTemplate = `
<div style="
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20mm;
font-size: 9pt;
font-family: Arial, sans-serif;
color: #555;
">
<span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
<img src="${qrDataUri}" style="width: 25mm; height: 25mm;" alt="Verify" />
</div>
`;
return axios.post(
'https://pdf.funbrew.cloud/api/v1/generate',
{
html,
displayHeaderFooter: true,
footerTemplate,
marginTop: '15mm',
marginBottom: '35mm', // Leave space for the QR footer
},
{
headers: { 'Authorization': `Bearer ${process.env.FUNBREW_API_KEY}` },
responseType: 'arraybuffer',
}
);
}
QR Code Size Recommendations
| Use Case | Recommended Size | Error Correction |
|---|---|---|
| Document verification | 25–30mm | M (15%) |
| Event ticket | 30–40mm | Q (25%) |
| Certificate (A4) | 30–35mm | M (15%) |
| Invoice corner | 20–25mm | M (15%) |
| Page footer | 20–25mm | L (7%) |
Use error correction level Q (25%) when the QR code may be printed at small sizes or on lower-quality printers. Level L (7%) is only appropriate for large, clean prints.
Common Issues and Fixes
Broken image in PDF output
Cause: The QR code is referenced as a file path (/tmp/qr.png) instead of a data URI.
Fix: Use QRCode.toDataURL() (Node.js) or encode to Base64 with base64.b64encode() (Python) and embed directly in the <img src> attribute.
QR code not scannable
Cause: The rendered size is too small, or the margin (quiet zone) is insufficient.
Fix: Increase width to at least 150px for the source image, keep margin at 2 or higher, and ensure the printed size is at least 20mm.
QR code looks blurry
Cause: The source PNG is generated at a low resolution relative to the display size in the PDF.
Fix: Generate the QR code at width: 300 or higher and constrain the displayed size via CSS (width: 30mm). The PDF renderer will downsample cleanly.
Certificate Use Case
Combining QR code verification with bulk certificate generation is one of the most common production patterns. For a complete implementation including recipient CSV import, HTML template variables, and S3 archival, see the PDF Certificate Automation guide.
The QR code in each certificate should point to a verification endpoint that accepts the certificate ID and returns its status:
https://verify.example.com/cert/{certificateId}
Generate the certificateId deterministically (hash of recipient email + issue date) so it can be reconstructed without a database lookup, or store it in a simple key-value store for O(1) lookups.
Next Steps
- PDF Certificate Automation — bulk certificate generation with QR verification
- HTML to PDF CSS Tips — layout and print CSS for precise positioning
- PDF API Batch Processing — concurrency patterns for high-volume PDF generation
- Automate PDF Reports — scheduling, charting, and email delivery in one pipeline
- FUNBREW PDF Playground — test your QR-embedded HTML templates before writing code
- API Documentation — full reference for the FUNBREW PDF generate endpoint