How to Print Order Receipts from a Kiosk Machine Using Node.js and ESC/POS Nov 25, 2025 | 17 minutes read 9 Likes Why Self-Service Kiosks Need Reliable Receipt PrintingSelf-service kiosks are becoming the standard in restaurants, cafes, retail, and automated checkout systems. A key part of that experience is the printing of receipts from order summaries to cardholder copies. In this guide, we break down how to implement thermal printer receipt printing using Node.js, ESC/POS commands, and network printing. Why Kiosks Need Reliable Receipt Printing Whether you’re operating a restaurant kiosk or a self-checkout terminal, receipts serve several important roles: Customer validation (order number, items, payment confirmation) Kitchen and staff workflow Proof of payment (especially for card transactions) Branding and professionalism Network thermal printers are the preferred solution because they are: Fast Low maintenance Silent Support ESC/POS formatting How Kiosk Receipt Printing Works Your Node.js server sends a formatted ESC/POS text buffer to the printer using a TCP connection on port 9100.Print flow:Build receipt textApply ESC/POS formatting commandsEncode using the correct code pageSend buffer to the printer via the socket 1. Receipt Builder Code (buildCombinedReceipt) Below is your complete, production-grade receipt builder. Centered headings Column formatting Qty/Item/Price layout Customizations Euro symbol Automatic cutting Store header & footer Payment details (cardholder copy) const iconv = require('iconv-lite'); const net = require("net"); const RECEIPT_WIDTH = process.env.RECEIPT_WIDTH ? parseInt(process.env.RECEIPT_WIDTH) : 48; const EURO = '€'; function getTextLength(text) { return text .replace(/[^\x00-\x7F]/g, " ") .replace(/€/g, " ") .length; } function formatPrice(amount, width) { const safeAmount = typeof amount === "string" ? parseFloat(amount) : amount; if (isNaN(safeAmount)) throw new Error("Invalid amount for formatting"); const withEuro = `${EURO}${safeAmount.toFixed(2)}`; return withEuro.padStart(width); } function centerText(text, width = RECEIPT_WIDTH) { const textLength = getTextLength(text); if (textLength >= width) return text; const space = Math.floor((width - textLength) / 2); return " ".repeat(space) + text; } function padText(left, right, width = RECEIPT_WIDTH) { const leftLen = getTextLength(left); const rightLen = getTextLength(right); const spacing = width - leftLen - rightLen; return left + " ".repeat(Math.max(spacing, 0)) + right; } function getColumnWidths(width = RECEIPT_WIDTH) { const qtyWidth = 4; const priceWidth = 10; const itemWidth = Math.max(width - qtyWidth - priceWidth, 10); return { qtyWidth, itemWidth, priceWidth }; } function buildHeader(width = RECEIPT_WIDTH) { const { qtyWidth, itemWidth, priceWidth } = getColumnWidths(width); const qtyStr = "QTY".padEnd(qtyWidth); const itemStr = "ITEM".padEnd(itemWidth); const priceStr = "PRICE".padEnd(priceWidth); return `${qtyStr}${itemStr}${priceStr}`; } function padColumns(qty, item, price, width = RECEIPT_WIDTH) { const { qtyWidth, itemWidth, priceWidth } = getColumnWidths(width); const qtyStr = qty.toString().padEnd(qtyWidth); let itemStr = item; if (getTextLength(itemStr) > itemWidth) { while (getTextLength(itemStr + "…") > itemWidth) { itemStr = itemStr.slice(0, -1); } itemStr += "…"; } itemStr = itemStr.padEnd(itemWidth); const priceStr = formatPrice(price, priceWidth); return `${qtyStr}${itemStr}${priceStr}`; } function safeToFixed(value, digits = 2) { if (value === null || value === undefined) return "0.00"; const num = typeof value === "string" ? parseFloat(value) : value; if (isNaN(num)) return "0.00"; return num.toFixed(digits); } function buildCombinedReceipt(order) { const { payment = {}, store = {} } = order; const lines = []; const ESC = '\x1B'; const GS = '\x1D'; const safeTotal = safeToFixed(order.total); const safeAmount = safeToFixed(payment.amount); lines.push("=".repeat(RECEIPT_WIDTH)); lines.push(`${ESC}a\x01`); lines.push(`${GS}!` + '\x11'); lines.push("Your order number is"); lines.push(`${order.orderNo}`); lines.push(`${GS}!` + '\x00'); lines.push(`${ESC}a\x00`); lines.push(centerText(order.eatingLocation || "N/A")); lines.push("=".repeat(RECEIPT_WIDTH)); const rawHeaderLines = [ store.name || "", store.contact_person || "", store.address || "", `Tel: ${store.phone || ""}` ]; rawHeaderLines.forEach(rawLine => { const txt = rawLine.trim(); if (!txt) return; const splitLines = txt.split(/\r?\n/); splitLines.forEach(line => { const cleanLine = line.trim(); if (!cleanLine) return; const len = cleanLine.replace(/€/g, " ").length; const leftPad = Math.floor((RECEIPT_WIDTH - len) / 2); lines.push(" ".repeat(leftPad) + cleanLine); }); }); lines.push("-".repeat(RECEIPT_WIDTH)); lines.push(centerText(order.date || "")); lines.push("-".repeat(RECEIPT_WIDTH)); lines.push(buildHeader()); lines.push("-".repeat(RECEIPT_WIDTH)); (order.items || []).forEach((item, index) => { const qtyStr = (item.qty || 0).toString(); const name = item.name || `Product ${item.Product_id || ""}`; const priceStr = safeToFixed(item.Retail_Price); lines.push(padColumns(qtyStr, name, priceStr)); if (item.customizations?.length) { const display = item.customizations.filter(c => { const preSel = c.pivot?.input_type === "checkbox" && c.pivot?.pre_selected == 1; return preSel? c.removed === true: true; }); const grouped = {}; display.forEach(c => { const label = c.pivot?.product_groups_name || c.label || c.pivot?.label || "Other"; grouped[label] = grouped[label] || []; grouped[label].push(c); }); const { qtyWidth } = getColumnWidths(); const indentGroup = " ".repeat(qtyWidth); const indentItem = " ".repeat(qtyWidth + 3); Object.entries(grouped).forEach(([label, items]) => { lines.push(indentGroup + `${label}:`); items.forEach(c => { const qty = c.qty || c.quantity || 1; const qtyText = qty > 1 ? `${qty} x ` : ""; const price = parseFloat(c.price ?? c.pivot?.price ?? 0); const total = safeToFixed(price * qty); const name = c.name || c.ItemName || ""; if (label === "Remove") { lines.push(indentItem + `${qtyText}No ${name}`); return; } if (price === 0) { const free = c.free === true ? " (FREE)": ""; lines.push(indentItem + `${qtyText}${name}${free}`); return; } lines.push(indentItem + `${qtyText}${name}: ${EURO}${total}`); }); }); } if (index < (order.items?.length || 0) - 1) { lines.push(""); } }); lines.push("-".repeat(RECEIPT_WIDTH)); lines.push(padText("TOTAL (incl. VAT):", `${EURO}${safeTotal}`)); lines.push("-".repeat(RECEIPT_WIDTH)); lines.push(`${ESC}a\x01`); lines.push(`${GS}!` + '\x11'); lines.push("CARDHOLDER COPY"); lines.push(`${GS}!` + '\x00'); lines.push(`${ESC}a\x00`); lines.push("-".repeat(RECEIPT_WIDTH)); lines.push(`Transaction ID : ${payment.transactionId || ""}`); lines.push(`Auth No : ${payment.authCode || ""}`); lines.push(`Card Type : ${payment.cardType || ""}`); lines.push(`Card Number : ****${payment.cardLast4 || ""}`); lines.push(`Amount Paid : ${EURO}${safeAmount}`); lines.push("-".repeat(RECEIPT_WIDTH)); if (store.footer_text) { store.footer_text.split(/\r?\n/).forEach(l => { if (l.trim()) lines.push(centerText(l.trim())); }); } lines.push("\n\n\n"); const receiptText = lines.join("\r\n"); const cutCommand = Buffer.from([0x1D, 0x56, 0x00]); const initCodePage = Buffer.from([0x1B, 0x74, 0x13]); const encoded = iconv.encode(receiptText, "cp858"); return Buffer.concat([initCodePage, encoded, cutCommand]); } module.exports = { buildCombinedReceipt }; 2. Network Printing Code (sendToNetworkPrinter) function sendToNetworkPrinter(buffer, printerInfo) { return new Promise((resolve, reject) => { const client = new net.Socket(); const ip = printerInfo.ip || printerInfo; const port = printerInfo.port || 9100; client.connect(port, ip, () => { console.log(`âś… Connected to printer at ${ip}:${port}`); client.write(buffer); client.end(); resolve(); }); client.on("error", (err) => { console.error("❌ Printer connection error:", err.message); reject(err); }); client.on("close", () => { console.log("🔌 Connection to printer closed."); }); }); } module.exports = { sendToNetworkPrinter }; 3. Print API Route const express = require('express'); const router = express.Router(); const { buildCombinedReceipt, sendToNetworkPrinter } = require('../utils/print-receipt-utils'); router.post('/combined-receipt', async (req, res) => { try { const { orderNo, date, items, total, tax, eatingLocation, payment, printer, store } = req.body; console.log("items", items); if (!items || !payment || !store) return res.status(400).json({ error: "Missing order, payment info or store info" }); const order = { orderNo, date, items, total, tax, eatingLocation, payment, store }; const buffer = buildCombinedReceipt(order); await sendToNetworkPrinter(buffer, printer); res.json({ success: true, orderDetails: order, message: "Combined receipt printed" }); } catch (err) { res.status(500).json({ error: "Failed to print receipt", details: err.message }); } }); module.exports = router; Print Professional Receipts Directly from Your KioskGet Implementation SupportThe Way ForwardWith this Node.js-based receipt printing system, your kiosk can generate a complete, professional, and fully formatted receipt for every order. The solution supports printing all essential order details, including items, quantities, prices, and applied modifications, ensuring customers receive an accurate breakdown of their purchase. It also prints payment information, cardholder copies, store address details, and optional footer text when provided through the API. The receipt layout includes the order number, eating location (such as dine-in or takeaway), and clear column alignment for easy readability. Unlike traditional POS systems, this setup prints directly through your own server and thermal printer without relying on a JCC terminal for receipt output. To enhance reliability, you can also track printer status and errors in Node.js to avoid failed print attempts and ensure smooth kiosk operations.Overall, this system delivers a reliable, flexible, and kiosk-friendly printing experience that enhances customer satisfaction and ensures staff receive clear order documentation. Free Consultation Hire Dedicated Node js DeveloperHire Node.js DeveloperNode js Service ProvidersNode.js Development ServicesMayur DosiNov 25 2025I am Assistant Project Manager at iFlair, specializing in PHP, Laravel, CodeIgniter, Symphony, JavaScript, JS frameworks ,Python, and DevOps. With extensive experience in web development and cloud infrastructure, I play a key role in managing and delivering high-quality software solutions. I am Passionate about technology, automation, and scalable architectures, I am ensures seamless project execution, bridging the gap between development and operations. I am adept at leading teams, optimizing workflows, and integrating cutting-edge solutions to enhance performance and efficiency. Project planning and good strategy to manage projects tasks and deliver to clients on time. Easy to adopt new technologies learn and work on it as per the new requirments and trends. When not immersed in code and project planning, I am enjoy exploring the latest advancements in AI, cloud computing, and open-source technologies.You may also like Database Simplified: TypeORM in Node.js Read More Nov 19 2025 How to Detect Printer Status (Online, Offline, Paper Out, or Errors) in Node.js with Kiosk Read More Nov 18 2025 Network Discovery for JCC Terminals in Node.js – Best Practices & Code Samples Read More Nov 18 2025 Next JS Development Services Driving Innovation in Modern Web Architecture Read More Sep 11 2025 Scaling SaaS Platforms Using SSR Strategies with Hire Next.js Developers Read More Aug 12 2025 Implementing Microservices Architecture with Node.js and MongoDB Read More Jun 13 2025