How to Generate a PDF Shipping Label and Packing Slip Automatically
Generate print-ready PDF shipping labels and packing slips when an order is placed — from your own template, with your branding, triggered by a webhook. No label printer SDK, no carrier API complexity.
Shipping labels and packing slips are the last paper in most e-commerce operations. Platforms like Shopify and WooCommerce generate them, but they're branded with the platform's chrome, locked to their layout, and require a manual download step.
Here's how to generate print-ready PDFs on order placement — your template, your brand, your workflow.
Packing slip template
function renderPackingSlipHtml({ order }) {
const lineRows = order.items
.map((item) => `
<tr>
<td>${item.sku}</td>
<td>${item.name}${item.variant ? ` — ${item.variant}` : ""}</td>
<td style="text-align:center">${item.qty}</td>
<td style="text-align:right">${item.weight ?? ""}</td>
</tr>`)
.join("");
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page { size: letter; margin: 0.75in; }
body { font-family: -apple-system, 'Segoe UI', sans-serif; color: #111; font-size: 12px; }
.header { display: flex; justify-content: space-between; margin-bottom: 20px; padding-bottom: 16px; border-bottom: 2px solid #111; }
.logo { font-size: 20px; font-weight: 800; }
.order-meta { text-align: right; }
.order-meta h1 { font-size: 18px; font-weight: 900; margin: 0 0 4px; }
.addresses { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-bottom: 20px; }
.addr-block .label { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #999; margin-bottom: 4px; }
.addr-block p { line-height: 1.7; margin: 0; }
table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
thead tr { border-bottom: 2px solid #111; }
th { text-align: left; padding: 6px 0; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; }
td { padding: 8px 0; border-bottom: 1px solid #eee; }
.totals { text-align: right; font-size: 12px; margin-top: 8px; }
.barcode { text-align: center; margin-top: 24px; font-family: monospace; font-size: 14px; letter-spacing: 4px; }
.barcode-label { font-size: 10px; color: #999; margin-top: 4px; }
.footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid #eee; font-size: 10px; color: #999; }
</style>
</head>
<body>
<div class="header">
<div>
<div class="logo">YourStore</div>
<div style="color:#666;font-size:11px">123 Warehouse Way, Oakland CA 94601</div>
</div>
<div class="order-meta">
<h1>PACKING SLIP</h1>
<div>Order #${order.number}</div>
<div style="color:#666">${new Date(order.createdAt).toLocaleDateString()}</div>
</div>
</div>
<div class="addresses">
<div class="addr-block">
<div class="label">Ship To</div>
<p>
<strong>${order.shippingAddress.name}</strong><br>
${order.shippingAddress.line1}<br>
${order.shippingAddress.line2 ? order.shippingAddress.line2 + "<br>" : ""}
${order.shippingAddress.city}, ${order.shippingAddress.state} ${order.shippingAddress.zip}<br>
${order.shippingAddress.country}
</p>
</div>
<div class="addr-block">
<div class="label">Order Details</div>
<p>
<strong>Method:</strong> ${order.shippingMethod}<br>
<strong>Items:</strong> ${order.items.reduce((s, i) => s + i.qty, 0)}<br>
${order.trackingNumber ? `<strong>Tracking:</strong> ${order.trackingNumber}` : ""}
</p>
</div>
</div>
<table>
<thead>
<tr>
<th>SKU</th><th>Item</th>
<th style="text-align:center">Qty</th>
<th style="text-align:right">Weight</th>
</tr>
</thead>
<tbody>${lineRows}</tbody>
</table>
<div class="totals">
Total items: <strong>${order.items.reduce((s, i) => s + i.qty, 0)}</strong>
</div>
${order.giftMessage ? `
<div style="margin-top:16px;padding:12px;background:#f9f9f9;border-radius:4px;font-size:11px">
<strong>Gift message:</strong> ${order.giftMessage}
</div>` : ""}
<div class="barcode">
||| ${order.number} |||
<div class="barcode-label">Order ${order.number}</div>
</div>
<div class="footer">
Questions about your order? Contact us at orders@yourstore.com · yourstore.com
</div>
</body>
</html>`;
}
Shipping label template (4x6 format)
function renderShippingLabelHtml({ order }) {
const { shippingAddress: addr, number, shippingMethod, trackingNumber } = order;
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
@page { size: 4in 6in; margin: 0.2in; }
body {
font-family: -apple-system, 'Segoe UI', sans-serif;
color: #111;
width: 3.6in;
height: 5.6in;
display: flex;
flex-direction: column;
}
.top-bar { display: flex; justify-content: space-between; align-items: center;
border-bottom: 2px solid #111; padding-bottom: 8px; margin-bottom: 12px; }
.carrier { font-size: 16px; font-weight: 900; letter-spacing: 1px; }
.service { font-size: 11px; font-weight: 700; background: #111; color: white; padding: 3px 8px; }
.section-label { font-size: 9px; font-weight: 700; text-transform: uppercase;
letter-spacing: 1px; color: #999; margin-bottom: 4px; }
.from-section { margin-bottom: 12px; }
.from-section p { font-size: 11px; line-height: 1.5; margin: 0; }
.to-section { flex: 1; border: 2px solid #111; padding: 10px; margin-bottom: 12px; }
.to-name { font-size: 20px; font-weight: 900; line-height: 1.2; margin-bottom: 6px; }
.to-address { font-size: 14px; line-height: 1.6; }
.barcode-section { text-align: center; border-top: 1px solid #eee; padding-top: 10px; }
.tracking { font-family: monospace; font-size: 13px; letter-spacing: 3px; }
.tracking-label { font-size: 9px; color: #666; margin-top: 2px; }
.order-ref { font-size: 10px; color: #999; text-align: right; margin-top: 8px; }
</style>
</head>
<body>
<div class="top-bar">
<div class="carrier">YOUR STORE</div>
<div class="service">${shippingMethod.toUpperCase()}</div>
</div>
<div class="from-section">
<div class="section-label">From</div>
<p>YourStore Inc. · 123 Warehouse Way · Oakland CA 94601</p>
</div>
<div class="to-section">
<div class="section-label">Ship To</div>
<div class="to-name">${addr.name}</div>
<div class="to-address">
${addr.line1}<br>
${addr.line2 ? addr.line2 + "<br>" : ""}
${addr.city}, ${addr.state} ${addr.zip}<br>
${addr.country}
</div>
</div>
<div class="barcode-section">
<div class="tracking">||| ${trackingNumber ?? "PENDING"} |||</div>
<div class="tracking-label">${trackingNumber ?? "Tracking not yet assigned"}</div>
</div>
<div class="order-ref">Order #${number}</div>
</body>
</html>`;
}
Generate both on order placement
async function generateOrderDocuments(order) {
const [packingSlipRes, labelRes] = await Promise.all([
fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: { "x-api-key": process.env.PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ html: renderPackingSlipHtml({ order }) }),
}),
fetch("https://pagebolt.dev/api/v1/pdf", {
method: "POST",
headers: { "x-api-key": process.env.PAGEBOLT_API_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ html: renderShippingLabelHtml({ order }) }),
}),
]);
return {
packingSlip: Buffer.from(await packingSlipRes.arrayBuffer()),
shippingLabel: Buffer.from(await labelRes.arrayBuffer()),
};
}
Shopify webhook handler
app.post("/webhooks/shopify/orders/create", async (req, res) => {
if (!verifyShopifyWebhook(req.rawBody, req.headers["x-shopify-hmac-sha256"])) {
return res.status(401).send("Unauthorized");
}
res.json({ ok: true });
const shopifyOrder = req.body;
const order = mapShopifyOrder(shopifyOrder);
const { packingSlip, shippingLabel } = await generateOrderDocuments(order);
// Store both in S3
await Promise.all([
uploadToS3(packingSlip, `orders/${order.number}/packing-slip.pdf`),
uploadToS3(shippingLabel, `orders/${order.number}/shipping-label.pdf`),
]);
// Print automatically if you have a connected label printer
if (process.env.AUTO_PRINT === "true") {
await printDocument(shippingLabel, { printer: "ZebraLP2844", copies: 1 });
await printDocument(packingSlip, { printer: "OfficeJet", copies: 1 });
}
console.log(`Documents generated for order ${order.number}`);
});
Warehouse dashboard endpoint
// GET /orders/:id/documents — warehouse staff download link
app.get("/orders/:id/documents", requireWarehouseAuth, async (req, res) => {
const order = await db.orders.findById(req.params.id);
if (!order) return res.status(404).send("Not found");
// Regenerate on demand (or serve from S3 cache)
const { packingSlip, shippingLabel } = await generateOrderDocuments(order);
// Return as a combined PDF or zip
res.json({
packingSlip: `/orders/${order.number}/packing-slip.pdf`,
shippingLabel: `/orders/${order.number}/shipping-label.pdf`,
});
});
Both documents generate in parallel in under 2 seconds. No carrier SDK, no label printer library, no PDF generation library — just HTML and one API call each.
Try it free
100 requests/month, no credit card required. OG images, screenshots, PDFs, and video — one API.
Get API Key — Free