Overview
Push notifications are messages automatically sent to your POS client when events occur. Unlike actions where you send a request and receive a response, notifications are initiated by the server when:
Terminals connect, disconnect, or enter reconnecting state
Payments complete (success or failure)
Payments are automatically voided due to late terminal reconnection
Notification Description terminalStatusUpdateTerminal connection state changed paymentCompletePayment finished processing paymentVoidedPayment was automatically voided
terminalStatusUpdate
Sent when a terminal connects, disconnects, or enters the reconnecting state.
Terminal Connected
{
"action" : "terminalStatusUpdate" ,
"terminalId" : "TERM-001" ,
"connectionId" : "abc123xyz" ,
"deviceId" : "TERM-001" ,
"status" : "connected" ,
"terminalInfo" : {
"connectionId" : "abc123xyz" ,
"terminalId" : "TERM-001" ,
"deviceId" : "TERM-001" ,
"connectedAt" : "2024-01-15T10:30:00.000Z" ,
"lastActivity" : "2024-01-15T10:30:00.000Z" ,
"status" : "online"
},
"timestamp" : "2024-01-15T10:30:00.000Z" ,
"groupId" : "group-123" ,
"version" : "v1"
}
Terminal Reconnecting
Sent when a terminal with a deviceId disconnects but may reconnect within the 60-second grace period:
{
"action" : "terminalStatusUpdate" ,
"terminalId" : "TERM-001" ,
"deviceId" : "TERM-001" ,
"status" : "reconnecting" ,
"timestamp" : "2024-01-15T10:45:00.000Z" ,
"groupId" : "group-123" ,
"version" : "v1"
}
Terminal Disconnected
Sent when a terminal is fully offline (either no deviceId configured or grace period expired):
{
"action" : "terminalStatusUpdate" ,
"terminalId" : "TERM-001" ,
"status" : "disconnected" ,
"timestamp" : "2024-01-15T10:45:00.000Z" ,
"groupId" : "group-123" ,
"version" : "v1"
}
Status Values
Status Description connectedTerminal has connected and is online reconnectingTerminal disconnected but may reconnect (60-second grace period) disconnectedTerminal is fully disconnected (no grace period)
Response Fields
Field Type Description actionstring terminalStatusUpdateterminalIdstring Terminal identifier connectionIdstring Connection ID (only for connected status) deviceIdstring Stable device identifier (if configured) statusstring connected, reconnecting, or disconnectedterminalInfoobject Full terminal info (only for connected status) timestampstring ISO 8601 timestamp groupIdstring Group identifier versionstring API version
Handling Terminal Status Updates
const terminals = new Map ();
function handleTerminalStatusUpdate ( message ) {
const { terminalId , status , terminalInfo } = message ;
switch ( status ) {
case 'connected' :
terminals . set ( terminalId , terminalInfo );
console . log ( `Terminal ${ terminalId } is now online` );
updateUI ( 'terminal-online' , terminalId );
break ;
case 'reconnecting' :
const terminal = terminals . get ( terminalId );
if ( terminal ) {
terminal . status = 'reconnecting' ;
}
console . log ( `Terminal ${ terminalId } is reconnecting (60s grace period)` );
updateUI ( 'terminal-reconnecting' , terminalId );
break ;
case 'disconnected' :
terminals . delete ( terminalId );
console . log ( `Terminal ${ terminalId } is offline` );
updateUI ( 'terminal-offline' , terminalId );
break ;
}
}
ws . on ( 'message' , ( data ) => {
const message = JSON . parse ( data . toString ());
if ( message . action === 'terminalStatusUpdate' ) {
handleTerminalStatusUpdate ( message );
}
});
paymentComplete
Sent when a terminal finishes processing a payment, regardless of success or failure.
Successful Payment
{
"action" : "paymentComplete" ,
"terminalId" : "TERM-001" ,
"connectionId" : "abc123xyz" ,
"terminalDeviceId" : "TERM-001" ,
"paymentResponse" : {
"transactionId" : "TXN-20240115-001" ,
"status" : "SUCCESS" ,
"amount" : "99.99" ,
"currency" : "USD" ,
"paymentMethod" : "CARD" ,
"authorizationCode" : "AUTH123456" ,
"receiptData" : "..." ,
"timestamp" : "2024-01-15T10:37:30.000Z" ,
"metadata" : {
"orderId" : "ORD-12345"
}
},
"timestamp" : "2024-01-15T10:37:30.000Z"
}
Failed Payment
{
"action" : "paymentComplete" ,
"terminalId" : "TERM-001" ,
"connectionId" : "abc123xyz" ,
"terminalDeviceId" : "TERM-001" ,
"paymentResponse" : {
"transactionId" : "TXN-20240115-002" ,
"status" : "FAILED" ,
"amount" : "99.99" ,
"currency" : "USD" ,
"paymentMethod" : "CARD" ,
"errorCode" : "INSUFFICIENT_FUNDS" ,
"errorMessage" : "Card declined due to insufficient funds" ,
"timestamp" : "2024-01-15T10:38:00.000Z" ,
"metadata" : {
"orderId" : "ORD-12346"
}
},
"timestamp" : "2024-01-15T10:38:00.000Z"
}
Response Fields
Field Type Description actionstring paymentCompleteterminalIdstring Terminal identifier connectionIdstring Terminal’s connection ID terminalDeviceIdstring Terminal’s stable device ID paymentResponseobject Payment result (see below) timestampstring ISO 8601 timestamp
paymentResponse Fields
Field Type Description transactionIdstring Your original transaction identifier statusstring SUCCESS, FAILED, PENDING, or CANCELLEDamountstring Payment amount currencystring Currency code paymentMethodstring Payment method used authorizationCodestring Authorization code (success only) errorCodestring Error code (failure only) errorMessagestring Human-readable error (failure only) receiptDatastring Receipt data from terminal timestampstring ISO 8601 timestamp metadataobject Your custom metadata returned
Payment Status Definitions
Status Description SUCCESSPayment was successfully processed and approved FAILEDPayment was declined or encountered an error PENDINGPayment is still being processed CANCELLEDPayment was cancelled by user or operator
Handling Payment Completions
const pendingPayments = new Map ();
function handlePaymentComplete ( message ) {
const { paymentResponse } = message ;
const { transactionId , status } = paymentResponse ;
// Remove from pending
pendingPayments . delete ( transactionId );
switch ( status ) {
case 'SUCCESS' :
console . log ( `Payment ${ transactionId } successful` );
console . log ( `Authorization: ${ paymentResponse . authorizationCode } ` );
// Update order status
updateOrderStatus ( paymentResponse . metadata . orderId , 'PAID' );
// Print receipt
if ( paymentResponse . receiptData ) {
printReceipt ( paymentResponse . receiptData );
}
// Notify staff
showNotification ( 'Payment successful' , 'success' );
break ;
case 'FAILED' :
console . log ( `Payment ${ transactionId } failed: ${ paymentResponse . errorMessage } ` );
// Update order status
updateOrderStatus ( paymentResponse . metadata . orderId , 'PAYMENT_FAILED' );
// Notify staff with error
showNotification ( `Payment failed: ${ paymentResponse . errorMessage } ` , 'error' );
break ;
case 'CANCELLED' :
console . log ( `Payment ${ transactionId } cancelled` );
updateOrderStatus ( paymentResponse . metadata . orderId , 'CANCELLED' );
showNotification ( 'Payment cancelled' , 'warning' );
break ;
case 'PENDING' :
console . log ( `Payment ${ transactionId } pending` );
// Re-add to pending with timeout check
pendingPayments . set ( transactionId , paymentResponse );
break ;
}
}
ws . on ( 'message' , ( data ) => {
const message = JSON . parse ( data . toString ());
if ( message . action === 'paymentComplete' ) {
handlePaymentComplete ( message );
}
});
paymentVoided
Sent when a payment is automatically voided because the terminal reconnected after the 60-second grace period with a successful payment result.
Why This Happens
If a terminal disconnects during payment processing and reconnects after the 60-second grace period:
Your POS system has already given up waiting for the result
The terminal may have actually processed the payment successfully
To prevent the customer being charged without your knowledge, the payment is automatically voided
You receive a paymentVoided notification with the original payment details
{
"action" : "paymentVoided" ,
"terminalId" : "TERM-001" ,
"transactionId" : "TXN-20240115-001" ,
"reason" : "Terminal reconnected after grace period expired" ,
"originalPaymentResponse" : {
"transactionId" : "TXN-20240115-001" ,
"status" : "SUCCESS" ,
"amount" : "99.99" ,
"currency" : "USD"
},
"timestamp" : "2024-01-15T10:38:30.000Z"
}
Response Fields
Field Type Description actionstring paymentVoidedterminalIdstring Terminal identifier transactionIdstring Original transaction ID reasonstring Reason for voiding originalPaymentResponseobject The original successful payment that was voided timestampstring ISO 8601 timestamp
Handling Voided Payments
Always handle paymentVoided notifications to inform staff that a late payment was automatically reversed. This prevents confusion when the terminal shows “approved” but no payment was actually collected.
function handlePaymentVoided ( message ) {
const { transactionId , reason , originalPaymentResponse } = message ;
console . log ( `Payment ${ transactionId } was automatically voided` );
console . log ( `Reason: ${ reason } ` );
console . log ( `Original amount: ${ originalPaymentResponse . amount } ` );
// Update order to require new payment
updateOrderStatus ( transactionId , 'VOIDED' );
// Alert staff
showNotification (
`Payment for ${ originalPaymentResponse . amount } ${ originalPaymentResponse . currency } ` +
`was voided: Terminal reconnected too late. Please retry the payment.` ,
'warning'
);
// Log for audit
logVoidedPayment ({
transactionId ,
reason ,
originalAmount: originalPaymentResponse . amount ,
voidedAt: message . timestamp
});
}
ws . on ( 'message' , ( data ) => {
const message = JSON . parse ( data . toString ());
if ( message . action === 'paymentVoided' ) {
handlePaymentVoided ( message );
}
});
Complete Message Handler
Here’s a complete example that handles all notification types:
const WebSocket = require ( 'ws' );
const ws = new WebSocket ( 'wss://{your-api-endpoint}/v1' , {
headers: { 'x-api-key' : process . env . MODULUS_API_KEY }
});
ws . on ( 'message' , ( data ) => {
const message = JSON . parse ( data . toString ());
switch ( message . action ) {
// Actions responses
case 'terminalsResponse' :
handleTerminalsResponse ( message );
break ;
case 'pong' :
handlePong ();
break ;
// Push notifications
case 'terminalStatusUpdate' :
handleTerminalStatusUpdate ( message );
break ;
case 'paymentComplete' :
handlePaymentComplete ( message );
break ;
case 'paymentVoided' :
handlePaymentVoided ( message );
break ;
// Connection events
case 'forceDisconnect' :
handleForceDisconnect ( message );
break ;
// Errors
case undefined :
if ( message . error ) {
handleError ( message );
}
break ;
default :
console . log ( 'Unknown message:' , message );
}
});
Best Practices
Handle All Notifications Implement handlers for all notification types to avoid missing important events
Idempotency Design handlers to be idempotent. The same notification may arrive multiple times due to network issues.
Logging Log all notifications with timestamps for debugging and audit trails
UI Updates Update your UI immediately when notifications arrive for a responsive user experience
Error Handling Wrap notification handlers in try/catch to prevent a single bad message from crashing your app
State Management Maintain local state (terminal list, pending payments) that updates based on notifications
Next Steps