Skip to main content

Token Authentication Overview

88Pay uses JWT tokens for API authentication. Each token is valid for 60 seconds and must be paired with a session ID for all API requests.
Why tokens? Long-lived credentials (API Keys) never leave your server. Short-lived tokens authenticate individual requests, limiting exposure if intercepted.

Authentication Architecture

88Pay integration flow diagram
Benefits:
  • ๐Ÿ”’ API Keys never exposed in requests
  • ๐Ÿšซ Limited blast radius if token leaked
  • ๐Ÿ“Š Session tracking and auditing
  • ๐Ÿ”„ Easy credential rotation

Generate Token

Endpoint

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

Headers

HeaderRequiredDescription
x-api-keyโœ…Your API Key (starts with sk_test_ or sk_live_)
x-merchant-idโœ…Your Merchant ID (format: MCH-{COUNTRY}-{ID})
No request body required. Authentication is header-based only.

Request 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-TEST-12345"

Response Structure

    {
      "status": "Success",
      "code": 200,
      "message": "Token generated successfully",
      "data": {
        "access_token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJtZXJjaGFudF9pZCI6Ik1DSC1DT0wtMjlaVFA0IiwiaWF0IjoxNzM3MTQwNDAwLCJleHAiOjE3MzcxNDA0NjB9.MEUCIQDx...",
        "expires_in": 60,
        "session_id": "sess_53a2cfc4-441e-4554-b560-4e008784d98e"
      }
    }
Field Descriptions:
FieldTypeDescription
access_tokenstringJWT token for API authorization (use in Authorization header)
expires_innumberToken lifetime in seconds (always 60)
session_idstringSession identifier (use in x-session-id header)

Token Caching Strategy

Critical: Donโ€™t generate a new token for every request! This wastes resources and triggers rate limits.
Cache tokens for 50 seconds (10-second safety buffer before 60s expiry):
    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 (10s safety margin)
        if (this.cache && Date.now() < this.expiresAt - 10000) {
          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
          }
        });
        
        if (!response.ok) {
          throw new Error(`Token generation failed: ${response.status}`);
        }
        
        const { data } = await response.json();
        
        // Cache with 50s TTL
        this.cache = data;
        this.expiresAt = Date.now() + 50000;
        
        return data;
      }
      
      // Convenience method for making authenticated requests
      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'
          }
        });
      }
    }
    
    // Initialize once (singleton pattern)
    const tokenManager = new TokenManager(
      process.env.EIGHTY_EIGHT_PAY_API_KEY,
      process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID,
      process.env.EIGHTY_EIGHT_PAY_BASE_URL || 'https://api-sandbox.88pay.io'
    );
    
    // Usage
    const payment = await tokenManager.makeRequest('/api/transactions/charges', {
      method: 'POST',
      body: JSON.stringify({ /* payment data */ })
    });

Cache Performance Impact

ApproachTokens/HourAPI Calls NeededRate Limit Risk
No caching72 tokens72 token requestsโš ๏ธ High (72% of limit)
50s caching~72 tokens72 token requestsโœ… Low (72% of limit but spread out)
Shared instance1-2 tokens1-2 token requestsโœ… Very low (less than 2%)
Use a singleton pattern (one TokenManager instance per application) to maximize cache effectiveness across all requests.

Using Tokens in Requests

Once you have a token, include it in all API calls:

Required Headers

Authorization: Bearer {access_token}
x-session-id: {session_id}
Content-Type: application/json

Example Authenticated Request

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"
  }'

Rate Limits

TimeframeLimitRecommended Max with Caching
Per minute10 requests1-2 requests
Per hour100 requests72 requests
Exceeding rate limits returns 429 Too Many Requests. Implement exponential backoff and token caching to stay well below limits.

Troubleshooting

Symptoms:
  • 401 errors on API requests
  • โ€œToken expiredโ€ messages
Causes:
  • Network latency between token generation and use
  • System clock drift
  • Processing delay in application
Solutions:
  1. Generate token immediately before use (within same function)
  2. Synchronize system clock with NTP
  3. Use 10-second safety margin in caching
    // Good: Generate and use immediately
    const { access_token, session_id } = await getToken();
    const payment = await createPayment(access_token, session_id);
    
    // Bad: Long delay between generation and use
    const token = await getToken();
    await someOtherOperation(); // Time passes...
    const payment = await createPayment(token); // May be expired!
