Shipping Labels and Barcodes in Generated PDFs
Shipping labels look simple: a sender block, a recipient block, some service metadata, and a barcode. But once you start generating them programmatically, you run into the details that carriers actually care about — exact label dimensions, barcode symbology, quiet zones, and print resolution. Get any of those wrong and the label either gets rejected at the carrier's API or, worse, fails to scan at the depot.
This post walks through the practical decisions you need to make when producing shipping-label PDFs, with examples you can adapt to whatever renderer you're using.
Pick the right label size
Most carriers accept one of two physical sizes:
- 4x6 inches (288x432 points at 72 DPI) — the industry standard for thermal printers
- A4 or Letter — used when the label is embedded inside a packing slip
If you're generating labels for warehouses using Zebra or similar thermal printers, always go with 4x6. Set the page size explicitly in your PDF renderer — don't rely on defaults.
ts// With pdf-lib import { PDFDocument } from "pdf-lib"; const doc = await PDFDocument.create(); const page = doc.addPage([288, 432]); // 4x6 at 72 DPI
If you're using an HTML-to-PDF pipeline, define it in CSS:
css@page { size: 4in 6in; margin: 0; }
Margins should be zero. Thermal printers already have their own physical margins, and any PDF-level padding will shift your barcode off-center.
Choose a barcode symbology
The symbology depends on what the label carries:
- Code 128 — general-purpose, alphanumeric, used by most carriers for internal tracking IDs
- Code 39 — older, less dense, still seen in some warehouse systems
- PDF417 — 2D stacked barcode required by USPS, FedEx, and UPS for their routing data
- QR / Data Matrix — increasingly used for parcel lockers, returns, and DHL routing
For a typical shipping label you'll often need two barcodes: a 1D Code 128 for the tracking number and a PDF417 for the carrier's routing block.
Generating the barcode image
The cleanest approach is to render the barcode as SVG, then embed it in the PDF. SVG scales without pixelation and stays crisp at any print resolution.
Here's an example using bwip-js, which handles nearly every symbology:
tsimport bwipjs from "bwip-js"; const svg = bwipjs.toSVG({ bcid: "code128", text: "1Z999AA10123456784", height: 15, // in mm includetext: true, textxalign: "center", });
For PDF417:
tsconst pdf417Svg = bwipjs.toSVG({ bcid: "pdf417", text: "420900209261290100528000000000", columns: 10, eclevel: 5, });
If you're rendering via HTML, drop the SVG directly into the template. If you're using pdf-lib, convert it to a PNG at 300 DPI first — pdf-lib doesn't parse SVG natively.
Quiet zones and scan reliability
The single most common reason a printed barcode fails to scan is a missing quiet zone — the blank margin around the barcode. For Code 128, you need at least 10x the width of the narrowest bar (usually ~2.5mm) of clear space on both left and right. For PDF417, keep at least 2 modules of white space on all sides.
Do not put borders, text, or background colors inside the quiet zone. It's the number one issue we see in support tickets from developers integrating shipping providers.
A minimal label layout
Here's the structure most labels follow, top to bottom:
txt+---------------------------------------+ | CARRIER LOGO SERVICE CLASS | +---------------------------------------+ | FROM: sender address | +---------------------------------------+ | TO: recipient address (LARGE) | +---------------------------------------+ | [ PDF417 routing barcode ] | +---------------------------------------+ | [ CODE128 tracking barcode ] | | 1Z999AA10123456784 | +---------------------------------------+
The recipient address should be the largest text on the label — typically 14–18pt. Sorters read it visually as a fallback when barcode scans fail.
Generating the PDF with Kamy
If you'd rather not maintain a rendering pipeline yourself, you can define the label as an HTML/CSS template and let Kamy handle rasterization, page sizing, and barcode rendering. Kamy supports barcode helpers directly in templates:
tsimport { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "shipping-label-4x6", data: { trackingNumber: "1Z999AA10123456784", routingCode: "420900209261290100528000000000", recipient: { name: "Jane Doe", street: "123 Market St", city: "San Francisco", state: "CA", zip: "94103", }, }, });
Inside the template, barcodes are just tags:
html<div class="barcode-block"> {{ barcode trackingNumber symbology="code128" height="15mm" }} {{ barcode routingCode symbology="pdf417" columns=10 }} </div>
The rendered PDF comes back sized to 4x6 with barcodes embedded as vector paths, so it prints cleanly on both thermal and laser printers.