e-bon
e-bon.ro
API reference

Events WebSocket

Wire-protocol reference for the e-bon real-time events WebSocket — connect URL, JWT and API-key auth, the eight event types, scope-based filtering, frame shape, heartbeat, rate limit, close codes and reconnect guidance.

Events WebSocket

The e-bon backend exposes a single real-time channel that pushes device.*, receipt.*, command.* and app.* events to subscribers as they happen.

wss://api.e-bon.ro/ws?subscribe=events

This page is the wire-protocol reference for integrators connecting from any language (Python, Go, PHP, raw wscat, browser WebSocket, etc.).

Choose a delivery channel

e-bon offers three ways to learn that something happened on a device or in your organization. They are not mutually exclusive — most production integrations subscribe to webhooks for durability and to this WebSocket for low-latency UX.

ChannelTransportLatencyDurabilityUse for
Events WebSocketPersistent WSS push< 100 msNone — events sent while disconnected are lostLive dashboards, POS UX, dev tools
WebhooksHTTPS POST per event~ secondsDurable retry with [60s, 5m, 30m, 2h, 12h] backoffSee Webhooks
PollingGET /api/v1/...poll-ratePull-drivenLast resort when neither push channel is reachable

Connect with a JWT

First-party tooling that already holds a Firebase-issued access token connects with a token query parameter:

wss://api.e-bon.ro/ws?subscribe=events&token=<jwt>

Portal subscribers receive every event for their organization — no per-scope filter is applied.

import WebSocket from 'ws';

const ws = new WebSocket(
  `wss://api.e-bon.ro/ws?subscribe=events&token=${jwt}`
);

ws.on('open', () => console.log('connected'));
ws.on('message', (raw) => {
  const event = JSON.parse(raw.toString());
  console.log(event.type, event.data);
});
ws.on('close', (code, reason) => {
  console.log('closed', code, reason.toString());
});

Connect with an API key

Server-to-server integrations holding an ebon_live_* API key connect with the apiKey query parameter:

wss://api.e-bon.ro/ws?subscribe=events&apiKey=ebon_live_<key>

Partner subscribers receive only events permitted by the key's scopes (see Filter by scope below).

import WebSocket from 'ws';

const ws = new WebSocket(
  `wss://api.e-bon.ro/ws?subscribe=events&apiKey=${apiKey}`
);

ws.on('message', (raw) => {
  const event = JSON.parse(raw.toString());
  console.log(event.type, event.data);
});
Pick exactly one auth parameter — never send both token and apiKey in the same connection.

Authentication failures

If authentication fails, or if neither token nor apiKey is provided, the server closes the connection immediately with code 4001 and one of these literal close reasons:

TriggerClose reason
Invalid or expired JWTInvalid or expired token
Invalid or inactive API keyInvalid or inactive API key
Unexpected error during API-key checkAuthentication error
Neither token nor apiKey providedAuthentication required

Receive events

Every event the server sends is a single text WebSocket frame containing one JSON object:

{
  "type": "<event-type>",
  "data": { /* event-specific fields */ },
  "timestamp": "2026-04-26T08:09:55.123Z"
}

timestamp is an ISO 8601 string captured at dispatch time. There is no envelope, no id, no per-event ack — frames are fire-and-forget.

Event catalogue

There are eight event types:

typeMeaningTypical data fields
device.statusA device's online/offline status changed.deviceId, status
device.claimedA previously-unclaimed device was bound to your organization.deviceId, claimedAt
device.alertA device-level alert was raised (low paper, drawer open, error condition).deviceId, severity, message
receipt.createdA fiscal or non-fiscal receipt was emitted.receiptId, deviceId, total, currency
command.completedA queued command finished successfully on the device.commandId, deviceId, result
command.failedA queued command failed on the device.commandId, deviceId, errorCode, errorMessage
app.connectedA device-app instance opened a WebSocket to the cloud.deviceId, connectedAt
app.disconnectedA device-app instance closed (cleanly or by heartbeat timeout).deviceId, disconnectedAt, reason

