Use case
Shipping Labels PDF API
Logistics platforms generate millions of shipping documents weekly. HTML templates with a clean barcode or QR code image and structured label layout make it easy to produce printer-ready PDFs at scale, for any carrier or fulfillment platform.
4x6 thermal shipping label
A Mustache template for a standard 4x6 thermal label. Includes sender and receiver address blocks, tracking number, a barcode image (supply a data:image/png;base64,... URI from your barcode library, or an external barcode service URL), carrier name, service level badge, and a weight/dimensions/order metadata row. The @page rule sets size: 4in 6in; margin: 0 so the output maps directly to thermal label stock with no rescaling.
// label-generator.ts
import Mustache from "mustache";
import { readFileSync } from "fs";
const template = readFileSync("./templates/shipping-label.html", "utf-8");
// templates/shipping-label.html
const LABEL_HTML = `<!DOCTYPE html>
<html>
<head>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Arial', sans-serif; color: #000; background: #fff;
width: 4in; height: 6in; padding: 0.2in; }
.label-wrap { width: 100%; height: 100%; display: flex; flex-direction: column;
border: 2px solid #000; padding: 0.15in; gap: 0.1in; }
.header { display: flex; justify-content: space-between; align-items: center;
border-bottom: 1px solid #ccc; padding-bottom: 0.1in; }
.carrier-logo { font-size: 18px; font-weight: 900; letter-spacing: -0.02em; }
.service-badge { font-size: 11px; font-weight: 700; text-transform: uppercase;
border: 1.5px solid #000; padding: 3px 8px; letter-spacing: 0.08em; }
.addresses { display: flex; gap: 0.15in; }
.addr-block { flex: 1; }
.addr-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em;
color: #666; margin-bottom: 4px; }
.addr-name { font-size: 12px; font-weight: 700; }
.addr-line { font-size: 11px; line-height: 1.4; color: #222; }
.tracking-section { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc;
padding: 0.1in 0; text-align: center; }
.tracking-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.1em;
color: #666; margin-bottom: 4px; }
.tracking-number { font-size: 13px; font-weight: 700; letter-spacing: 0.05em; }
.barcode { margin-top: 6px; }
.barcode img { width: 100%; height: 48px; object-fit: fill; }
.meta { display: flex; justify-content: space-between; font-size: 10px; color: #555; }
.meta span { display: flex; flex-direction: column; }
.meta strong { font-size: 11px; color: #000; }
@page { size: 4in 6in; margin: 0; }
</style>
</head>
<body>
<div class="label-wrap">
<div class="header">
<div class="carrier-logo">{{carrier}}</div>
<div class="service-badge">{{serviceLevel}}</div>
</div>
<div class="addresses">
<div class="addr-block">
<div class="addr-label">From</div>
<div class="addr-name">{{sender.name}}</div>
<div class="addr-line">{{sender.company}}</div>
<div class="addr-line">{{sender.address1}}</div>
{{#sender.address2}}<div class="addr-line">{{sender.address2}}</div>{{/sender.address2}}
<div class="addr-line">{{sender.city}}, {{sender.state}} {{sender.zip}}</div>
</div>
<div class="addr-block">
<div class="addr-label">To</div>
<div class="addr-name">{{recipient.name}}</div>
<div class="addr-line">{{recipient.address1}}</div>
{{#recipient.address2}}<div class="addr-line">{{recipient.address2}}</div>{{/recipient.address2}}
<div class="addr-line">{{recipient.city}}, {{recipient.state}} {{recipient.zip}}</div>
<div class="addr-line">{{recipient.country}}</div>
</div>
</div>
<div class="tracking-section">
<div class="tracking-label">Tracking Number</div>
<div class="tracking-number">{{trackingNumber}}</div>
<div class="barcode">
<!-- Barcode as base64 PNG from your barcode library, or a barcode service URL -->
<img src="{{barcodeImage}}" alt="{{trackingNumber}}" />
</div>
</div>
<div class="meta">
<span><strong>{{weight}} lbs</strong>Weight</span>
<span><strong>{{dimensions}}</strong>Dimensions</span>
<span><strong>{{orderId}}</strong>Order</span>
</div>
</div>
</body>
</html>`;
type ShipmentData = {
orderId: string;
trackingNumber: string;
barcodeImage: string; // data:image/png;base64,... or external URL
carrier: string;
serviceLevel: "Ground" | "Express" | "Priority" | "First Class";
weight: string;
dimensions: string;
sender: {
name: string;
company?: string;
address1: string;
address2?: string;
city: string;
state: string;
zip: string;
};
recipient: {
name: string;
address1: string;
address2?: string;
city: string;
state: string;
zip: string;
country: string;
};
};
export async function generateShippingLabel(shipment: ShipmentData): Promise<string> {
const html = Mustache.render(LABEL_HTML, shipment);
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
filename: `label-${shipment.orderId}.pdf`,
store: true,
options: {
width: "4in",
height: "6in",
margin: { top: "0", right: "0", bottom: "0", left: "0" },
printBackground: true,
},
}),
});
const { document_url } = await res.json();
return document_url;
}Order fulfillment webhook (label and packing slip)
When your warehouse management system fires an order.fulfilled event, generate both the shipping label and the packing slip in a single batch call. The batch endpoint accepts up to 50 documents per request and returns results in input order, so you get both URLs in one round trip and write them back to the WMS record together.
# wms_hooks/order_fulfilled.py
# Triggered by an order.fulfilled event from your WMS
import os
import httpx
PDFPIPE_KEY = os.environ["PDFPIPE_KEY"]
PDFPIPE_URL = "https://api.pdfpipe.xyz/v1/pdf"
PDFPIPE_BATCH_URL = "https://api.pdfpipe.xyz/v1/pdf/batch"
PACKING_SLIP_TEMPLATE = """
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Arial', sans-serif; color: #1a1a1a; padding: 40px 48px; }}
.header {{ display: flex; justify-content: space-between; margin-bottom: 32px; }}
.brand {{ font-size: 20px; font-weight: 700; }}
.order-meta {{ font-size: 12px; color: #666; text-align: right; }}
h2 {{ font-size: 13px; text-transform: uppercase; letter-spacing: 0.07em;
color: #999; margin: 24px 0 12px; }}
table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
th {{ text-align: left; padding: 8px 0; border-bottom: 2px solid #1a1a1a;
font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: #666; }}
td {{ padding: 10px 0; border-bottom: 1px solid #eee; }}
.qty {{ width: 50px; }}
.sku {{ color: #888; font-size: 12px; }}
.total-row td {{ border-bottom: none; font-weight: 600; padding-top: 16px; }}
@page {{ margin: 0; size: A4; }}
</style>
</head>
<body>
<div class="header">
<div class="brand">{company_name}</div>
<div class="order-meta">
<div><strong>Order #</strong> {order_id}</div>
<div><strong>Date</strong> {order_date}</div>
<div><strong>Ship to:</strong> {recipient_name}</div>
</div>
</div>
<h2>Items in this shipment</h2>
<table>
<thead>
<tr><th class="qty">Qty</th><th>Item</th><th>SKU</th><th>Price</th></tr>
</thead>
<tbody>
{items_rows}
</tbody>
<tfoot>
<tr class="total-row">
<td colspan="3">Order Total</td>
<td>{order_total}</td>
</tr>
</tfoot>
</table>
</body>
</html>
"""
def build_items_rows(items: list[dict]) -> str:
rows = []
for item in items:
rows.append(
f'<tr>'
f'<td class="qty">{item["quantity"]}</td>'
f'<td>{item["name"]}</td>'
f'<td class="sku">{item["sku"]}</td>'
f'<td>${item["price"]:.2f}</td>'
f'</tr>'
)
return "".join(rows)
def handle_order_fulfilled(event: dict) -> dict:
"""
Handles an order.fulfilled webhook from a WMS.
Generates a shipping label and a packing slip in a single batch call.
Returns both document URLs to write back to the WMS record.
"""
order = event["order"]
shipment = event["shipment"]
label_html = build_label_html(shipment) # your label template renderer
packing_slip_html = PACKING_SLIP_TEMPLATE.format(
company_name="Acme Fulfillment",
order_id=order["id"],
order_date=order["created_at"][:10],
recipient_name=shipment["recipient"]["name"],
items_rows=build_items_rows(order["items"]),
order_total=f"${order['total']:.2f}",
)
# One HTTP call for both documents
batch_res = httpx.post(
PDFPIPE_BATCH_URL,
headers={
"Authorization": f"Bearer {PDFPIPE_KEY}",
"Content-Type": "application/json",
},
json={
"documents": [
{
"html": label_html,
"filename": f"label-{order['id']}.pdf",
"store": True,
"options": {
"width": "4in",
"height": "6in",
"margin": {"top": "0", "right": "0", "bottom": "0", "left": "0"},
"printBackground": True,
},
},
{
"html": packing_slip_html,
"filename": f"packing-slip-{order['id']}.pdf",
"store": True,
"options": {"format": "A4"},
},
]
},
timeout=30,
)
batch_res.raise_for_status()
results = batch_res.json()["results"]
label_url = results[0]["document_url"]
packing_slip_url = results[1]["document_url"]
# Write both URLs back to the WMS order record
wms.orders.update(order["id"], {
"label_pdf_url": label_url,
"packing_slip_pdf_url": packing_slip_url,
})
return {"label_url": label_url, "packing_slip_url": packing_slip_url}Customs declaration (international shipments)
For international shipments, generate a customs declaration form alongside the label. The form includes HS codes, declared value, country of origin per line item, and shipment purpose. Call it conditionally when shipment.international === true.
// customs.ts
// Called when shipment.international === true
type LineItem = {
description: string;
hsCode: string;
quantity: number;
unitValue: number;
countryOfOrigin: string;
};
type CustomsPayload = {
orderId: string;
shipmentId: string;
sender: { name: string; address: string; country: string };
recipient: { name: string; address: string; country: string };
items: LineItem[];
declaredValue: number;
currency: string;
shipmentPurpose: "gift" | "commercial" | "sample" | "returned_goods";
};
export async function generateCustomsDeclaration(payload: CustomsPayload): Promise<string> {
const itemRows = payload.items
.map(
(item) => `
<tr>
<td>${item.description}</td>
<td>${item.hsCode}</td>
<td>${item.quantity}</td>
<td>${item.countryOfOrigin}</td>
<td>${payload.currency} ${(item.unitValue * item.quantity).toFixed(2)}</td>
</tr>`
)
.join("");
const html = `<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; font-size: 12px; padding: 32px 40px; color: #000; }
h1 { font-size: 15px; font-weight: 700; margin-bottom: 20px; text-transform: uppercase;
letter-spacing: 0.06em; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 24px; }
.section-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
color: #777; margin-bottom: 6px; }
.section-value { font-size: 12px; line-height: 1.5; }
table { width: 100%; border-collapse: collapse; margin-bottom: 20px; }
th { font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; color: #666;
text-align: left; padding: 6px 0; border-bottom: 1.5px solid #000; }
td { padding: 8px 0; border-bottom: 1px solid #ddd; font-size: 12px; }
.declared { font-weight: 700; text-align: right; margin-bottom: 20px; }
.purpose { margin-bottom: 20px; }
.purpose strong { text-transform: capitalize; }
.sig-block { margin-top: 40px; display: flex; gap: 60px; }
.sig-col .sig-line { border-bottom: 1px solid #000; height: 30px; margin-bottom: 4px; }
.sig-col .sig-label { font-size: 10px; color: #888; }
@page { margin: 0; size: A4; }
</style>
</head>
<body>
<h1>Customs Declaration</h1>
<div class="grid">
<div>
<div class="section-label">Shipper</div>
<div class="section-value">${payload.sender.name}<br>${payload.sender.address}<br>${payload.sender.country}</div>
</div>
<div>
<div class="section-label">Consignee</div>
<div class="section-value">${payload.recipient.name}<br>${payload.recipient.address}<br>${payload.recipient.country}</div>
</div>
</div>
<table>
<thead>
<tr>
<th>Description</th><th>HS Code</th><th>Qty</th>
<th>Country of Origin</th><th>Value</th>
</tr>
</thead>
<tbody>${itemRows}</tbody>
</table>
<div class="declared">
Total Declared Value: ${payload.currency} ${payload.declaredValue.toFixed(2)}
</div>
<div class="purpose">
Shipment Purpose: <strong>${payload.shipmentPurpose.replace("_", " ")}</strong>
</div>
<div class="sig-block">
<div class="sig-col">
<div class="sig-line"></div>
<div class="sig-label">Shipper Signature</div>
</div>
<div class="sig-col">
<div class="sig-line"></div>
<div class="sig-label">Date</div>
</div>
</div>
</body>
</html>`;
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
html,
filename: `customs-${payload.shipmentId}.pdf`,
store: true,
options: { format: "A4" },
}),
});
const { document_url } = await res.json();
return document_url;
}
// In your shipment creation pipeline:
async function processShipment(shipment: Shipment) {
const labelUrl = await generateShippingLabel(shipment);
if (shipment.international) {
const customsUrl = await generateCustomsDeclaration({
orderId: shipment.orderId,
shipmentId: shipment.id,
...buildCustomsPayload(shipment),
});
await wms.shipments.update(shipment.id, { customs_pdf_url: customsUrl });
}
await wms.shipments.update(shipment.id, { label_pdf_url: labelUrl });
}Print queue integration
The document_url returned by the API can be passed directly to any print service or queued for batch printing. Common integration patterns include passing the URL to Printful or Printavo as the label source, driving direct IPP printing via a local print server pointed at a Zebra or similar thermal printer, or adding the URL to an in-process queue for overnight batch print runs. The URL remains stable for the full retention window of your plan, so offline queuing is safe.
// print-queue.ts
// After generating a label, pass document_url directly to your print service.
import { generateShippingLabel } from "./label-generator";
async function fulfillAndPrint(shipment: ShipmentData): Promise<void> {
const documentUrl = await generateShippingLabel(shipment);
// Option 1: Add to an in-process print queue for batch printing
await printQueue.add(documentUrl, {
copies: 1,
mediaSize: "4x6", // thermal label media
priority: shipment.serviceLevel === "Express" ? "high" : "normal",
});
// Option 2: Printful or Printavo — pass the URL as the label source
// await printfulApi.shipments.setLabel(shipment.printfulId, { label_url: documentUrl });
// Option 3: Direct IPP printing via a local print server
// const ipp = new IPPClient("ipp://192.168.1.50/printers/zebra-zp450");
// await ipp.printUri(documentUrl, { "media": "oe_4x6-label_4x6in" });
// The stored URL stays valid for the retention window of your plan, so you
// can also queue it offline and print during the next batch window.
}Plans
| Plan | Documents/mo | Storage | Price |
|---|---|---|---|
| Hobby | 500 | 1 day | Free |
| Starter | 3,000 | 30 days | $19/mo |
| Growth | 15,000 | 365 days | $49/mo |
| Scale | 50,000 | 2 years | $149/mo |
| Business | 100,000 | 2 years | $499/mo |
Need more than 100,000 documents per month? Contact us for enterprise pricing.