A React Native / Nitro module for the Italian ECR17 payment protocol β drive Nexi Group POS terminals over LAN, straight from your cash-register app.
The most complete open-source ECR17 toolkit for React Native & native mobile (iOS/Android).
π Using PHP / Laravel? There's a sibling port: padosoft/laravel-ecr17 β the same ECR17 protocol as a Laravel package + debug console.
- What is ECR17?
- Why this exists
- Highlights
- Screenshots
- Feature status
- Requirements
- Installation
- Quick start
- React hook example
- Configuration
- API reference
- Events
- Protocol cheat-sheet
- Architecture
- Testing
- Vibe-coding batteries included
- License
ECR17 is the Italian standard protocol β supported by Nexi Group terminals β that integrates an Electronic Cash Register (ECR) with an EFT-POS payment terminal over a local LAN connection. The cash register sends a request (payment, reversal, statusβ¦), the terminal talks to the acquiring host, and replies synchronously.
This library speaks that protocol from React Native, with the protocol engine written in C++ and bridged via Nitro Modules.
π Official protocol reference (public): https://developer.nexigroup.com/traditionalpos/en-EU/docs/ β the authoritative source. Field positions, message codes and
lrcModemay vary by terminal/firmware; always check against the official docs.
Integrating Italian POS terminals has long been needlessly painful. The ECR17
protocol is not publicly documented β the specifications are shared under NDA,
mostly with established point-of-sale software vendors β so everyone else
reverse-engineers it by trial and error across terminals and firmware versions.
(The classic trap that blocks almost everyone: the LRC is computed over a base of
0x7F, not 0x00 β handled here, and configurable per terminal.)
A few community efforts exist for server-side languages, but there was nothing for React Native or native mobile (iOS/Android). To our knowledge this is the most complete open-source ECR17 toolkit for React Native and native mobile: the full command set, response parsing, the ACK/NAK + retransmit orchestration, configurable LRC modes, and payment-safety β all tested.
The goal is simple: low-level, Android and iOS developers should no longer
struggle to talk to Italian POS terminals. No NDA hunting, no guesswork β just
await client.pay({ amountCents }). These protocols should be this approachable
for everyone, and now, for mobile, they are.
π€ Compatibility notes (lrcMode, field quirks per terminal/firmware) are welcome as issues, so we can build, together, the reference the ecosystem never had.
- β‘οΈ C++ protocol core, Nitro-bridged β framing/LRC/orchestration run natively on iOS & Android.
- π Async, Promise-based API β
await client.pay({ amountCents }). - π§± Full command set β payment, extended payment, reversal, pre-auth (request/incremental/closure), card verification, close session, totals, last result, ECR printing, reprint, VAS.
- π‘οΈ Robust by design β fixed-width field validation, defensive response parsing, ACK/NAK handshake with retransmit-up-to-3 and timeouts.
- π‘ Live events β progress messages, streamed receipt lines, connection state.
- π§© Shared C++ β native bridge β one C++ protocol engine talks to the native TCP socket (Kotlin/Swift) through Nitro's auto-generated C++βKotlin JNI bridge β a notoriously fiddly piece on Android, here done cleanly with no hand-written JNI.
- β Heavily tested β 83 C++ unit/flow/safety tests (LRC, codec, every builder, every parser, full session orchestration) run in CI.
- π€ Vibe-coding batteries included β ships first-class AI-agent context
(
AGENTS.md,CLAUDE.md,docs/LESSON.md,PROGRESS.md) so contributors using AI assistants get accurate, instant project context. See below.
The repo ships an example Debug Console app (iOS & Android) that exercises every ECR17 command against a real terminal and streams the behind-the-scenes log (sent / progress / receipt / result / error) live.
![]() Android β commands & configuration |
![]() iOS β commands & configuration |
![]() iOS β live logs tab |
This module handles real money, so correctness and failure handling are first-class:
- Physical handshake β every application frame is confirmed with ACK/NAK and retransmitted up to 3 times (per spec) on NAK or timeout, with separate ACK and response timeouts.
- Integrity β LRC validated on every received frame; invalid frames are NAKed to request retransmission. Outgoing fixed-width fields are validated, so a malformed frame is never sent to the terminal.
- No double charge β on a connection drop,
autoReconnectrestores the socket but a financial command is never blindly re-sent (a re-send could charge the cardholder twice). Read-only/idempotent commands (status, totals,sendLastResult, enable-printing) are retried; payments/reversals/pre-auths reconnect and surface the error so you recover the outcome viasendLastResult()(the spec'sGcommand). This invariant is unit-tested. - Defensive parsing β response parsers never read out of bounds on short or malformed payloads.
- One transaction at a time β matches the protocol's request/response model.
- Tested β 83 C++ unit/flow/safety tests in CI, plus an opt-in real-terminal integration test.
| Area | Status |
|---|---|
| Packet framing + LRC (4 modes) | β |
All request builders (P X p i c H U C T G E R K s S) |
β |
Response parsing (E/V/s/T/C/e/K, incl. DCC) |
β |
| Session orchestration (ACK/NAK, retransmit, timeout, progress/receipt) | β |
| Async client API + events | β |
Auto-connect, tokenization (U) flow, receipt streaming |
β |
| Android native transport (Kotlin TCP) | β (CI-built) |
| iOS native transport (Swift / Network.framework) | β (verified on device) |
- React Native 0.76+ (new architecture) β the example uses Expo SDK 56 / RN 0.85
- react-native-nitro-modules (peer dependency)
- A Nexi Group ECR17-compatible terminal configured for LAN integration
bun add react-native-ecr17 react-native-nitro-modules
# or: npm install react-native-ecr17 react-native-nitro-modules
cd ios && pod install # iOSNitro module: requires the RN new architecture (default on 0.76+).
import { createEcr17Client } from 'react-native-ecr17';
const client = createEcr17Client({
host: '192.168.1.50', // terminal IP on the LAN
port: 10000, // configured ECR port
terminalId: '12345678',
cashRegisterId: '00000001',
lrcMode: 'std',
responseTimeoutMs: 60000,
});
await client.connect();
const result = await client.pay({ amountCents: 650 });
if (result.outcome === 'ok') {
console.log('Approved', result.authCode, 'PAN', result.pan);
} else {
console.warn('Declined:', result.errorDescription);
}
// Reversal ("annullamento") of the last transaction:
await client.reverse({});
const status = await client.status(); // PosStatusResponse
await client.disconnect();import { useEffect, useMemo, useState } from 'react';
import { createEcr17Client, type Ecr17Config, type ProgressEvent } from 'react-native-ecr17';
export function useEcr17(config: Ecr17Config) {
const client = useMemo(() => createEcr17Client(config), [config]);
const [progress, setProgress] = useState<string>('');
useEffect(() => {
client.setOnProgress((e: ProgressEvent) => setProgress(e.message));
client.connect();
return () => client.disconnect();
}, [client]);
return {
progress,
pay: (amountCents: number) => client.pay({ amountCents }),
reverse: () => client.reverse({}),
status: () => client.status(),
};
}Ecr17Config: host (required), port?, terminalId (required), cashRegisterId
(required), lrcMode?, keepAlive?, autoReconnect?, connectionTimeoutMs?,
responseTimeoutMs?, ackTimeoutMs?, retryCount?, retryDelayMs?, debug?.
All commands are async (Promise) and perform a full request/response
exchange. configure/configuration are synchronous.
| Method | Command | Returns |
|---|---|---|
connect() / disconnect() / isConnected() |
β | Promise<void> / void / bool |
status() |
s |
PosStatusResponse |
pay(req) / payExtended(req) |
P / X |
PaymentResult |
reverse(req) |
S |
ReversalResult |
preAuth(req) / incrementalAuth(req) / preAuthClosure(req) |
p / i / c |
PreAuthResult / PaymentResult |
verifyCard(req) |
H |
CardVerificationResult |
closeSession() / totals() |
C / T |
CloseSessionResult / TotalsResult |
sendLastResult() |
G |
PaymentResult |
enableEcrPrinting(bool) / reprint(bool) |
E / R |
Promise<void> |
vas(xml) |
K |
VasResult |
Commands require an open connection (connect() first) and reject on
timeout / retransmission exhaustion / disconnect.
client.setOnProgress((e) => {/* e.message β display text during a procedure */});
client.setOnReceiptLine((l) => {/* l.text β a receipt line when ECR printing is on */});
client.setOnConnectionStateChange((s) => {/* 'disconnected' | 'connecting' | 'connected' */});App frame: STX(0x02) Β· payload Β· ETX(0x03) Β· LRC. Progress: SOH(0x01) Β·
20 chars Β· EOT(0x04). Confirmation: ACK(0x06) / NAK(0x15) Β· ETX Β· LRC.
LRC = 0x7F XOR-folded; framing bytes folded in are selectable via lrcMode
(stx / std / noext / stx_noext).
package/cpp/
βββ Lcr/ # LRC (4 modes, base 0x7F)
βββ PacketCodec/ # framing: STXΒ·ETXΒ·SOHΒ·EOTΒ·ACKΒ·NAK + LRC
βββ Ecr17Protocol/ # request builders (all commands), fixed-width + validated
βββ Ecr17Response/ # response field parsers -> plain structs
βββ Session/ # ACK/NAK + retransmit + timeout orchestration
βββ Transport/ # abstract Transport + NativeTransportAdapter + FakeTransport (tests)
βββ Ecr17Client/ # HybridEcr17Client (Nitro async API)
package/android/.../HybridEcr17Transport.kt # Kotlin TCP transport
package/ios/HybridEcr17Transport.swift # Swift (Network.framework) transport
cmake -S package/cpp/tests -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build && ctest --test-dir build --output-on-failure83 tests cover LRC, packet (de)framing edge cases, every builder's byte layout,
every response parser, and the documented payment / reversal / re-pay / progress
/ receipt / NAK-retransmit / timeout flows (against an in-memory FakeTransport).
// Tokenization: attach a contract to a payment/preAuth/verifyCard. The 'U'
// additional-data message is sent automatically (P -> ACK -> U -> ACK -> result).
await client.pay({
amountCents: 1000,
tokenization: { service: 'recurring', contractCode: '1666354841608' },
});
// Receipts printed by the ECR: enable printing, set receiptDrainMs in the config,
// and receive lines via the event.
await client.enableEcrPrinting(true);
client.setOnReceiptLine((l) => appendToReceipt(l.text));An opt-in C++ integration test runs the full core over a real TCP socket. It is
skipped unless ECR17_TERMINAL_HOST is set:
cmake -S package/cpp/tests -B build && cmake --build build
ECR17_TERMINAL_HOST=192.168.1.50 ECR17_TERMINAL_PORT=10000 \
ECR17_TERMINAL_ID=00000000 ECR17_LRC_MODE=std \
ctest --test-dir build -R Integration --output-on-failureBuilding on an undocumented payment protocol is exactly where AI assistants get things subtly wrong. This repo ships the context to prevent that, so an agent (or a new contributor) is productive and safe from minute one:
AGENTS.md/CLAUDE.mdβ project guide, the mandatory per-phase workflow, CI strategy, and the money-critical rules (e.g. never blindly retry a payment).docs/LESSON.mdβ accumulated, verified engineering lessons (Nitro APIs, C++βKotlin JNI, build traps, payment-safety) β the gotchas already solved.PROGRESS.mdβ crash-safe resume state across sessions.
The result: less hallucination, fewer footguns, and changes that respect the payment-safety invariants by default.
Disclaimer: independent integration library. "ECR17", "Nexi" and related marks belong to their respective owners and are referenced for interoperability only.


