Authentication & Security

This guide covers all authentication methods supported by the Piaxis API, security best practices, and implementation examples for different integration scenarios.

Overview

Piaxis supports multiple authentication methods depending on your integration needs:

  • API Key Authentication: For server-to-server integrations

  • OAuth2 Flow: For applications acting on behalf of users

  • Merchant Authorization: For escrow and disbursement operations

  • OTP Verification: For sensitive operations requiring user confirmation

Authentication Methods

API Key Authentication

The simplest authentication method for server-to-server integrations. Your API key provides full access to your merchant account.

Headers Required:

api-key: YOUR_MERCHANT_API_KEY
Content-Type: application/json

Example Request:

POST /api/payments/create HTTP/1.1
Host: api.gopiaxis.com
api-key: pk_live_1234567890abcdef
Content-Type: application/json

{
  "amount": 5000,
  "currency": "UGX",
  "payment_method": "mtn"
}

Security Notes: - Keep your API key secure and never expose it in client-side code - Use environment variables to store API keys - Rotate API keys regularly through your merchant dashboard

OAuth2 Flow

OAuth2 is required for applications that act on behalf of users, particularly for Piaxis External payments and user account operations.

Step 1: Authorization Request

GET /api/authorize

Redirect users to start the OAuth2 authorization flow.

Query Parameters:
  • merchant_id – Your merchant ID (UUID format)

  • redirect_uri – Your callback URL (must be registered in your merchant dashboard)

  • response_type – Must be “code”

  • scope – Requested permissions (space-separated)

  • state – Optional security parameter to prevent CSRF attacks

Available Scopes: - payments:create - Create payments - payments:read - Read payment information - escrow:create - Create escrow transactions - escrow:manage - Manage escrow operations - profile:read - Read user profile information

Example Authorization URL:

GET /api/authorize?merchant_id=096b723a-45c5-4957-94d7-747835136265&redirect_uri=https://yourapp.com/callback&response_type=code&scope=payments:create%20escrow:create&state=abc123 HTTP/1.1
Host: api.gopiaxis.com

Step 2: Handle Authorization Callback

After user approval, Piaxis redirects to your callback URL with an authorization code:

GET /callback?code=AUTH_CODE_HERE&state=abc123 HTTP/1.1
Host: yourapp.com

Step 3: Exchange Code for Access Token

POST /api/oauth/token

Exchange authorization code for access token.

Form Parameters:
  • grant_type – Must be “authorization_code”

  • code – Authorization code from callback

  • redirect_uri – Same redirect URI used in authorization request

  • client_id – Your merchant ID

Example Request:

POST /api/oauth/token HTTP/1.1
Host: api.gopiaxis.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=AUTH_CODE_HERE&redirect_uri=https://yourapp.com/callback&client_id=096b723a-45c5-4957-94d7-747835136265

Example Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "scope": "payments:create escrow:create"
}

Step 4: Use Access Token

Include the access token in API requests:

POST /api/payments/create HTTP/1.1
Host: api.gopiaxis.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

Token Refresh

POST /api/oauth/refresh

Refresh expired access token using refresh token.

Form Parameters:
  • grant_type – Must be “refresh_token”

  • refresh_token – Your refresh token

  • client_id – Your merchant ID

Example Request:

POST /api/oauth/refresh HTTP/1.1
Host: api.gopiaxis.com
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=REFRESH_TOKEN_HERE&client_id=096b723a-45c5-4957-94d7-747835136265

OTP Verification Flow

Certain sensitive operations require OTP (One-Time Password) verification for additional security.

Operations Requiring OTP: - Large escrow releases (configurable threshold) - Bulk disbursements - Account settings changes - High-value transactions

Step 1: Request OTP

POST /api/auth/request-otp

Request OTP for sensitive operation.

Example Request:

POST /api/auth/request-otp HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
Content-Type: application/json

{
  "operation": "escrow_release",
  "escrow_id": "e123e4567-e89b-12d3-a456-426614174000",
  "phone_number": "+256700000000"
}

Response:

{
  "status": "success",
  "message": "OTP sent successfully",
  "otp_id": "otp_1234567890",
  "expires_in": 300
}

Step 2: Verify OTP

POST /api/auth/verify-otp

Verify the OTP code.

Example Request:

