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 merchant-specific credentials.

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-Client-IDProcessor/merchant IDproc_abc123
X-TimestampUnix timestamp (seconds)1704067200
X-SignatureHMAC-SHA256 signaturea1b2c3d4e5...

Signature Generation

const crypto = require('crypto');

function signRequest(clientId, hmacSecret, method, path, body) {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  
  // Canonical string: METHOD|PATH|BODY|TIMESTAMP
  const canonicalString = body 
    ? `${method}|${path}|${JSON.stringify(body)}|${timestamp}`
    : `${method}|${path}||${timestamp}`;
  
  const signature = crypto
    .createHmac('sha256', hmacSecret)
    .update(canonicalString)
    .digest('hex');
  
  return {
    'X-Client-ID': clientId,
    'X-Timestamp': timestamp,
    'X-Signature': signature,
    'Content-Type': 'application/json'
  };
}

// Example usage
const headers = signRequest(
  'proc_abc123',
  'secret_xyz789',
  'POST',
  '/api/v1/pos/payment',
  { card_serial: 'OLIV0001', amount: 5000, pin: '1234' }
);
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 /pos/verify-card to validate card
4

Collect PIN

If valid, prompt customer for 4-digit PIN
5

Process Payment

Call POST /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://olive-gateway-a6ba.onrender.com/api/v1/pos/verify-card" \
  -H "X-Client-ID: proc_abc123" \
  -H "X-Timestamp: 1704067200" \
  -H "X-Signature: a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -d '{
    "card_serial": "OLIV0001"
  }'
Response:
{
  "success": true,
  "card": {
    "serial": "OLIV0001",
    "status": "active",
    "cardholder": "John Doe",
    "balance": 150000,
    "currency": "SLE"
  }
}

Process Payment

Execute payment after PIN verification:
curl -X POST "https://olive-gateway-a6ba.onrender.com/api/v1/pos/payment" \
  -H "X-Client-ID: proc_abc123" \
  -H "X-Timestamp: 1704067200" \
  -H "X-Signature: a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -d '{
    "card_serial": "OLIV0001",
    "amount": 5000,
    "pin": "1234",
    "reference": "INV-12345"
  }'
Response:
{
  "success": true,
  "transaction_id": "txn_abc123",
  "message": "Payment successful",
  "data": {
    "amount": 5000,
    "fee": 50,
    "currency": "SLE",
    "cardholder": "John Doe",
    "new_balance": 145000,
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

Refund Payment

Refund a previous transaction:
curl -X POST "https://olive-gateway-a6ba.onrender.com/api/v1/pos/refund" \
  -H "X-Client-ID: proc_abc123" \
  -H "X-Timestamp: 1704067200" \
  -H "X-Signature: a1b2c3d4..." \
  -H "Content-Type: application/json" \
  -d '{
    "original_transaction_id": "txn_abc123",
    "amount": 5000,
    "reason": "Customer request"
  }'

Error Handling

Error CodeMeaningTerminal Action
CARD_NOT_FOUNDCard serial not in systemAsk for different card
CARD_BLOCKEDCard is blocked/disabledCard cannot be used
INSUFFICIENT_BALANCENot enough fundsShow balance, reduce amount
INVALID_PINWrong PIN enteredRetry (max 3 attempts)
SIGNATURE_EXPIREDTimestamp too oldResync terminal clock
INVALID_SIGNATUREHMAC verification failedCheck credentials

Error Response Example

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Cardholder has insufficient balance",
    "details": {
      "requested": 50000,
      "available": 15000
    }
  }
}

Terminal Implementation

Sample Terminal Code

const axios = require('axios');
const crypto = require('crypto');

class OlivePOS {
  constructor(clientId, hmacSecret, baseUrl) {
    this.clientId = clientId;
    this.hmacSecret = hmacSecret;
    this.baseUrl = baseUrl;
  }

  signRequest(method, path, body) {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const canonical = body 
      ? `${method}|${path}|${JSON.stringify(body)}|${timestamp}`
      : `${method}|${path}||${timestamp}`;
    
    const signature = crypto
      .createHmac('sha256', this.hmacSecret)
      .update(canonical)
      .digest('hex');
    
    return {
      'X-Client-ID': this.clientId,
      'X-Timestamp': timestamp,
      'X-Signature': signature,
      'Content-Type': 'application/json'
    };
  }

  async verifyCard(cardSerial) {
    const path = '/api/v1/pos/verify-card';
    const body = { card_serial: cardSerial };
    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, reference) {
    const path = '/api/v1/pos/payment';
    const body = { card_serial: cardSerial, amount, pin, reference };
    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(
  'proc_abc123',
  'secret_xyz789',
  'https://olive-gateway-a6ba.onrender.com'
);

// Verify card
const cardInfo = await pos.verifyCard('OLIV0001');
console.log(`Cardholder: ${cardInfo.card.cardholder}`);

// Process payment
const result = await pos.processPayment('OLIV0001', 5000, '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
Productionhttps://olive-gateway-a6ba.onrender.com

Test Cards

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