April 28, 2026

Embed QR Codes in PDF: Node.js, Python & API Guide (2026)

QR CodePDF GenerationNode.jsPython

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

Powered by FUNBREW PDF