Subscribe to Terminal Events
In cloud integration mode, after you place an order via Nexus API, the terminal goes through a series of steps — card presentation, PIN entry, authorization, signature capture, receipt printing — before returning to idle. Terminal Events push these intermediate states to you in real time, so you can:
- Drive your UI with live progress (e.g., show “Waiting for card”, “Processing”, “Printing receipt”)
- Know exactly when the terminal is idle and ready for the next transaction
- Avoid sending a new order while the terminal is still busy (which would be rejected)
Terminal Events vs. Transaction Result Webhook: The Transaction Result Webhook tells you the payment outcome (success / failure). Terminal Events tell you what the terminal is doing throughout the process. They serve different purposes and are complementary.
Why You Need This
After your server receives the transaction result webhook, the terminal may still be capturing a signature or printing a receipt. If you immediately dispatch another order, the terminal will reject it because it can only process one transaction at a time.
Terminal Events solve this by giving you a TRANSACTION_ENDED event that signals the terminal has completed all post-authorization steps and is back to idle — the safe moment to send the next order.
Interactive Demo
The animation below shows a POS application placing a transaction via the SUNBAY cloud, the events triggered during the transaction being pushed back through webhooks, and how the POS UI updates in real time. Click “Play” to try it:
Event Lifecycle
Every transaction produces events across four stages:
Stage Overview
| Stage | Event | Required | Description |
|---|---|---|---|
| Pre-Payment | ORDER_RECEIVED | Yes | Terminal accepted the order. If the terminal rejects (busy / invalid params / not signed in), it skips directly to TRANSACTION_ENDED. |
| Payment | PAYMENT_PRESENTED | No | Payment medium presented: card swipe / insert / tap, or QR code scan. |
PIN_ENTERING | No | Cardholder is entering PIN (only when PIN is required). | |
PAYMENT_PROCESSING | No | Transaction is being processed (authorization in progress). | |
| Post-Payment | SIGNING | No | Capturing signature (skipped for no-signature transactions). |
PRINTING | No | Receipt is being printed or electronic receipt is being generated. | |
PRINT_COMPLETED | No | Printing finished (success or failure). | |
| End | TRANSACTION_ENDED | Yes | Terminal has completed all steps and returned to idle. Safe to dispatch next order. |
TRANSACTION_ENDED does not indicate transaction success or failure. It only means the terminal is idle. Use the Transaction Result Webhook or Transaction Query API to determine the payment outcome.
Handling Tips
- Required events guarantee a stable state-machine skeleton. Your integration can rely on receiving at least:
ORDER_RECEIVED→TRANSACTION_ENDED. - Optional events fire only when the corresponding step actually happens. Drive your UI by event type — do not assume a fixed sequence of all events.
- Abnormal exits: Any stage can terminate early. The flow always ends with
TRANSACTION_ENDEDregardless of where the interruption occurred.
Event Payload
Field Reference
| Name | Type | Required | Description |
|---|---|---|---|
topic | string | Yes | Fixed value: SEMI_INT_CLOUD_TERMINAL_EVENT |
messageId | string | Yes | Globally unique ID for idempotent deduplication |
terminalSn | string | Yes | Terminal serial number |
timestamp | string | Yes | RFC 3339 timestamp when the event was generated on the terminal |
payload | object | Yes | Event business data |
Examples
PAYMENT_PRESENTED (card tap):
{
"topic": "SEMI_INT_CLOUD_TERMINAL_EVENT",
"messageId": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"terminalSn": "P200-SN12345",
"timestamp": "2026-07-08T10:15:30.123Z",
"payload": {
"eventStage": "PAYMENT",
"eventType": "PAYMENT_PRESENTED",
"appId": "app_001",
"merchantId": "M0001",
"transactionRequestId": "TR20260708001",
"transactionId": "TXN20260708001",
"paymentMethod": {
"category": "CARD",
"entryMode": "CONTACTLESS"
}
}
}PRINT_COMPLETED (print failed):
{
"topic": "SEMI_INT_CLOUD_TERMINAL_EVENT",
"messageId": "c5f9d4e3af6a6b8c1e3d7fa4c9b5d2e",
"terminalSn": "P200-SN12345",
"timestamp": "2026-07-08T10:15:45.789Z",
"payload": {
"eventStage": "POST_PAYMENT",
"eventType": "PRINT_COMPLETED",
"appId": "app_001",
"merchantId": "M0001",
"transactionRequestId": "TR20260708001",
"transactionId": "TXN20260708001",
"receipt": {
"printOption": "MERCHANT",
"printResult": "FAILED",
"printFailReason": "Out of paper"
}
}
}TRANSACTION_ENDED:
{
"topic": "SEMI_INT_CLOUD_TERMINAL_EVENT",
"messageId": "d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2",
"terminalSn": "P200-SN12345",
"timestamp": "2026-07-08T10:15:50.456Z",
"payload": {
"eventStage": "END",
"eventType": "TRANSACTION_ENDED",
"appId": "app_001",
"merchantId": "M0001",
"transactionRequestId": "TR20260708001",
"transactionId": "TXN20260708001"
}
}How to Subscribe
Subscription is per-transaction: include a terminalEventNotifyUrl parameter when placing an order, and SUNBAY will push all terminal events for that transaction to that URL. Different transactions can subscribe to different URLs, or opt out entirely by omitting the parameter.
Supported APIs
The following transaction APIs accept terminalEventNotifyUrl:
| API | Path | Always produces terminal events? |
|---|---|---|
| Sale | /v1/semi-integration/transaction/sale | Yes |
| Pre-authorization | /v1/semi-integration/transaction/auth | Yes |
| Forced Authorization | /v1/semi-integration/transaction/forced-auth | Yes |
| Incremental Authorization | /v1/semi-integration/transaction/incremental-auth | Only when pushToTerminal=true |
| Pre-Authorization Completion | /v1/semi-integration/transaction/post-auth | Only when pushToTerminal=true |
| Refund | /v1/semi-integration/transaction/refund | Only when pushToTerminal=true |
| Void | /v1/semi-integration/transaction/void | Only when pushToTerminal=true |
About pushToTerminal: Incremental Authorization, Pre-Authorization Completion, Refund, and Void can be executed either purely on the server or dispatched to the terminal (e.g., a refund that requires the cardholder to re-present the card). The transaction only involves the terminal — and therefore only produces terminal events — when pushToTerminal=true. When pushToTerminal=false, the transaction is server-only and no terminal events will be pushed even if terminalEventNotifyUrl is set.
Request Parameter
Add the following field to your transaction request body:
| Field | Type | Required | Description |
|---|---|---|---|
terminalEventNotifyUrl | string | No | HTTPS URL to receive terminal events. If omitted, no terminal events are pushed. |
Example:
{
"transactionRequestId": "TR20260708001",
"referenceOrderId": "ORDER_10001",
"priceCurrency": "USD",
"orderAmount": 1000,
"notifyUrl": "https://your-server.com/webhooks/transaction-result",
"terminalEventNotifyUrl": "https://your-server.com/webhooks/terminal-events"
}notifyUrl and terminalEventNotifyUrl are independent parameters. Configure them to point to two separate endpoints — the payloads have different structures (transaction result vs. terminal event envelope), and keeping the endpoints separate keeps your routing and handling logic clean and decoupled.
Delivery
Events are delivered via HTTP POST with Content-Type: application/json, using the exact same headers and security model as the Transaction Result Webhook:
- HMAC-SHA256 signature via
X-Signatureheader - Same Webhook Secret as the transaction result webhook
X-Client-Request-Id/X-Timestampheaders for tracing and replay protection
Your endpoint must return HTTP 200 to acknowledge receipt. Signature verification code from your transaction result webhook can be reused as-is.
Reliability
Ordering
Events for the same transactionId are ordered by timestamp. If you receive an event with a timestamp earlier than the latest event you already processed for that transaction, treat it as stale and discard it.
Idempotency
Use messageId to deduplicate. You may receive the same event more than once due to network retries.
Retry
Terminal events are time-sensitive — delivering them late defeats the purpose. We therefore use short, fast retries only. If your endpoint returns a non-HTTP 200 response or times out:
| Attempt | Interval |
|---|---|
| 1 | 1 second |
| 2 | 5 seconds |
| 3 | 30 seconds |
Events that still fail after three attempts are dropped and not replayed. This is intentionally different from the transaction result webhook’s long retry policy: terminal events are a live state stream, and stale events are of no use to the developer.
Circuit Breaking
If your endpoint fails consecutively past our threshold, we suspend delivery for 10 minutes to protect both systems. Delivery resumes automatically after the cooldown. Events generated during the suspension are not replayed; only events for new transactions are pushed after recovery.
Best Practices
-
Gate your next order on
TRANSACTION_ENDED: Do not dispatch a new transaction to the same terminal until you receive this event. The terminal will reject concurrent orders. -
Use events for UI, not for business logic: Terminal Events tell you process state, not payment outcome. Always rely on the Transaction Result Webhook or Query API for final payment status.
-
Handle missing optional events gracefully: Not every transaction will produce
PIN_ENTERING,SIGNING, orPRINTING. Design your UI to skip smoothly to whatever event arrives next. -
Deduplicate by
messageId: Network conditions may cause duplicate delivery. UsemessageIdas an idempotency key (e.g., store in Redis with a 24-hour TTL). -
Implement a timeout fallback: If you do not receive
TRANSACTION_ENDEDwithin a reasonable window (e.g., 5 minutes), query the terminal status via API rather than waiting indefinitely. -
Subscribe only when you need it:
terminalEventNotifyUrlis optional. For transactions where live process feedback isn’t needed (e.g., back-office server-initiated flows), omit the parameter to avoid unnecessary webhook traffic.