Scan and Pay
Paysera offers a secure API that lets customers easily pay for products or services using QR codes. This guide covers static QR code implementation for POS/restaurant scenarios.
Enable contactless payments with static or dynamic QR codes at your physical locations.
Use Case: Restaurant/Cafe Payment​
Customer scans a QR code at their table, reviews the order, and pays instantly from their Paysera app without waiting for a waiter.
QR Code Format: PAYSERA*{locationId}:{identifier}
Example: PAYSERA*9975:table1
Payment Flow​
QR Code Types​
1. Static Location QR
Format: PAYSERA*{locationId}:{identifier}
Use for: Restaurant tables, hotel rooms, parking spots, service desks
Example: PAYSERA*9975:table12
2. Transaction QR
Format: PAYSERA%{transactionKey}
Use for: One-time payments, invoice payments, quick checkouts
Example: PAYSERA%ATaNL6X6CivO2Ou0FMPt
3. Wallet QR
Format: EVP{walletId} (padded to 12 digits)
Use for: Direct wallet payments, tipping, donations
Example: EVP000000014471
Implementation Steps​
- Generate QR Codes - Create static QR codes for locations
- Monitor Spots - Poll for spots with "waiting" status:
GET /rest/v1/spots?status=waiting - Create Transaction - When spot is waiting, create transaction for order
- Create Order - Link transaction to the spot
- Wait for Confirmation - Customer sees order and confirms payment
- Confirm Transaction - Confirm the reserved transaction
- Close Spot - Close spot after payment is complete
Implementation Examples​
- PHP
- Python
- QR Generation
<?php
require_once 'vendor/autoload.php';
use Paysera\WalletApi\ClientFactory;
class ScanAndPayHandler
{
private $client;
private $locationId;
public function __construct($clientId, $secret, $locationId)
{
$factory = ClientFactory::create([
'auth' => [
'mac' => [
'mac_id' => $clientId,
'mac_secret' => $secret,
],
],
]);
$this->client = $factory->getWalletClient();
$this->locationId = $locationId;
}
public function checkWaitingSpots()
{
$spots = $this->client->getSpots([
'status' => 'waiting',
'location_id' => $this->locationId,
]);
return $spots;
}
public function createOrderTransaction($orderDetails)
{
$transaction = $this->client->createTransaction([
'payments' => [[
'description' => $orderDetails['description'],
'price' => $orderDetails['total'],
'currency' => 'EUR',
'parameters' => [
'order_id' => $orderDetails['order_id'],
'table' => $orderDetails['table'],
],
]],
]);
return $transaction;
}
public function createSpotOrder($spotId, $transactionKey)
{
$order = $this->client->createSpotOrder($spotId, [
'transaction_key' => $transactionKey,
]);
return $order;
}
public function getReservedTransactions()
{
$transactions = $this->client->getTransactions([
'status' => 'reserved',
'project_id' => $this->locationId,
]);
return $transactions;
}
public function confirmTransaction($transactionKey)
{
return $this->client->confirmTransaction($transactionKey);
}
public function closeSpot($spotId)
{
return $this->client->closeSpot($spotId);
}
}
// Usage Example
$handler = new ScanAndPayHandler(
'wkVd93h2uS',
'IrdTc8uQodU7PRpLzzLTW6wqZAO6tAMU',
9975
);
// Poll for waiting spots
$waitingSpots = $handler->checkWaitingSpots();
foreach ($waitingSpots as $spot) {
$orderDetails = [
'order_id' => 1234,
'table' => $spot->getIdentifier(),
'description' => 'Order #1234',
'total' => 2599,
];
$transaction = $handler->createOrderTransaction($orderDetails);
$order = $handler->createSpotOrder($spot->getId(), $transaction->getKey());
}
// Check for reserved transactions
$reservedTransactions = $handler->getReservedTransactions();
foreach ($reservedTransactions as $transaction) {
$handler->confirmTransaction($transaction->getKey());
}
import requests
import time
import threading
from flask import Flask, jsonify
app = Flask(__name__)
class PayseraScanner:
def __init__(self, client_id, mac_secret, location_id):
self.client_id = client_id
self.mac_secret = mac_secret
self.location_id = location_id
self.base_url = 'https://wallet.paysera.com/rest/v1'
def get_waiting_spots(self):
url = f'{self.base_url}/spots'
params = {
'status': 'waiting',
'location_id': self.location_id
}
headers = self.generate_mac_headers('GET', '/rest/v1/spots')
response = requests.get(url, params=params, headers=headers)
return response.json().get('spots', [])
def create_transaction(self, order_details):
url = f'{self.base_url}/transaction'
data = {
'payments': [{
'description': order_details['description'],
'price': order_details['total'],
'currency': 'EUR',
'parameters': {
'order_id': order_details['order_id']
}
}]
}
headers = self.generate_mac_headers('POST', '/rest/v1/transaction')
headers['Content-Type'] = 'application/json'
response = requests.post(url, json=data, headers=headers)
return response.json()
def create_spot_order(self, spot_id, transaction_key):
url = f'{self.base_url}/spot/{spot_id}/order'
data = {'transaction_key': transaction_key}
headers = self.generate_mac_headers('POST', f'/rest/v1/spot/{spot_id}/order')
headers['Content-Type'] = 'application/json'
response = requests.post(url, json=data, headers=headers)
return response.json()
def get_reserved_transactions(self):
url = f'{self.base_url}/transactions'
params = {'status': 'reserved'}
headers = self.generate_mac_headers('GET', '/rest/v1/transactions')
response = requests.get(url, params=params, headers=headers)
return response.json().get('transactions', [])
def confirm_transaction(self, transaction_key):
url = f'{self.base_url}/transaction/{transaction_key}/confirm'
headers = self.generate_mac_headers('PUT', f'/rest/v1/transaction/{transaction_key}/confirm')
response = requests.put(url, headers=headers)
return response.json()
def generate_mac_headers(self, method, uri):
import hmac
import hashlib
timestamp = str(int(time.time()))
nonce = f'nonce_{timestamp}'
normalized = f"{timestamp}\n{nonce}\n{method}\n{uri}\nwallet.paysera.com\n443\n\n"
mac = hmac.new(self.mac_secret.encode(), normalized.encode(), hashlib.sha256).hexdigest()
return {
'Authorization': f'MAC id="{self.client_id}", ts="{timestamp}", nonce="{nonce}", mac="{mac}"'
}
def spot_scanner_worker(scanner):
while True:
try:
spots = scanner.get_waiting_spots()
for spot in spots:
order_details = {
'order_id': 1234,
'description': f"Order for {spot['identifier']}",
'total': 2599
}
transaction = scanner.create_transaction(order_details)
order = scanner.create_spot_order(spot['id'], transaction['transaction_key'])
reserved = scanner.get_reserved_transactions()
for txn in reserved:
scanner.confirm_transaction(txn['transaction_key'])
except Exception as e:
print(f"Error: {e}")
time.sleep(5)
scanner = PayseraScanner('wkVd93h2uS', 'IrdTc8uQodU7PRpLzzLTW6wqZAO6tAMU', 9975)
worker = threading.Thread(target=spot_scanner_worker, args=(scanner,))
worker.daemon = True
worker.start()
if __name__ == '__main__':
app.run(debug=True)
Python with qrcode:
import qrcode
def generate_location_qr(location_id, identifier, filename):
qr_data = f"PAYSERA*{location_id}:{identifier}"
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=10,
border=4,
)
qr.add_data(qr_data)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
img.save(filename)
# Generate QR codes for restaurant tables
for table_num in range(1, 21):
generate_location_qr(9975, f"table{table_num}", f"qr_table_{table_num}.png")
PHP with phpqrcode:
<?php
require_once 'phpqrcode/qrlib.php';
function generateLocationQR($locationId, $identifier, $filename) {
$qrData = "PAYSERA*{$locationId}:{$identifier}";
QRcode::png($qrData, $filename, QR_ECLEVEL_L, 10, 4);
}
for ($i = 1; $i <= 20; $i++) {
generateLocationQR(9975, "table{$i}", "qr_table_{$i}.png");
}
Advanced Topics​
Troubleshooting
Spot stuck in "waiting"
- Close the spot manually
- Check if transaction was created
- Verify spot_id is correct
Transaction not found
- Ensure transaction was created successfully
- Check transaction_key is correct
- Verify project_id matches
QR code not working
- Verify QR format:
PAYSERA*{locationId}:{identifier} - Check location_id is registered
- Ensure identifier is unique per location
Testing
Wallet API does not have a sandbox environment. All testing must be done in production with real transactions.
Testing Strategy:
- Use small amounts - Test with minimal values (1-2 EUR)
- Test location - Set up dedicated test QR codes
- Monitor carefully - Watch all test transactions
- Test scenarios:
- Generate test QR codes
- Scan with Paysera app
- Verify spot appears in waiting status
- Create and confirm test transaction
- Close spot after completion
Resources​
- Transaction API - Transaction handling
- PHP Library - PHP SDK
Next Steps​
- Implement spot monitoring service
- Set up automatic transaction confirmation
- Add order management system
- Integrate with your POS
- Deploy to production
Use a dedicated worker process to monitor spots continuously. Don't rely on web requests.