· 1 min read

Payment Gateway Integration: A Developer's Practical Guide

Myles Ndlovu
Myles Ndlovu
Fintech Entrepreneur & Developer
Payment Gateway Integration: A Developer's Practical Guide

Integrating a payment gateway sounds straightforward until you actually do it. Myles Ndlovu has integrated with multiple payment providers across different markets and learned that the devil is in the details — webhooks, error handling, reconciliation, and edge cases.

Choosing a Provider

Before writing a single line of code, choose the right provider for your market and use case:

Key Criteria

  • Market coverage: Do they support your target countries and currencies?
  • Payment methods: Cards, bank transfers, mobile money, USSD — which do your customers use?
  • Pricing: Transaction fees, setup costs, monthly minimums
  • API quality: Documentation, SDK availability, sandbox environment
  • Settlement speed: Daily, weekly, or custom settlement to your bank account
  • Support: Response time for integration issues and production incidents

African Market Options

  • Paystack: Strong in Nigeria, Ghana, South Africa. Clean API, good documentation.
  • Flutterwave: Broad African coverage. Supports many payment methods.
  • Yoco: South African focus. Card terminals and online payments.
  • DPO Group: Pan-African. Extensive payment method support.
  • Ozow: South African instant EFT specialist.

The Integration Flow

Most payment gateways follow a similar flow:

1. Customer clicks "Pay"
2. Your server creates a payment session via the gateway API
3. Customer is redirected to the gateway's payment page (or uses an embedded form)
4. Customer enters payment details and authorises
5. Gateway processes the payment
6. Gateway redirects customer back to your site
7. Gateway sends a webhook to your server confirming the result
8. Your server updates the order status

Server-Side Implementation

Creating a Payment Session

async function createPayment(order: Order) {
  const response = await paymentGateway.createSession({
    amount: order.total,
    currency: order.currency,
    reference: order.id, // Your unique order reference
    callbackUrl: 'https://yoursite.com/api/webhooks/payment',
    returnUrl: 'https://yoursite.com/order/complete',
    metadata: {
      orderId: order.id,
      customerId: order.customerId
    }
  });

  // Save the gateway session ID against your order
  await db.orders.update(order.id, {
    paymentSessionId: response.sessionId,
    status: 'payment_pending'
  });

  return response.paymentUrl;
}

Handling Webhooks

Webhooks are the most important part of the integration. They’re the reliable notification that a payment succeeded or failed.

async function handlePaymentWebhook(req: Request) {
  // 1. Verify the webhook signature
  const signature = req.headers['x-gateway-signature'];
  if (!verifySignature(req.body, signature, WEBHOOK_SECRET)) {
    return { status: 401 };
  }

  // 2. Parse the event
  const event = JSON.parse(req.body);

  // 3. Idempotency check — don't process the same event twice
  const processed = await db.webhookEvents.findByEventId(event.id);
  if (processed) return { status: 200 };

  // 4. Process based on event type
  switch (event.type) {
    case 'payment.success':
      await fulfillOrder(event.data.reference);
      break;
    case 'payment.failed':
      await handleFailedPayment(event.data.reference);
      break;
  }

  // 5. Record that we processed this event
  await db.webhookEvents.create({ eventId: event.id, processedAt: new Date() });

  return { status: 200 };
}

Critical webhook rules:

  • Always verify signatures: Never trust unverified webhooks
  • Always respond with 200: Even if processing fails. Handle errors internally and retry processing yourself. If you return an error, the gateway will retry, potentially causing duplicate processing.
  • Always implement idempotency: Webhooks can be delivered multiple times
  • Never rely solely on redirects: The customer might close their browser before the redirect. Webhooks are the authoritative confirmation.

Testing

Sandbox Environment

Every reputable payment gateway provides a sandbox with test credentials. Use test card numbers and test mobile money numbers to simulate:

  • Successful payments
  • Failed payments (insufficient funds, expired card, etc.)
  • Pending payments (3D Secure, mobile money approval)
  • Webhook delivery

Test Scenarios Checklist

  • Happy path: successful payment, webhook received, order fulfilled
  • Failed payment: customer sees appropriate error, can retry
  • Webhook failure: your server is down when the webhook arrives — does the gateway retry?
  • Duplicate webhooks: same event delivered twice — does your system handle it?
  • Timeout: customer starts payment but doesn’t complete — how does your system handle abandoned payments?
  • Currency mismatch: amount differs between your records and the gateway — how do you detect this?

Error Handling

Payments fail for many reasons. Handle each gracefully:

function getCustomerMessage(errorCode: string): string {
  const messages: Record<string, string> = {
    'insufficient_funds': 'Your payment was declined due to insufficient funds.',
    'card_expired': 'Your card has expired. Please use a different card.',
    'do_not_honour': 'Your bank declined this transaction. Please contact your bank.',
    'processing_error': 'We encountered an issue processing your payment. Please try again.',
  };
  return messages[errorCode] || 'Payment could not be processed. Please try again.';
}

Never show raw error codes to customers. Map gateway errors to friendly messages.

Reconciliation

Daily reconciliation ensures your records match the gateway’s:

  1. Download the gateway’s settlement report
  2. Compare each transaction against your database
  3. Flag discrepancies: missing transactions, amount mismatches, status differences
  4. Investigate and resolve discrepancies

Automate this process. Manual reconciliation doesn’t scale.

Security

  • Never log full card numbers or CVVs: PCI DSS compliance requires it
  • Use the gateway’s hosted payment page: Avoids handling card data on your servers
  • Rotate API keys: Change keys periodically and immediately if compromised
  • Restrict webhook endpoints: Only accept requests from the gateway’s known IP addresses
  • Use HTTPS: For all API calls and webhook endpoints

Going Live

Before switching from sandbox to production:

  1. Complete all test scenarios in sandbox
  2. Verify webhook handling with the gateway’s webhook testing tool
  3. Set up monitoring for payment success rates and processing times
  4. Create an incident response plan for payment failures
  5. Start with low transaction limits and increase gradually

Payment integration is one of those things that seems simple but rewards careful, detail-oriented implementation. Get it right, and your customers never think about it. Get it wrong, and it’s all they think about.

Share: