Cloud WebSocket — trigger terminals over the internet
Trigger a payment, refund, cancel, or receipt-print on a PayUs terminal from anywhere on the internet. Same command surface as LAN, but the PayUs server relays the command — your POS controller doesn't need to be on the same Wi-Fi as the card terminal.
A note on {system} and accessId
Every REST path on this page is /api/v1/{system}/…. The only value integrators use today is pos — the system that hosts the PayUs POS terminal API. Hard-code it for now; future systems will be documented here.
accessIdis a per-branch access token baked into the merchant's activation payload. You can read it two ways:
- From the
accessIdfield on each entry oflistTerminals(REST — see API Reference → Terminals). - From the activation credentials the admin issued to the branch (
access_idon the terminal config).
The value is stable per branch — it does not rotate on reconnect. Treat it as a credential: never log, never ship in a customer build.
Shape of the flow
Hybrid REST + STOMP. The trigger is a plain HTTPS call; the server relays it to the terminal over STOMP, and the hub collects the result by polling a REST status endpoint.
- Discover which terminals exist in your branch —
GET /api/v1/{system}/listTerminals?branchId={branchId}. - Trigger the command —
POST /api/v1/{system}/requestTerminalwith arequestType+ fields. Returns the request entity immediately, with a numericdata.idyou poll with. - Server relays the command to the target terminal over STOMP as a
PAYMENT_REQUEST/REFUND_REQUEST/CANCEL_REQUEST/PRINT_REQUESTframe. - Terminal processes it locally (same code path as Intent or LAN) and sends the result to
/app/commandResult/{wsToken}. - Collect the result — poll
POST /api/v1/{system}/requestStatuswith{ id: 7 }(the numericdata.idfrom the trigger). There is no STOMP result push to the hub.
Authentication
STOMP connection is Bearer-authed with a short-lived wsToken. REST calls use your normal OAuth JWT.
// 1. Exchange your OAuth app credentials for a JWT — see /docs/api/auth
POST /api/v1/oauth/token
Authorization: Basic base64(appId:appSecret)
// 2. Exchange the JWT for a WebSocket token tied to a specific terminal
POST /api/v1/terminal/ws-token
Authorization: Bearer <jwt>
Content-Type: application/json
{ "configId": 42 }
// Response — wsToken is the credential STOMP uses; it also tells you
// WHICH queue to subscribe to.
{
"status": true,
"data": {
"wsToken": "<opaque>",
"configId": 42,
"terminalId": "T-123",
"queueDestination": "/queue/terminal/42",
"expiresInSeconds": 3600
}
}Required OAuth scopes: ws:subscribe for the WS token, plus whatever the REST trigger needs (payments:direct, refunds:direct, etc. — see OAuth Scopes).
STOMP connect + subscribe
Plain STOMP 1.2 over wss://payus.co.nz/webs (raw STOMP); a SockJS endpoint is also available at /websocket. Pass the wsToken as a Bearer header on CONNECT.
CONNECT
accept-version:1.2
host:payus.co.nz
authorization:Bearer <wsToken>
heart-beat:10000,10000
^@
// Server replies with CONNECTED, then you subscribe to the queue
// the ws-token call told you about:
SUBSCRIBE
id:sub-0
destination:/queue/terminal/42
^@The server also broadcasts network-wide events on /topic/terminals; subscribe there too if you want to react to other terminals coming online.
Discover terminals
REST call. Returns every terminal in your branch — configId + terminalId are what you pass to requestTerminal.
GET /api/v1/pos/listTerminals?branchId=B-456
Authorization: Bearer <jwt>
// Optional: exclude the controller itself if it's also a terminal.
GET /api/v1/pos/listTerminals?branchId=B-456&excludeConfigId=42Trigger a command (requestTerminal)
The hub-side trigger. Four request types available today, dispatched by the server to the target terminal as STOMP frames.
POST /api/v1/pos/requestTerminal
Authorization: Bearer <jwt>
Content-Type: application/json
{
"configId": 42, // from listTerminals
"terminalId": "T-123", // " "
"branchId": "B-456",
"accessId": "<branch access id>",
"requestType": "PAY", // PAY | REFUND | CANCEL | PRINT
"grandTotal": "12.50", // amount (decimal string) — required for PAY and REFUND
"referenceId": "ORD-42", // optional, but the PAY idempotency key (see below)
"channel": "WECHAT", // optional for PAY — gateway
"tipAmount": "0.00", // optional PAY modifiers (decimal strings: tipAmount/feeAmount/
// donationAmount/discount) — declare the tip so a basket reconciles
"lineItems": [ /* optional itemised basket — integer-cents amounts; see note below */ ],
"body": "===…===" // required for PRINT (receipt text)
}
// Response — the full WinSockRequest entity. Poll with the numeric data.id.
{
"status": true,
"data": { "id": 7, "requestType": "PAY", "deliveryStatus": "SENT", /* … */ }
}Idempotency (PAY): send a stable referenceId — a retry with the same one returns the original request (same numeric id) instead of charging twice. Opt-in: omit it and each call is a fresh trigger. If the target terminal is offline the command is not queued — the response still carries anid, and requestStatus reports deliveryStatus: "TERMINAL_OFFLINE"; re-issue when it is back online.
Line items (PAY): attach an optional lineItems basket (same per-line shape as the REST payNow endpoint — name,quantity, unitPriceCents,lineTotalCents, taxCents?,sku?, category?; amounts in integer cents). It is reconciled server-side (strict, ±1c) against the merchandise total —grandTotal − tipAmount − feeAmount − donationAmount + discount — and a bad basket is rejected at requestTerminal time with HTTP 400 (ERR_INVALID_LINE_ITEMS / ERR_LINE_ITEM_MISMATCH) before anything reaches the terminal. The modifier fields are decimal strings on this REST surface (cents on LAN/Intent), declaration-only, and are not forwarded to the terminal. The basket itself IS forwarded in the PAYMENT_REQUEST (in requestInfo) so the terminal can itemise a receipt, and is persisted against the sale keyed by referenceId — so send one.
| requestType | Required payload fields | Server pushes to terminal |
|---|---|---|
| PAY | grandTotal | PAYMENT_REQUEST |
| REFUND | grandTotal | REFUND_REQUEST |
| CANCEL | (none — cancels in-flight payment on terminal) | CANCEL_REQUEST |
| body | PRINT_REQUEST |
Collect the result
Poll requestStatus with the numeric id the trigger returned. There is no STOMP result push to the hub — this REST poll is the only way a hub collects the outcome.
POST /api/v1/pos/requestStatus
Authorization: Bearer <jwt>
Content-Type: application/json
{ "id": 7 }
// Response — the WinSockRequest entity. status/executed are booleans;
// deliveryStatus is SENT or TERMINAL_OFFLINE; the terminal's actual
// result JSON is nested as a STRING inside requestInfo.
{
"status": true,
"data": {
"id": 7,
"requestType": "PAY",
"status": true,
"executed": true,
"deliveryStatus": "SENT",
"requestInfo": "{\"status\":\"SUCCESS\",\"transactionId\":\"TXN-123\",\"referenceId\":\"ORD-42\",\"amount\":\"12.50\",\"currency\":\"NZD\"}"
}
}Server → terminal action vocabulary
Full list of actionvalues the server can send on a terminal's STOMP queue — useful if you're implementing the terminal side. (POS controllers don't read these to collect a trigger result; they poll requestStatus.)
Trigger — server tells terminal to do something
PAYMENT_REQUESTREFUND_REQUESTCANCEL_REQUESTPRINT_REQUESTPAYMENT_DETAILS_REQUESTADMIN_MESSAGEStatus — server echoes progress + result
PAYMENT_STATUSREFUND_STATUSCANCEL_STATUSCOMMAND_RESULTConfig / live update
CONFIG_UPDATEDGATEWAY_TOGGLEDPROMO_UPDATEDFORCE_REFRESHNEW_ORDERSync + session
SYNC_ACKSETTINGS_APPLIEDRECEIPT_PUSHSESSION_KICKHELLOCHECK_INHEALTH_REQUESTTerminal-side SEND destinations
If you're writing a terminal (not a POS controller), these are the STOMP destinations the terminal sends TO — included for completeness.
| Destination | When |
|---|---|
| /app/hello/{wsToken} | On connect — identifies the terminal. |
| /app/checkIn/{wsToken} | Periodic heartbeat / presence. |
| /app/transactionComplete/{wsToken} | After any payment / refund completes locally. |
| /app/commandResult/{wsToken} | After a server-triggered command finishes. |
| /app/syncSettings/{wsToken} | Terminal pushes its config up for admin sync. |
Error handling
requestTerminalreturns HTTP 400 with{ status: false, message: "…" }on validation errors — norequestIdis issued.- A terminal that is offline (no live STOMP connection) will not receive the push — there is no durable queue, so the command is not delivered and is not replayed later. The trigger still returns
200+ arequestId, andrequestStatusreportsdeliveryStatus: "TERMINAL_OFFLINE"(vs"SENT") so you can tell "not delivered" from "still processing" instead of polling forever. Re-issue the command once the terminal is backonline(seelistTerminals.online). - Terminal errors surface in the
requestStatusresponse — a non-success result inside therequestInfoblob (with itsgatewayMessage). - STOMP connect failures surface as a STOMP
ERRORframe. Reconnect with a freshws-tokenif the token expired.
See also
- LAN integration — same command surface, no cloud.
- On-device integration (Intent API).
- WebSocket Events reference — read-only subscription with
ws:subscribe. - OAuth scopes.