POS Integration
Complete guide for integrating Point-of-Sale terminals with SmartPay for NFC card payments.
Overview
POS (Point-of-Sale) terminals allow merchants to accept SmartPay card payments. Each terminal authenticates using HMAC signatures with a per-client API key ID and secret.
Read contactless SmartPay cards
Secure signature-based authentication
Instant payment processing
Prerequisites
Register as a processor via /processors endpoint (admin only)
Obtain API key and HMAC secret (shown once at creation)
Configure terminal with credentials
Perform test payment in sandbox
API key and HMAC secret are only shown once at processor creation. Store them securely!
Authentication
POS terminals use HMAC-SHA256 signatures for authentication.
Required Headers
| Header | Description | Example |
|---|---|---|
X-API-Key-ID | POS API key ID | pos_key_abc123 |
X-Timestamp | RFC3339 timestamp | 2026-03-10T12:00:00Z |
X-Signature | HMAC-SHA256 signature | a1b2c3d4e5... |
Signature Generation
const crypto = require('crypto');
function signRequest(apiKeyId, hmacSecret, method, path, rawBody) {
const timestamp = new Date().toISOString();
const canonicalString = `${method}\n${path}\n${timestamp}\n${rawBody}`;
const signature = crypto
.createHmac('sha256', hmacSecret)
.update(canonicalString)
.digest('hex');
return {
'X-API-Key-ID': apiKeyId,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
};
}
// Example usage
const headers = signRequest(
'pos_key_abc123',
'secret_xyz789',
'POST',
'/api/v1/pos/payment',
JSON.stringify({
card_serial: 'OLIV0001',
merchant_id: 'MERCHANT001',
terminal_id: 'TERM001',
amount: '150.00',
currency: 'SLE',
transaction_ref: 'POS-TXN-123',
pin: '1234',
processor_id: 'proc-uuid-123'
})
);
Signatures are valid for 5 minutes. Ensure your terminal clock is synchronized with NTP.
Payment Flow
Customer taps NFC card on terminal
Terminal reads card serial number via NFC
Call POST /api/v1/pos/verify-card to validate card
If valid, prompt customer for 4-digit PIN
Call POST /api/v1/pos/payment with amount and PIN
Show success/failure message to customer
Sequence Diagram
API Endpoints
Verify Card
Check if card is valid and get cardholder info:
curl -X POST "https://demo.api.vultlocal.com/api/v1/pos/verify-card" \
-H "X-API-Key-ID: pos_key_abc123" \
-H "X-Timestamp: 2026-03-10T12:00:00Z" \
-H "X-Signature: a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"card_serial": "OLIV0001",
"pin": "1234"
}'
Response:
{
"success": true,
"message": "Card verified successfully",
"is_active": true,
"balance": "1500.00",
"holder_name": "John Doe"
}
Process Payment
Execute payment after PIN verification:
curl -X POST "https://demo.api.vultlocal.com/api/v1/pos/payment" \
-H "X-API-Key-ID: pos_key_abc123" \
-H "X-Timestamp: 2026-03-10T12:00:00Z" \
-H "X-Signature: a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"card_serial": "OLIV0001",
"merchant_id": "MERCHANT001",
"terminal_id": "TERM001",
"amount": "150.00",
"currency": "SLE",
"pin": "1234",
"transaction_ref": "INV-12345",
"processor_id": "proc-uuid-123"
}'
Response:
fee_amount is dynamic and comes from the active POS fee configuration. If the merchant setup charges a fee for the transaction, this field returns that amount instead of 0.00.
{
"success": true,
"message": "Payment successful",
"transaction_id": "txn_abc123",
"approval_code": "882211",
"amount": "150.00",
"remaining_balance": "1450.00",
"fee_amount": "1.50"
}
Refund Payment
Refund a previous transaction:
curl -X POST "https://demo.api.vultlocal.com/api/v1/pos/refund" \
-H "X-API-Key-ID: pos_key_abc123" \
-H "X-Timestamp: 2026-03-10T12:00:00Z" \
-H "X-Signature: a1b2c3d4..." \
-H "Content-Type: application/json" \
-d '{
"original_transaction_id": "txn_abc123",
"amount": "150.00",
"reason": "Customer request",
"initiated_by": "admin_user_01"
}'
Error Handling
| Error Code | Meaning | Terminal Action |
|---|---|---|
| HMAC auth error | Missing/invalid HMAC headers | Check API key ID, timestamp, and signature |
| Validation error | Missing or malformed request fields | Fix the request payload |
| Invalid PIN | Wrong PIN entered | Retry with the correct PIN |
| Insufficient funds | Not enough balance | Reduce amount or use another card |
Error Response Example
{
"success": false,
"success": false,
"error": "Insufficient funds"
}
Terminal Implementation
Sample Terminal Code
const axios = require('axios');
const crypto = require('crypto');
class SmartPayPOS {
constructor(apiKeyId, hmacSecret, baseUrl) {
this.apiKeyId = apiKeyId;
this.hmacSecret = hmacSecret;
this.baseUrl = baseUrl;
}
signRequest(method, path, body) {
const timestamp = new Date().toISOString();
const rawBody = JSON.stringify(body);
const canonical = `${method}\n${path}\n${timestamp}\n${rawBody}`;
const signature = crypto
.createHmac('sha256', this.hmacSecret)
.update(canonical)
.digest('hex');
return {
'X-API-Key-ID': this.apiKeyId,
'X-Timestamp': timestamp,
'X-Signature': signature,
'Content-Type': 'application/json'
};
}
async verifyCard(cardSerial, pin) {
const path = '/api/v1/pos/verify-card';
const body = { card_serial: cardSerial, pin };
const headers = this.signRequest('POST', path, body);
const response = await axios.post(
`${this.baseUrl}${path}`,
body,
{ headers }
);
return response.data;
}
async processPayment(cardSerial, amount, pin, transactionRef) {
const path = '/api/v1/pos/payment';
const body = {
card_serial: cardSerial,
merchant_id: 'MERCHANT001',
terminal_id: 'TERM001',
amount,
currency: 'SLE',
pin,
transaction_ref: transactionRef,
processor_id: 'proc-uuid-123'
};
const headers = this.signRequest('POST', path, body);
const response = await axios.post(
`${this.baseUrl}${path}`,
body,
{ headers }
);
return response.data;
}
}
// Usage
const pos = new SmartPayPOS(
'pos_key_abc123',
'secret_xyz789',
'https://demo.api.vultlocal.com'
);
// Verify card
const cardInfo = await pos.verifyCard('OLIV0001', '1234');
console.log(`Cardholder: ${cardInfo.holder_name}`);
// Process payment
const result = await pos.processPayment('OLIV0001', '150.00', '1234', 'INV-001');
console.log(`Transaction: ${result.transaction_id}`);
Best Practices
- Store HMAC secret securely (encrypted)
- Use HTTPS only
- Never log credentials
- Implement PIN attempt limits
- Show cardholder name for verification
- Display balance before payment
- Print/show receipts
- Clear error messages
- Handle network errors gracefully
- Implement retry with same reference
- Sync terminal clock with NTP
- Queue failed transactions
- Log all transactions locally
- Daily reconciliation
- Keep receipts/records
- Report issues promptly
Testing
Sandbox Environment
Use sandbox credentials for testing:
| Environment | Base URL |
|---|---|
| Sandbox | https://sandbox-api.smartpay.example.com |
| Demo | https://demo.api.vultlocal.com |
Test Cards
| Card Serial | PIN | Behavior |
|---|---|---|
TEST0001 | 1234 | Success (balance: 100,000) |
TEST0002 | 1234 | Insufficient balance |
TEST0003 | any | Card blocked |