Skip to main content

POS Terminal Integration

This guide walks through integrating Stable Genius payments into a physical point-of-sale terminal. By the end, your terminal will display a QR code, detect payment, and confirm the sale — all through two API calls.

Overview

The integration pattern for POS terminals is:
  1. Your terminal creates a payment intent when the cashier finalizes an order
  2. The terminal displays the QR code on screen
  3. The customer scans and pays from their wallet
  4. Your terminal receives a webhook and shows a success screen

Prerequisites

  • Stable Genius API key (sk_test_* for development)
  • A merchant onboarded via the dashboard
  • A terminal with a screen capable of displaying QR codes
  • Network connectivity (WiFi or cellular) for API calls and webhooks

Step 1: Create Payment Intent at Checkout

When the cashier presses “Pay” or “Charge,” your terminal calls the API:
// Terminal-side code (runs on your POS hardware)
async function initiatePayment(amount, orderId) {
  const response = await fetch('https://api.stablegenius.co/v1/payment-intents', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      amount: amount,
      currency: 'usd',
      merchant_id: MERCHANT_ID,
      metadata: {
        order_id: orderId,
        terminal_id: TERMINAL_ID,
      },
    }),
  });

  return response.json();
}

Step 2: Display QR Code

Render the QR code on the terminal screen. You have two options: Option A: Render from payload (recommended for terminals with QR libraries)
import QRCode from 'qrcode'; // or your platform's QR library

const paymentIntent = await initiatePayment(4.50, 'order_456');
const qrImage = await QRCode.toDataURL(paymentIntent.qr_payload, {
  width: 300,
  margin: 2,
  color: { dark: '#000000', light: '#FFFFFF' },
});

// Display qrImage on terminal screen
displayOnScreen(qrImage, {
  amount: paymentIntent.amount,
  expiresAt: paymentIntent.expires_at,
});
Option B: Display hosted image (simpler, no QR library needed)
const paymentIntent = await initiatePayment(4.50, 'order_456');

// Display the hosted QR image directly
displayImageOnScreen(paymentIntent.qr_image_url, {
  amount: paymentIntent.amount,
});

Step 3: Wait for Payment Confirmation

Your backend receives the webhook when the customer pays. Push the confirmation to the terminal:
// Your backend server
app.post('/webhooks/stablegenius', (req, res) => {
  res.status(200).json({ received: true });

  const event = req.body;

  if (event.type === 'payment_intent.confirmed') {
    const { metadata, amount, tx_hash } = event.data;

    // Notify the specific terminal
    pushToTerminal(metadata.terminal_id, {
      type: 'payment_confirmed',
      amount: amount,
      tx_hash: tx_hash,
      order_id: metadata.order_id,
    });
  }

  if (event.type === 'payment_intent.expired') {
    const { metadata } = event.data;
    pushToTerminal(metadata.terminal_id, {
      type: 'payment_expired',
      order_id: metadata.order_id,
    });
  }
});

Step 4: Show Success on Terminal

When the terminal receives the confirmation push:
// Terminal-side code
onPaymentUpdate((update) => {
  switch (update.type) {
    case 'payment_confirmed':
      showSuccessScreen({
        amount: update.amount,
        message: 'Payment received!',
      });
      playSuccessSound();
      printReceipt(update);
      break;

    case 'payment_expired':
      showExpiredScreen({
        message: 'Payment timed out. Tap to retry.',
      });
      break;
  }
});

Terminal-to-Server Communication

The webhook fires to your backend, not directly to the terminal. You need a way to push updates from your backend to the terminal. Common patterns:
MethodBest ForLatency
WebSocketAlways-connected terminalsUnder 100ms
MQTTIoT/embedded terminalsUnder 200ms
Server-Sent Events (SSE)Web-based POSUnder 500ms
PollingSimple integrations1-3s
Stable Genius uses MQTT for its own POS terminals. If your terminal supports MQTT, contact us about direct MQTT integration for the lowest latency payment notifications.

Handling Edge Cases

Customer sends wrong amount: The payment intent tracks the expected amount. If the customer sends more or less, the transaction.created webhook fires with the actual amount. Your system should reconcile and handle over/underpayments per your business logic. Network disconnection: If the terminal loses connectivity after displaying the QR code, the payment still works — the customer’s on-chain transaction is independent of your terminal’s connection. When connectivity resumes, the webhook will be retried and delivered. Terminal reboot mid-payment: Store the active payment_intent_id locally. On reboot, call GET /v1/payment-intents/{id} to check if payment was received while offline.

Full Example

See our example POS integration on GitHub for a complete working implementation.
Building on Android? Our first-party POS terminal (Genie) runs on Android with Kotlin/Jetpack Compose. Contact us about licensing the terminal software or building on our hardware platform.