POST /api/auth/verify-otp HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
Content-Type: application/json

{
  "otp_id": "otp_1234567890",
  "otp_code": "123456"
}

Response:

{
  "status": "success",
  "verification_token": "verify_token_here",
  "expires_in": 600
}

Step 3: Use Verification Token

Include the verification token in your sensitive operation:

POST /api/escrow/release HTTP/1.1
Host: api.gopiaxis.com
api-key: YOUR_API_KEY
X-Verification-Token: verify_token_here
Content-Type: application/json

Security Best Practices

API Key Security

  1. Environment Variables: Store API keys in environment variables, never in code

  2. Key Rotation: Rotate API keys regularly through your merchant dashboard

  3. Access Control: Use different API keys for different environments (dev, staging, prod)

  4. Monitoring: Monitor API key usage for unusual activity

# Good - using environment variables
export piaxis_API_KEY="pk_live_1234567890abcdef"

OAuth2 Security

  1. State Parameter: Always use the state parameter to prevent CSRF attacks

  2. HTTPS Only: Only use OAuth2 over HTTPS connections

  3. Token Storage: Store tokens securely (encrypted, server-side)

  4. Scope Limitation: Request only the scopes your application needs

Request Security

  1. HTTPS Required: All API requests must use HTTPS

  2. Request Signing: Consider implementing request signing for additional security

  3. Idempotency: Use idempotency keys for critical operations

  4. Rate Limiting: Implement client-side rate limiting to avoid hitting API limits

Error Handling

Authentication Errors

HTTP Code

Error Code

Description

401

INVALID_API_KEY

API key is invalid or missing

401

EXPIRED_TOKEN

OAuth access token has expired

403

INSUFFICIENT_SCOPE

Token doesn’t have required permissions

403

OTP_REQUIRED

Operation requires OTP verification

400

INVALID_OTP

OTP code is invalid or expired

429

RATE_LIMIT_EXCEEDED

Too many requests, implement backoff

Example Error Response:

{
  "error": {
    "code": "INVALID_API_KEY",
    "message": "The provided API key is invalid",
    "details": {
      "timestamp": "2024-01-15T10:30:00Z",
      "request_id": "req_1234567890"
    }
  }
}

Implementation Examples

Python Implementation

import os
import requests
from datetime import datetime, timedelta

class PiaxisAuth:
    def __init__(self, api_key=None, client_id=None):
        self.api_key = api_key or os.getenv('piaxis_API_KEY')
        self.client_id = client_id or os.getenv('piaxis_CLIENT_ID')
        self.access_token = None
        self.refresh_token = None
        self.token_expires = None
        self.base_url = 'https://api.gopiaxis.com'

    def get_api_headers(self):
        """Get headers for API key authentication"""
        return {
            'api-key': self.api_key,
            'Content-Type': 'application/json'
        }

    def get_oauth_headers(self):
        """Get headers for OAuth authentication"""
        if not self.access_token or self.is_token_expired():
            raise Exception("No valid access token available")

        return {
            'Authorization': f'Bearer {self.access_token}',
            'Content-Type': 'application/json'
        }

    def get_authorization_url(self, redirect_uri, scopes, state=None):
        """Generate OAuth2 authorization URL"""
        params = {
            'merchant_id': self.client_id,
            'redirect_uri': redirect_uri,
            'response_type': 'code',
            'scope': ' '.join(scopes)
        }
        if state:
            params['state'] = state

        query_string = '&'.join([f"{k}={v}" for k, v in params.items()])
        return f"{self.base_url}/api/authorize?{query_string}"

    def exchange_code_for_token(self, code, redirect_uri):
        """Exchange authorization code for access token"""
        data = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': redirect_uri,
            'client_id': self.client_id
        }

        response = requests.post(
            f"{self.base_url}/api/oauth/token",
            data=data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )

        if response.status_code == 200:
            token_data = response.json()
            self.access_token = token_data['access_token']
            self.refresh_token = token_data.get('refresh_token')
            self.token_expires = datetime.now() + timedelta(seconds=token_data['expires_in'])
            return token_data
        else:
            raise Exception(f"Token exchange failed: {response.text}")

    def refresh_access_token(self):
        """Refresh expired access token"""
        if not self.refresh_token:
            raise Exception("No refresh token available")

        data = {
            'grant_type': 'refresh_token',
            'refresh_token': self.refresh_token,
            'client_id': self.client_id
        }

        response = requests.post(
            f"{self.base_url}/api/oauth/refresh",
            data=data,
            headers={'Content-Type': 'application/x-www-form-urlencoded'}
        )

        if response.status_code == 200:
            token_data = response.json()
            self.access_token = token_data['access_token']
            self.token_expires = datetime.now() + timedelta(seconds=token_data['expires_in'])
            return token_data
        else:
            raise Exception(f"Token refresh failed: {response.text}")

    def is_token_expired(self):
        """Check if access token is expired"""
        if not self.token_expires:
            return True
        return datetime.now() >= self.token_expires

    def request_otp(self, operation, **kwargs):
        """Request OTP for sensitive operation"""
        data = {'operation': operation, **kwargs}

        response = requests.post(
            f"{self.base_url}/api/auth/request-otp",
            json=data,
            headers=self.get_api_headers()
        )

        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"OTP request failed: {response.text}")

    def verify_otp(self, otp_id, otp_code):
        """Verify OTP code"""
        data = {
            'otp_id': otp_id,
            'otp_code': otp_code
        }

        response = requests.post(
            f"{self.base_url}/api/auth/verify-otp",
            json=data,
            headers=self.get_api_headers()
        )

        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"OTP verification failed: {response.text}")

