Shipping Labels and Barcodes in Generated PDFs

A practical guide to generating shipping labels with barcodes in PDFs, covering label sizes, barcode encoding, carrier requirements, and rendering strategies that actually work in production.

pdfshippingbarcodeslogisticsnode.js

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:

ts
import 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:

ts
import 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:

  • scale controls the module width. For thermal printers at 203 DPI, scale: 2 or 3 gives you readable modules. Too low and the printer merges bars.
  • height is 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:

ts
doc.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:

ts
import { 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:

bash
pdftoppm -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