QR Card Scan System
The QR Card Scan system allows POS terminals to instantly identify customers by displaying a QR code. When a customer scans this QR code with their Loyalty.lt mobile app, their loyalty card data is automatically sent back to the POS system in real-time.
Unlike QR Login which authenticates users for web sessions, QR Card Scan is designed specifically for POS integration - instantly identifying customers and retrieving their loyalty card data without any confirmation steps.
How It Works
Key Differences from QR Login
Feature QR Login QR Card Scan Purpose Authenticate user for web session Identify customer at POS User Action Scan + Confirm in app Scan only (instant) Returns JWT tokens for session Loyalty card data Use Case Shop plugins, web integrations POS systems, kiosks Ably Channel qr-login:{session_id}qr-card:{session_id}Deep Link loyaltylt://qr-login?...loyaltylt://qr-card?...
Generate QR Card Session
Create a new QR session for customer identification at POS.
Endpoint
POST /{locale}/shop/qr-card/generate
Authentication
API key from Partners Portal
API secret from Partners Portal
Request Body
Name for the POS terminal or device Example: "POS Terminal 1", "Checkout Counter 3"
Shop ID for multi-location partners (optional)
Response
Indicates if session was created successfully
Unique session identifier (UUID) - used for Ably channel subscription
Deep link URL for QR code: loyaltylt://qr-card?code={code}&session={session_id}
Ably channel name to subscribe to: qr-card:{session_id}
Partner’s unique identifier
Name of the partner business
Shop ID if provided (nullable)
ISO 8601 timestamp when session expires (5 minutes)
curl -X POST "https://staging-api.loyalty.lt/en/shop/qr-card/generate" \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-H "X-API-Secret: your_api_secret" \
-d '{
"device_name": "POS Terminal 1",
"shop_id": 123
}'
{
"success" : true ,
"data" : {
"session_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"qr_code" : "loyaltylt://qr-card?code=abcd1234efgh5678&session=550e8400-e29b-41d4-a716-446655440000" ,
"ably_channel" : "qr-card:550e8400-e29b-41d4-a716-446655440000" ,
"partner_id" : 45 ,
"partner_name" : "Coffee Paradise" ,
"shop_id" : 123 ,
"expires_at" : "2024-12-10T16:05:00.000Z"
}
}
Generate Ably Token
Get a secure Ably JWT token to subscribe to real-time card identification events.
Endpoint
POST /{locale}/shop/ably/token
Request Body
The session ID from the generate endpoint
Response
Ably JWT token for WebSocket connection (valid for 60 minutes)
Unix timestamp when token expires
Ably channel name (automatically determined based on session type):
For card scan: qr-card:{session_id}
For login: qr-login:{session_id}
Type of session: card_scan or login
curl -X POST "https://staging-api.loyalty.lt/en/shop/ably/token" \
-H "Content-Type: application/json" \
-H "X-API-Key: your_api_key" \
-H "X-API-Secret: your_api_secret" \
-d '{
"session_id": "550e8400-e29b-41d4-a716-446655440000"
}'
{
"success" : true ,
"data" : {
"token" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ,
"expires" : 1702234500 ,
"channel" : "qr-card:550e8400-e29b-41d4-a716-446655440000" ,
"session_type" : "card_scan"
}
}
Real-time Integration (Ably)
After generating a session and obtaining an Ably token, subscribe to the WebSocket channel to receive card data in real-time.
Channel Name
Example: qr-card:550e8400-e29b-41d4-a716-446655440000
Event to Subscribe
Event Name Description card_identifiedCustomer scanned QR code, card data available
Ably Connection Example
import Ably from 'ably' ;
// 1. Get token from API response
const { token , channel } = ablyTokenResponse . data ;
// 2. Connect to Ably
const ably = new Ably . Realtime ({ token: token });
// 3. Subscribe to channel
const ablyChannel = ably . channels . get ( channel );
// 4. Listen for card identification
ablyChannel . subscribe ( 'card_identified' , ( message ) => {
const { card_data } = message . data ;
console . log ( 'Customer:' , card_data . user . name );
console . log ( 'Card:' , card_data . card_number );
console . log ( 'Points:' , card_data . points );
// Process customer data...
});
Event Payload: card_identified
{
"session_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"status" : "authenticated" ,
"card_data" : {
"loyalty_card_id" : 12345 ,
"card_number" : "123-456-789" ,
"points" : 1500 ,
"user" : {
"id" : 789 ,
"name" : "Jonas Jonaitis" ,
"email" : "[email protected] " ,
"phone" : "+37060000000"
},
"redemption" : {
"enabled" : true ,
"points_per_currency" : 100 ,
"currency_amount" : 1.00 ,
"min_points" : 100 ,
"max_points" : 10000
},
"scanned_at" : "2024-12-10T16:02:30.000Z"
},
"timestamp" : "2024-12-10T16:02:30.500Z"
}
Poll Session Status (Fallback)
Alternative to WebSockets: poll the session status periodically. Use this as a fallback if real-time connection is unavailable.
Endpoint
GET /{locale}/shop/qr-card/status/{sessionId}
Path Parameters
The QR card scan session ID
Response Statuses
Status Description pendingWaiting for customer to scan QR code authenticatedCustomer scanned QR, card data available expiredSession expired (after 5 minutes)
curl -X GET "https://staging-api.loyalty.lt/en/shop/qr-card/status/550e8400-e29b-41d4-a716-446655440000" \
-H "X-API-Key: your_api_key" \
-H "X-API-Secret: your_api_secret"
Pending Status
Authenticated Status (Card Data Available)
Expired Status (404)
{
"success" : true ,
"data" : {
"session_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"status" : "pending" ,
"expires_at" : "2024-12-10T16:05:00.000Z"
}
}
Card Data Structure
When a customer scans the QR code, you receive comprehensive loyalty card data:
Unique identifier of the loyalty card
Formatted card number (e.g., 123-456-789)
Current available points balance (excluding expired points)
Customer phone (nullable)
Points redemption settings (if enabled for this card type) Whether points redemption is enabled
Points needed per currency unit
Currency amount per redemption unit
Minimum points required to redeem
Maximum points per single redemption
ISO 8601 timestamp when QR was scanned
Complete POS Integration Example
import Ably from 'ably' ;
class LoyaltyPOS {
constructor ( apiKey , apiSecret ) {
this . apiKey = apiKey ;
this . apiSecret = apiSecret ;
this . baseUrl = 'https://api.loyalty.lt/en/shop' ;
this . ably = null ;
this . currentSession = null ;
}
async startCustomerIdentification () {
// 1. Generate QR session
const sessionResponse = await fetch ( ` ${ this . baseUrl } /qr-card/generate` , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-API-Key' : this . apiKey ,
'X-API-Secret' : this . apiSecret
},
body: JSON . stringify ({
device_name: 'POS Terminal 1'
})
});
const { data : session } = await sessionResponse . json ();
this . currentSession = session ;
// 2. Get Ably token (universal endpoint)
const tokenResponse = await fetch ( ` ${ this . baseUrl } /ably/token` , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
'X-API-Key' : this . apiKey ,
'X-API-Secret' : this . apiSecret
},
body: JSON . stringify ({
session_id: session . session_id
})
});
const { data : tokenData } = await tokenResponse . json ();
// 3. Connect to Ably and subscribe
this . ably = new Ably . Realtime ({ token: tokenData . token });
const channel = this . ably . channels . get ( session . ably_channel );
return new Promise (( resolve , reject ) => {
// Set timeout for session expiration
const timeout = setTimeout (() => {
this . cleanup ();
reject ( new Error ( 'Session expired' ));
}, 5 * 60 * 1000 ); // 5 minutes
channel . subscribe ( 'card_identified' , ( message ) => {
clearTimeout ( timeout );
this . cleanup ();
resolve ( message . data . card_data );
});
});
}
getQRCodeUrl () {
return this . currentSession ?. qr_code ;
}
cleanup () {
if ( this . ably ) {
this . ably . close ();
this . ably = null ;
}
this . currentSession = null ;
}
}
// Usage
const pos = new LoyaltyPOS ( 'your_api_key' , 'your_api_secret' );
try {
// Display QR code on POS screen
const qrUrl = await pos . startCustomerIdentification ();
displayQRCode ( pos . getQRCodeUrl ()); // Your QR code display function
// Wait for customer to scan
const cardData = await pos . startCustomerIdentification ();
console . log ( `Welcome ${ cardData . user . name } !` );
console . log ( `You have ${ cardData . points } points` );
if ( cardData . redemption ?. enabled ) {
// Calculate available discount
const maxDiscount = Math . floor ( cardData . points / cardData . redemption . points_per_currency )
* cardData . redemption . currency_amount ;
console . log ( `Available discount: € ${ maxDiscount } ` );
}
} catch ( error ) {
console . error ( 'Customer identification failed:' , error );
}
Error Codes
Code Description HTTP Status AUTH_FORBIDDENInvalid or missing API credentials 403 RESOURCE_NOT_FOUNDSession not found 404 RESOURCE_EXPIREDQR code/session has expired 410 RESOURCE_ALREADY_EXISTSQR code already used 409 INTERNAL_ERRORServer error 500
Best Practices
Use WebSockets Always prefer Ably WebSockets for instant notifications. Use polling only as a fallback.
Show Countdown Sessions expire after 5 minutes. Display a countdown timer and auto-refresh QR codes.
Handle Expiration Automatically generate a new QR session when the current one expires.
Secure Display Ensure QR code is only visible to the intended customer at the checkout.
Session Timeline
Generate Session
POS creates QR card scan session (valid for 5 minutes)
Display QR Code
Convert deep link to QR code image on POS screen
Subscribe to Events
Connect to Ably WebSocket channel qr-card:{session_id}
Customer Scans
Customer opens Loyalty.lt app and scans the QR code
Instant Identification
Card data is immediately sent to POS via Ably (no confirmation needed!)
Process Transaction
Apply loyalty discounts, award points, or redeem rewards