react-native-epos-soap-printer
v1.0.1
Published
A React Native library for printing receipts to Epson ePOS-Print thermal printers over LAN (SOAP/XML). Bring your own data, customize the timeout (min 60s), and use the included PNG → monochrome bitmap helpers to prepare images before printing.
Downloads
257
Maintainers
Readme
react-native-epos-soap-printer
A small, dependency-light React Native library that prints receipts to Epson ePOS-Print compatible thermal printers (e.g. TM-m30, TM-T88, TM-T20, TM-T82) over LAN, using the printer's built-in SOAP/XML HTTP service.
It is a standalone, fully configurable version of an in-app helper that used to live inside a POS application.
- Bring your own data – pass any array of receipt elements (or even raw ePOS XML) you already have.
- Pure JavaScript – no native modules; works on iOS, Android, and Expo.
- Minimum 60-second SOAP timeout enforced by design, with room to raise it.
- Paper-cut–aware chunking so long receipts don't blow past printer memory.
- PNG → monochrome bitmap helpers exposed publicly so you can pre-process images.
- Escape hatch – build the XML yourself and ship it via
printCompleteReceipt.
Table of contents
- Installation
- Quick start
- Which API should I use?
- Receipt elements
- Working with images
- Bring-your-own XML (
printCompleteReceipt) - Paper-cut–aware chunking
- Configuration reference
- Full API reference
- Error handling
- FAQ & troubleshooting
- License
Installation
npm i react-native-epos-soap-printer
# or
yarn add react-native-epos-soap-printeraxios and pako are declared as regular dependencies, so they will be installed automatically. There are no native modules to link – the library is 100% JS.
Network setup
The Epson printer must be reachable from the device on the same LAN (Wi-Fi or Ethernet). The library talks to:
http://<PRINTER_IP>/cgi-bin/epos/service.cgiMake sure your app allows plain HTTP requests to the printer's IP:
- iOS – add a localized App Transport Security exception in
Info.plistfor the printer IP / subnet. - Android – add
android:usesCleartextTraffic="true"(or anetwork_security_config.xmlallow-list) for the printer IP.
Quick start
import { givePrint, type ReceiptElement } from "react-native-epos-soap-printer";
const elements: ReceiptElement[] = [
{ Type: "TEXT", Data: " Acme Coffee Co. " },
{ Type: "TEXT", Data: "------------------------------" },
{ Type: "TEXT", Data: "Latte $4.50" },
{ Type: "TEXT", Data: "Croissant $3.20" },
{ Type: "TEXT", Data: "------------------------------" },
{ Type: "TEXT", Data: "Total $7.70" },
{ Type: "TEXT", Data: "" },
{ Type: "TEXT", Data: " Thank you! " },
{ Type: "PAPER_CUT" },
];
const result = await givePrint(elements, {
printerIp: "192.168.1.50",
timeoutMs: 90_000, // optional, minimum is 60_000 (60s)
});
if (result.success) {
console.log(
`Printed ${result.chunksPrinted} chunk(s) at ${result.printTime}`,
);
} else {
console.error("Print failed:", result.error);
}That's it – the printer should spit out a receipt.
Recommended pattern – createPrinter
When the printer config is fixed at app boot, bind it once and reuse:
import { createPrinter } from "react-native-epos-soap-printer";
export const printer = createPrinter({
printerIp: "192.168.1.50",
deviceId: "local_printer", // optional – this is the default
timeoutMs: 60_000, // optional – this is the floor
logger: ({ msg, data }) => console.log("[printer]", msg, data),
});
// Anywhere in the app:
await printer.print(elements);
await printer.print(elements, { duplicate: true });
await printer.printRaw("<text>Custom XML</text>");Which API should I use?
The library exposes three layers. Pick the highest-level one that does what you need.
| You want to… | Use |
| --------------------------------------------------------------------------- | ----------------------------------------------------- |
| Print an array of text/image/cut elements with sensible defaults | givePrint or printer.print |
| Build the ePOS XML once, inspect/cache it, then send it later or repeatedly | buildEpsonSOAP(Chunks) + printCompleteReceipt |
| Send hand-written ePOS XML (barcodes, QR, ruled lines, custom layouts) | printCompleteReceipt |
| Pre-process an image (resize, dither, brand it) before printing | pngToMonochromeBitmap + encodeRawBitmapToBase64 |
import {
givePrint,
createPrinter,
buildEpsonSOAP,
buildEpsonSOAPChunks,
printCompleteReceipt,
pngToMonochromeBitmap,
encodeRawBitmapToBase64,
} from "react-native-epos-soap-printer";Receipt elements
A receipt is just an array of ReceiptElement objects:
type ReceiptElement = {
Type: "TEXT" | "IMAGE" | "BARCODE_TEXT" | "PAPER_CUT" | string;
Data?: string; // text, base64 PNG, etc.
[key: string]: unknown; // vendor-specific extra fields are allowed
};| Type | Data | Behaviour |
| -------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| TEXT | the line content (newline is appended) | Plain text. The marker * * * * * DUPLICATE * * * * * is stripped unless duplicate: true is passed. |
| IMAGE | base64-encoded PNG (with or without prefix) | Converted to a 1-bit monochrome bitmap and embedded as a centered <image> element. |
| PAPER_CUT | (omit) | Forces a chunk boundary, causing the printer to cut between sections (or just emit a <cut /><feed line="3" /> in single-XML mode). |
| BARCODE_TEXT | reserved | Currently skipped silently. Use printCompleteReceipt with custom XML if you need barcodes today (see below). |
| (other) | text | Treated like TEXT – content is appended verbatim. Useful for forward-compat with custom types. |
Example: a multi-section receipt
const elements: ReceiptElement[] = [
// ----- Header -----
{ Type: "IMAGE", Data: logoBase64 },
{ Type: "TEXT", Data: " Acme Coffee Co. " },
{ Type: "TEXT", Data: " 123 Main St · Springfield " },
{ Type: "TEXT", Data: "" },
// ----- Body -----
{ Type: "TEXT", Data: "Order #1042 2026-05-27 19:30" },
{ Type: "TEXT", Data: "------------------------------------" },
{ Type: "TEXT", Data: "2 x Latte $9.00" },
{ Type: "TEXT", Data: "1 x Croissant $3.20" },
{ Type: "TEXT", Data: "------------------------------------" },
{ Type: "TEXT", Data: "Subtotal $12.20" },
{ Type: "TEXT", Data: "Tax $1.10" },
{ Type: "TEXT", Data: "TOTAL $13.30" },
{ Type: "TEXT", Data: "" },
{ Type: "TEXT", Data: " Thank you! " },
{ Type: "PAPER_CUT" },
// ----- Kitchen copy (printed after the cut) -----
{ Type: "TEXT", Data: "*** KITCHEN COPY ***" },
{ Type: "TEXT", Data: "2 x Latte" },
{ Type: "TEXT", Data: "1 x Croissant" },
{ Type: "PAPER_CUT" },
];
await printer.print(elements);Because PAPER_CUT triggers chunk boundaries, this sends two SOAP requests – one per cut – so each section is delivered, cut, and only then is the next sent.
Working with images
IMAGE elements take a base64-encoded PNG. The library will:
- Decode the PNG.
- Inflate the IDAT chunk (via
pako). - Down-convert each pixel to luminance.
- Apply a 50% threshold to produce a 1-bit monochrome bitmap.
- Embed it as
<image width="…" height="…" align="center">…</image>.
import { Image } from "react-native";
import { Asset } from "expo-asset"; // or any way you get base64
import { givePrint } from "react-native-epos-soap-printer";
const logoBase64 = await loadLogoAsBase64();
await givePrint(
[
{ Type: "IMAGE", Data: logoBase64 },
{ Type: "TEXT", Data: "Welcome!" },
{ Type: "PAPER_CUT" },
],
{ printerIp: "192.168.1.50" },
);Tip: Most TM-series printers print 576px wide on 80mm paper, 384px wide on 58mm paper. Resize your logos accordingly before passing them in.
Customizing the bitmap before printing
When you need control – e.g. apply your own dithering, invert, crop, or rebrand:
import {
pngToMonochromeBitmap,
encodeRawBitmapToBase64,
} from "react-native-epos-soap-printer";
const bitmap = pngToMonochromeBitmap(myBase64Png);
// Example: invert every byte.
for (let i = 0; i < bitmap.data.length; i++) {
bitmap.data[i] = ~bitmap.data[i] & 0xff;
}
const base64 = encodeRawBitmapToBase64(bitmap);
// Then embed in your own XML:
const xml = `<image width="${bitmap.width}" height="${bitmap.height}" align="center">${base64}</image><cut />`;
await printCompleteReceipt(xml, { printerIp: "192.168.1.50" });Bring-your-own XML (printCompleteReceipt)
printCompleteReceipt is the escape hatch: you give it a string of ePOS XML, it wraps it in a SOAP envelope, appends a final <cut />, and posts it.
This is what you reach for when:
- you need ePOS features the higher-level API doesn't model (barcodes, QR codes, ruled lines, font sizes, alignment, drawer kick, etc.);
- you have an external service that returns ready-to-print XML;
- you want to send the exact same payload many times and avoid rebuilding it.
import { printCompleteReceipt } from "react-native-epos-soap-printer";
const xml = `
<text align="center" font="font_a" width="2" height="2">Acme</text>
<feed line="1"/>
<text>Order #1042</text>
<feed line="1"/>
<barcode type="code39" hri="below" width="2" height="64">123456</barcode>
<feed line="1"/>
<symbol type="qrcode_model_2" level="level_l" width="6">https://acme.example/r/1042</symbol>
<feed line="2"/>
`;
await printCompleteReceipt(xml, {
printerIp: "192.168.1.50",
timeoutMs: 90_000,
});Things to keep in mind:
Do not include the outer
<s:Envelope>/<s:Header>/<s:Body>/<epos-print>– the library adds those.A final
<cut />is always appended automatically. You can still emit your own<cut />earlier in the XML to cut between sections.Any text you embed inside
<text>tags must already be XML-escaped. The library exportsescapeXml(text)for you:import { escapeXml } from "react-native-epos-soap-printer"; const safe = `<text>${escapeXml(userInput)}</text>`;
Refer to Epson's official ePOS-Print XML User's Manual for the full tag vocabulary (<text>, <feed>, <image>, <cut>, <barcode>, <symbol>, <pulse> for drawer kick, etc.).
Building the XML from elements first, then sending it
If you want the convenience of the element model and the ability to inspect/cache the resulting XML, combine buildEpsonSOAP + printCompleteReceipt:
import {
buildEpsonSOAP,
printCompleteReceipt,
} from "react-native-epos-soap-printer";
const xml = buildEpsonSOAP(elements, /* duplicate */ false);
// Inspect / log / cache `xml` here ...
await printCompleteReceipt(xml, { printerIp: "192.168.1.50" });Paper-cut–aware chunking
Long receipts (e.g. KOT + customer copy + duplicate) can exceed printer-side memory if sent as a single SOAP request. givePrint therefore uses buildEpsonSOAPChunks internally to split the receipt at every PAPER_CUT boundary and post one chunk at a time:
const chunks = buildEpsonSOAPChunks(elements);
// chunks.length === number of PAPER_CUT segments (at most)
for (let i = 0; i < chunks.length; i++) {
await printCompleteReceipt(chunks[i], config);
}If you'd rather drive this loop yourself (e.g. to show per-section progress UI), use the public chunk builder + the low-level transport. Alternatively, just pass onChunkPrinted to givePrint:
await givePrint(elements, config, {
onChunkPrinted: (i, total) => setProgress(`${i}/${total}`),
});Configuration reference
PrinterConfig
| Field | Type | Default | Description |
| ----------- | --------------------------------- | ------------------------ | ------------------------------------------------------------------------ |
| printerIp | string | required | LAN IP of the printer (e.g. 192.168.1.50). |
| deviceId | string | 'local_printer' | ePOS device id registered on the printer. |
| timeoutMs | number | 60_000 | SOAP timeout in ms. Minimum 60_000; lower values are clamped up. |
| url | string | derived from printerIp | Full service URL override (use when behind a proxy or non-default path). |
| logger | (entry: { msg, data? }) => void | no-op | Diagnostic logger; called on errors from givePrint. |
GivePrintOptions
| Field | Type | Description |
| ---------------- | ------------------------ | ----------------------------------------------------------------------------------------- |
| duplicate | boolean | When true, the * * * * * DUPLICATE * * * * * marker is preserved instead of stripped. |
| overrides | Partial<PrinterConfig> | Merged on top of the bound PrinterConfig for this call only. |
| onChunkPrinted | (idx, total) => void | Invoked after each chunk is successfully sent. Useful for progress UI. |
Full API reference
givePrint(data, config, options?) → Promise<GivePrintResult>
High-level entry point. Builds chunked ePOS XML from data, then posts each chunk through the SOAP transport.
type GivePrintResult =
| { success: true; chunksPrinted: number; printTime: string } // ISO-8601
| { success: false; error: string; chunksPrinted: number };givePrint does not throw; it always resolves with a discriminated union so you can branch on success.
createPrinter(config) → { config, print, printRaw }
Convenience factory:
const printer = createPrinter(config);
printer.print(elements, options?); // delegates to givePrint
printer.printRaw(xml, overrides?); // delegates to printCompleteReceipt
printer.config; // the bound PrinterConfigprintCompleteReceipt(xml, config) → Promise<void>
Low-level transport. Wraps xml in a SOAP envelope, appends a <cut />, and POSTs it. Throws on HTTP / network errors.
buildEpsonSOAP(elements, duplicate?) → string
Builds a single ePOS XML body from an elements array.
buildEpsonSOAPChunks(elements, duplicate?) → string[]
Same as above, but splits the result at every PAPER_CUT boundary.
Constants
| Constant | Value | Description |
| ------------------- | ----------------- | ---------------------------------------- |
| MIN_TIMEOUT_MS | 60_000 | The lower bound enforced on timeoutMs. |
| DEFAULT_DEVICE_ID | 'local_printer' | The Epson default ePOS device id. |
Utilities
| Export | Description |
| ------------------------- | --------------------------------------------------------------------------- |
| pngToMonochromeBitmap | Decode a base64 PNG into a 1-bit MonochromeBitmap. |
| encodeRawBitmapToBase64 | Encode a MonochromeBitmap back into a base64 string for an <image> tag. |
| unfilterPngData | Apply PNG row filters – internal but exported for advanced use. |
| paethPredictor | PNG paeth filter predictor (internal). |
| escapeXml | Escape the 5 XML special characters. |
| generatePrintJobId | Generate a JOB_<ts>_<rand> id. |
| getPrintDate | new Date().toISOString(). |
| inflateSync | Decompress a zlib buffer via pako. |
| readUInt32BE | Read a 32-bit big-endian uint from a Uint8Array. |
| uint8ArrayToString | 1-byte-per-char Uint8Array → string conversion. |
| arraysEqual | Strict byte-by-byte equality. |
Types
ReceiptElement, ReceiptElementType, PrinterConfig, GivePrintOptions, GivePrintResult, GivePrintSuccess, GivePrintFailure, MonochromeBitmap are all exported.
Error handling
givePrint returns a result rather than throwing:
const result = await givePrint(elements, config);
if (!result.success) {
// result.error is a string (axios message, builder error, etc.)
// result.chunksPrinted tells you how far we got.
showToast(
`Print failed after ${result.chunksPrinted} chunk(s): ${result.error}`,
);
}printCompleteReceipt throws, so wrap it:
try {
await printCompleteReceipt(xml, config);
} catch (err) {
console.error("Printer rejected the request:", err);
}Common errors:
| Message contains | Likely cause |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| Network Error / ECONNREFUSED | Wrong IP, printer powered off, or device on different subnet. |
| timeout of … ms exceeded | Printer busy, paper low, or USB-LAN slowdown. Raise timeoutMs. |
| Invalid PNG signature | The base64 you passed isn't actually a PNG. |
| HTTP error! status: 4xx/5xx | The ePOS service rejected the XML – check for unescaped characters or invalid tags. |
FAQ & troubleshooting
Why is 60 s the minimum timeout?
Long receipts, low-paper conditions, or Wi-Fi hiccups can each cause the printer to delay its HTTP response by several tens of seconds. Anything under ~60 s tends to surface as ETIMEDOUT errors mid-print and leaves the cutter in an awkward state. The library therefore clamps timeoutMs upward to 60_000.
Can I print barcodes / QR codes?
Yes – use printCompleteReceipt with hand-written <barcode> / <symbol> XML (see Bring-your-own XML). The BARCODE_TEXT element type is currently a no-op placeholder for future expansion.
Can I open a cash drawer?
Yes, via custom XML:
await printCompleteReceipt(
'<pulse drawer="drawer_1" time="pulse_100"/>',
config,
);Does this work with USB / Bluetooth printers?
No. This library only talks to printers exposing the Epson ePOS-Print HTTP service over the network (LAN/Wi-Fi). If you need USB or Bluetooth, you'll need a native module that wraps the ESC/POS protocol directly.
Does this work in Expo Go?
Yes – it's pure JS. You don't need a custom dev client. Just make sure HTTP traffic to the printer's IP is allowed by your platform (see Network setup).
How wide should my images be?
- 80 mm paper: 576 px maximum.
- 58 mm paper: 384 px maximum.
Images wider than that are still accepted but will be truncated by the printer.
How do I print the same receipt twice (e.g. customer + merchant)?
Either:
- Insert a
PAPER_CUTbetween two identical groups of elements; or - Call
printer.print(elements)twice.
If you have a "duplicate" marker (* * * * * DUPLICATE * * * * *) in your data, pass { duplicate: true } to keep it on the second pass:
await printer.print(elements); // strips marker
await printer.print(elements, { duplicate: true }); // keeps markerLicense
MIT
