Skip to main content

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.

NFC Cards

Read contactless SmartPay cards

HMAC Auth

Secure signature-based authentication

Real-time

Instant payment processing


Prerequisites

Merchant Registration

Register as a processor via /processors endpoint (admin only)

Receive Credentials

Obtain API key and HMAC secret (shown once at creation)

Terminal Setup

Configure terminal with credentials

Test Transaction

Perform test payment in sandbox

Warning

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

HeaderDescriptionExample
X-API-Key-IDPOS API key IDpos_key_abc123
X-TimestampRFC3339 timestamp2026-03-10T12:00:00Z
X-SignatureHMAC-SHA256 signaturea1b2c3d4e5...

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'
})
);
Info

Signatures are valid for 5 minutes. Ensure your terminal clock is synchronized with NTP.


Payment Flow

Card Tap

Customer taps NFC card on terminal

Read Card

Terminal reads card serial number via NFC

Verify Card

Call POST /api/v1/pos/verify-card to validate card

Collect PIN

If valid, prompt customer for 4-digit PIN

Process Payment

Call POST /api/v1/pos/payment with amount and PIN

Display Result

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 CodeMeaningTerminal Action
HMAC auth errorMissing/invalid HMAC headersCheck API key ID, timestamp, and signature
Validation errorMissing or malformed request fieldsFix the request payload
Invalid PINWrong PIN enteredRetry with the correct PIN
Insufficient fundsNot enough balanceReduce 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

Security
  • Store HMAC secret securely (encrypted)
  • Use HTTPS only
  • Never log credentials
  • Implement PIN attempt limits
User Experience
  • Show cardholder name for verification
  • Display balance before payment
  • Print/show receipts
  • Clear error messages
Reliability
  • Handle network errors gracefully
  • Implement retry with same reference
  • Sync terminal clock with NTP
  • Queue failed transactions
Compliance
  • Log all transactions locally
  • Daily reconciliation
  • Keep receipts/records
  • Report issues promptly

Testing

Sandbox Environment

Use sandbox credentials for testing:

EnvironmentBase URL
Sandboxhttps://sandbox-api.smartpay.example.com
Demohttps://demo.api.vultlocal.com

Test Cards

Card SerialPINBehavior
TEST00011234Success (balance: 100,000)
TEST00021234Insufficient balance
TEST0003anyCard blocked