Skip to main content

Two-Layer Security Model

88Pay uses a dual authentication system to protect your transactions:

Layer 1: API Credentials

Long-lived identifiers
  • API Key (never expires)
  • Merchant ID (unique to your account)

Layer 2: Access Tokens

Short-lived authorization
  • JWT tokens (60-second lifetime)
  • Session IDs (paired with tokens)
Every API request requires both an access token and session ID in headers.

Authentication Flow

88Pay authentication flow diagram
How it works:
  1. Exchange credentials (API Key + Merchant ID) for a token
  2. Use token + session ID to make API requests
  3. Token expires after 60 seconds
  4. Generate new token as needed

Getting Your Credentials

1

Login to Dashboard

Visit dash.88pay.io and sign in
2

Complete KYC

Upload required documents in Account Details (credentials unlock after approval)
3

Copy Credentials

Navigate to Settings → API Credentials:
  • API Key: sk_test_... (sandbox) or sk_live_... (production)
  • Merchant ID: MCH-{COUNTRY}-{ID} (e.g., MCH-COL-29ZTP4)
Security Alert: Never commit credentials to Git, expose them client-side, or share them publicly.

Generating Tokens

Endpoint

POST /api/auth/token
    https://api-sandbox.88pay.io/api/auth/token

Request Headers

HeaderTypeDescription
x-api-keystringYour API Key (starts with sk_test_ or sk_live_)
x-merchant-idstringYour Merchant ID (format: MCH-XXX-XXX)

Code Examples

curl -X POST "https://api-sandbox.88pay.io/api/auth/token" \
  -H "x-api-key: sk_test_a1b2c3d4e5f6g7h8" \
  -H "x-merchant-id: MCH-COL-29ZTP4"

Response

    {
      "status": "Success",
      "code": 200,
      "message": "Token generated successfully",
      "data": {
        "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9...",
        "expires_in": 60,
        "session_id": "sess_53a2cfc4-441e-4554-b560-4e008784d98e"
      }
    }

Using Tokens in Requests

Include the token and session ID in all API calls:

Required Headers

HeaderValueExample
AuthorizationBearer {access_token}Bearer eyJhbGci...
x-session-id{session_id}sess_53a2cfc4...
Content-Typeapplication/jsonRequired for POST/PUT

Example API Call

curl -X POST "https://api-sandbox.88pay.io/api/transactions/charges" \
  -H "Authorization: Bearer eyJhbGciOiJFUzI1NiIs..." \
  -H "x-session-id: sess_53a2cfc4-441e-4554-b560-4e008784d98e" \
  -H "Content-Type: application/json" \
  -d '{
    "flow": "PAYIN",
    "method_code": "CARDS",
    "amount": 50000,
    "currency": "COP",
    "country": "COL"
  }'

Token Lifecycle Management

Lifecycle Stages

88Pay lifecycle token
1

Generate

Call /api/auth/token → receive token + session ID
2

Use

Make API requests within 60 seconds
3

Monitor

Watch for 401 errors (expired token)
4

Refresh

Generate new token when needed
Cache tokens for 50 seconds to avoid rate limits:
class TokenManager {
  constructor(apiKey, merchantId, baseUrl) {
    this.apiKey = apiKey;
    this.merchantId = merchantId;
    this.baseUrl = baseUrl;
    this.cache = null;
    this.expiresAt = null;
  }
  
  async getToken() {
    // Return cached token if valid
    if (this.cache && Date.now() < this.expiresAt) {
      return this.cache;
    }
    
    // Generate new token
    const response = await fetch(`${this.baseUrl}/api/auth/token`, {
      method: 'POST',
      headers: {
        'x-api-key': this.apiKey,
        'x-merchant-id': this.merchantId
      }
    });
    
    const { data } = await response.json();
    
    // Cache with 50s TTL (10s safety buffer)
    this.cache = data;
    this.expiresAt = Date.now() + 50000;
    
    return data;
  }
  
