Skip to main content

QR Card Scan

QR Card Scan enables POS systems to identify customers by displaying a QR code that customers scan with their Loyalty.lt app. Unlike QR Login, this returns the customer’s loyalty card data without authentication tokens.

How It Works

1

Generate QR Session

POS system generates a QR code for customer identification
2

Display on Customer Screen

QR code is displayed on the customer-facing display
3

Customer Scans

Customer scans the QR code with Loyalty.lt mobile app
4

Automatic Identification

POS receives customer’s loyalty card data automatically (no confirmation needed)
5

Process Transaction

POS can now award points, apply discounts, etc.

Implementation

1. Generate QR Card Session

const session = await sdk.generateQrCardSession(
  'POS Terminal #1',  // Device name
  shopId              // Optional shop ID
);

console.log(session.session_id);  // Unique session ID
console.log(session.qr_code);     // Deep link for QR code
console.log(session.expires_at);  // Expiration time

2. Display QR Code

// Generate QR code image URL using Loyalty.lt API
const qrImageUrl = `https://api.loyalty.lt/qr?data=${encodeURIComponent(session.qr_code)}&size=250`;

// Update customer display
document.getElementById('customer-qr').src = qrImageUrl;

3. Subscribe to Card Identification Events

import Ably from 'ably';

// Get Ably client options with automatic token renewal
const ablyOptions = await sdk.createAblyClientOptions(session.session_id);

// Connect to Ably (token will auto-renew before expiry)
const ably = new Ably.Realtime(ablyOptions);
const tokenResponse = await sdk.getAblyToken(session.session_id);
const channel = ably.channels.get(tokenResponse.channel);

// Listen for card identification
channel.subscribe('card_identified', (message) => {
  const cardData = message.data.card_data;
  
  console.log('Customer identified!');
  console.log('Name:', cardData.user?.name);
  console.log('Card Number:', cardData.card_number);
  console.log('Points Balance:', cardData.points);
  
  // Update POS with customer data
  setCurrentCustomer(cardData);
});
The createAblyClientOptions method includes authCallback for automatic token renewal. This ensures your connection stays alive without manual token refresh.

4. Polling Fallback

async function pollCardStatus(sessionId: string) {
  const interval = setInterval(async () => {
    try {
      const status = await sdk.pollQrCardStatus(sessionId);
      
      if (status.status === 'completed' && status.card_data) {
        clearInterval(interval);
        setCurrentCustomer(status.card_data);
      } else if (status.status === 'expired') {
        clearInterval(interval);
        regenerateQR();
      }
    } catch (error) {
      console.error('Polling error:', error);
    }
  }, 2000);
  
  // Stop polling after 5 minutes
  setTimeout(() => clearInterval(interval), 5 * 60 * 1000);
}

Complete POS Example

import { LoyaltySDK } from '@loyaltylt/sdk';
import Ably from 'ably';

class POSSystem {
  private sdk: LoyaltySDK;
  private currentCustomer: any = null;
  private ablyClient: Ably.Realtime | null = null;
  
  constructor() {
    this.sdk = new LoyaltySDK({
      apiKey: 'lty_...',
      apiSecret: '...',
      environment: 'production'
    });
  }
  
  async startCustomerIdentification() {
    // Generate QR session
    const session = await this.sdk.generateQrCardSession('POS Terminal');
    
    // Display QR on customer screen
    this.updateCustomerDisplay(session.qr_code);
    
    // Get Ably options with automatic token renewal
    const ablyOptions = await this.sdk.createAblyClientOptions(session.session_id);
    const tokenResponse = await this.sdk.getAblyToken(session.session_id);
    
    // Connect to Ably (token auto-renews)
    this.ablyClient = new Ably.Realtime(ablyOptions);
    const channel = this.ablyClient.channels.get(tokenResponse.channel);
    
    channel.subscribe('card_identified', (message) => {
      this.handleCustomerIdentified(message.data.card_data);
    });
    
    // Auto-regenerate on expiry
    setTimeout(() => {
      if (!this.currentCustomer) {
        this.startCustomerIdentification();
      }
    }, 5 * 60 * 1000);
  }
  
  handleCustomerIdentified(cardData: any) {
    this.currentCustomer = {
      name: cardData.user?.name || 'Customer',
      phone: cardData.user?.phone,
      cardId: cardData.id,
      cardNumber: cardData.card_number,
      points: cardData.points_balance || 0
    };
    
    // Update POS display
    this.updatePOSDisplay();
    
    // Calculate available discount
    const pointsValue = this.currentCustomer.points * 0.01; // €0.01 per point
    console.log(`Customer can redeem up to €${pointsValue.toFixed(2)}`);
  }
  
  async processTransaction(cartTotal: number, pointsToRedeem: number = 0) {
    if (!this.currentCustomer) {
      throw new Error('No customer identified');
    }
    
    // Calculate points to award
    const pointsToAward = Math.floor(cartTotal * 10); // 10 points per €1
    
    // Create transaction
    const transaction = await this.sdk.createTransaction({
      card_id: this.currentCustomer.cardId,
      amount: cartTotal,
      points: pointsToAward,
      type: 'earn',
      description: 'Purchase',
      reference: `TXN-${Date.now()}`
    });
    
    return {
      transactionId: transaction.id,
      pointsEarned: pointsToAward,
      pointsRedeemed: pointsToRedeem
    };
  }
  
  clearCustomer() {
    this.currentCustomer = null;
    this.startCustomerIdentification();
  }
}

Card Data Structure

When a customer is identified, you receive:
interface CardData {
  id: number;
  card_number: string;
  points_balance: number;
  status: 'active' | 'blocked' | 'expired';
  user: {
    id: number;
    name: string;
    phone: string;
    email: string;
  };
  partner: {
    id: number;
    name: string;
  };
  redemption?: {
    enabled: boolean;
    points_per_currency: number;  // e.g., 100 points = 1 EUR
    currency_amount: number;       // e.g., 1
    min_points: number;           // Minimum points to redeem
  };
  created_at: string;
  updated_at: string;
}

Ably Channel Events

EventDescriptionData
card_identifiedCustomer scanned QR{ card_data: CardData }

Auto-Regeneration

QR codes expire after 5 minutes. Implement auto-regeneration:
let qrTimeout: NodeJS.Timeout;

async function generateAndDisplayQR() {
  // Clear previous timeout
  if (qrTimeout) clearTimeout(qrTimeout);
  
  // Generate new session
  const session = await sdk.generateQrCardSession('POS');
  displayQR(session.qr_code);
  subscribeToEvents(session.session_id);
  
  // Schedule regeneration
  qrTimeout = setTimeout(generateAndDisplayQR, 4.5 * 60 * 1000);
}

Next Steps