Operations Overview
Operations are transactions that move money. 88Pay supports two flows: collecting money from customers (PAYIN) and sending money to recipients (PAYOUT).
Operation Flows
PAYIN (Collect)
PAYOUT (Disburse)
Accept payments from customers Method Processing Time User Experience Cards Instant Redirect to checkout Bank Transfer 1-3 business days Bank details provided Cash Up to 72 hours Voucher generated Digital Wallets Instant App redirect/QR Crypto (USDT) ~10 minutes Address provided
Use cases:
E-commerce checkouts
Subscription billing
Invoice payments
Service fees
Explore payment methods → Send money to recipients Method Processing Time Requirements Bank Transfer Same/next day Bank account details Cash Pickup Instant Recipient ID Wallet Transfer Instant Wallet number
Use cases:
Vendor payments
Marketplace settlements
Refunds
Salary disbursements
Payouts require additional KYC verification and minimum balance thresholds.
Create Payment (PAYIN)
Endpoint
POST https://api-sandbox.88pay.io/api/transactions/charges
Authorization: Bearer {access_token}
x-session-id: {session_id}
Content-Type: application/json
Request Body
Operation flow type Value: "PAYIN"
Payment method category Options: "CARD", "WALLET", "CASH", "BANK", "CRYPTO"
Transaction amount (supports decimals) Examples: 50000, 50000.50 Note: For crypto, use cryptocurrency denomination (e.g., 50 for 50 USDT)
ISO 4217 currency code Examples: "COP" (Colombian Peso), "USD" (US Dollar), "MXN" (Mexican Peso) Must match country’s primary currency
ISO 3166-1 alpha-3 country code Examples: "COL" (Colombia), "MEX" (Mexico), "BRA" (Brazil)
Transaction description (shown to customer) Example: "Order #12345 - Premium Subscription" Max length: 255 characters
Your internal customer identifier Example: "user_abc123", "cust_001" Used for reconciliation and support
Your webhook endpoint for transaction updates Must be publicly accessible HTTPS URL Example: "https://yoursite.com/webhooks/88pay" Setup webhooks →
Customer redirect URL after payment Required for: Cards, some walletsNot used for: Bank transfers, cashExample: "https://yoursite.com/payment/success?order=12345"
Payment Methods Examples
Cards (Visa/MC/Amex)
Bank Transfer
Cash (Voucher)
Digital Wallets
Crypto (USDT)
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:
Redirect to urlCheckout
Enter card details (supports 3D Secure)
Complete payment
Return to return_url
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 → Bank Transfer Payment {
"flow" : "PAYIN" ,
"method_code" : "BANK_TRANSFER" ,
"method_category" : "BANK" ,
"amount" : 100000 ,
"currency" : "COP" ,
"country" : "COL" ,
"description" : "Invoice #789" ,
"customer_id" : "company_xyz" ,
"notification_url" : "https://yoursite.com/webhook"
}
Customer Flow:
Receive bank account details
Make transfer from their bank
Webhook sent when transfer detected (1-3 business days)
Response: {
"status" : "Success" ,
"code" : 200 ,
"data" : {
"reference" : "IP9BD7CJES6" ,
"bank_details" : {
"bank_name" : "Banco de Colombia" ,
"account_number" : "1234567890" ,
"account_type" : "savings" ,
"holder_name" : "88Pay Colombia SAS"
},
"amount" : 100000 ,
"currency" : "COP"
}
}
Bank transfers don’t require return_url as there’s no redirect flow.
Bank transfer guide → Cash Payment (OXXO, Baloto, etc.) {
"flow" : "PAYIN" ,
"method_code" : "CASH" ,
"method_category" : "CASH" ,
"amount" : 50000 ,
"currency" : "COP" ,
"country" : "COL" ,
"description" : "Order #456" ,
"customer_id" : "user_002" ,
"notification_url" : "https://yoursite.com/webhook"
}
Customer Flow:
Receive payment voucher (PDF/barcode)
Visit authorized collection point (OXXO, Baloto, etc.)
Pay with cash
Webhook sent when confirmed (up to 72 hours)
Response: {
"status" : "Success" ,
"code" : 200 ,
"data" : {
"reference" : "IP9BD7CJES6" ,
"voucher_url" : "https://vouchers.88pay.io/ABC123.pdf" ,
"payment_code" : "88PAY123456789" ,
"expires_at" : "2025-01-24T23:59:59Z" ,
"amount" : 50000 ,
"currency" : "COP"
}
}
Cash vouchers expire after 7 days. Customer must pay before expiration.
Cash payments guide → Digital Wallet Payment (Nequi, Daviplata) {
"flow" : "PAYIN" ,
"method_code" : "NEQUI" ,
"method_category" : "WALLET" ,
"amount" : 50000 ,
"currency" : "COP" ,
"country" : "COL" ,
"description" : "Subscription #789" ,
"customer_id" : "user_003" ,
"notification_url" : "https://yoursite.com/webhook" ,
"return_url" : "https://yoursite.com/success"
}
Available Wallets: Code Name Countries NEQUINequi Colombia DAVIPLATADaviplata Colombia PAYPALPayPal Global MERCADOPAGOMercado Pago LATAM
Customer Flow:
Redirect to wallet app or QR scan
Approve payment in wallet
Instant confirmation
Return to return_url
Digital wallets guide → Cryptocurrency Payment (USDT) {
"flow" : "PAYIN" ,
"method_code" : "USDT" ,
"method_category" : "CRYPTO" ,
"amount" : 50 ,
"currency" : "USD" ,
"country" : "COL" ,
"description" : "Premium Plan" ,
"customer_id" : "user_004" ,
"notification_url" : "https://yoursite.com/webhook" ,
"return_url" : "https://yoursite.com/success"
}
Supported Networks:
TRC20 (Tron) - Recommended (lower fees)
ERC20 (Ethereum)
Customer Flow:
Receive deposit address
Send USDT to address
Wait for blockchain confirmations (~10 min)
Webhook sent when confirmed
Response: {
"status" : "Success" ,
"code" : 200 ,
"data" : {
"reference" : "IP9BD7CJES6" ,
"deposit_address" : "TXYZabc123..." ,
"network" : "TRC20" ,
"amount" : 50 ,
"currency" : "USD" ,
"qr_code_url" : "https://qr.88pay.io/ABC123.png"
}
}
Important: Amount should be in cryptocurrency denomination (50 = 50 USDT), not fiat.
Crypto 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
Recipient bank account details
bank_account.account_number
Bank account number
bank_account.account_type
Account type: "savings" or "checking"
Full name of account holder (must match bank records)
bank_account.holder_document
ID document number
bank_account.holder_document_type
Document type Options: "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:
Status Meaning Next Action PENDINGAwaiting customer action Wait for webhook PROCESSINGBeing processed Wait for webhook COMPLETEDSuccessfully processed Fulfill order REJECTEDFailed/cancelled/expired Notify 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
400 Bad Request
401 Unauthorized
422 Unprocessable
500 Server Error
{
"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
}
{
"status" : "Error" ,
"code" : 401 ,
"message" : "Invalid or expired token"
}
Cause: Token expired (60s lifetime)Fix: async function createPaymentWithRetry ( data ) {
try {
return await createPayment ( data );
} catch ( error ) {
if ( error . status === 401 ) {
// Regenerate token and retry
await tokenManager . getToken (); // Generates new token
return await createPayment ( data );
}
throw error ;
}
}
{
"status" : "Error" ,
"code" : 422 ,
"message" : "Payment method not available for this country"
}
Common causes:
Method not supported in specified country
Currency doesn’t match country
Amount outside allowed range
Fix: Check available methods before creating payment: const supportedMethods = {
COL: [ 'CARDS' , 'NEQUI' , 'DAVIPLATA' , 'CASH' , 'BANK_TRANSFER' ],
MEX: [ 'CARDS' , 'OXXO' , 'SPEI' ],
BRA: [ 'CARDS' , 'PIX' , 'BOLETO' ]
};
if ( ! supportedMethods [ country ]. includes ( method_code )) {
throw new Error ( ` ${ method_code } not available in ${ country } ` );
}
{
"status" : "Error" ,
"code" : 500 ,
"message" : "Internal server error"
}
Cause: Temporary service disruptionFix: Implement exponential backoff: async function createPaymentWithBackoff ( data , retries = 3 ) {
for ( let i = 0 ; i < retries ; i ++ ) {
try {
return await createPayment ( data );
} catch ( error ) {
if ( error . status === 500 && i < retries - 1 ) {
const delay = Math . pow ( 2 , i ) * 1000 ; // 1s, 2s, 4s
await new Promise ( resolve => setTimeout ( resolve , delay ));
} else {
throw error ;
}
}
}
}
Best Practices
Store Transaction References
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
Implement Webhooks (Critical)
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 Thoroughly in Sandbox
Test matrix for each payment method: Scenario Expected Status Test Successful payment COMPLETED ✅ Use test card 4111… Declined payment REJECTED ✅ Use test card 4000…0002 Pending payment PENDING ✅ Use test card 4000…0069 Expired voucher REJECTED ✅ Wait 7 days (or simulate) Webhook failure RETRY ✅ Return non-200 status Token expiration 401 ✅ Wait 61 seconds
Testing guide →
Next Steps