Get started
How to add a QR code to a PDF (HTML to PDF, with code)

How to add a QR code to a PDF (HTML to PDF, with code)

Add a QR code to a PDF from HTML using Node.js, Python, or PHP. Embed it as a base64 data URI or inline SVG so it always renders, with copy-paste code.

9 min read

To add a QR code to a PDF built from HTML, generate the QR as a base64 data URI in your backend code, drop it into an <img> tag in your template, and render the HTML to PDF. The QR becomes part of the page and prints at full resolution. Do not link to an external QR image URL: the network request can finish after the page snapshot fires and leave a blank box.

This guide shows the data-URI method in Node.js, Python, and PHP, the inline-SVG alternative for crisp printing, the right error correction level to pick, and how to put a dynamic QR on every invoice with PDF4.dev.

What is the most reliable way to put a QR code in a PDF?

The most reliable method is to generate the QR code as a base64 data URI and embed it directly in the HTML before rendering. A data URI carries the image bytes inside the src attribute, so there is nothing to fetch. The renderer already has the pixels when it snapshots the page.

The alternative, pointing an <img> at a remote QR service, introduces a network round trip during rendering. If the request is slow or the service rate-limits you, the PDF engine may capture the page before the image loads, producing a missing-image box. For a document you email to a customer, that failure is permanent.

MethodRenders reliably?Works offline?Print qualityBest for
Base64 data URI (PNG)YesYesGood if sized rightMost documents
Inline SVGYesYesSharp at any sizeHigh-resolution print
External image URLNo, network-dependentNoVariesAvoid for PDFs
Free QR image APINo, can rate-limit or 404NoVariesAvoid for PDFs

Google's Image Charts QR endpoint, once a popular one-line trick, was shut off on March 14, 2019 and now returns 404s. Generating the QR yourself removes that class of breakage entirely.

How do you generate a QR code as a data URI?

Use a QR library to encode your text or URL into a base64 data URI, then inject that string into your HTML. The qrcode package for Node.js, segno for Python, and endroid/qr-code for PHP all produce a data URI in a few lines.

import QRCode from 'qrcode';
 
// Returns a string like "data:image/png;base64,iVBORw0KGgo..."
const dataUri = await QRCode.toDataURL('https://pdf4.dev/invoice/INV-001', {
  errorCorrectionLevel: 'M',
  margin: 1,
  width: 300,
});
 
const html = `
  <div style="text-align:center">
    <img src="${dataUri}" alt="Scan to pay" style="width:30mm;height:30mm" />
    <p>Scan to pay invoice INV-001</p>
  </div>
`;

The width and scale options control the source pixel size. Render larger than you display so the code stays crisp when printed. Sizing the <img> in millimeters keeps the printed dimensions predictable regardless of screen DPI.

When should you use an inline SVG QR code instead of PNG?

Use an inline SVG QR code when the document will be printed at high resolution or scaled to different sizes. An SVG QR is vector: it is a set of rectangles, not pixels, so every module stays sharp at any zoom or print DPI. A PNG QR, by contrast, is fixed-resolution and softens if you scale it up in CSS.

import QRCode from 'qrcode';
 
// Returns an <svg>...</svg> string you can drop straight into the page
const svg = await QRCode.toString('https://pdf4.dev/invoice/INV-001', {
  type: 'svg',
  errorCorrectionLevel: 'H',
  margin: 1,
});
 
const html = `<div style="width:30mm;height:30mm">${svg}</div>`;

For invoices and tickets that get printed and physically scanned, inline SVG with error correction level H is the safest combination: vector sharpness plus around 30 percent damage tolerance.

Which QR code error correction level should you choose?

Error correction level is the amount of redundant data a QR code carries so it still scans when partly damaged. There are four levels defined in the ISO/IEC 18004 standard. Higher levels recover more damage but make the code denser, so each module is smaller at the same physical size.

LevelRecovery capacityUse case
L (low)~7%Clean screens, large codes, short payloads
M (medium)~15%Default for documents, good balance
Q (quartile)~25%Codes that may get scuffed or printed small
H (high)~30%Logo overlay, harsh print or scan conditions

Pick M for a standard invoice or report. Move to H when you place a logo in the middle of the code (the logo covers data, so you need the redundancy) or when the page will be folded, stamped, or scanned from a low-quality printout.

How do you add a dynamic QR code to every invoice?

