Buy the label
Your existing carrier integration returns label data. Ask for ZPL when the destination is a thermal printer.
carrier.buyLabel(shipment)Shipping & fulfillment
Your system already knows the moment a label is bought. The only missing step is getting it onto the thermal printer at the right pack bench — without a browser, a print dialog, or a person.
The gap
Carrier APIs are excellent. The last six feet — from an API response to a specific printer in a specific building — is where fulfillment teams end up with browser extensions, shared folders, and a person who knows the trick.
Someone has to have the tab open, pick the right printer, and click. At volume that is a full-time job nobody applied for.
And it should not be. Your backend cannot reach port 9100 in a warehouse, and no IT team will make it possible.
A rasterized 4×6 label scans worse than the ZPL the carrier already gave you. Carriers return ZPL for a reason.
Without a job record, "did that label come out?" is answered by walking to the printer.
The flow
One request added to the code path you already have. No queue to run, no polling loop, no local software to write.
Your existing carrier integration returns label data. Ask for ZPL when the destination is a thermal printer.
carrier.buyLabel(shipment)Base64 the ZPL, name the printer for that bench, and add an idempotency key derived from your shipment ID.
POST /v1/print-jobsStore it against the shipment. When someone asks whether a label printed, the answer is a lookup rather than a walk to the warehouse.
shipment.printJobId = job.idThe integration
This is the whole thing. There is no daemon to run and no local dependency to ship.
import RocketPrint from "@rocketprint/node";
const rocketprint = new RocketPrint({ apiKey: process.env.ROCKETPRINT_KEY });
export async function onLabelPurchased(shipment) {
// The carrier already gave us ZPL — send it through untouched.
const job = await rocketprint.printJobs.create({
printerId: shipment.packBench.printerId, // stored per bench
title: `Shipment ${shipment.id}`,
contentType: "raw_base64",
content: Buffer.from(shipment.labelZpl).toString("base64"),
source: "fulfillment-worker",
}, {
// Derived from your own ID: a retry can never print a second label.
idempotencyKey: `shipment-${shipment.id}-label`,
});
await shipments.update(shipment.id, { printJobId: job.id });
}Store a printer ID against each bench, station, or location in your own data model. Choosing where something prints becomes an ordinary business decision in your code.
Derive the idempotency key from your shipment ID. Your worker can retry as aggressively as it likes; the second request returns the first job.
What changes operationally
Labels appear at the bench as orders are processed. The packer's job is to pick the label up, not to operate a browser.
Jobs queue while a bench machine reboots and print when it returns, in order. The shift does not stop because of a Windows update.
Every label has a job ID, a status, and timestamps. "Did label 1042 print?" is a query with a real answer, including the failure reason if it did not.
Carrier ZPL reaches the printer unmodified, so the barcode is generated by the printer at its native resolution instead of being rasterized on the way. See the formats →
Questions
Yes, but ask your carrier for ZPL if the destination is a thermal printer. Most carrier APIs offer it. A PDF prints through the station's driver, which decides scaling — fine for a packing slip, less predictable for a 4×6 label.
Each printer has a stable ID and belongs to a station. Store the printer ID against your bench, pick list, or location record, and pass it on the job. Routing is a field in your request, not a network problem.
The job queues and prints when the station wakes. Your order webhook does not fail, and nothing needs a retry queue on your side.
Send an Idempotency-Key derived from your own order or shipment ID. A retried request returns the original job instead of printing a second label — which in fulfillment is a mis-ship waiting to happen.
Send two jobs — the label as raw ZPL to the thermal printer, the packing slip as a PDF to the laser printer. Same API, two printer IDs.
Start with one bench
If it works for one bench it works for fifty — the only thing that changes is which printer ID you send.