A lightweight, dependency-free PHP client for the Italian "Protocollo 17" / ECR17 amount-exchange protocol, used between a cash register (ECR) and a payment terminal (POS) over TCP/IP.
Client PHP leggero e senza dipendenze per il protocollo italiano "Protocollo 17" / ECR17 di scambio importo tra registratore di cassa (ECR) e terminale POS via TCP/IP.
🇬🇧 English below · 🇮🇹 Versione italiana più sotto
Il "Protocollo 17" (noto anche come ECR17 o protocollo Ingenico) è lo standard di fatto in Italia per lo scambio importo tra cassa e POS. Con la normativa che impone il collegamento POS–registratore telematico, sempre più sviluppatori hanno bisogno di integrarlo — ma le specifiche non sono pubblicate dagli acquirer e vengono fornite solo sotto NDA agli integratori di gestionali. Il risultato è che le community di sviluppatori italiani si arrabattano da anni senza una documentazione aperta.
Questa libreria nasce per colmare quel vuoto: è il punto di partenza che avremmo voluto trovare noi. Il framing è stato ricostruito basandosi sulla documentazione pubblica Nexi "Traditional POS", liberamente accessibile, dello stesso standard ECR17.
📖 Fonte ufficiale del protocollo (pubblica): https://developer.nexigroup.com/traditionalpos/en-EU/docs/
Non è un pacchetto Composer: copia semplicemente src/Protocol17Client.php nel tuo
progetto e adatta il namespace alle tue esigenze. Richiede PHP 8.1+ (usa la
sintassi dei parametri nominati e match).
use Protocollo17\Pos\Protocol17Client;
$client = new Protocol17Client(
host: '192.168.1.100', // IP del terminale POS
port: 10000, // porta TCP (spesso 10000)
terminalId: '00000000', // TID del POS; '00000000' = jolly per test
cashRegisterId: '00000001', // ID cassa, a tua scelta
lrcMode: 'stx', // modalità LRC (vedi sotto)
);
// Importo SEMPRE in centesimi: 100 = 1,00 €
$result = $client->pay(100, function (string $status) {
echo "Stato POS: $status\n"; // es. "Inserire carta", "Digitare PIN"
});
if ($result['approved']) {
echo "Approvata! Autorizzazione: {$result['auth_code']}\n";
} else {
echo "Negata: {$result['description']}\n";
}L'LRC (carattere di controllo) si calcola in XOR partendo da un valore base 0x7F
(non 0x00 come negli LRC standard — questo è il dettaglio che blocca quasi tutti).
Quali byte entrano nel calcolo dipende però dal terminale/acquirer. Sono previste
quattro modalità:
lrcMode |
Calcolo |
|---|---|
stx |
0x7F ^ STX ^ payload ^ ETX |
std |
0x7F ^ payload ^ ETX |
noetx |
0x7F ^ payload |
stx_noetx |
0x7F ^ STX ^ payload |
Come trovare quella giusta: parti con stx. Se ricevi un NAK, prova le altre una
per una con un importo minimo (1,00 €). Quando il display del POS si accende e chiede
la carta, hai trovato la modalità corretta per il tuo terminale.
In caso di transazione approvata (result === '00') l'array contiene tra l'altro:
approved, pan, auth_code, read_type (ICC/MAG/CLM…), card_type, stan,
datetime. In caso di transazione negata contiene description con il motivo.
Il campo raw è sempre presente: contiene il payload grezzo, utilissimo per il debug
e per verificare le posizioni dei campi sul TUO terminale.
Fatevi confermare dall'installatore/acquirer e impostate sul terminale:
- IP statico (DHCP disabilitato)
- Protocollo 17 abilitato
- IP cassa = l'IP della macchina dove gira questo codice
- Porta TCP (spesso 10000)
- Su alcuni PAX: voce "Conferma importo da ECR" impostata a NO
- Una transazione alla volta: il POS gestisce una sola sessione attiva.
- Usate importi minimi nei test e gestite gli storni con cura.
- Il
readTimeoutdeve coprire l'intera transazione (attesa carta + PIN + host): 90–120s. - I messaggi di stato (
onStatus) sono perfetti per aggiornare il display cassa in tempo reale.
"Protocollo 17" (also known as ECR17 or the Ingenico protocol) is the de-facto Italian standard for amount exchange between a cash register and a POS terminal. As regulations increasingly require linking the POS to fiscal cash registers, more developers need to integrate it — but the specs are not published by acquirers and are only shared under NDA with point-of-sale software vendors. As a result, Italian developer communities have struggled for years without open documentation.
This library aims to fill that gap: it's the starting point we wished we had found. The framing was reconstructed from the public Nexi "Traditional POS" documentation, freely available, of the same ECR17 standard.
📖 Official (public) protocol reference: https://developer.nexigroup.com/traditionalpos/en-EU/docs/
Not a Composer package: simply copy src/Protocol17Client.php into your project and
adapt the namespace as needed. Requires PHP 8.1+ (uses named arguments and match).
use Protocollo17\Pos\Protocol17Client;
$client = new Protocol17Client(
host: '192.168.1.100', // POS terminal IP
port: 10000, // TCP port (often 10000)
terminalId: '00000000', // POS TID; '00000000' = wildcard for testing
cashRegisterId: '00000001', // cash register ID, your choice
lrcMode: 'stx', // LRC mode (see below)
);
// Amount ALWAYS in cents: 100 = EUR 1.00
$result = $client->pay(100, function (string $status) {
echo "POS status: $status\n"; // e.g. "Insert card", "Enter PIN"
});
if ($result['approved']) {
echo "Approved! Auth code: {$result['auth_code']}\n";
} else {
echo "Declined: {$result['description']}\n";
}The LRC (control byte) is computed with an XOR starting from a base value of 0x7F
(not 0x00 as in standard LRCs — this is the detail that blocks almost everyone). Which
bytes enter the calculation depends on the terminal/acquirer. Four modes are provided:
lrcMode |
Calculation |
|---|---|
stx |
0x7F ^ STX ^ payload ^ ETX |
std |
0x7F ^ payload ^ ETX |
noetx |
0x7F ^ payload |
stx_noetx |
0x7F ^ STX ^ payload |
Finding the right one: start with stx. If you get a NAK, try the others one by one
with a minimal amount (EUR 1.00). When the POS display lights up asking for the card,
you've found the correct mode for your terminal.
For an approved transaction (result === '00') the array includes, among others:
approved, pan, auth_code, read_type (ICC/MAG/CLM…), card_type, stan,
datetime. For a declined transaction it includes description with the reason.
The raw field is always present: it holds the raw payload, very useful for debugging
and for verifying field positions on YOUR terminal.
Have the installer/acquirer confirm and set on the terminal:
- Static IP (DHCP disabled)
- Protocollo 17 enabled
- Cash register IP = the IP of the machine running this code
- TCP port (often 10000)
- On some PAX devices: "Confirm amount from ECR" set to NO
- One transaction at a time: the terminal handles a single active session.
- Use minimal amounts in tests and handle reversals carefully.
readTimeoutmust cover the whole transaction (card wait + PIN + host): 90–120s.- Status messages (
onStatus) are perfect for updating the cashier display in real time.
This is an independent, community project. It is NOT affiliated with, endorsed by, or
supported by Nexi, PAX, Ingenico, UniCredit, or any acquirer or manufacturer. The
protocol is not officially documented by acquirers; the correct lrcMode and field
positions may vary between terminals and firmware versions. Test thoroughly with
minimal amounts before any production use. The authors accept no liability for any
damage, financial loss, or misbehaviour arising from the use of this software. Use at
your own risk.
Questo è un progetto indipendente e community-driven. Non è affiliato, approvato o
supportato da Nexi, PAX, Ingenico, UniCredit o alcun acquirer/produttore. Il protocollo
non è documentato ufficialmente dagli acquirer; la lrcMode corretta e le posizioni dei
campi possono variare tra terminali e versioni firmware. Testare a fondo con importi
minimi prima di qualsiasi uso in produzione. Gli autori non si assumono alcuna
responsabilità per danni, perdite economiche o malfunzionamenti derivanti dall'uso di
questo software. Usare a proprio rischio.
Released under the MIT License.
Contributions are welcome! If you get this working on a terminal/acquirer not yet
documented here, please open an issue or PR reporting the terminal model, the
acquirer, and the lrcMode that worked. Building the compatibility table the
community never had is the whole point.
Contributi benvenuti! Se lo fai funzionare su un terminale/acquirer non ancora
documentato, apri una issue o una PR indicando modello del terminale, acquirer
e lrcMode che ha funzionato. Costruire la tabella di compatibilità che alla
community è sempre mancata è proprio lo scopo del progetto.