Loyalty.lt
SDKsPHP

QR Features

QR Login and QR Card Scan with PHP SDK

QR Features

Implement QR-based authentication and customer identification.

QR Login

Allow customers to authenticate by scanning a QR code with their Loyalty.lt mobile app.

Generate QR Session

$session = $sdk->generateQrLogin('POS Terminal #1', $shopId);

echo $session['session_id'];   // Unique session ID
echo $session['qr_code'];      // Deep link for QR code
echo $session['expires_at'];   // Expiration time (5 min)

Display QR Code

// Generate QR image URL using Loyalty.lt API
$qrImageUrl = "https://api.loyalty.lt/qr?data=" 
    . urlencode($session['qr_code']) . "&size=250";

echo "<img src='{$qrImageUrl}' alt='Scan to login'>";

Poll for Status

// Poll every 2 seconds until authenticated or expired
$maxAttempts = 150; // 5 minutes
$attempt = 0;

while ($attempt < $maxAttempts) {
    $status = $sdk->pollQrLogin($session['session_id']);
    
    switch ($status['status']) {
        case 'authenticated':
            $user = $status['user'];
            $token = $status['token'];
            echo "Welcome, " . $user['name'];
            break 2;
            
        case 'scanned':
            echo "QR scanned, waiting for confirmation...";
            break;
            
        case 'expired':
            echo "Session expired";
            break 2;
    }
    
    sleep(2);
    $attempt++;
}

Real-time with Laravel Reverb

The PHP SDK is server-side, so it does not open the WebSocket itself. Fetch the Reverb connection details and hand them to your frontend, which connects with pusher-js:

// Get Reverb connection details (no arguments — the config is not session-specific)
$config = $sdk->getRealtimeConfig();

// e.g. return it to the browser as JSON
return response()->json([
    'realtime' => $config,
    'channel'  => $config['channel_prefix']['qr_login'] . $session['session_id'],
]);

In the browser:


// 1. Reverb connection details fetched from your backend
const config = realtime;

// 2. Connect to Reverb (Pusher protocol)
const pusher = new Pusher(config.key, {
  wsHost: config.host,
  wsPort: config.port,
  wssPort: config.port,
  forceTLS: config.scheme === 'https',
  enabledTransports: ['ws', 'wss'],
  cluster: '',        // Reverb has no clusters, but pusher-js throws without this option
  disableStats: true
});

// 3. Subscribe to the public session channel
const channel = pusher.subscribe(`${config.channel_prefix.qr_login}${sessionId}`);

// 4. Listen for events
channel.bind(config.event, (payload) => {
  console.log('Status:', payload.status);
});

Note

getRealtimeConfig() maps to GET /{locale}/shop/realtime/config and uses the same X-API-Key / X-API-Secret headers as every other Shop endpoint. QR channels are public — the unguessable session ID is the secret — so no realtime tokens are involved. Server-side polling (above) remains the fallback if you cannot run a WebSocket client.

QR Card Scan (POS)

Identify customers by displaying a QR code that they scan with their app.

Generate Session

$session = $sdk->generateQrCardSession('POS Terminal', $shopId);

echo $session['session_id'];
echo $session['qr_code'];
echo $session['expires_at'];

Display & Poll

// Display QR using Loyalty.lt API
$qrUrl = "https://api.loyalty.lt/qr?data=" 
    . urlencode($session['qr_code']) . "&size=250";

// Poll for customer identification
while (true) {
    $result = $sdk->pollQrCardStatus($session['session_id']);
    
    if ($result['status'] === 'completed') {
        $card = $result['card_data'];
        
        echo "Customer: " . $card['user']['name'];
        echo "Card: " . $card['card_number'];
        echo "Points: " . $card['points_balance'];
        
        // Process transaction
        $sdk->createTransaction([
            'card_id' => $card['id'],
            'amount' => $orderTotal,
            'points' => (int) $orderTotal,
            'type' => 'earn',
        ]);
        
        break;
    }
    
    if ($result['status'] === 'expired') {
        // Regenerate QR
        break;
    }
    
    sleep(2);
}

Send app download link if customer doesn't have the app:

$sdk->sendAppLink(
    '+37060000000',  // Phone number
    $shopId,         // Shop ID (required)
    'Jonas',         // Optional customer name
    'lt'             // Language (lt/en)
);

Complete POS Example

<?php

use LoyaltyLt\SDK\LoyaltySDK;
use LoyaltyLt\SDK\Exceptions\LoyaltySDKException;

class POSController
{
    private LoyaltySDK $sdk;
    private ?array $currentCustomer = null;
    private ?array $qrSession = null;
    
    public function __construct()
    {
        $this->sdk = new LoyaltySDK([
            'apiKey' => env('LOYALTY_API_KEY'),
            'apiSecret' => env('LOYALTY_API_SECRET'),
        ]);
    }
    
    public function startCustomerIdentification(): array
    {
        $this->qrSession = $this->sdk->generateQrCardSession('POS');
        
        return [
            'qr_url' => "https://api.loyalty.lt/qr?data=" 
                . urlencode($this->qrSession['qr_code']) . "&size=300",
            'session_id' => $this->qrSession['session_id'],
            'expires_at' => $this->qrSession['expires_at'],
        ];
    }
    
    public function checkCustomerStatus(): ?array
    {
        if (!$this->qrSession) return null;
        
        $result = $this->sdk->pollQrCardStatus($this->qrSession['session_id']);
        
        if ($result['status'] === 'completed') {
            $this->currentCustomer = $result['card_data'];
            return $this->currentCustomer;
        }
        
        return null;
    }
    
    public function processCheckout(float $total): array
    {
        if (!$this->currentCustomer) {
            throw new \Exception('No customer identified');
        }
        
        $pointsToAward = (int) ($total * 10); // 10 points per €1
        
        $transaction = $this->sdk->createTransaction([
            'card_id' => $this->currentCustomer['id'],
            'amount' => $total,
            'points' => $pointsToAward,
            'type' => 'earn',
            'description' => 'Purchase',
            'reference' => 'POS-' . time(),
        ]);
        
        return [
            'transaction_id' => $transaction['id'],
            'points_awarded' => $pointsToAward,
            'customer' => $this->currentCustomer['user']['name'],
        ];
    }
}

Realtime Events Reference

ChannelEventDescription
qr-login.{session_id}status_updateQR Login status changed
qr-card.{session_id}card_identifiedCustomer scanned QR Card
qr-card.{session_id}status_updateQR Card session status changed

Both channels are public — the unguessable session ID is the secret — so no channel authentication is required.

Status Values

StatusDescription
pendingWaiting for scan
scannedQR scanned, waiting confirmation
authenticatedLogin successful
completedCard identified
expiredSession expired

On this page