Skip to main content

POS Integration Guide

Complete guide for integrating Point-of-Sale terminals with OLIVE for NFC card payments.

Overview

POS (Point-of-Sale) terminals allow merchants to accept OLIVE card payments. Each terminal authenticates using HMAC signatures with a per-client API key ID and secret.

NFC Cards

Read contactless OLIVE cards

HMAC Auth

Secure signature-based authentication

Real-time

Instant payment processing

Prerequisites

1

Merchant Registration

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

Receive Credentials

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

Terminal Setup

Configure terminal with credentials
4

Test Transaction

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

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'
  })
);
Signatures are valid for 5 minutes. Ensure your terminal clock is synchronized with NTP.

Payment Flow

1

Card Tap

Customer taps NFC card on terminal
2

Read Card

Terminal reads card serial number via NFC
3

Verify Card

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

Collect PIN

If valid, prompt customer for 4-digit PIN
5

Process Payment

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

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 OlivePOS {
  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 OlivePOS(
  '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.olive.example.com
Demohttps://demo.api.vultlocal.com

Test Cards

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

POS Payment API

Payment endpoint reference

Verify Card API

Card verification endpoint

Processor Setup

Register as processor

Security Guide

HMAC authentication details