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?subscribe=events
Această pagină este referința de protocol pentru integratorii care se conectează din orice limbaj (Python, Go, PHP, wscat direct, WebSocket din browser etc.).
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 conectează cu parametrul de query token:
wss://api.e-bon.ro/ws?subscribe=events&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?subscribe=events&token=${jwt}`
);
ws.on('open', () => console.log('conectat'));
ws.on('message', (raw) => {
const eveniment = JSON.parse(raw.toString());
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):
url = f"wss://api.e-bon.ro/ws?subscribe=events&token={jwt}"
async with websockets.connect(url) as ws:
async for raw in ws:
eveniment = json.loads(raw)
print(eveniment["type"], eveniment["data"])
asyncio.run(run("<jwt>"))
package main
import (
"fmt"
"github.com/gorilla/websocket"
)
func main() {
url := "wss://api.e-bon.ro/ws?subscribe=events&token=" + jwt
ws, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
panic(err)
}
defer ws.Close()
for {
_, msg, err := ws.ReadMessage()
if err != nil {
return
}
fmt.Println(string(msg))
}
}
wscat -c "wss://api.e-bon.ro/ws?subscribe=events&token=<jwt>"
Conectare cu o cheie API
Integrările server-to-server care dețin o cheie API ebon_live_* se conectează cu parametrul de query apiKey:
wss://api.e-bon.ro/ws?subscribe=events&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?subscribe=events&apiKey=${apiKey}`
);
ws.on('message', (raw) => {
const eveniment = JSON.parse(raw.toString());
console.log(eveniment.type, eveniment.data);
});
import asyncio, json, websockets
async def run(api_key: str):
url = f"wss://api.e-bon.ro/ws?subscribe=events&apiKey={api_key}"
async with websockets.connect(url) as ws:
async for raw in ws:
eveniment = json.loads(raw)
print(eveniment["type"], eveniment["data"])
asyncio.run(run("ebon_live_..."))
<?php
require 'vendor/autoload.php';
use WebSocket\Client;
$url = "wss://api.e-bon.ro/ws?subscribe=events&apiKey={$apiKey}";
$client = new Client($url);
while (true) {
$raw = $client->receive();
$eveniment = json_decode($raw, true);
echo $eveniment['type'] . PHP_EOL;
}
wscat -c "wss://api.e-bon.ro/ws?subscribe=events&apiKey=ebon_live_<cheie>"
token și apiKey în aceeași conexiune.Erori de autentificare
Dacă autentificarea eșuează, sau dacă nu se trimite nici token, nici apiKey, 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 token |
| Cheie API invalidă sau inactivă | Invalid or inactive API key |
| Eroare neașteptată la verificarea cheii API | Authentication error |
Nici token, nici apiKey nu au fost trimise | Authentication required |
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 client→server de subscribe — 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ă conectare — nu există un mesaj JSON de tip subscribe de la client la server și nicio confirmare per eveniment. Abonații ar trebui să stea tăcuți.
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, 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?subscribe=events&apiKey=${apiKey}`
);
ws.on('open', () => { delay = 1000; });
ws.on('message', (raw) => handle(JSON.parse(raw.toString())));
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):
url = f"wss://api.e-bon.ro/ws?subscribe=events&apiKey={api_key}"
delay = 1
while True:
try:
async with websockets.connect(url) as ws:
delay = 1
async for raw in ws:
handle(json.loads(raw))
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ă, autentificare lipsă sau o eroare neașteptată la verificarea cheii API. Vezi motivele închiderii la Erori de autentificare. |
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.