Skip to main content

Operations Overview

Operations are transactions that move money. 88Pay supports two flows: collecting money from customers (PAYIN) and sending money to recipients (PAYOUT).
Prerequisites: Valid authentication token required for all operations.

Operation Flows

Accept payments from customers
MethodProcessing TimeUser Experience
CardsInstantRedirect to checkout
Bank Transfer1-3 business daysBank details provided
CashUp to 72 hoursVoucher generated
Digital WalletsInstantApp redirect/QR
Crypto (USDT)~10 minutesAddress provided
Use cases:
  • E-commerce checkouts
  • Subscription billing
  • Invoice payments
  • Service fees
Explore payment methods →

Create Payment (PAYIN)

Endpoint

POST https://api-sandbox.88pay.io/api/transactions/charges

Authentication Headers

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

Request Body

flow
string
required
Operation flow typeValue: "PAYIN"
method_code
string
required
Payment method identifierExamples: "CARDS", "NEQUI", "CASH", "BANK_TRANSFER", "USDT"View all method codes →
method_category
string
required
Payment method categoryOptions: "CARD", "WALLET", "CASH", "BANK", "CRYPTO"
amount
number
required
Transaction amount (supports decimals)Examples: 50000, 50000.50Note: For crypto, use cryptocurrency denomination (e.g., 50 for 50 USDT)
currency
string
required
ISO 4217 currency codeExamples: "COP" (Colombian Peso), "USD" (US Dollar), "MXN" (Mexican Peso)Must match country’s primary currency
country
string
required
ISO 3166-1 alpha-3 country codeExamples: "COL" (Colombia), "MEX" (Mexico), "BRA" (Brazil)
description
string
required
Transaction description (shown to customer)Example: "Order #12345 - Premium Subscription"Max length: 255 characters
customer_id
string
required
Your internal customer identifierExample: "user_abc123", "cust_001"Used for reconciliation and support
notification_url
string
required
Your webhook endpoint for transaction updatesMust be publicly accessible HTTPS URLExample: "https://yoursite.com/webhooks/88pay"Setup webhooks →
return_url
string
Customer redirect URL after paymentRequired for: Cards, some walletsNot used for: Bank transfers, cashExample: "https://yoursite.com/payment/success?order=12345"

Payment Methods Examples

Credit/Debit Card Payment

    curl -X POST "https://api-sandbox.88pay.io/api/transactions/charges" \
      -H "Authorization: Bearer YOUR_TOKEN" \
      -H "x-session-id: YOUR_SESSION_ID" \
      -H "Content-Type: application/json" \
      -d '{
        "flow": "PAYIN",
        "method_code": "CARDS",
        "method_category": "CARD",
        "amount": 50000,
        "currency": "COP",
        "country": "COL",
        "description": "Order #12345",
        "customer_id": "user_001",
        "notification_url": "https://yoursite.com/webhook",
        "return_url": "https://yoursite.com/success"
      }'
Customer Flow:
  1. Redirect to urlCheckout
  2. Enter card details (supports 3D Secure)
  3. Complete payment
  4. Return to return_url
  5. Webhook sent with final status
Response:
    {
      "status": "Success",
      "code": 200,
      "data": {
        "urlCheckout": "https://link.88pay.io/ABC123",
        "reference": "IP9BD7CJES6",
        "amount": 50000,
        "currency": "COP"
      }
    }
Card payments guide →

Create Payout (PAYOUT)

Endpoint

POST https://api-sandbox.88pay.io/api/transactions/cashout-bank-transfer

Bank Transfer Payout

curl -X POST "https://api-sandbox.88pay.io/api/transactions/cashout-bank-transfer" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "x-session-id: YOUR_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "flow": "PAYOUT",
    "method_code": "BANK_TRANSFER",
    "method_category": "BANK",
    "amount": 100000,
    "currency": "COP",
    "country": "COL",
    "description": "Vendor payment #789",
    "customer_id": "vendor_xyz",
    "notification_url": "https://yoursite.com/webhook",
    "bank_account": {
      "account_number": "1234567890",
      "account_type": "savings",
      "bank_code": "001",
      "holder_name": "Juan Perez",
      "holder_document": "1234567890",
      "holder_document_type": "CC"
    }
  }'

