npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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

  1. Installation
  2. Quick start
  3. Which API should I use?
  4. Receipt elements
  5. Working with images
  6. Bring-your-own XML (printCompleteReceipt)
  7. Paper-cut–aware chunking
  8. Configuration reference
  9. Full API reference
  10. Error handling
  11. FAQ & troubleshooting
  12. License

Installation

npm i react-native-epos-soap-printer
# or
yarn add react-native-epos-soap-printer

axios 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.cgi

Make sure your app allows plain HTTP requests to the printer's IP:

  • iOS – add a localized App Transport Security exception in Info.plist for the printer IP / subnet.
  • Android – add android:usesCleartextTraffic="true" (or a network_security_config.xml allow-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:

  1. Decode the PNG.
  2. Inflate the IDAT chunk (via pako).
  3. Down-convert each pixel to luminance.
  4. Apply a 50% threshold to produce a 1-bit monochrome bitmap.
  5. 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 exports escapeXml(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 PrinterConfig

printCompleteReceipt(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_CUT between 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 marker

License

MIT