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 accessId field on each entry of listTerminals (REST — see API Reference → Terminals).
  • From the activation credentials the admin issued to the branch (access_id on 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.

  1. Discover which terminals exist in your branch — GET /api/v1/{system}/listTerminals?branchId={branchId}.
  2. Trigger the command — POST /api/v1/{system}/requestTerminal with a requestType + fields. Returns the request entity immediately, with a numeric data.id you poll with.
  3. Server relays the command to the target terminal over STOMP as a PAYMENT_REQUEST / REFUND_REQUEST / CANCEL_REQUEST / PRINT_REQUEST frame.
  4. Terminal processes it locally (same code path as Intent or LAN) and sends the result to /app/commandResult/{wsToken}.
  5. Collect the result — poll POST /api/v1/{system}/requestStatus with { id: 7 } (the numeric data.id from 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=42

Trigger 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.

requestTypeRequired payload fieldsServer pushes to terminal
PAYgrandTotalPAYMENT_REQUEST
REFUNDgrandTotalREFUND_REQUEST
CANCEL(none — cancels in-flight payment on terminal)CANCEL_REQUEST
PRINTbodyPRINT_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_MESSAGE

Status — server echoes progress + result

PAYMENT_STATUSREFUND_STATUSCANCEL_STATUSCOMMAND_RESULT

Config / live update

CONFIG_UPDATEDGATEWAY_TOGGLEDPROMO_UPDATEDFORCE_REFRESHNEW_ORDER

Sync + session

SYNC_ACKSETTINGS_APPLIEDRECEIPT_PUSHSESSION_KICKHELLOCHECK_INHEALTH_REQUEST

Terminal-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.

DestinationWhen
/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

  • requestTerminal returns HTTP 400 with { status: false, message: "…" } on validation errors — no requestId is 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 + a requestId, and requestStatus reports deliveryStatus: "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 back online (see listTerminals.online).
  • Terminal errors surface in the requestStatus response — a non-success result inside the requestInfo blob (with itsgatewayMessage).
  • STOMP connect failures surface as a STOMP ERROR frame. Reconnect with a fresh ws-token if the token expired.

See also