A print call is one HTTP request, which makes order printing look like the easiest integration on the roadmap. It stays easy until the evening a customer receives two tickets for one order, or the evening forty orders never reach the kitchen and nobody notices until closing.
The short answer: three decisions prevent nearly every production incident. Answer the webhook before you print, give every job a stable key so a repeat is detectable, and decide explicitly what happens when the printer is offline. None of them is difficult, and all three are easier to add now than after the first incident.
The three failure modes
| Symptom | Actual cause | Fix |
|---|---|---|
| The same ticket prints twice | The platform retried a slow webhook, or a status was re-applied | A stable key per order |
| Orders never printed and nobody knew | The printer was offline and the failure was swallowed | An explicit offline policy plus an alert |
| Checkout feels slow, or times out | The print call runs inside the request that confirms the order | Respond first, print asynchronously |
The three are related. A slow print call causes the retry that causes the duplicate, so fixing the first one removes most of the second.
1. Answer the webhook before you print
Shopify, WooCommerce, Stripe and every serious platform retry when your endpoint is slow or returns an error. If your handler prints synchronously and the print API takes two seconds, you have built a retry generator.
export default async function handler(req, res) {
const order = req.body;
// Acknowledge immediately: the platform stops retrying.
res.status(200).end();
// Then print, outside the request the platform is waiting on.
await printOrder(order).catch((err) => {
logger.error({ err, orderId: order.id }, 'print failed');
alertOps(order);
});
}
The rule is that the platform's timeout budget and your printer's latency must never share a deadline. This is the same reason the Shopify and WooCommerce connectors do not print inside the webhook transaction.
2. Key every job
Idempotency here is not a distributed-systems luxury. An order can legitimately trigger your handler more than once: a gateway callback arriving twice, a merchant re-applying a paid status by hand, a webhook retry that raced your acknowledgement.
Put a stable value in origin, and store a printed flag on your side:
async function printOrder(order) {
const key = `order-${order.id}`;
if (await alreadyPrinted(key)) return;
await fetch(`https://www.expedy.fr/api/v2/printers/${PRINTER_UID}/print`, {
method: 'POST',
headers: {
Authorization: `${process.env.API_SID}:${process.env.API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ printer_msg: buildTicket(order), origin: key }),
});
await markPrinted(key);
}
Two things earn their keep here. The local flag stops the duplicate before it reaches the network, and origin makes a duplicate that slipped through visible in the print history, which turns "I think it printed twice" into a question you can answer.
Write the flag in the same transaction as the rest of your order handling if you can. A flag written after a successful HTTP call is still a small window, and that window is exactly where the double tickets come from.
3. Decide the offline policy on purpose
Ask what should happen when the printer is out of paper, unplugged or behind a router that rebooted. There are only three sensible answers, and the wrong one is not choosing:
- Queue and deliver later. Correct for shipping labels and warehouse documents, where a ticket printed twenty minutes late is still useful.
- Retry briefly, then alert. Correct for kitchen tickets, where a ticket printed twenty minutes late is worse than useless because service has moved on.
- Alert immediately. Correct when a human can act, and the only option that turns a silent loss into a known problem.
Whichever you pick, implement the alert. The difference between a minor annoyance and a bad evening is not whether the printer failed, it is whether anyone knew.
What you should not build
Teams routinely rebuild machinery the API already provides. Before writing a queue, a scheduler and a retry ladder, check what delivery guarantees you already have: a push API that holds the job and delivers it when the device reconnects removes the need for most of that code. Build the parts that encode your business rules, such as which printer gets which order, and leave transport reliability to the transport.
Testing it before production
Three tests catch most of what reaches production:
- Replay the same webhook twice. Exactly one ticket should come out.
- Unplug the printer, then send an order. Your chosen policy should visibly happen, including the alert.
- Order a product with an emoji in its name. Thermal printers speak ESC/POS; transliterate before sending, or you will discover this at the worst moment. The receipt layout reference documents what the format accepts.
Run all three against a real printer, not a mock. The interesting failures are physical.
Create a free account and wire the three tests into your integration branch before the first customer order goes through it.
Related reading: printing orders from Shopify and from WooCommerce show where these hooks live per platform.
FAQ
Why does the same order sometimes print twice?
Almost always because the platform retried a webhook your endpoint answered slowly, or because a paid status was applied more than once. Acknowledge the webhook before printing and key each job with a stable value such as the order id.
Should I print inside the webhook handler?
No. Respond 200 immediately, then print outside that request. Sharing a deadline between the platform timeout and the printer latency is what generates retries, and retries are what generate duplicates.
What should happen when the printer is offline?
Pick one policy deliberately: queue and deliver later, retry briefly then alert, or alert immediately. Queueing suits shipping labels, alerting suits kitchen tickets. The one wrong answer is leaving it undefined.
Do I need to build my own print queue?
Usually not. A push API that holds the job and delivers it when the device reconnects already provides that. Write the code that encodes your business rules, such as printer routing, not a second transport layer.
How do I test printing reliability before going live?
Replay one webhook twice and expect a single ticket, unplug the printer and check that your offline policy and alert both fire, and order a product with an emoji in its name. Run all three against a real printer.