Generate the QR payload from each invoice's data, encode it to a data URI per render, and pass it into your template as a variable. The QR then reflects that specific invoice: a payment link, an invoice ID, or a verification URL.

With PDF4.dev, the QR data URI travels in the data object of the render request, and the template references it with a Handlebars variable. Your template uses an <img> whose src is the variable, for example {{qr_code}}, and the API substitutes the value at render time.

import QRCode from 'qrcode';
 
const invoice = { id: 'INV-001', total: '$1,500.00' };
 
const qrCode = await QRCode.toDataURL(
  `https://pay.example.com/${invoice.id}`,
  { errorCorrectionLevel: 'M', margin: 1, width: 300 }
);
 
await fetch('https://pdf4.dev/api/v1/render', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer p4_live_xxx',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    template_id: 'invoice',
    data: { ...invoice, qr_code: qrCode },
    delivery: 'url',
  }),
});

In the template, the image tag is just <img src="{{qr_code}}" style="width:30mm;height:30mm" />. Because the QR is generated server-side and passed in as data, no part of the render reaches out to a QR service. See the Handlebars templates guide for how variables and helpers work, and the invoice generation guide for the full invoice flow.

How do you add a QR code with raw Playwright?

If you render PDFs yourself with Playwright, embed the data URI in the HTML and enable printBackground so the QR's surrounding styles print. The pattern is the same as PDF4.dev under the hood, because PDF4.dev runs Chromium through Playwright.

import { chromium } from 'playwright';
import QRCode from 'qrcode';
 
const qr = await QRCode.toDataURL('https://pdf4.dev/invoice/INV-001', {
  errorCorrectionLevel: 'M',
  margin: 1,
  width: 300,
});
 
const html = `
  <html><body style="font-family: sans-serif; text-align:center; padding:40px">
    <h1>Invoice INV-001</h1>
    <img src="${qr}" style="width:30mm;height:30mm" />
    <p>Scan to pay</p>
  </body></html>
`;
 
const browser = await chromium.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'load' });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
await browser.close();

Because the QR is a data URI, there is no networkidle wait to worry about: the image is already decoded when setContent resolves. This is one fewer race condition than loading a remote image. For the broader Playwright setup, see generate PDF from HTML in Node.js.

Common QR-in-PDF mistakes and how to avoid them

The failures below cause most "the QR won't scan" support tickets. All of them are avoidable at generation time.

MistakeResultFix
Linking a remote QR imageBlank box if the fetch is slowEmbed a base64 data URI
Scaling a small PNG up in CSSBlurry, unscannable modulesRender at higher width, or use SVG
No quiet zone (margin: 0)Scanners fail to lock onKeep margin of at least 1 module
Low contrast or inverted colorsPhone cameras miss the codeDark modules on a light background
Long payload at small sizeDense code, hard to scanUse a short redirect URL
QR across a page foldPhysical damage breaks the codePlace it in a flat, clear area

Always test the final PDF by scanning it with a real phone at the printed size, not at full-screen zoom. A QR that scans on a 27-inch monitor can be unreadable at 2 cm on paper.

Quick comparison: build it yourself vs use an API

Both paths use the same QR libraries and the same Chromium engine. The difference is who runs and maintains the rendering infrastructure.

FactorDIY PlaywrightPDF4.dev API
QR generationYour code (qrcode, segno)Your code, passed as data
Browser pool, memory, crashesYou manageManaged
Render latencyCold start unless warmedWarm pool
SetupInstall Chromium, ~300 MB imageOne API call
Template reuseYour own systemBuilt-in templates and variables

If you already run a Chromium-based pipeline, embedding a QR is a one-line addition. If you would rather not operate headless browsers in production, generate the data URI in your code and send it to the HTML to PDF API or the render endpoint. You can also try the free HTML to PDF tool with a QR <img> to see the output before writing any integration code.

Conclusion

Adding a QR code to a PDF comes down to one rule: generate the code as a base64 data URI or inline SVG and embed it in the HTML, never link to an external image. Pick error correction level M for clean documents and H when a logo or rough printing is involved, size the code in millimeters, and keep a quiet zone. For dynamic codes on invoices or tickets, generate the data URI per render and pass it in as a template variable. Whether you run Playwright yourself or call PDF4.dev, the QR is just another part of the page.

Free tools mentioned:

Html To PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.