DEV Community

Custodia-Admin
Custodia-Admin

Posted on • Originally published at pagebolt.dev

How to generate a PDF shipping label and packing slip automatically

How to Generate a PDF Shipping Label and Packing Slip Automatically

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>`;
}
Enter fullscreen mode Exit fullscreen mode

Shipping label template (4×6 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>`;
}
Enter fullscreen mode Exit fullscreen mode

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()),
  };
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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`,
  });
});
Enter fullscreen mode Exit fullscreen mode

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. → Get started in 2 minutes

Top comments (0)