WebSocket de evenimente
WebSocket de evenimente

Backend-ul e-bon expune un singur canal în timp real care livrează evenimentele device.*, receipt.*, command.* și app.* către abonați pe măsură ce se produc.
wss://api.e-bon.ro/ws
URL-ul nu conține parametri de query. Identitatea și credențialele călătoresc într-un cadru hello — primul mesaj pe care clientul îl trimite după deschiderea socket-ului — iar serverul confirmă abonamentul cu un cadru welcome. Această pagină este referința de protocol pentru integratorii care se conectează din orice limbaj (Python, Go, PHP, wscat direct, WebSocket din browser etc.).
Handshake-ul
- Deschide un WebSocket către
wss://api.e-bon.ro/ws. - Trimite un cadru
helloca prim mesaj, în cel mult 10 secunde:
{ "type": "hello", "protocolVersion": 1, "role": "subscriber", "auth": { "token": "<jwt>" } }
- Așteaptă
welcome-ul serverului:
{ "type": "welcome", "protocolVersion": 1, "connectionId": "…", "heartbeatIntervalMs": 30000, "serverTime": "2026-07-07T08:10:00.000Z" }
- Evenimentele încep să curgă după
welcome. Nu trimite niciun alt cadru înainte de el — serverul închide cu codul4002dacă o faci.
Alege canalul de livrare
e-bon oferă trei moduri de a afla că s-a întâmplat ceva pe un dispozitiv sau în organizația ta. Nu se exclud reciproc — majoritatea integrărilor de producție folosesc webhook-uri pentru durabilitate și acest WebSocket pentru latență mică în UX.
| Canal | Transport | Latență | Durabilitate | Folosit pentru |
|---|---|---|---|---|
| WebSocket de evenimente | Push WSS persistent | < 100 ms | Niciuna — evenimentele emise în timpul deconectării se pierd | Tablouri de bord live, UX la POS, instrumente de dezvoltare |
| Webhook-uri | POST HTTPS pe eveniment | ~ secunde | Reîncercări durabile cu backoff [60s, 5m, 30m, 2h, 12h] | Vezi Webhook-uri |
| Polling | GET /api/v1/... | rata interogării | Tras de client | Ultima soluție când niciun canal de tip push nu este accesibil |
Conectare cu un jeton JWT
Tooling-ul first-party care deține deja un jeton de acces emis de Firebase se autentifică prin auth.token în cadrul hello:
{ "type": "hello", "protocolVersion": 1, "role": "subscriber", "auth": { "token": "<jwt>" } }
Abonații din Portal primesc fiecare eveniment al organizației lor — nu se aplică niciun filtru pe permisiuni.
import WebSocket from 'ws';
const ws = new WebSocket('wss://api.e-bon.ro/ws');
ws.on('open', () => {
ws.send(JSON.stringify({
type: 'hello',
protocolVersion: 1,
role: 'subscriber',
auth: { token: jwt },
}));
});
ws.on('message', (raw) => {
const eveniment = JSON.parse(raw.toString());
if (eveniment.type === 'welcome') return console.log('conectat');
console.log(eveniment.type, eveniment.data);
});
ws.on('close', (code, reason) => {
console.log('închis', code, reason.toString());
});
import asyncio, json, websockets
async def run(jwt: str):
async with websockets.connect("wss://api.e-bon.ro/ws") as ws:
await ws.send(json.dumps({
"type": "hello",
"protocolVersion": 1,
"role": "subscriber",
"auth": {"token": jwt},
}))
async for raw in ws:
eveniment = json.loads(raw)
if eveniment["type"] == "welcome":
continue
print(eveniment["type"], eveniment["data"])
asyncio.run(run("<jwt>"))
package main
import (
"fmt"
"github.com/gorilla/websocket"
)
func main() {
ws, _, err := websocket.DefaultDialer.Dial("wss://api.e-bon.ro/ws", nil)
if err != nil {
panic(err)
}
defer ws.Close()
hello := fmt.Sprintf(
`{"type":"hello","protocolVersion":1,"role":"subscriber","auth":{"token":%q}}`, jwt)
if err := ws.WriteMessage(websocket.TextMessage, []byte(hello)); err != nil {
panic(err)
}
for {
_, msg, err := ws.ReadMessage()
if err != nil {
return
}
fmt.Println(string(msg))
}
}
wscat -c "wss://api.e-bon.ro/ws"
# apoi lipește cadrul hello ca prim mesaj:
# {"type":"hello","protocolVersion":1,"role":"subscriber","auth":{"token":"<jwt>"}}
Conectare cu o cheie API
Integrările server-to-server care dețin o cheie API ebon_live_* se autentifică prin auth.apiKey în cadrul hello:
{ "type": "hello", "protocolVersion": 1, "role": "subscriber", "auth": { "apiKey": "ebon_live_<cheie>" } }
Abonații parteneri primesc doar evenimentele permise de permisiunile cheii (vezi Filtrare după permisiuni mai jos).
import WebSocket from 'ws';
const ws = new WebSocket('wss://api.e-bon.ro/ws');
ws.on('open', () => {
ws.send(JSON.stringify({
type: 'hello',
protocolVersion: 1,
role: 'subscriber',
auth: { apiKey },
}));
});
ws.on('message', (raw) => {
const eveniment = JSON.parse(raw.toString());
if (eveniment.type === 'welcome') return;
console.log(eveniment.type, eveniment.data);
});
import asyncio, json, websockets
async def run(api_key: str):
async with websockets.connect("wss://api.e-bon.ro/ws") as ws:
await ws.send(json.dumps({
"type": "hello",
"protocolVersion": 1,
"role": "subscriber",
"auth": {"apiKey": api_key},
}))
async for raw in ws:
eveniment = json.loads(raw)
if eveniment["type"] == "welcome":
continue
print(eveniment["type"], eveniment["data"])
asyncio.run(run("ebon_live_..."))
<?php
require 'vendor/autoload.php';
use WebSocket\Client;
$client = new Client("wss://api.e-bon.ro/ws");
$client->send(json_encode([
'type' => 'hello',
'protocolVersion' => 1,
'role' => 'subscriber',
'auth' => ['apiKey' => $apiKey],
]));
while (true) {
$raw = $client->receive();
$eveniment = json_decode($raw, true);
if ($eveniment['type'] === 'welcome') {
continue;
}
echo $eveniment['type'] . PHP_EOL;
}
wscat -c "wss://api.e-bon.ro/ws"
# apoi lipește cadrul hello ca prim mesaj:
# {"type":"hello","protocolVersion":1,"role":"subscriber","auth":{"apiKey":"ebon_live_<cheie>"}}
token și apiKey în același cadru hello. Pentru că credențialele călătoresc în primul cadru și nu în URL, ele nu apar niciodată în log-urile de acces HTTP.Erori de autentificare
Dacă autentificarea eșuează, sau dacă hello-ul nu conține credențiale, serverul închide conexiunea imediat cu codul 4001 și unul dintre aceste motive de închidere literale:
| Declanșator | Motivul închiderii |
|---|---|
| JWT invalid sau expirat | Invalid or expired credentials |
| Cheie API invalidă sau inactivă | Invalid or inactive API key |
Hello fără obiectul auth | Authentication required |
Eșecurile de protocol ale handshake-ului închid cu 4002 (primul cadru nu este un hello valid, cadre trimise înainte de welcome sau protocolVersion nesuportat) sau 4008 (niciun hello în 10 secunde de la deschiderea socket-ului).
Primește evenimente
Fiecare eveniment trimis de server este un singur cadru WebSocket text care conține un singur obiect JSON:
{
"type": "<tip-eveniment>",
"data": { /* câmpuri specifice evenimentului */ },
"timestamp": "2026-04-26T08:09:55.123Z"
}
timestamp este un șir ISO 8601 capturat la momentul livrării. Nu există un plic suplimentar, niciun id, nicio confirmare per eveniment — cadrele sunt de tip „trimite și uită”.
Catalogul de evenimente
Există opt tipuri de evenimente:
type | Înțeles | Câmpuri tipice în data |
|---|---|---|
device.status | Statusul online/offline al unui dispozitiv s-a schimbat. | deviceId, status |
device.claimed | Un dispozitiv anterior nerevendicat a fost atașat organizației tale. | deviceId, claimedAt |
device.alert | A fost ridicată o alertă la nivel de dispozitiv (hârtie scăzută, sertar deschis, eroare). | deviceId, severity, message |
receipt.created | A fost emis un bon fiscal sau nefiscal. | receiptId, deviceId, total, currency |
command.completed | O comandă din coadă s-a finalizat cu succes pe dispozitiv. | commandId, deviceId, result |
command.failed | O comandă din coadă a eșuat pe dispozitiv. | commandId, deviceId, errorCode, errorMessage |
app.connected | O instanță a aplicației de pe dispozitiv a deschis un WebSocket către cloud. | deviceId, connectedAt |
app.disconnected | O instanță a aplicației de pe dispozitiv s-a închis (curat sau prin timeout de heartbeat). | deviceId, disconnectedAt, reason |
Pentru schemele canonice ale încărcăturilor per eveniment, vezi Evenimente webhook — WebSocket-ul și webhook-urile emit același obiect data pentru tipurile de evenimente care se potrivesc.
Cadre de exemplu
{ "type": "device.status", "data": { "deviceId": "dev_pos_01", "status": "online" }, "timestamp": "2026-04-26T08:09:55.123Z" }
{ "type": "receipt.created", "data": { "receiptId": "rcp_abc", "deviceId": "dev_pos_01", "total": 4250, "currency": "RON" }, "timestamp": "2026-04-26T08:10:01.456Z" }
{ "type": "command.completed", "data": { "commandId": "cmd_xyz", "deviceId": "dev_pos_01", "result": { "fiscalId": "F00012345" } }, "timestamp": "2026-04-26T08:10:02.789Z" }
Filtrare după permisiuni
Abonații parteneri (autentificare prin cheie API) primesc doar evenimentele al căror type începe cu unul dintre prefixele mapate de pe permisiunile cheii. Abonații din Portal (autentificare JWT) ocolesc acest filtru complet și văd fiecare eveniment al organizației.
| Permisiune cheie API | Prefixe permise pentru tipul evenimentului |
|---|---|
receipts | receipt. |
receipts:read | receipt. |
receipts:admin | receipt. |
devices | device. |
devices:read | device. |
devices:write | device. |
commands | command. |
all | device., receipt., command., app. |
Verificarea este OR pe permisiuni — dacă oricare dintre permisiunile cheii mapează la un prefix cu care începe type-ul evenimentului, evenimentul este livrat. O cheie fără permisiuni recunoscute nu primește nimic.
app.* (app.connected, app.disconnected) sunt accesibile doar prin permisiunea all. Nu există o permisiune mai îngustă mapată la prefixul app..Endpoint-ul nu are un protocol suplimentar client→server de subscribe dincolo de hello — nu poți restrânge filtrul mai mult decât permite credențialul. Dacă vrei să consumi un singur tip de eveniment, filtrează după eveniment.type în codul clientului.
Pentru catalogul complet de permisiuni, vezi Autentificare API › Permisiuni.
Tratează semnalul de viață
Serverul trimite un cadru ping binar de protocol WebSocket către fiecare abonament activ la fiecare 30 de secunde și așteaptă 10 secunde pong-ul corespunzător. Dacă pong-ul nu sosește la timp, serverul închide conexiunea.
{"type":"ping"}. Majoritatea clienților răspund automat:WebSocketdin browser — gestionat de browser, nu este expus în JS.- Python
websockets— răspunde automat. Nu setaping_interval=Nonedecât dacă implementezi propriul handler de pong. - Node
ws— răspunde automat. - Go
gorilla/websocket— apeleazăSetPongHandlerși răspunde din bucla de citire. wscat— răspunde automat.
Endpoint-ul este doar push după handshake — dincolo de singurul cadru hello nu există niciun mesaj client→server și nicio confirmare per eveniment. Abonații ar trebui să stea tăcuți după hello.
Rămâi sub plafonul de rată
Serverul instalează un plafon de rată per conexiune pentru intrări de 20 de mesaje pe secundă. Mesajele în exces sunt eliminate tăcut — nu se trimite niciun cadru de eroare, iar conexiunea rămâne deschisă. Pentru că protocolul este doar de tip push după hello, clienții normali nu ating niciodată acest plafon.
Reluare conexiune după o deconectare
Serverul nu trimite jetoane de reluare a conexiunii, identificatori de sesiune sau cursori de continuare. Odată ce un client se deconectează (închidere curată, pierdere de rețea, timeout de heartbeat sau eșec de autentificare), evenimentele emise în timp ce clientul era pe dinafară nu sunt redate la următoarea conectare. Folosește Webhook-urile dacă ai nevoie de livrare durabilă.
Strategia recomandată pe partea de client este backoff exponențial cu plafon per încercare:
- Începe cu o întârziere de 1 secundă după prima deconectare.
- Dublează întârzierea la fiecare eșec consecutiv (factor 2).
- Plafonează la 30 de secunde.
- Resetează întârzierea înapoi la 1 secundă după o conectare reușită.
- La un close
4001, nu încerca din nou până când operatorul nu rotește credențialul vinovat — un4001repetat înseamnă că JWT-ul a expirat sau cheia API a fost dezactivată.
import WebSocket from 'ws';
let delay = 1000;
const max = 30_000;
function connect() {
const ws = new WebSocket('wss://api.e-bon.ro/ws');
ws.on('open', () => {
ws.send(JSON.stringify({
type: 'hello',
protocolVersion: 1,
role: 'subscriber',
auth: { apiKey },
}));
});
ws.on('message', (raw) => {
const eveniment = JSON.parse(raw.toString());
if (eveniment.type === 'welcome') { delay = 1000; return; }
handle(eveniment);
});
ws.on('close', (code) => {
if (code === 4001) return; // rotește credențialul înainte de reîncercare
setTimeout(connect, delay);
delay = Math.min(delay * 2, max);
});
}
connect();
import asyncio, json, websockets
async def run(api_key: str):
delay = 1
while True:
try:
async with websockets.connect("wss://api.e-bon.ro/ws") as ws:
await ws.send(json.dumps({
"type": "hello",
"protocolVersion": 1,
"role": "subscriber",
"auth": {"apiKey": api_key},
}))
async for raw in ws:
eveniment = json.loads(raw)
if eveniment["type"] == "welcome":
delay = 1
continue
handle(eveniment)
except websockets.ConnectionClosed as exc:
if exc.code == 4001:
return # rotește credențialul înainte de reîncercare
await asyncio.sleep(delay)
delay = min(delay * 2, 30)
Coduri de închidere
| Cod | Când |
|---|---|
1000 | Oricare parte a inițiat o închidere curată (de exemplu ai apelat ws.close() curat). |
1001 | Oprire curată a serverului — motivul închiderii Server shutting down. |
1006 | Pierdere de rețea, reset TCP sau serverul a închis conexiunea după un timeout de pong la heartbeat. În acest caz nu se trimite niciun cadru de close. |
4001 | Eșec de autentificare — JWT invalid/expirat, cheie API invalidă/inactivă sau hello fără credențiale. Vezi motivele închiderii la Erori de autentificare. |
4002 | Eroare de protocol — primul cadru nu a fost un hello valid, s-a trimis un cadru înainte de welcome sau protocolVersion este nesuportat. |
4008 | Timeout de handshake — niciun cadru hello în 10 secunde de la deschiderea socket-ului. |
Ce urmează
Idempotență
Folosește antetul Idempotency-Key pentru a face reluările sigure în POS — răspunsul memorat este returnat 24 de ore și eviți bonurile tipărite de două ori.
Autentificare
Cum te autentifici la API-ul e-bon — formatul cheii API, cele nouă permisiuni, JWT pentru sesiunile din Portal, erori uzuale de autentificare și exemple curl gata de folosit.