Shipping Labels and Barcodes in Generated PDFs

A practical guide to generating shipping labels with barcodes in PDFs, covering formats like Code 128, QR, and GS1, plus layout, DPI, and printing gotchas.

pdfbarcodesshippinglogisticsnode.js

Shipping Labels and Barcodes in Generated PDFs

Shipping labels look simple until you have to generate one. Then you find out that carriers reject labels for the wrong barcode symbology, thermal printers smear your QR codes because you rendered them at 72 DPI, and your 4x6 label has 2mm of margin drift because you used CSS cm units inconsistently. This post walks through the practical decisions you need to make when generating shipping labels as PDFs, and how to keep barcodes scannable at the other end.

Pick the right barcode symbology

Before writing any code, know what the receiving system expects. The wrong symbology will scan-fail 100% of the time.

  • Code 128 — general-purpose 1D barcode. Used for internal tracking, SKUs, order numbers. Compact and supports full ASCII.
  • Code 39 — older, less dense, but still required by some legacy WMS systems and government/military shipping.
  • GS1-128 (UCC/EAN-128) — Code 128 with structured Application Identifiers (AIs). This is what UPS, FedEx, and most carriers actually want on shipping labels. The AIs identify what each field means (e.g., (00) = SSCC, (01) = GTIN).
  • QR Code — 2D. Good for encoding URLs, JSON payloads, or long tracking references. Scannable from a phone.
  • Data Matrix — 2D. Smaller than QR at the same data density. Common in pharma and electronics.
  • PDF417 — 2D. Required for USPS IMpb (Intelligent Mail package barcode) and driver's licenses.

If you're integrating with a carrier's API (UPS, FedEx, DHL), they usually return the label as a PNG or PDF directly. You only need to generate barcodes yourself when you're doing internal fulfillment, 3PL handoff, or custom manifests.

Generating barcodes in Node.js

For 1D barcodes, bwip-js is the reliable choice — it supports every symbology you're likely to need and can emit SVG, which scales cleanly in PDFs.

bash
npm install bwip-js qrcode

Here's a Code 128 barcode as an SVG string:

ts
import bwipjs from "bwip-js"; const svg = bwipjs.toSVG({ bcid: "code128", text: "1Z999AA10123456784", height: 15, // mm includetext: true, textxalign: "center", });

For QR codes:

ts
import QRCode from "qrcode"; const qrSvg = await QRCode.toString( "https://track.example.com/1Z999AA10123456784", { type: "svg", errorCorrectionLevel: "M", margin: 1 } );

Use error correction level M or Q for shipping labels. Level L looks smaller but fails when the label gets scuffed in transit.

DPI, size, and the thermal printer problem

Most shipping labels are printed on 4x6 inch thermal printers running at 203 DPI (Zebra ZPL-class) or 300 DPI. If you generate a PDF at 72 DPI and let the printer scale it up, barcodes will blur and fail to scan.

Two rules that solve 90% of scan failures:

  1. Render barcodes as SVG or high-DPI raster images, not low-DPI PNGs.
  2. Respect the X-dimension (the width of the narrowest bar). For Code 128 at 203 DPI, the minimum X-dimension is about 0.25mm. Under that and scanners struggle.

If you're generating a 4x6 label PDF, set the page size explicitly:

ts
// pdf-lib const page = pdfDoc.addPage([288, 432]); // 4in x 6in at 72pt/in

Or in CSS for HTML-to-PDF pipelines:

css
@page { size: 4in 6in; margin: 0; }

Assembling the label

For a typical carrier-agnostic label, you need: sender block, recipient block, service level, weight, a routing barcode, and a tracking barcode. Here's a minimal HTML template you can render to PDF:

html
<div class="label"> <div class="header"> <div class="service">GROUND</div> <div class="weight">2.4 LB</div> </div> <div class="from">FROM: Acme Co, 123 Main St...</div> <div class="to">TO: Jane Doe, 456 Oak Ave...</div> <div class="routing">{{routingBarcodeSvg}}</div> <div class="tracking"> {{trackingBarcodeSvg}} <div class="tracking-number">1Z999AA10123456784</div> </div> </div>

Inline the SVG directly — do not reference external files, because the PDF renderer may not have network access, and embedded SVG stays crisp at any print size.

Using Kamy for label generation

If you'd rather not run a headless Chrome instance in your fulfillment service, Kamy can render templated labels and embed barcodes server-side. You send your data, Kamy returns the PDF:

ts
import { kamy } from "@kamydev/sdk"; const label = await kamy.documents.render({ template: "shipping-label-4x6", data: { service: "GROUND", weightLb: 2.4, from: { name: "Acme Co", address1: "123 Main St", city: "Austin", state: "TX", zip: "78701" }, to: { name: "Jane Doe", address1: "456 Oak Ave", city: "Portland", state: "OR", zip: "97201" }, tracking