Skip to main content

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
NotificationDescription
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

StatusDescription
connectedTerminal has connected and is online
reconnectingTerminal disconnected but may reconnect (60-second grace period)
disconnectedTerminal is fully disconnected (no grace period)

Response Fields

FieldTypeDescription
actionstringterminalStatusUpdate
terminalIdstringTerminal identifier
connectionIdstringConnection ID (only for connected status)
deviceIdstringStable device identifier (if configured)
statusstringconnected, reconnecting, or disconnected
terminalInfoobjectFull terminal info (only for connected status)
timestampstringISO 8601 timestamp
groupIdstringGroup identifier
versionstringAPI 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

FieldTypeDescription
actionstringpaymentComplete
terminalIdstringTerminal identifier
connectionIdstringTerminal’s connection ID
terminalDeviceIdstringTerminal’s stable device ID
paymentResponseobjectPayment result (see below)
timestampstringISO 8601 timestamp

paymentResponse Fields

FieldTypeDescription
transactionIdstringYour original transaction identifier
statusstringSUCCESS, FAILED, PENDING, or CANCELLED
amountstringPayment amount
currencystringCurrency code
paymentMethodstringPayment method used
authorizationCodestringAuthorization code (success only)
errorCodestringError code (failure only)
errorMessagestringHuman-readable error (failure only)
receiptDatastringReceipt data from terminal
timestampstringISO 8601 timestamp
metadataobjectYour custom metadata returned

Payment Status Definitions

StatusDescription
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:
  1. Your POS system has already given up waiting for the result
  2. The terminal may have actually processed the payment successfully
  3. To prevent the customer being charged without your knowledge, the payment is automatically voided
  4. You receive a paymentVoided notification with the original payment details

Message Format

{
  "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

FieldTypeDescription
actionstringpaymentVoided
terminalIdstringTerminal identifier
transactionIdstringOriginal transaction ID
reasonstringReason for voiding
originalPaymentResponseobjectThe original successful payment that was voided
timestampstringISO 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