Shipping Labels and Barcodes in Generated PDFs
Shipping labels look simple until you have to generate one. You start with a 4x6 PDF, add an address, and then reality shows up: carriers want specific barcode symbologies, scanners are picky about quiet zones and DPI, and your warehouse team will complain if the label jams the printer. This post walks through what actually matters when generating shipping labels and barcodes in PDFs programmatically.
Pick the right page size first
Most thermal label printers (Zebra, Dymo, Rollo) use 4x6 inch labels. If you render on A4 or Letter and expect the printer to figure it out, you'll get scaled-down, unscannable labels. Set the page size explicitly:
tsimport PDFDocument from "pdfkit"; const doc = new PDFDocument({ size: [288, 432], // 4x6 inches at 72 DPI margin: 0, });
For 4x6 at 72 DPI you get 288x432 points. If you need 2x1 inch labels for small parcels, that's 144x72 points. Keep margins at 0 — thermal printers handle their own physical margins, and anything you add will shift the barcode out of the scannable area.
Choose the correct barcode symbology
Different carriers and use cases require different barcode formats. Picking the wrong one guarantees rejected shipments:
- Code 128 — General purpose, high density, used by UPS and FedEx for internal tracking
- GS1-128 (UCC-128) — Code 128 with GS1 application identifiers, required for retail shipments
- PDF417 — 2D stacked barcode, required for USPS IMpb and used on FedEx labels
- QR Code — Common for consumer-facing tracking links, not for carrier routing
- MaxiCode — Required by UPS for sortation
- Interleaved 2 of 5 (ITF) — Carton labels, numeric only
- Data Matrix — Small parcels, DHL international
If you're generating a USPS label, you need PDF417 for the IMpb. If you're generating a UPS label, you need both Code 128 (for the tracking number) and MaxiCode (for sortation). Mixing these up is the most common mistake.
Generating the barcode itself
For 1D barcodes, bwip-js is the most reliable library. It supports every symbology you'll need and outputs to PNG or SVG:
tsimport bwipjs from "bwip-js"; const png = await bwipjs.toBuffer({ bcid: "code128", text: "1Z999AA10123456784", scale: 3, height: 20, includetext: true, textxalign: "center", });
A few things that matter here:
scalecontrols the module width. For thermal printers at 203 DPI,scale: 2or3gives you readable modules. Too low and the printer merges bars.heightis in millimeters, not pixels. Keep carrier barcodes at 20mm minimum.- Quiet zone — leave at least 10x the module width of blank space on either side. Scanners need it.
For PDF417, use the same library with bcid: "pdf417" and set the columns option explicitly. USPS IMpb requires specific column counts depending on data length.
Embedding into the PDF
Once you have the barcode buffer, drop it into the PDF at an exact position. Don't rely on auto-layout for labels — barcodes need predictable coordinates:
tsdoc.image(png, 20, 300, { width: 248 }); doc.fontSize(10).text("1Z999AA10123456784", 20, 405, { width: 248, align: "center", }); doc.end();
Position the barcode in the lower third of the label where scanners expect it. The human-readable number goes directly below the barcode.
Using a template-based approach
Hand-coding coordinates works for one label format, but if you support multiple carriers you'll want templates. HTML-to-PDF with a barcode font or inline SVG is more maintainable:
html<div class="label"> <div class="address">{{address}}</div> <svg class="barcode">{{barcodeSvg}}</svg> <div class="tracking">{{trackingNumber}}</div> </div>
css@page { size: 4in 6in; margin: 0; } .barcode { width: 90%; height: 60px; margin: 0 auto; }
With Kamy, you can register this as a template and render labels through the API without managing a headless browser yourself:
tsimport { kamy } from "@kamydev/sdk"; const pdf = await kamy.documents.render({ template: "shipping-label-4x6", data: { address: "123 Main St, Springfield, IL 62701", trackingNumber: "1Z999AA10123456784", barcodeSvg: await generateBarcodeSvg("1Z999AA10123456784"), }, });
Pre-generate the barcode as SVG on your end and pass it in. Keeping barcode generation client-side gives you deterministic output and avoids re-rendering issues at scale.
Testing before you ship
Printing a label and squinting at it isn't verification. Scan it. The free ZBar CLI will decode barcodes directly from PDF rasters:
bashpdftoppm -r 300 label.pdf label zbarimg label-1.ppm
If zbarimg can't read it, no warehouse scanner will either. Run this in CI against a sample label for every carrier you support — barcode reg