Additional Payout Parameters

bank_account
object
required
Recipient bank account details
bank_account.account_number
string
required
Bank account number
bank_account.account_type
string
required
Account type: "savings" or "checking"
bank_account.bank_code
string
required
Bank identifier codeSee bank codes by country →
bank_account.holder_name
string
required
Full name of account holder (must match bank records)
bank_account.holder_document
string
required
ID document number
bank_account.holder_document_type
string
required
Document typeOptions: "CC" (Cédula), "NIT", "CE" (Foreign ID), "PP" (Passport)
Response:
{
  "status": "Success",
  "code": 200,
  "data": {
    "reference": "PO9BD7CJES6",
    "status": "PROCESSING",
    "estimated_completion": "2025-01-18T10:00:00Z",
    "amount": 100000,
    "currency": "COP"
  }
}

Transaction Lifecycle

Status Definitions:
StatusMeaningNext Action
PENDINGAwaiting customer actionWait for webhook
PROCESSINGBeing processedWait for webhook
COMPLETEDSuccessfully processedFulfill order
REJECTEDFailed/cancelled/expiredNotify customer, offer retry

Complete Integration Example

Full flow from token generation to payment:
// Complete payment integration
class PaymentService {
  constructor(apiKey, merchantId, baseUrl) {
    this.tokenManager = new TokenManager(apiKey, merchantId, baseUrl);
  }
  
  async createCardPayment(orderData) {
    try {
      // 1. Get authentication token
      const { access_token, session_id } = await this.tokenManager.getToken();
      
      // 2. Create payment
      const response = await fetch(
        `${this.tokenManager.baseUrl}/api/transactions/charges`,
        {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${access_token}`,
            'x-session-id': session_id,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            flow: "PAYIN",
            method_code: "CARDS",
            method_category: "CARD",
            amount: orderData.total,
            currency: orderData.currency,
            country: orderData.country,
            description: `Order #${orderData.id}`,
            customer_id: orderData.customerId,
            notification_url: `${process.env.BASE_URL}/webhooks/88pay`,
            return_url: `${process.env.BASE_URL}/orders/${orderData.id}/confirmation`
          })
        }
      );
      
      if (!response.ok) {
        throw new Error(`Payment creation failed: ${response.status}`);
      }
      
      const { data } = await response.json();
      
      // 3. Save reference to database
      await this.savePaymentReference(orderData.id, data.reference);
      
      // 4. Return checkout URL
      return {
        checkoutUrl: data.urlCheckout,
        reference: data.reference
      };
      
    } catch (error) {
      console.error('Payment creation failed:', error);
      throw error;
    }
  }
  
  async savePaymentReference(orderId, reference) {
    // Save to your database
    await db.orders.update(orderId, {
      payment_reference: reference,
      payment_status: 'PENDING',
      updated_at: new Date()
    });
  }
}

// Usage
const paymentService = new PaymentService(
  process.env.EIGHTY_EIGHT_PAY_API_KEY,
  process.env.EIGHTY_EIGHT_PAY_MERCHANT_ID,
  process.env.EIGHTY_EIGHT_PAY_BASE_URL
);

// In your checkout handler
app.post('/checkout', async (req, res) => {
  const order = await createOrder(req.body);
  
  const payment = await paymentService.createCardPayment({
    id: order.id,
    total: order.total,
    currency: 'COP',
    country: 'COL',
    customerId: req.user.id
  });
  
  // Redirect to 88Pay checkout
  res.redirect(payment.checkoutUrl);
});

