QR Features
QR Login and QR Card Scan with Python SDK
QR Features
Implement QR-based authentication and customer identification.
QR Login
Allow customers to authenticate by scanning a QR code.
Generate QR Session
session = sdk.generate_qr_login('POS Terminal #1', shop_id=1)
print(session['session_id']) # Unique session ID
print(session['qr_code']) # Deep link for QR code
print(session['expires_at']) # Expiration time (5 min)Display QR Code
from urllib.parse import quote
# Generate QR code URL using Loyalty.lt API
qr_image_url = f"https://api.loyalty.lt/qr?data={quote(session['qr_code'])}&size=250"
print(f"<img src='{qr_image_url}'>")Poll for Status
import time
max_attempts = 150 # 5 minutes
attempt = 0
while attempt < max_attempts:
status = sdk.poll_qr_login(session['session_id'])
if status['status'] == 'authenticated':
user = status['user']
token = status['token']
print(f"Welcome, {user['name']}")
break
elif status['status'] == 'scanned':
print("QR scanned, waiting for confirmation...")
elif status['status'] == 'expired':
print("Session expired")
break
time.sleep(2)
attempt += 1Real-time with Laravel Reverb
The Python 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.get_realtime_config()
# e.g. return it to the browser as JSON
return {
'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
get_realtime_config() 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.
Generate Session
session = sdk.generate_qr_card_session('POS Terminal', shop_id=1)
print(session['session_id'])
print(session['qr_code'])
print(session['expires_at'])Poll for Customer
import time
while True:
result = sdk.poll_qr_card_status(session['session_id'])
if result['status'] == 'completed':
card = result['card_data']
print(f"Customer: {card['user']['name']}")
print(f"Card: {card['card_number']}")
print(f"Points: {card['points_balance']}")
# Process transaction
sdk.create_transaction(
card_id=card['id'],
amount=order_total,
points=int(order_total),
transaction_type='earn'
)
break
if result['status'] == 'expired':
# Regenerate QR
break
time.sleep(2)Send App Link
Send app download link if customer doesn't have the app:
sdk.send_app_link(
phone='+37060000000',
shop_id=1, # Required
customer_name='Jonas', # Optional
language='lt' # Optional, default 'lt'
)Complete POS Example
from loyalty_sdk import LoyaltySDK, LoyaltyAPIError
from urllib.parse import quote
import time
import os
class POSSystem:
def __init__(self):
self.sdk = LoyaltySDK(
api_key=os.environ['LOYALTY_API_KEY'],
api_secret=os.environ['LOYALTY_API_SECRET']
)
self.current_customer = None
self.qr_session = None
def start_customer_identification(self) -> dict:
"""Generate QR code for customer identification."""
self.qr_session = self.sdk.generate_qr_card_session('POS')
return {
'qr_url': f"https://api.loyalty.lt/qr?data={quote(self.qr_session['qr_code'])}&size=300",
'session_id': self.qr_session['session_id'],
'expires_at': self.qr_session['expires_at']
}
def check_customer_status(self) -> dict | None:
"""Poll for customer identification."""
if not self.qr_session:
return None
result = self.sdk.poll_qr_card_status(self.qr_session['session_id'])
if result['status'] == 'completed':
self.current_customer = result['card_data']
return self.current_customer
return None
def wait_for_customer(self, timeout: int = 300) -> dict | None:
"""Wait for customer to scan QR code."""
self.start_customer_identification()
start_time = time.time()
while time.time() - start_time < timeout:
customer = self.check_customer_status()
if customer:
return customer
time.sleep(2)
return None
def process_checkout(self, total: float) -> dict:
"""Process checkout and award points."""
if not self.current_customer:
raise Exception('No customer identified')
points_to_award = int(total * 10) # 10 points per €1
transaction = self.sdk.create_transaction(
card_id=self.current_customer['id'],
amount=total,
points=points_to_award,
transaction_type='earn',
description='Purchase',
reference=f'POS-{int(time.time())}'
)
return {
'transaction_id': transaction['id'],
'points_awarded': points_to_award,
'customer': self.current_customer['user']['name']
}
def clear_customer(self):
"""Clear current customer."""
self.current_customer = None
self.qr_session = None
# Usage
if __name__ == '__main__':
pos = POSSystem()
print("Waiting for customer...")
customer = pos.wait_for_customer(timeout=60)
if customer:
print(f"Customer identified: {customer['user']['name']}")
print(f"Current points: {customer['points_balance']}")
# Process checkout
result = pos.process_checkout(total=45.50)
print(f"Awarded {result['points_awarded']} points")
pos.clear_customer()
else:
print("No customer identified")Async Support
For async frameworks like FastAPI:
import asyncio
from loyalty_sdk import LoyaltySDK
async def wait_for_customer_async(sdk: LoyaltySDK, session_id: str, timeout: int = 300):
"""Async version of customer wait."""
import time
start = time.time()
while time.time() - start < timeout:
result = sdk.poll_qr_card_status(session_id)
if result['status'] == 'completed':
return result['card_data']
await asyncio.sleep(2)
return NoneRealtime Events Reference
| Channel | Event | Description |
|---|---|---|
qr-login.{session_id} | status_update | QR Login status changed |
qr-card.{session_id} | card_identified | Customer scanned QR Card |
qr-card.{session_id} | status_update | QR Card session status changed |
Both channels are public — the unguessable session ID is the secret — so no channel authentication is required.
Status Values
| Status | Description |
|---|---|
pending | Waiting for scan |
scanned | QR scanned, waiting confirmation |
authenticated | Login successful |
completed | Card identified |
expired | Session expired |