WebSocket API

Real-time bidirectional communication between the backend and connected terminals. Built on STOMP 1.2 over WebSocket.

Connection

Endpoint (STOMP):wss://payus.co.nz/webs
Endpoint (SockJS):https://payus.co.nz/websocket
Protocol:STOMP 1.2
Heartbeat:10s bidirectional
Auth:Authorization: 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>

\0

Server 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

\0

Each 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

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

Server requests the terminal to re-fetch its configuration from /terminal/info.

{
  "action": "FORCE_REFRESH"
}
SESSION_KICK

Terminal is being disconnected (e.g. deactivation, admin action). The terminal should close gracefully.

{
  "action": "SESSION_KICK",
  "reason": "Terminal deactivated by admin"
}
PRINT

Print command sent from a sibling terminal via the REST requestTerminal endpoint.

{
  "action": "PRINT",
  "body": "Receipt text content..."
}
CONFIG_UPDATE

A specific configuration value has changed. Terminal should apply the delta.

{
  "action": "CONFIG_UPDATE",
  "key": "tipping.enabled",
  "value": true
}
SYNC_REQUEST

Server requests the terminal to upload its current state (settings, pending transactions).

{
  "action": "SYNC_REQUEST"
}
SESSION_TIMEOUT

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.

/app/hello/{token}

Session registration. Sent immediately after CONNECTED. Maps this WebSocket session to a configId.

// No body required. The token path variable contains the wsToken.
/app/checkIn/{token}

Heartbeat / keep-alive. Recommended every 30–60 seconds. Server responds with CHECK_IN acknowledgment.

// No body required.
/app/transactionComplete/{token}

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"
}
/app/syncSettings/{token}

Terminal uploads its current settings snapshot to the server.

{
  "settings": { ... }
}
/app/commandResult/{token}

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

FrameValidation
CONNECTJWT signature + expiry verified. configId extracted from token claims.
SUBSCRIBEDestination must match /queue/terminal/<own-configId>. Cross-terminal subscriptions rejected.
SENDOnly whitelisted /app/* destinations allowed. Direct /queue/* sends blocked.
DISCONNECTSession→configId mapping removed. Terminal marked offline.

Token Refresh Strategy

The wsToken has a 30-minute TTL. To maintain a continuous connection:

  1. Open STOMP connection with the initial wsToken.
  2. At the 25-minute mark, call POST /api/v1/terminal/ws-token to mint a fresh token.
  3. Gracefully disconnect the current STOMP session.
  4. Reconnect with the new token.
  5. 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.