# Usage example
auth = PiaxisAuth()

# API Key authentication
headers = auth.get_api_headers()

# OAuth2 flow
auth_url = auth.get_authorization_url(
    redirect_uri='https://yourapp.com/callback',
    scopes=['payments:create', 'escrow:create'],
    state='random-state-string'
)
print(f"Redirect user to: {auth_url}")

# After callback, exchange code for token
token_data = auth.exchange_code_for_token('auth_code_here', 'https://yourapp.com/callback')

# Use OAuth headers for subsequent requests
oauth_headers = auth.get_oauth_headers()

Node.js Implementation

const axios = require('axios');

class PiaxisAuth {
    constructor(options = {}) {
        this.apiKey = options.apiKey || process.env.piaxis_API_KEY;
        this.clientId = options.clientId || process.env.piaxis_CLIENT_ID;
        this.accessToken = null;
        this.refreshToken = null;
        this.tokenExpires = null;
        this.baseUrl = 'https://api.gopiaxis.com';
    }

    getApiHeaders() {
        return {
            'api-key': this.apiKey,
            'Content-Type': 'application/json'
        };
    }

    getOAuthHeaders() {
        if (!this.accessToken || this.isTokenExpired()) {
            throw new Error('No valid access token available');
        }

        return {
            'Authorization': `Bearer ${this.accessToken}`,
            'Content-Type': 'application/json'
        };
    }

    getAuthorizationUrl(redirectUri, scopes, state = null) {
        const params = new URLSearchParams({
            merchant_id: this.clientId,
            redirect_uri: redirectUri,
            response_type: 'code',
            scope: scopes.join(' ')
        });

        if (state) {
            params.append('state', state);
        }

        return `${this.baseUrl}/api/authorize?${params.toString()}`;
    }

    async exchangeCodeForToken(code, redirectUri) {
        const data = new URLSearchParams({
            grant_type: 'authorization_code',
            code: code,
            redirect_uri: redirectUri,
            client_id: this.clientId
        });

        try {
            const response = await axios.post(`${this.baseUrl}/api/oauth/token`, data, {
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
            });

            const tokenData = response.data;
            this.accessToken = tokenData.access_token;
            this.refreshToken = tokenData.refresh_token;
            this.tokenExpires = new Date(Date.now() + tokenData.expires_in * 1000);

            return tokenData;
        } catch (error) {
            throw new Error(`Token exchange failed: ${error.response?.data || error.message}`);
        }
    }

    async refreshAccessToken() {
        if (!this.refreshToken) {
            throw new Error('No refresh token available');
        }

        const data = new URLSearchParams({
            grant_type: 'refresh_token',
            refresh_token: this.refreshToken,
            client_id: this.clientId
        });

        try {
            const response = await axios.post(`${this.baseUrl}/api/oauth/refresh`, data, {
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
            });

            const tokenData = response.data;
            this.accessToken = tokenData.access_token;
            this.tokenExpires = new Date(Date.now() + tokenData.expires_in * 1000);

            return tokenData;
        } catch (error) {
            throw new Error(`Token refresh failed: ${error.response?.data || error.message}`);
        }
    }