Error Handling

    {
      "status": "Error",
      "code": 400,
      "message": "Invalid request parameters",
      "errors": {
        "amount": ["Amount must be greater than 0"],
        "currency": ["Currency code must be 3 characters"]
      }
    }
Common causes:
  • Missing required fields
  • Invalid parameter format
  • Type mismatch (string vs number)
Fix:
    // Validate before sending
    function validatePaymentData(data) {
      if (!data.amount || data.amount <= 0) {
        throw new Error('Amount must be positive');
      }
      if (!data.currency || data.currency.length !== 3) {
        throw new Error('Invalid currency code');
      }
      // ... more validations
    }

Best Practices

Always save the reference returned by 88Pay:
    // Save to database immediately
    await db.transactions.create({
      order_id: order.id,
      payment_reference: data.reference,
      amount: order.total,
      status: 'PENDING',
      created_at: new Date()
    });
Why:
  • Track payment status
  • Handle webhooks correctly
  • Support customer inquiries
  • Reconcile transactions
Never rely solely on return_url:
    // ❌ BAD: Only checking return_url
    app.get('/payment/success', (req, res) => {
      markOrderAsPaid(req.query.order_id); // UNSAFE!
    });
    
    // ✅ GOOD: Using webhooks
    app.post('/webhooks/88pay', (req, res) => {
      const { transaction_status, transaction_reference } = req.body;
      
      if (transaction_status === 'COMPLETED') {
        markOrderAsPaid(transaction_reference);
      }
      
      res.status(200).json({ received: true });
    });
Why:
  • return_url can be manipulated
  • Customer might close browser before return
  • Webhooks are server-to-server (secure)
Setup webhooks →
Implement logic for every status:
    async function handleTransactionStatus(status, reference) {
      switch (status) {
        case 'PENDING':
          await updateOrderStatus(reference, 'awaiting_payment');
          await sendEmail('payment_pending');
          break;
          
        case 'PROCESSING':
          await updateOrderStatus(reference, 'processing');
          break;
          
        case 'COMPLETED':
          await updateOrderStatus(reference, 'paid');
          await fulfillOrder(reference);
          await sendEmail('payment_success');
          break;
          
        case 'REJECTED':
          await updateOrderStatus(reference, 'failed');
          await sendEmail('payment_failed');
          await offerRetryOption(reference);
          break;
          
        default:
          console.error(`Unknown status: ${status}`);
      }
    }
Client-side and server-side validation:
    class PaymentValidator {
      static validate(data) {
        const errors = [];
        
        // Amount validation
        if (!data.amount || data.amount <= 0) {
          errors.push('Amount must be positive');
        }
        
        // Currency validation
        if (!['COP', 'USD', 'MXN', 'BRL'].includes(data.currency)) {
          errors.push('Invalid currency');
        }
        
        // URL validation
        if (data.notification_url && !data.notification_url.startsWith('https://')) {
          errors.push('Webhook URL must be HTTPS');
        }
        
        // Country-currency match
        const currencyMap = {
          COL: 'COP',
          MEX: 'MXN',
          BRA: 'BRL'
        };
        
        if (currencyMap[data.country] !== data.currency) {
          errors.push(`Currency ${data.currency} not valid for ${data.country}`);
        }
        
        if (errors.length > 0) {
          throw new ValidationError(errors.join(', '));
        }
        
        return true;
      }
    }
    
    // Usage
    try {
      PaymentValidator.validate(paymentData);
      await createPayment(paymentData);
    } catch (error) {
      console.error('Validation failed:', error.message);
    }
Test matrix for each payment method:
ScenarioExpected StatusTest
Successful paymentCOMPLETED✅ Use test card 4111…
Declined paymentREJECTED✅ Use test card 4000…0002
Pending paymentPENDING✅ Use test card 4000…0069
Expired voucherREJECTED✅ Wait 7 days (or simulate)
Webhook failureRETRY✅ Return non-200 status
Token expiration401✅ Wait 61 seconds
Testing guide →

Next Steps