Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.stablegenius.co/llms.txt

Use this file to discover all available pages before exploring further.

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
Need the lowest latency? Contact us about direct integration options for real-time payment notifications on your terminals.

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.
Building on Android? Our first-party POS terminal (Genie) runs on Android. Contact us about licensing the terminal software or building on our hardware platform.