    isTokenExpired() {
        if (!this.tokenExpires) return true;
        return new Date() >= this.tokenExpires;
    }

    async requestOtp(operation, additionalData = {}) {
        const data = { operation, ...additionalData };

        try {
            const response = await axios.post(`${this.baseUrl}/api/auth/request-otp`, data, {
                headers: this.getApiHeaders()
            });
            return response.data;
        } catch (error) {
            throw new Error(`OTP request failed: ${error.response?.data || error.message}`);
        }
    }

    async verifyOtp(otpId, otpCode) {
        const data = {
            otp_id: otpId,
            otp_code: otpCode
        };

        try {
            const response = await axios.post(`${this.baseUrl}/api/auth/verify-otp`, data, {
                headers: this.getApiHeaders()
            });
            return response.data;
        } catch (error) {
            throw new Error(`OTP verification failed: ${error.response?.data || error.message}`);
        }
    }
}

// Usage example
const auth = new PiaxisAuth();

// API Key authentication
const headers = auth.getApiHeaders();

// OAuth2 flow
const authUrl = auth.getAuthorizationUrl(
    'https://yourapp.com/callback',
    ['payments:create', 'escrow:create'],
    'random-state-string'
);
console.log('Redirect user to:', authUrl);

// After callback, exchange code for token
auth.exchangeCodeForToken('auth_code_here', 'https://yourapp.com/callback')
    .then(tokenData => {
        console.log('Token obtained:', tokenData);

        // Use OAuth headers for subsequent requests
        const oauthHeaders = auth.getOAuthHeaders();
    })
    .catch(error => {
        console.error('Authentication failed:', error.message);
    });

module.exports = PiaxisAuth;

Rate Limiting & Monitoring

Rate Limits

Piaxis implements rate limiting to ensure fair usage and system stability:

Endpoint Category

Rate Limit

Description

Authentication

100/hour

OAuth token requests, OTP operations

Payment Creation

1000/hour

Creating new payments

Escrow Operations

500/hour

Creating, releasing, reversing escrows

Data Retrieval

5000/hour

Getting payment/escrow status

Webhooks

N/A

Unlimited (outbound from Piaxis)

Rate Limit Headers:

All API responses include rate limiting information:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1642694400

Handling Rate Limits:

import time
import requests

def make_api_request_with_backoff(url, headers, data, max_retries=3):
    for attempt in range(max_retries):
        response = requests.post(url, headers=headers, json=data)

        if response.status_code == 429:
            # Rate limited, check retry-after header
            retry_after = int(response.headers.get('Retry-After', 60))
            print(f"Rate limited, waiting {retry_after} seconds...")
            time.sleep(retry_after)
            continue

        return response

    raise Exception("Max retries exceeded due to rate limiting")

Monitoring & Logging

Request ID Tracking:

Every API response includes a unique request ID for tracking:

{
  "data": {...},
  "meta": {
    "request_id": "req_1234567890abcdef",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

Recommended Logging:

import logging
import json

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def log_api_call(method, url, request_data, response_data, status_code, request_id):
    log_entry = {
        'method': method,
        'url': url,
        'request_data': request_data,
        'response_data': response_data,
        'status_code': status_code,
        'request_id': request_id,
        'timestamp': datetime.now().isoformat()
    }

    if status_code >= 400:
        logger.error(f"API Error: {json.dumps(log_entry)}")
    else:
        logger.info(f"API Success: {json.dumps(log_entry)}")

Support & Resources

Documentation: - API Reference: https://docs.gopiaxis.com/api/ - Integration Guides: https://docs.gopiaxis.com/guides/ - SDKs: https://github.com/piaxis/

Support Channels: - Email: api-support@piaxis.com - Developer Forum: https://forum.gopiaxis.com/ - Status Page: https://status.gopiaxis.com/

Emergency Contact: For critical production issues: emergency@piaxis.com

Testing: - Sandbox API: https://sandbox-api.gopiaxis.com - Test Cards: Available in merchant dashboard - Webhook Testing: Use ngrok or similar tools for local development