  async makeRequest(endpoint, options = {}) {
    const { access_token, session_id } = await this.getToken();
    
    return fetch(`${this.baseUrl}${endpoint}`, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${access_token}`,
        'x-session-id': session_id,
        'Content-Type': 'application/json'
      }
    });
  }
}

// Usage
const client = new TokenManager(
  process.env.EIGHTY_EIGHT_PAY_API_KEY,
  process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID,
  'https://api-sandbox.88pay.io'
);

// Automatic token management
const payment = await client.makeRequest('/api/transactions/charges', {
  method: 'POST',
  body: JSON.stringify({ /* payment data */ })
});

Security Best Practices

Never hardcode credentials:
    # .env file
    EIGHTY_EIGHT_PAY_API_KEY=sk_test_a1b2c3d4e5f6g7h8
    EIGHTY_EIGHT_PAY_MERCHANT_ID=MCH-COL-29ZTP4
    EIGHTY_EIGHT_PAY_BASE_URL=https://api-sandbox.88pay.io
    // Load from environment
    const apiKey = process.env.EIGHTY_EIGHT_PAY_API_KEY;
    const merchantId = process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID;
Add to .gitignore:
    .env
    .env.local
    .env.production
Backend handles authentication:
    // ✅ CORRECT: Backend API route
    // /api/create-payment.js
    export default async function handler(req, res) {
      const token = await getToken(); // Server-side only
      
      const payment = await fetch('https://api-sandbox.88pay.io/...', {
        headers: { 'Authorization': `Bearer ${token.access_token}` }
      });
      
      res.json(await payment.json());
    }
    // ❌ WRONG: Client-side code
    const apiKey = 'sk_test_...'; // Exposed in browser!
  • All requests must use https:// (not http://)
  • HTTP requests are automatically rejected
  • Valid SSL certificates required in production
Enable in Dashboard → Settings → Security:
  1. Add your server IPs (IPv4 and IPv6)
  2. API rejects requests from non-whitelisted IPs
  3. Update list when scaling infrastructure
Recommended schedule:
  • Rotate every 90 days (proactive)
  • Rotate immediately if compromised
  • Keep old key active for 24h during transition
How to rotate:
  1. Generate new key in dashboard
  2. Update environment variables
  3. Deploy changes
  4. Deactivate old key after 24h
Track suspicious activity:
  • Unexpected 401/403 errors
  • Rate limit hits (429 errors)
  • Requests from unknown IPs
  • Failed authentication attempts
Integrate with monitoring tools:
  • Sentry, Datadog, New Relic, etc.
  • Set up alerts for auth failures

Environment Configuration

Purpose: Testing and development
    Base URL: https://api-sandbox.88pay.io
    API Key: sk_test_...
    Merchant ID: MCH-COL-TEST...
Features:
  • No real money transferred
  • Test cards/accounts available
  • Instant transaction approvals
  • Relaxed rate limits
  • Separate dashboard section
Credentials are environment-specific. Sandbox keys fail in production and vice versa.

Rate Limits

TimeframeSandboxProduction
Per minute10 tokens10 tokens
Per hour100 tokens100 tokens
Exceeding limits:
  • Returns 429 Too Many Requests
  • Retry after 60 seconds
  • Implement exponential backoff
Stay under limits: Cache tokens for 50 seconds = max 72 tokens/hour (28% of limit).

Testing Checklist

1

Verify Credentials Format

  • API Key: sk_test_... (sandbox) or sk_live_... (production)
  • Merchant ID: MCH-{COUNTRY}-{ID}
  • No extra spaces or line breaks
2

Test Token Generation

  • Successful response (200 OK)
  • access_token is a valid JWT
  • session_id is a UUID
  • expires_in equals 60
3

Test API Requests

  • Include Authorization header
  • Include x-session-id header
  • Successful transaction creation
4

Test Error Scenarios

  • Handle expired token (401)
  • Handle invalid credentials (401)
  • Handle rate limiting (429)
  • Automatic retry with new token

Troubleshooting

Symptoms:
    { "code": 401, "message": "Unauthorized" }
Causes:
  • Token older than 60 seconds
  • Malformed Bearer header
  • Wrong token for environment
Solutions:
  1. Generate fresh token
  2. Check header format: Authorization: Bearer {token}
  3. Verify environment (sandbox vs production)
Symptoms:
    { "code": 401, "message": "Invalid API key or merchant ID" }
Causes:
  • Typo in API Key or Merchant ID
  • Using production keys in sandbox (or vice versa)
  • Extra whitespace in credentials
Solutions:
  1. Copy credentials directly from dashboard
  2. Trim whitespace: apiKey.trim()
  3. Check environment matches credentials
Symptoms:
    { "code": 400, "message": "Missing required header: x-session-id" }
Solution: Always include session ID from token response:
    headers: {
      'Authorization': `Bearer ${access_token}`,
      'x-session-id': session_id  // Don't forget this!
    }
Symptoms:
    { "code": 429, "message": "Rate limit exceeded" }
Causes:
  • Generating tokens too frequently
  • Not caching tokens between requests
Solutions:
  1. Implement token caching (see example above)
  2. Wait 60 seconds before retry
  3. Use exponential backoff for retries

What’s Next?