WebSocket API
Real-time bidirectional communication between the backend and connected terminals. Built on STOMP 1.2 over WebSocket.
Connection
wss://payus.co.nz/webshttps://payus.co.nz/websocketSTOMP 1.210s bidirectionalAuthorization: Bearer <wsToken>The wsToken is a short-lived JWT (30-minute expiry) obtained from the REST endpoint:
POST /api/v1/terminal/ws-token
{
"configId": 123
}
Response:
{
"wsToken": "<short-lived-jwt>",
"configId": 123,
"terminalId": "TERM001",
"queueDestination": "/queue/terminal/123",
"expiresInSeconds": 1800
}STOMP CONNECT Frame
CONNECT
accept-version:1.2
host:payus.co.nz
heart-beat:10000,10000
Authorization:Bearer <wsToken>
\0Server responds with CONNECTED on success. Invalid or expired tokens receive ERROR and the connection is closed.
Subscribing to Your Terminal Queue
SUBSCRIBE
id:sub-123
destination:/queue/terminal/123
ack:auto
\0Each terminal subscribes to /queue/terminal/<configId>. The backend's WebSocketAuthInterceptorrejects subscriptions to any other terminal's queue — cross-terminal eavesdropping is blocked server-side.
Server → Terminal Messages (Inbound)
All inbound frames are JSON with at minimum an action field. Unknown actions should be ignored (forward-compatibility).
Payment status update pushed after a gateway callback resolves.
{
"action": "PAYMENT_STATUS",
"referenceId": "REF-001",
"status": "SUCCESS",
"tradeNo": "TRADE-123",
"grandTotal": "10.00",
"channel": "WECHAT"
}Server requests the terminal to re-fetch its configuration from /terminal/info.
{
"action": "FORCE_REFRESH"
}Terminal is being disconnected (e.g. deactivation, admin action). The terminal should close gracefully.
{
"action": "SESSION_KICK",
"reason": "Terminal deactivated by admin"
}Print command sent from a sibling terminal via the REST requestTerminal endpoint.
{
"action": "PRINT",
"body": "Receipt text content..."
}A specific configuration value has changed. Terminal should apply the delta.
{
"action": "CONFIG_UPDATE",
"key": "tipping.enabled",
"value": true
}Server requests the terminal to upload its current state (settings, pending transactions).
{
"action": "SYNC_REQUEST"
}WS token is about to expire. Terminal should re-mint via /api/v1/terminal/ws-token and reconnect.
{
"action": "SESSION_TIMEOUT"
}Terminal → Server Messages (Outbound)
Terminals send messages to /app/* destinations. The WebSocketAuthInterceptor whitelists only the following destinations; all others are rejected.
Session registration. Sent immediately after CONNECTED. Maps this WebSocket session to a configId.
// No body required. The token path variable contains the wsToken.Heartbeat / keep-alive. Recommended every 30–60 seconds. Server responds with CHECK_IN acknowledgment.
// No body required.Terminal reports a completed transaction for server-side persistence. Idempotent via specificTId.
{
"referenceId": "REF-001",
"terminalId": "TERM001",
"status": "TRANS_SUCCESS",
"gateway": "SKYZER_PAY",
"specificTId": "GW-TX-789",
"amount": 10000,
"currency": "NZD"
}Terminal uploads its current settings snapshot to the server.
{
"settings": { ... }
}Terminal reports the result of a command it received (e.g. PRINT). Updates the WinSockRequest record in the DB.
{
"requestId": "12345",
"requestType": "PRINT",
"status": "COMPLETED",
"result": { "printed": true }
}Admin Broadcast Topic
Admin dashboards can subscribe to /topic/admin/events to receive aggregated events from all terminals:
{
"type": "command-result",
"configId": 123,
"requestId": "12345",
"requestType": "PRINT",
"status": "COMPLETED"
}Security Model
| Frame | Validation |
|---|---|
| CONNECT | JWT signature + expiry verified. configId extracted from token claims. |
| SUBSCRIBE | Destination must match /queue/terminal/<own-configId>. Cross-terminal subscriptions rejected. |
| SEND | Only whitelisted /app/* destinations allowed. Direct /queue/* sends blocked. |
| DISCONNECT | Session→configId mapping removed. Terminal marked offline. |
Token Refresh Strategy
The wsToken has a 30-minute TTL. To maintain a continuous connection:
- Open STOMP connection with the initial
wsToken. - At the 25-minute mark, call
POST /api/v1/terminal/ws-tokento mint a fresh token. - Gracefully disconnect the current STOMP session.
- Reconnect with the new token.
- On unexpected disconnect, reconnect with exponential backoff (1s → 2s → 4s → … capped at 30s).
Legacy Auth (Migration)
For terminals that haven't upgraded to JWT-based WebSocket auth, the backend supports a legacy header-based authentication mode controlled by the property ws.migration.legacy-auth.enabled (default true). When enabled, CONNECT without a JWT is accepted and authentication is deferred to SUBSCRIBE via header fields (config_id, terminal_id, access_id). Set to false after all terminals are upgraded.