pos-hardware-bridge¶
Print sharp receipts from any POS (web, Node or Electron) on any ESC/POS or Star thermal printer, kitchen printer or A4 printer, through POS Hardware Bridge.
π Documentation: https://thisarahetz.github.io/pos-hardware-bridge/
import { BridgeClient, ReceiptBuilder } from 'pos-hardware-bridge';
const bridge = new BridgeClient({ token: '<access token from the bridge settings>' });
const bill = new ReceiptBuilder()
.center('MY SHOP', { bold: true, size: 'large' })
.center('12 Main St Β· 077 123 4567')
.banner('PAID')
.rule()
.row('Bill', '#1042')
.row('Date', '25 Oct 2026 10:41')
.rule()
.item({ name: 'Latte', qty: 2, unitPrice: '4.50', total: '9.00', notes: ['Oat milk'] })
.item({ name: 'Croissant', qty: 1, total: '3.50' })
.rule('double')
.total('TOTAL', 'LKR 1,250.00')
.qr('https://myshop.example/r/1042', 'Scan for your e-receipt')
.center('Thank you!', { bold: true });
await bridge.printReceipt(bill); // default receipt printer
await bridge.printReceipt(bill, { printer: 'kitchen' }); // a named printer
No dependencies, typed, works with import and require. React apps also get a
ready-made printer settings widget from pos-hardware-bridge/react.
Receipts, line by line¶
A receipt is a list of lines (blocks): text, row, rule, qr, image and
feed. The bridge prints each line with the printer's own commands and
fonts β nothing is turned into HTML, a PDF or a picture β and wraps text to
the paper: 32 characters on 58mm, 42 or 48 on 80mm. The same receipt prints
correctly on every paper width.
ReceiptBuilder method |
Prints |
|---|---|
text(text, options?) |
Text, wrapped at word boundaries. options: align, bold, underline, invert, size, indent |
center(text, options?) |
Centred text |
row(left, right, options?) |
Label left, value right; a long label wraps beside the value |
item({ name, total, qty?, unitPrice?, notes? }) |
2 x Latte ..... 9.00, then unit price and notes indented |
total(label, value, { emphasis? }) |
A bold, double-height total (emphasis: false for a plain row) |
banner(text) |
White-on-black label, e.g. PAID, REPRINT |
rule(style?) |
----- (single) or ===== (double) across the paper |
qr(data, label?, size?) |
A centred QR code (small/medium/large) |
image(base64, size?) |
A centred logo, PNG or JPEG as base64 (small/medium/large/full), printed black and white |
feed(lines?) |
Blank lines |
build() |
{ blocks } β send it with printReceipt(), or in a print job |
Sizes: normal, wide (double width), tall (double height), large (both).
Preview exactly what prints¶
layoutReceipt(blocks, columns) is the bridge's own layout code. It returns
the lines exactly as the printer prints them, so your on-screen preview always
matches the paper:
import { layoutReceipt } from 'pos-hardware-bridge';
const columns = (await bridge.printers()).find((p) => p.id === 'receipt')?.paperWidthChars ?? 42;
for (const line of layoutReceipt(bill.build().blocks, columns)) {
if (line.kind === 'line') console.log(line.text); // also: align, bold, size, invert
if (line.kind === 'qr') console.log(`[QR ${line.data}]`);
if (line.kind === 'image') console.log('[LOGO]'); // line.data is the base64 picture
}
Rules every POS should follow
- Format money yourself and pass strings (
'LKR 1,250.00'): the bridge prints what you send. - Don't assume a paper width or pad with spaces β use
row()andrule(). - Keep the right-hand values short (amounts, dates, references).
- Leave the cut to the bridge. Each printer's settings decide full cut, partial cut or tear-off feed.
- Pick printers by API name (
receipt,kitchen,invoice), not by device. The shop maps names to printers in the bridge.
ReceiptLine receipts (alternative)¶
The SDK can also write ReceiptLine
documents (an open "Markdown for receipts"), which add barcodes and logos.
Build them with receipt() and print with bridge.print():
import { receipt } from 'pos-hardware-bridge';
await bridge.print(
receipt().title('My Shop').row('Bill', '#1042').barcode('1042', { type: 'code128' }),
);
Building ReceiptLine receipts¶
| Method | Prints |
|---|---|
title(text, size?) |
Big bold centred heading (default large = 2Γ) |
center(text, style?) |
Centred text |
text(text, style?) |
A line of text; style: align, bold, underline, invert, size, indent |
banner(text) |
White-on-black label, e.g. TAX INVOICE, REPRINT |
row(label, value, style?) |
Label left, value right |
item({ name, total, qty?, unitPrice?, notes? }) |
An item line, with unit price and notes underneath |
total(label, value, { emphasis? }) |
A totals row; emphasis = bold, double height |
divider() |
A full-width rule |
qr(data, { cell?, level?, caption? }) |
A QR code |
barcode(data, { type?, width?, height?, hri? }) |
A barcode (code128 by default) |
image(pngBase64) |
A logo |
feed(lines?) |
Blank lines |
cut() |
Cut here |
raw(receiptLine) |
Your own ReceiptLine |
toString() |
The ReceiptLine document |
Sizes: normal, wide (double width), tall (double height), large (2Γ),
xlarge (3Γ). Special characters such as | { } - = ~ _ " ^ are escaped
for you, so Coca-Cola | 50% prints as written.
Talking to the bridge¶
const bridge = new BridgeClient({
url: 'http://127.0.0.1:9191', // default
token: 'β¦', // bridge settings β Connect your POS
});
| Method | Does |
|---|---|
printReceipt(receipt, { printer?, copies?, cut?, title? }) |
Print a line-by-line receipt (ReceiptBuilder or { blocks }) |
print(receipt, { printer?, copies?, cut?, title? }) |
Print a ReceiptLine receipt |
preview(receipt, { printer? }) |
{ svg, cpl }: a ReceiptLine receipt as it will print, for your screen |
printDocument({ html } \| { pdfBase64 }, { printer?, paperSize?, landscape?, copies? }) |
Print an A4/A5 invoice |
printPage(html, { printer?, format?, copies?, title? }) |
Print a ready-made HTML bill |
printTestPage(printer?) |
The bridge's test page |
printers() |
Printers with API names and live status |
status() |
Plain-language status to show the cashier |
health() |
Is the bridge running? Its id and name (no token needed) |
me() |
{ kind: 'local' } on the printer computer, or the paired device's name |
BridgeClient.pair({ url, code, name }) |
Pair a phone/tablet/till; returns { url, token } to save |
onStatus(listener) |
Live updates (printer unplugged, back online); returns stop() |
Errors are BridgeErrors whose message is written for the cashier ("The
receipt printer is not connected. Check the cableβ¦"). status holds the HTTP
code, details lists bad fields, and unreachable is true when the bridge
isn't running.
try {
await bridge.printReceipt(bill);
} catch (err) {
showToast(err.message); // already cashier-friendly
if (err.unreachable) offerBrowserPrint();
}
A printer menu, with no printer settings in your POS¶
The bridge owns the printer setup. Your POS just lists what's there:
const printers = await bridge.printers();
// [{ id: 'receipt', name: 'Front counter', kind: 'receipt', isDefault: true, ready: true, status: {β¦} }, β¦]
const choices = printers.map((p) => ({ value: p.id, label: `${p.name}${p.ready ? '' : ` (${p.status?.title})`}` }));
await bridge.printReceipt(bill, { printer: chosenId }); // or leave printer out for the default
Printing from tablets and phones¶
On the shop Wi-Fi: the shop opens Connect your POS β Phones & tablets in the bridge, turns it on and clicks Get a code. Your app pairs once and saves the result:
const { url, token } = await BridgeClient.pair({
url: 'http://192.168.1.20:9191', // shown in the bridge's Phones & tablets tab
code: '482 913', // shown after "Get a code"
name: 'Kitchen tablet',
});
const bridge = new BridgeClient({ url, token });
Paired devices can print and read status, not take card payments. Native
apps can find bridges with mDNS (_pos-bridge._tcp). A web page served over
https:// can't call a local http:// address, so for a hosted web POS use:
Through your backend (any network): implement the
print station protocol
on your server, pair the bridge with it once, and queue the same receipt
(bill.build()) as a print job's receipt.
Full API reference: openapi.yaml,
or http://127.0.0.1:9191/docs on any computer running the bridge.
React printer widget¶
A ready-made Printers settings page and a status badge for your POS header. They save the settings on the device, so your code only prints by role:
import { PrinterSettings, PrinterStatusBadge } from 'pos-hardware-bridge/react';
import { printFor } from 'pos-hardware-bridge';
<PrinterSettings /> // Settings β Printers
<PrinterStatusBadge onClick={openPrinterSettings} /> // 'Printers ready', 'Kitchen: Out of paper'
const result = await printFor('receipt', bill); // never throws
if (!result.ok && !result.skipped) showReprint(result.message);
React 17+ (optional peer dependency). Custom roles, props, hooks
(usePrintSettings, usePrinterStatus) and styling:
React printer widget guide.
License¶
MIT. ReceiptLine is Β© Open Foodservice System Consortium / OpenReceipt Project, Apache-2.0.