For the canonical per-event payload schemas, see Webhook events — the WebSocket and webhooks emit the same data object for matching event types.

Example frames

{ "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" }

Filter by scope

Partner subscribers (API-key auth) receive only events whose type starts with one of the prefixes mapped from the key's scopes. Portal subscribers (JWT auth) bypass this filter entirely and see every event in the organization.

API-key scopeAllowed event-type prefixes
receiptsreceipt.
receipts:readreceipt.
receipts:adminreceipt.
devicesdevice.
devices:readdevice.
devices:writedevice.
commandscommand.
alldevice., receipt., command., app.

The check is OR across scopes — if any of the key's scopes maps to a prefix that the event's type starts with, the event is delivered. A key with no recognized scopes receives nothing.

app.* events (app.connected, app.disconnected) are reachable only via the all scope. There is no narrower scope mapped to the app. prefix.

The endpoint has no client→server subscribe protocol — you cannot narrow the filter beyond what the credential grants. To consume only one event type, filter on event.type in your client code.

For the full scope catalogue, see API authentication › Scopes.

Handle the heartbeat

The server sends a binary WebSocket-protocol ping frame to every active subscription every 30 seconds and waits 10 seconds for the matching pong. If the pong does not arrive in time, the server terminates the connection.

This is the binary WebSocket-protocol ping/pong (RFC 6455 §5.5.2 / §5.5.3) — not an application-level {"type":"ping"} JSON message. Most clients respond automatically:
  • Browser WebSocket — handled by the browser, not exposed to JS.
  • Python websockets — responds automatically. Do not set ping_interval=None unless you implement your own pong handler.
  • Node ws — responds automatically.
  • Go gorilla/websocket — call SetPongHandler and reply from your read loop.
  • wscat — responds automatically.
If you write a raw client and your library does not auto-respond to control pings, your subscription will be terminated 10 seconds after the first 30-second mark.

The endpoint is push-only after connect — there is no client→server subscribe JSON message and no per-event ack. Subscribers should stay silent.

Stay under the rate limit

The server installs a per-connection inbound throttle of 20 messages per second. Excess inbound messages are silently dropped — no error frame is sent and the connection stays open. Since the protocol is push-only, normal clients never trigger this limit.

Reconnect after a disconnect

The server does not push reconnect tokens, session IDs, or resume cursors. Once a client disconnects (clean close, network drop, heartbeat timeout, or auth failure), events emitted while the client was off the wire are never replayed on the next connection. Use Webhooks if you need durable delivery.

The recommended client-side strategy is exponential backoff with a per-attempt cap:

  • Start with a 1-second delay after the first disconnect.
  • Double the delay on every consecutive failure (factor 2).
  • Cap at 30 seconds.
  • Reset the delay back to 1 second after a successful connect.
  • On a 4001 close, do not retry until the operator has rotated the offending credential — repeated 4001s mean the JWT is expired or the API key has been disabled.
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; // rotate credential before retrying
    setTimeout(connect, delay);
    delay = Math.min(delay * 2, max);
  });
}

connect();

Close codes

CodeWhen
1000Either side initiated a clean close (e.g. you called ws.close() cleanly).
1001Server graceful shutdown — close reason Server shutting down.
1006Network drop, TCP reset, or the server terminated the connection after a heartbeat-pong timeout. No close frame is sent.
4001Authentication failure — invalid/expired JWT, invalid/inactive API key, missing auth, or an unexpected error during the API-key check. See the close reasons under Authentication failures.

Where to next

Webhook events

Same payload shapes, durable retry, no persistent connection. Cross-link for the canonical per-event data schemas.

Webhooks

CRUD for HTTP webhook subscriptions — create, rotate secrets, send test deliveries, inspect delivery history.

API authentication

The full API-key scope catalogue used by the events filter, plus JWT details for portal subscribers.