Symptoms:
  • 429 Too Many Requests responses
  • Intermittent authentication failures
Causes:
  • Generating token on every request
  • Multiple application instances without shared cache
  • Rapid testing/development
Solutions:
  1. Implement 50-second token caching
  2. Use singleton TokenManager
  3. Share cache across instances (Redis, Memcached)
    // Bad: New token every request
    app.post('/payment', async (req, res) => {
      const token = await generateToken(); // DON'T DO THIS
      // ...
    });
    
    // Good: Cached token
    const tokenManager = new TokenManager(...); // Created once
    app.post('/payment', async (req, res) => {
      const token = await tokenManager.getToken(); // Uses cache
      // ...
    });
Symptoms:
  • Manual requests succeed
  • Application requests fail with 401
Causes:
  • Environment variables not loaded
  • Incorrect header formatting
  • Missing x-session-id header
Solutions:
  1. Verify environment variables are loaded:
    console.log('API Key loaded:', !!process.env.EIGHTY_EIGHT_PAY_API_KEY);
    console.log('Merchant ID:', process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID);
  1. Check header format:
    // Correct
    headers: {
      'Authorization': `Bearer ${access_token}`,
      'x-session-id': session_id
    }
    
    // Wrong
    headers: {
      'Authorization': access_token, // Missing "Bearer"
      'session-id': session_id // Wrong header name
    }
Symptoms:
  • Some requests succeed, others fail
  • Intermittent 401 errors under load
Causes:
  • Race condition in token generation
  • Multiple instances generating tokens simultaneously
Solutions: Use mutex/lock for token generation:
    class TokenManager {
      constructor(...) {
        // ...
        this.generating = false;
        this.waitingCallbacks = [];
      }
      
      async getToken() {
        if (this.cache && Date.now() < this.expiresAt - 10000) {
          return this.cache;
        }
        
        // If already generating, wait for it
        if (this.generating) {
          return new Promise(resolve => {
            this.waitingCallbacks.push(resolve);
          });
        }
        
        this.generating = true;
        
        try {
          // Generate token...
          const data = await this._generateToken();
          
          // Notify waiting callbacks
          this.waitingCallbacks.forEach(cb => cb(data));
          this.waitingCallbacks = [];
          
          return data;
        } finally {
          this.generating = false;
        }
      }
    }

Security Considerations

โŒ Donโ€™t do this:
    console.log('Token:', access_token); // EXPOSED IN LOGS!
    logger.info({ token: access_token }); // EXPOSED!
โœ… Do this:
    // Log only non-sensitive metadata
    console.log('Token generated:', {
      expires_at: new Date(Date.now() + 60000),
      session_id: session_id // OK to log
    });
    
    // Or mask the token
    console.log('Token:', access_token.substring(0, 20) + '...');

Testing Your Implementation

Verify your token generation works correctly:
async function testTokenGeneration() {
  console.log('๐Ÿงช Testing token generation...\n');
  
  // Test 1: Basic generation
  console.log('Test 1: Basic token generation');
  try {
    const token1 = await tokenManager.getToken();
    console.log('โœ… Token generated successfully');
    console.log('   - Access token length:', token1.access_token.length);
    console.log('   - Session ID format:', token1.session_id.startsWith('sess_'));
  } catch (error) {
    console.error('โŒ Failed:', error.message);
  }
  
  // Test 2: Caching
  console.log('\nTest 2: Token caching');
  const start = Date.now();
  const token2 = await tokenManager.getToken();
  const duration = Date.now() - start;
  console.log(duration < 100 ? 'โœ… Cached (fast)' : 'โŒ Not cached (slow)');
  console.log('   - Duration:', duration + 'ms');
  
  // Test 3: Expiration
  console.log('\nTest 3: Token expiration');
  console.log('โณ Waiting 51 seconds for token to expire...');
  await new Promise(resolve => setTimeout(resolve, 51000));
  const token3 = await tokenManager.getToken();
  console.log('โœ… New token generated after expiry');
  console.log('   - Different from previous:', token1.access_token !== token3.access_token);
  
  console.log('\nโœจ All tests passed!');
}

// Run tests
testTokenGeneration();

Next Steps