> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moduluslabs.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Push Notifications

> Receive real-time updates for terminal status changes and payment completions

## 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                       |
| ---------------------- | --------------------------------- |
| `terminalStatusUpdate` | Terminal connection state changed |
| `paymentComplete`      | Payment finished processing       |
| `paymentVoided`        | Payment was automatically voided  |

## terminalStatusUpdate

Sent when a terminal connects, disconnects, or enters the reconnecting state.

### Terminal Connected

```json theme={null}
{
  "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:

```json theme={null}
{
  "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):

```json theme={null}
{
  "action": "terminalStatusUpdate",
  "terminalId": "TERM-001",
  "status": "disconnected",
  "timestamp": "2024-01-15T10:45:00.000Z",
  "groupId": "group-123",
  "version": "v1"
}
```

### Status Values

| Status         | Description                                                      |
| -------------- | ---------------------------------------------------------------- |
| `connected`    | Terminal has connected and is online                             |
| `reconnecting` | Terminal disconnected but may reconnect (60-second grace period) |
| `disconnected` | Terminal is fully disconnected (no grace period)                 |

### Response Fields

| Field          | Type   | Description                                      |
| -------------- | ------ | ------------------------------------------------ |
| `action`       | string | `terminalStatusUpdate`                           |
| `terminalId`   | string | Terminal identifier                              |
| `connectionId` | string | Connection ID (only for `connected` status)      |
| `deviceId`     | string | Stable device identifier (if configured)         |
| `status`       | string | `connected`, `reconnecting`, or `disconnected`   |
| `terminalInfo` | object | Full terminal info (only for `connected` status) |
| `timestamp`    | string | ISO 8601 timestamp                               |
| `groupId`      | string | Group identifier                                 |
| `version`      | string | API version                                      |

### Handling Terminal Status Updates

<CodeGroup>
  ```javascript Node.js theme={null}
  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);
    }
  });
  ```

  ```python Python theme={null}
  terminals = {}

  async def handle_terminal_status_update(message):
      terminal_id = message['terminalId']
      status = message['status']
      terminal_info = message.get('terminalInfo')

      if status == 'connected':
          terminals[terminal_id] = terminal_info
          print(f"Terminal {terminal_id} is now online")
          update_ui('terminal-online', terminal_id)

      elif status == 'reconnecting':
          if terminal_id in terminals:
              terminals[terminal_id]['status'] = 'reconnecting'
          print(f"Terminal {terminal_id} is reconnecting (60s grace period)")
          update_ui('terminal-reconnecting', terminal_id)

      elif status == 'disconnected':
          terminals.pop(terminal_id, None)
          print(f"Terminal {terminal_id} is offline")
          update_ui('terminal-offline', terminal_id)

  async def handle_message(message):
      if message.get('action') == 'terminalStatusUpdate':
          await handle_terminal_status_update(message)
  ```
</CodeGroup>

***

## paymentComplete

Sent when a terminal finishes processing a payment, regardless of success or failure.

### Successful Payment

```json theme={null}
{
  "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

```json theme={null}
{
  "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                 |
| ------------------ | ------ | --------------------------- |
| `action`           | string | `paymentComplete`           |
| `terminalId`       | string | Terminal identifier         |
| `connectionId`     | string | Terminal's connection ID    |
| `terminalDeviceId` | string | Terminal's stable device ID |
| `paymentResponse`  | object | Payment result (see below)  |
| `timestamp`        | string | ISO 8601 timestamp          |

### paymentResponse Fields

| Field               | Type   | Description                                    |
| ------------------- | ------ | ---------------------------------------------- |
| `transactionId`     | string | Your original transaction identifier           |
| `status`            | string | `SUCCESS`, `FAILED`, `PENDING`, or `CANCELLED` |
| `amount`            | string | Payment amount                                 |
| `currency`          | string | Currency code                                  |
| `paymentMethod`     | string | Payment method used                            |
| `authorizationCode` | string | Authorization code (success only)              |
| `errorCode`         | string | Error code (failure only)                      |
| `errorMessage`      | string | Human-readable error (failure only)            |
| `receiptData`       | string | Receipt data from terminal                     |
| `timestamp`         | string | ISO 8601 timestamp                             |
| `metadata`          | object | Your custom metadata returned                  |

### Payment Status Definitions

| Status      | Description                                     |
| ----------- | ----------------------------------------------- |
| `SUCCESS`   | Payment was successfully processed and approved |
| `FAILED`    | Payment was declined or encountered an error    |
| `PENDING`   | Payment is still being processed                |
| `CANCELLED` | Payment was cancelled by user or operator       |

### Handling Payment Completions

<CodeGroup>
  ```javascript Node.js theme={null}
  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);
    }
  });
  ```

  ```python Python theme={null}
  pending_payments = {}

  async def handle_payment_complete(message):
      payment_response = message['paymentResponse']
      transaction_id = payment_response['transactionId']
      status = payment_response['status']

      # Remove from pending
      pending_payments.pop(transaction_id, None)

      if status == 'SUCCESS':
          print(f"Payment {transaction_id} successful")
          print(f"Authorization: {payment_response['authorizationCode']}")

          # Update order status
          await update_order_status(
              payment_response['metadata']['orderId'],
              'PAID'
          )

          # Print receipt
          if payment_response.get('receiptData'):
              print_receipt(payment_response['receiptData'])

          # Notify staff
          show_notification('Payment successful', 'success')

      elif status == 'FAILED':
          print(f"Payment {transaction_id} failed: {payment_response['errorMessage']}")

          # Update order status
          await update_order_status(
              payment_response['metadata']['orderId'],
              'PAYMENT_FAILED'
          )

          # Notify staff with error
          show_notification(f"Payment failed: {payment_response['errorMessage']}", 'error')

      elif status == 'CANCELLED':
          print(f"Payment {transaction_id} cancelled")
          await update_order_status(
              payment_response['metadata']['orderId'],
              'CANCELLED'
          )
          show_notification('Payment cancelled', 'warning')

      elif status == 'PENDING':
          print(f"Payment {transaction_id} pending")
          # Re-add to pending with timeout check
          pending_payments[transaction_id] = payment_response

  async def handle_message(message):
      if message.get('action') == 'paymentComplete':
          await handle_payment_complete(message)
  ```
</CodeGroup>

***

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

```json theme={null}
{
  "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                                     |
| ------------------------- | ------ | ----------------------------------------------- |
| `action`                  | string | `paymentVoided`                                 |
| `terminalId`              | string | Terminal identifier                             |
| `transactionId`           | string | Original transaction ID                         |
| `reason`                  | string | Reason for voiding                              |
| `originalPaymentResponse` | object | The original successful payment that was voided |
| `timestamp`               | string | ISO 8601 timestamp                              |

### Handling Voided Payments

<Warning>
  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.
</Warning>

<CodeGroup>
  ```javascript Node.js theme={null}
  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);
    }
  });
  ```

  ```python Python theme={null}
  async def handle_payment_voided(message):
      transaction_id = message['transactionId']
      reason = message['reason']
      original_response = message['originalPaymentResponse']

      print(f"Payment {transaction_id} was automatically voided")
      print(f"Reason: {reason}")
      print(f"Original amount: {original_response['amount']}")

      # Update order to require new payment
      await update_order_status(transaction_id, 'VOIDED')

      # Alert staff
      show_notification(
          f"Payment for {original_response['amount']} {original_response['currency']} "
          f"was voided: Terminal reconnected too late. Please retry the payment.",
          'warning'
      )

      # Log for audit
      log_voided_payment({
          'transactionId': transaction_id,
          'reason': reason,
          'originalAmount': original_response['amount'],
          'voidedAt': message['timestamp']
      })

  async def handle_message(message):
      if message.get('action') == 'paymentVoided':
          await handle_payment_voided(message)
  ```
</CodeGroup>

***

## Complete Message Handler

Here's a complete example that handles all notification types:

<CodeGroup>
  ```javascript Node.js theme={null}
  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);
    }
  });
  ```

  ```python Python theme={null}
  import asyncio
  import websockets
  import json
  import os

  async def handle_message(message):
      action = message.get('action')

      # Actions responses
      if action == 'terminalsResponse':
          await handle_terminals_response(message)
      elif action == 'pong':
          handle_pong()

      # Push notifications
      elif action == 'terminalStatusUpdate':
          await handle_terminal_status_update(message)
      elif action == 'paymentComplete':
          await handle_payment_complete(message)
      elif action == 'paymentVoided':
          await handle_payment_voided(message)

      # Connection events
      elif action == 'forceDisconnect':
          await handle_force_disconnect(message)

      # Errors
      elif message.get('error'):
          handle_error(message)

      else:
          print(f"Unknown message: {message}")

  async def main():
      headers = {'x-api-key': os.getenv('MODULUS_API_KEY')}

      async with websockets.connect(
          'wss://{your-api-endpoint}/v1',
          extra_headers=headers
      ) as ws:
          async for message in ws:
              data = json.loads(message)
              await handle_message(data)

  asyncio.run(main())
  ```
</CodeGroup>

## Best Practices

<CardGroup cols={2}>
  <Card title="Handle All Notifications" icon="list-check">
    Implement handlers for all notification types to avoid missing important events
  </Card>

  <Card title="Idempotency" icon="repeat">
    Design handlers to be idempotent. The same notification may arrive multiple times due to network issues.
  </Card>

  <Card title="Logging" icon="file-lines">
    Log all notifications with timestamps for debugging and audit trails
  </Card>

  <Card title="UI Updates" icon="display">
    Update your UI immediately when notifications arrive for a responsive user experience
  </Card>

  <Card title="Error Handling" icon="triangle-exclamation">
    Wrap notification handlers in try/catch to prevent a single bad message from crashing your app
  </Card>

  <Card title="State Management" icon="database">
    Maintain local state (terminal list, pending payments) that updates based on notifications
  </Card>
</CardGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="Data Types Reference" icon="brackets-curly" href="../reference">
    Complete schema documentation for all data types
  </Card>

  <Card title="Actions Reference" icon="paper-plane" href="./actions">
    Send commands to terminals
  </Card>

  <Card title="Quickstart" icon="rocket" href="./quickstart">
    Review connection and authentication
  </Card>

  <Card title="Introduction" icon="book" href="../introduction">
    Return to Terminal Gateway overview
  </Card>
</CardGroup>
