Skip to main content
Understanding payment flows is essential for building reliable payment integrations. This guide explains the complete payment flow from initiation to completion, including error handling and edge cases.

Standard Payment Flow

Step-by-Step Flow

1

1. Create Recipient (Optional)

If recipient doesn’t exist, create recipient record with bank account details.
const recipient = await poolerClient.post('/payments/recipients', {
  account_number: '1234567890',
  account_name: 'John Doe',
  account_type: 'individual',
  account_currency: 'NGN',
  account_country_code: 'Nigeria',
  account_bank_name: 'Pooler Bank',
  account_bank_code: 'POOLER001'
});
2

2. Initiate Payment

Create payment quote by calling the initiate endpoint.
const quote = await poolerClient.post('/payments/initiate', {
  amount: 1000,
  currency: 'NGN',
  description: 'Payment for services',
  reference: 'PAY-001',
  recipient_id: recipient.data.data.id
});
3

3. Review Quote

Review the payment quote including:
  • Payment amount
  • Fees breakdown
  • Exchange rate (if applicable)
  • Total amount to deduct
  • Quote expiration time
const quoteData = quote.data.data;
console.log('Amount:', quoteData.amount);
console.log('Fees:', quoteData.fees);
console.log('Exchange Rate:', quoteData.exchange_rate);
console.log('Total:', quoteData.total_amount);
console.log('Expires At:', quoteData.expires_at);
4

4. Confirm Payment

Confirm the payment using quote ID and reference.
const payment = await poolerClient.post('/payments/complete', {
  quote_id: quoteData.quote_id,
  reference: quoteData.reference
});
5

5. Payment Processing

Pooler processes the payment:
  • Validates payment details
  • Checks account balance
  • Selects payment route
  • Initiates transfer through payment network
6

6. Payment Completion

Payment is completed and recipient receives funds.
const status = await poolerClient.get(`/payments/${payment.data.data.id}`);
console.log('Status:', status.data.data.status);

Payment States and Transitions

State Descriptions

StatusDescription
initiatedPayment quote created
Waiting for confirmation
Quote expires after 5 minutes
pendingPayment confirmed
Awaiting processing
Funds reserved
processingPayment being processed
Transfer initiated through payment network
Cannot be cancelled
completedPayment successfully completed
Recipient received funds
Final state
failedPayment processing failed
Funds returned (if deducted)
Can retry with new payment
rejectedPayment was rejected
May be due to compliance or validation
Funds returned

Error Handling

Common Errors and Handling

Insufficient Balance
try {
  await poolerClient.post('/payments/initiate', paymentData);
} catch (error) {
  if (error.response?.status === 400 && error.response?.data?.message?.includes('insufficient')) {
    // Handle insufficient balance
    await topUpAccount();
    // Retry payment
  }
}
Invalid Recipient
try {
  await poolerClient.post('/payments/initiate', paymentData);
} catch (error) {
  if (error.response?.status === 400 && error.response?.data?.message?.includes('recipient')) {
    // Verify recipient details
    const recipient = await verifyRecipient(recipientId);
    // Update recipient if needed
  }
}
Quote Expired
try {
  await poolerClient.post('/payments/complete', { quote_id, reference });
} catch (error) {
  if (error.response?.status === 400 && error.response?.data?.message?.includes('expired')) {
    // Create new quote
    const newQuote = await poolerClient.post('/payments/initiate', paymentData);
    // Confirm with new quote
  }
}

Error Flow Diagram

Webhook Events in Payment Flow

EventDescription
payment.initiatedFired when payment quote is created
Contains quote details
payment.completedFired when payment is successfully completed
Contains final payment details
payment.failedFired when payment processing fails
Contains error information
payment.rejectedFired when payment is rejected
Contains rejection reason

Webhook Handling Example

// Webhook handler
async function handlePaymentWebhook(webhook) {
  const { event, data } = webhook;
  
  switch (event) {
    case 'payment.completed':
      await updateOrderStatus(data.reference, 'paid');
      await notifyCustomer(data.reference);
      break;
      
    case 'payment.failed':
      await handlePaymentFailure(data);
      await notifyCustomerOfFailure(data.reference);
      break;
      
    case 'payment.rejected':
      await handlePaymentRejection(data);
      break;
  }
}

Payment Retry Strategies

When to Retry

Retry payments in these scenarios:
ScenarioDescription
Network ErrorsTemporary network issues
Timeout ErrorsRequest timeouts
Rate LimitingRate limit errors (with backoff)
Temporary FailuresTransient service errors

Retry Implementation

async function initiatePaymentWithRetry(paymentData, maxRetries = 3) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const quote = await poolerClient.post('/payments/initiate', paymentData);
      return quote;
    } catch (error) {
      if (attempt === maxRetries - 1) throw error;
      
      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Best Practices

  • Review payment quotes before confirming to understand all costs.
  • Implement logic to handle expired quotes and create new ones.
  • Set up webhooks for real-time payment status updates instead of polling.
  • Implement retry logic for transient failures with exponential backoff.
  • Regularly check payment status for pending payments.
  • Reconcile payments daily to catch issues early.