If you've integrated a payment API before, you've probably followed a quickstart guide, pasted in a secret key, hit an endpoint, and shipped it. It works — until a webhook doesn't fire, a customer double-clicks "Pay Now," or a refund silently fails and support gets flooded with tickets.
This article isn't a quickstart guide. It's an attempt to explain why payment APIs are built the way they are,the failure modes they're designed around, and the tradeoffs different providers make — using five real providers as concrete reference points: Flutterwave, Paystack, PesaPal, IntaSend, and Stripe.
If you're a backend engineer who's touched one payment API and wants to understand the category, not just the vendor, this is for you.
1. What a Payment API Actually Is
A Payment API is a service that lets your application move money between two parties — a customer and a merchant — without your application ever directly touching a bank's core systems, a card network's rails, or a mobile money operator's ledger.
That's the whole point: you are not allowed to talk to banks directly, and even if you could, you wouldn't want to.
Why payment APIs exist
Banks, card networks (Visa/Mastercard), and mobile money operators (M-Pesa, Airtel Money) each expose their own proprietary, heavily regulated, and often decades-old interfaces. Integrating with each one directly would require:
- Individual commercial agreements with every bank and network
- Compliance certification for each rail (PCI DSS, ISO 8583 for card switching, telco-specific protocols for mobile money)
- Handling settlement, reconciliation, and dispute resolution per rail
- Maintaining uptime and security to a regulator-grade standard A payment API — usually operated by a payment gateway or payment aggregator — absorbs all of that complexity and exposes a single, consistent REST interface. You send JSON over HTTPS; they deal with ISO 8583 messages, telco USSD callbacks, and bank settlement files. ### Payment Aggregator vs Payment Gateway
These terms get used interchangeably, but there's a real distinction:
- Payment Gateway: Routes a transaction to the correct processor/bank and returns a result. Conceptually similar to a network switch — it doesn't hold merchant funds.
- Payment Aggregator: Onboards many small merchants under its own master merchant account with an acquiring bank, then sub-routes transactions internally. This is why you can sign up for Paystack or Flutterwave in minutes instead of weeks — you're not opening your own merchant account with a bank, you're a sub-merchant of theirs. Flutterwave, Paystack, IntaSend, and PesaPal are all aggregators. Stripe operates as both, depending on market and product (Stripe Connect explicitly turns you into an aggregator for your own sub-merchants).
The technical contract
Regardless of aggregator vs gateway, almost every modern payment API shares the same technical contract:
- REST over HTTPS — no exceptions in 2026; TLS is non-negotiable given cardholder data sensitivity
- JSON request/response bodies
- Key-based authentication — API keys, bearer tokens, or OAuth, transmitted in headers, never in the URL or body
- Idempotent, stateless requests — each API call carries everything needed to process it; state lives server-side on the provider This shared contract is exactly why, once you understand one payment API deeply, the rest become 80% pattern-matching and 20% provider-specific quirks.
2. Payment Processing Architecture
Here's the full lifecycle of a single payment, end to end:
Customer
│
▼
Frontend (Web / Mobile App)
│ (collects payment method, redirects to checkout)
▼
Backend Server
│ (creates transaction via secret key — NEVER from frontend)
▼
Payment API (Flutterwave / Paystack / Stripe / etc.)
│
▼
Bank / Card Network / Mobile Money Operator
│ (actual money movement happens here)
▼
Payment Result
│
├──────────────► Webhook ──────► Backend ──────► Database Update
│
└──────────────► Redirect ─────► Frontend Confirmation (UNTRUSTED)
Breaking down each component
Frontend: Collects payment intent — amount, method, maybe card details (tokenized client-side, never touching your server) — and either redirects to a hosted checkout page or opens an embedded payment element. The frontend's job ends at initiating the payment. It should never be the source of truth for whether a payment succeeded.
Backend Server: This is where your secret key lives, and it's the only component authorized to create transactions and query their real status. It initializes the payment, generates a reference ID for your own reconciliation, and later verifies the outcome.
Payment API: Receives the transaction request, validates it, and hands off to the actual settlement rail — a card network, a bank's account-to-account rail, or a mobile money operator's collection API (e.g., M-Pesa STK Push).
Bank / Card Network / Mobile Money: This is where money actually moves. Everything before this point is orchestration; this is execution.
Payment Result → Webhook: The provider doesn't wait for you to ask "did it work?" — it pushes an event to a webhook URL you registered, asynchronously, the moment the result is known. This decouples payment completion from your server being "in the room" at that exact millisecond.
Backend → Database Update: On receiving the webhook, your backend re-verifies the transaction (never trusts the webhook payload blindly — more on this in Section 4) and updates your own database as the single source of truth for your application.
Frontend Confirmation: The user is redirected back to your app and shown a success/failure screen — but this is a UX convenience, not a trust boundary. The database record written from the verified webhook is what actually determines whether the order ships.
This separation — frontend shows status, backend decides truth — is the single most important architectural idea in payment systems, and it's the one junior implementations get wrong most often.
3. The Universal Integration Workflow
Nearly every provider in this article — despite very different dashboards, docs, and SDKs — implements the same eleven-step sequence:
- Create a merchant account with the provider (KYC/KYB verification happens here)
- Obtain API credentials — typically a public/publishable key, a secret key, and sometimes an OAuth-issued access token or consumer key
-
Authenticate requests using those credentials, almost always via an
Authorization: Bearer <SECRET_KEY>header - Create a payment request — amount, currency, customer info, and a merchant-generated reference ID
- Provider creates a transaction record and returns something you redirect the user to — a checkout URL, a Payment Intent, or a payment session
- Customer completes payment via card, mobile money, bank transfer, or wallet
- Provider communicates with the underlying rail (bank, card network, telco)
- Transaction completes (or fails) on that rail
- API returns a response to whichever party is polling or redirected
- Webhook fires, notifying your backend asynchronously
- Backend verifies the payment against the provider's API directly, then fulfills the order ### A representative request
POST /transaction/initialize
Authorization: Bearer sk_live_xxxxxxxxxxxx
Content-Type: application/json
{
"amount": 5000,
"currency": "KES",
"email": "user@email.com",
"reference": "order_9f21ac"
}
Why the secret key never leaves your server
The secret key can create charges, issue refunds, and initiate payouts — essentially, it can move money. If it leaks into frontend JavaScript, mobile app bytecode, or a public repo, anyone can use it to drain funds or create fraudulent transactions attributed to your account. Public/publishable keys, by contrast, are safe client-side because they can only be used to tokenize payment details or initiate a session — never to unilaterally move money or query sensitive account data.
Why step 11 (verification) is non-negotiable
Step 9's API response and the frontend redirect in step 6 can both be spoofed, cached, or manipulated by a malicious client. A user could, in theory, intercept the redirect and forge a "success" URL without ever paying. The only trustworthy source of truth is your backend independently asking the provider, "what is the real status of transaction order_9f21ac?" — using your secret key, over a server-to-server call the client can't touch.
4. Core Backend Concepts
This is where payment APIs stop being "just another REST API" and start requiring systems-level thinking.
Payment Initialization
Before money moves, providers require you to declare intent — create a transaction object describing what you're about to charge, for how much, in what currency, before the customer ever enters payment details.
Why not just charge directly in one call? Because payments are rarely atomic in practice:
- The customer might need to be redirected to a 3D Secure challenge, an OTP screen, or an STK push prompt on their phone
- The amount might need adjustment (tips, currency conversion) before capture
- You need a stable object to reference across multiple round trips (frontend polling, webhook correlation, retries)
Stripe makes this explicit with Payment Intents — a stateful object that moves through
requires_payment_method→requires_confirmation→requires_action→processing→succeeded. Flutterwave and Paystack achieve the same thing more implicitly: you call/transaction/initialize, get back a checkout URL, and the transaction object behind that URL tracks state server-side even though it's not exposed as a rich state machine to you.
Payment Verification
Rule: never trust a frontend "success" message.
The frontend can only tell you what the user's browser observed — a redirect parameter, a client-side SDK callback. None of that is cryptographically tied to an actual settled transaction. A user can manually craft a ?status=success URL, a proxy can rewrite responses, or a client bug can fire a success callback without the underlying payment ever completing.
The only reliable verification path is a server-to-server GET request, authenticated with your secret key, asking the provider directly:
GET /transaction/verify/{reference}
Authorization: Bearer sk_live_xxxxxxxxxxxx
You fulfill the order only when this call — not the redirect, not the webhook payload alone — confirms status: success.
Webhooks
Polling vs. webhooks is a classic distributed-systems tradeoff:
-
Polling (repeatedly calling
GET /transaction/{id}) is simple but wasteful — you're guessing when the result will be ready, burning API quota and adding latency. -
Webhooks invert the model: the provider pushes an event to you the instant a state change happens. This is event-driven architecture applied to payments — your backend reacts to events (
charge.completed,payment.failed,refund.completed) instead of asking "are we there yet?" in a loop. A typical webhook backend flow:
Provider ──POST──► /webhooks/payment
│
▼
Verify signature (HMAC)
│
▼
Is this event new? (idempotency check)
│
▼
Re-verify via GET /verify (don't trust payload)
│
▼
Update database, trigger fulfillment
│
▼
Return 200 OK (ack — or provider will retry)
Two details matter enormously here:
- Signature verification — providers sign webhook payloads (usually HMAC-SHA512 over the raw body, using a webhook secret you generate). Skipping this means anyone who knows your webhook URL can forge a payment event.
- Returning 200 quickly — providers retry failed/timeout webhook deliveries with backoff. Your handler should acknowledge fast and do heavy processing asynchronously (queue job), or you risk duplicate deliveries piling up. ### Idempotency
Networks fail. Clients retry. Users double-click. Any of these can cause the same payment intent to be submitted twice. Without protection, that's a double charge — a direct financial loss and a trust-destroying bug.
Idempotency keys solve this: the client generates a unique key per logical operation (not per HTTP call) and sends it in a header:
Idempotency-Key: order_9f21ac_attempt_1
If the provider sees the same key again — whether because of a genuine retry after a timeout, or a duplicate click — it returns the original result instead of creating a second charge. Stripe formalizes this as a first-class header (Idempotency-Key) with a documented retention window (typically 24 hours). Providers without a formal idempotency API (common among African aggregators) push this responsibility onto you: generate your own unique reference per attempt and treat duplicate references as a signal to fetch the existing transaction rather than create a new one.
This matters more in payments than almost any other domain — "eventually consistent, occasionally duplicated" is fine for a like button; it is not fine for a bank transfer.
Authentication
| Mechanism | What it is | Where it's used |
|---|---|---|
| API Key (secret) | Long-lived static credential, full account privileges | Backend-only, all providers |
| API Key (public/publishable) | Restricted to safe client-side operations (tokenization, session creation) | Frontend SDKs, Stripe/Flutterwave/Paystack |
| Bearer Token | Credential sent in Authorization header |
Universal transport mechanism for the above |
| OAuth Access Token | Short-lived, scoped, obtained via OAuth flow, often refreshable | Stripe Connect (platform-to-connected-account), some IntaSend flows |
| Consumer Key/Secret | OAuth 1.0/2.0-style credential pair used to obtain an access token | PesaPal, and the underlying M-Pesa Daraja API that several aggregators sit on top of |
The security principle underneath all of these: credential scope should match blast radius. A publishable key leaking is a non-event. A secret key leaking is an incident. An OAuth token scoped to a single connected account leaking is bad, but contained — unlike a platform-wide secret key.
PCI DSS
PCI DSS (Payment Card Industry Data Security Standard) is a compliance framework mandated by the card networks (Visa, Mastercard, etc.) for anyone who stores, processes, or transmits cardholder data. Compliance is expensive: network segmentation, quarterly vulnerability scans, annual audits, and strict access controls.
Payment APIs exist partly to keep you out of PCI scope. If your server never touches raw card numbers — because the card form is hosted by the provider (a redirect/iframe) or tokenized client-side via their JS SDK before it ever reaches your backend — your PCI compliance burden drops from "full assessment" (SAQ D) to a much lighter self-assessment (SAQ A), sometimes just a checklist.
The rule for merchants: never store raw card data. Ever. Not encrypted "for convenience," not in logs, not in error messages. Let the processor hold that liability.
Tokenization
Tokenization replaces sensitive data (a card number) with a non-sensitive token that's meaningless outside the system that issued it — it can't be reverse-engineered back into the original card number.
It's easy to confuse with two adjacent concepts:
- Encryption: Reversible. If you have the key, you get the original data back. The card number still exists, mathematically recoverable.
- Hashing: One-way and deterministic. Same input always produces the same output — which is exactly why hashing is wrong for card numbers (you could brute-force a hash of a 16-digit number space).
-
Tokenization: The mapping between token and real value lives only in the provider's secure vault. The token itself carries zero recoverable information. Even if your database of tokens leaks, an attacker gets nothing usable.
This is why you can safely store a Stripe
pm_xxxxxpayment method token in your own database for recurring billing, but you could never safely store the underlying card number, encrypted or not.
Payment States
Every provider models roughly the same underlying state machine, even if the exact labels differ:
Pending ──► Authorized ──► Captured ──► Completed
│ │ │
│ └──► Cancelled ├──► Refunded
│ └──► Reversed
└──► Failed
- Pending: Transaction created, awaiting customer action or processing
- Authorized: Funds reserved on the customer's account/card, not yet moved (common in card payments; less common in mobile money, which is usually a single-step debit)
- Captured: Merchant confirms the authorized amount should actually be collected
- Completed: Funds have settled to the merchant
- Failed: Payment attempt did not succeed (insufficient funds, declined, timeout)
- Cancelled: Authorized but voided before capture
- Refunded: Completed payment returned to customer, fully or partially
-
Reversed: Provider- or bank-initiated reversal, often for fraud or disputes
Understanding this state machine matters because your own database schema should mirror it — a transactions table with a loosely-typed
status: stringcolumn and no enforced transitions is a recipe for "refunded" orders that still show as "completed" in your admin panel.
5. Provider Deep Dives
Flutterwave
Overview: Pan-African payment infrastructure company, headquartered with operations spanning Nigeria, Kenya, Ghana, Uganda, South Africa, and beyond, with growing support for cross-border and international card payments.
Supported countries: 30+ African countries, plus international card acceptance for global customers paying African merchants.
Payment methods: Cards (Visa/Mastercard/Verve), bank transfers, USSD, mobile money (M-Pesa, MTN MoMo, Airtel Money), Barter (Flutterwave's own wallet), virtual accounts, and payment links.
Technical architecture: REST/JSON over HTTPS, secret + public key authentication, sandbox and live environment separation, SDKs for Node.js, PHP, Python, and mobile (React Native, Flutter). Webhooks are HMAC-signed with a verif-hash header you compare against a secret you set in the dashboard.
Representative endpoints:
POST /v3/payments # Initialize a transaction, get a checkout link
GET /v3/transactions/{id}/verify # Verify actual transaction status
POST /v3/transfers # Payout to a bank account or mobile money wallet
POST /v3/refunds # Refund a completed transaction
Strengths: Broad multi-country and multi-rail coverage in a single integration; strong mobile money support, which matters enormously for African markets where card penetration is low; virtual account and payment link features reduce custom checkout UI work.
Weaknesses: Documentation consistency has historically varied across API versions (v2 vs v3 migration pain is a known community complaint); error messages can be less granular than Stripe's.
Ideal use case: Multi-country African e-commerce or marketplace platforms that need one integration to reach customers across several currencies and payment rails, including mobile money.
Paystack
Overview: Nigerian-born payment gateway (acquired by Stripe in 2020), strongest in Nigeria and Ghana, expanding across the continent, widely praised for developer experience.
Supported countries: Nigeria, Ghana, South Africa, Kenya (growing), with card acceptance from a wider set of countries.
Payment methods: Cards, bank transfers, USSD, mobile money (Ghana), QR, and Paystack's own virtual accounts.
Technical architecture: Clean REST/JSON API, single secret-key bearer auth, extremely readable documentation with copy-pasteable examples in most major languages, well-maintained SDKs. Sandbox uses test-mode keys (sk_test_...) that mirror live behavior closely, making local development fast.
Representative endpoints:
POST /transaction/initialize # Create transaction, get authorization_url
GET /transaction/verify/{reference} # The canonical "did this actually work" check
POST /transfer # Send money out (payouts)
GET /customer/{code} # Customer object for recurring billing
POST /refund # Full or partial refund
Strengths: Arguably the best developer experience of the African providers covered here — predictable naming, consistent error shapes, a Customers API that makes recurring billing straightforward, and a verification endpoint that's simple to reason about.
Weaknesses: Narrower country coverage than Flutterwave; mobile money support is less comprehensive outside specific markets.
Ideal use case: Nigeria/Ghana-first products, or teams that prioritize integration speed and clean documentation over maximal geographic reach.
PesaPal
Overview: East African payment gateway, one of the earliest players in the region, historically strong in Kenya, Uganda, Tanzania, with particular traction in hospitality (hotels, travel) and education (school fee payments).
Supported countries: Kenya, Uganda, Tanzania, Malawi, Zambia, with a regional East/Southern Africa focus rather than pan-continental breadth.
Payment methods: Cards, M-Pesa and other regional mobile money, bank transfers, and PesaPal's own wallet.
Technical architecture: OAuth-based authentication (consumer key/secret exchanged for a bearer token) rather than a static secret key — an important difference from Flutterwave/Paystack's simpler model. IPN (Instant Payment Notification) is PesaPal's term for what other providers call a webhook — you register an IPN URL that receives callback notifications on transaction status change.
Representative endpoints:
POST /api/Auth/RequestToken # Exchange consumer key/secret for bearer token
POST /api/URLSetup/RegisterIPN # Register your callback/webhook URL
POST /api/Transactions/SubmitOrderRequest # Create a payment request
GET /api/Transactions/GetTransactionStatus # Verify status
Strengths: Deep, longstanding integrations in hospitality and education verticals in East Africa; reliable for recurring institutional use cases like school fees, which have specific reconciliation needs (matching payments to specific students/invoices).
Weaknesses: The OAuth token exchange step adds integration complexity versus a single static bearer key; smaller developer community and fewer third-party SDKs/tutorials than Paystack or Stripe; documentation is functional but less polished.
Ideal use case: Institutional East African use cases — schools, hotels, travel bookings — where PesaPal's existing vertical relationships and IPN reliability outweigh the extra OAuth complexity.
IntaSend
Overview: Kenyan payment infrastructure provider, positioned as a developer-friendly alternative for Kenya-specific and East African collections and payouts, with a strong focus on M-Pesa.
Supported countries: Primarily Kenya, with expanding regional support.
Payment methods: M-Pesa (STK Push is a headline feature — the "enter your PIN" prompt that pops up on the customer's phone), cards, bank transfers.
Technical architecture: REST/JSON, API key + publishable key pair similar to Paystack's model, dedicated Collections API (money coming in) and Payouts API (money going out) as clearly separated concerns rather than a single generic "transactions" endpoint. Sandbox environment available for testing STK Push flows without real Safaricom charges.
Representative endpoints:
POST /api/v1/payment/mpesa-stk-push/ # Trigger STK push to customer's phone
GET /api/v1/payment/status/{id}/ # Check payment status
POST /api/v1/send-money/mpesa/ # B2C payout via M-Pesa
POST /api/v1/send-money/bank/ # Payout to bank account
Strengths: Excellent for M-Pesa-first products — the STK Push flow is well-documented and the separation of Collections vs. Payouts APIs maps cleanly onto how most businesses think about cash flow (money in vs. money out are genuinely different operational concerns, often owned by different teams). Good fit for Kenyan fintech and SME tooling.
Weaknesses: Narrower geographic and payment-method scope than Flutterwave or Stripe — if you need broad card acceptance outside Kenya or multi-country mobile money, you'll likely need a second provider alongside it.
Ideal use case: Kenya-first products where M-Pesa collections and payouts (e.g., a savings app, a gig-economy payout system, a merchant till replacement) are the core transaction type — which is directly relevant if you're building something like a creator-payout or livestreaming monetization flow with M-Pesa as a primary rail.
Stripe
Overview: The global reference implementation for modern payment infrastructure. If you're trying to understand "how should a payment API be designed," Stripe's API is usually the answer, and it's worth studying even if you never integrate it, because most other providers' newer API versions are visibly influenced by its patterns.
Supported countries: 40+ countries for full merchant accounts, with broader reach for accepting international cards.
Payment methods: Cards, bank debits (ACH, SEPA), wallets (Apple Pay, Google Pay), buy-now-pay-later options, and — through Stripe Connect — the ability to build your own multi-sided marketplace with sub-merchants.
Technical architecture — the deepest of the five, deliberately
Payment Intents API: Rather than a single "charge" call, Stripe models a payment as an object that moves through an explicit state machine (requires_payment_method → requires_confirmation → requires_action → processing → succeeded/requires_capture). This exists because modern card payments frequently require multi-step customer interaction — 3D Secure authentication being the most common — and Stripe's model was purpose-built to represent "this payment is in progress and waiting on the customer" as a first-class state, not an edge case bolted on afterward.
Setup Intents: For saving a payment method without charging anything yet — critical for free trials, "add a card" flows, or authorizing future off-session charges.
Checkout Sessions: A hosted, Stripe-managed checkout page — the fastest path to PCI-light integration, similar in spirit to Flutterwave/Paystack's hosted checkout URLs, but with deep customization options.
Billing & Subscriptions: A full subscription engine — proration, metered billing, trial periods, dunning (automatic retry logic for failed recurring charges) — that most African providers don't attempt to replicate at the same depth.
Connect: Turns your platform into a marketplace. You can onboard your own sub-merchants (creators, sellers, drivers) each with their own Stripe-managed account, and split payments between platform and seller programmatically. This is directly the pattern underneath most creator-economy and marketplace platforms.
Webhooks: Same event-driven model as other providers, but with a notably larger, more granular event taxonomy (payment_intent.succeeded, charge.dispute.created, invoice.payment_failed, etc.) and official libraries for signature verification in every major language.
Idempotency: First-class Idempotency-Key header support baked into the core API, not something you have to simulate with your own reference field.
Radar: Stripe's built-in fraud detection engine, using machine learning trained across Stripe's entire transaction graph — an advantage of scale that's very hard for smaller regional providers to replicate.
Payment Element: A single embeddable UI component that automatically shows the right payment methods for the customer's location and adapts to Stripe's evolving method support without you shipping new frontend code.
Representative endpoints:
POST /v1/payment_intents # Create a Payment Intent
POST /v1/payment_intents/{id}/confirm # Confirm with a payment method
GET /v1/payment_intents/{id} # Verify current status
POST /v1/refunds # Refund a charge
POST /v1/transfers # Move funds to a connected account (Connect)
POST /v1/subscriptions # Create a recurring billing subscription
Strengths: Unmatched documentation quality, the richest state-modeling of any provider here, native idempotency, built-in fraud detection at scale, and Connect for marketplace architectures — this is genuinely the deepest, most "engineered" API in this comparison.
Weaknesses: Limited or no direct support for African mobile money rails (M-Pesa, Airtel Money) — you'd pair Stripe for global card/wallet coverage with a regional provider for mobile money if building for African markets specifically; pricing and payout timelines can be less favorable in some emerging markets.
Ideal use case: Global SaaS billing, marketplaces needing split payments (Connect), or any product where subscription logic, fraud tooling, and API maturity matter more than direct mobile money support.
6. Flow Diagrams
Payment Request Flow
Frontend Backend Payment API Bank/Telco
│ │ │ │
│ "Pay $50" │ │ │
├──────────────────►│ │ │
│ │ Initialize (secret key) │
│ ├─────────────────────►│ │
│ │ │ Route to rail │
│ │ ├───────────────────►│
│ │ checkout_url │ │
│ │◄─────────────────────┤ │
│ Redirect to │ │ │
│◄──────────────────┤ │ │
│ checkout_url │ │ │
│───────────────────┴─────────────────────►│ Customer pays │
│ ├───────────────────►│
Webhook Flow
Bank/Telco ──► Payment API ──► POST /webhooks/payment ──► Your Backend
│
┌──────────┴──────────┐
│ Verify HMAC signature│
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ GET /verify/{ref} │
│ (re-check via API) │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ Update DB, fulfill │
└──────────┬──────────┘
│
Return 200 OK
Refund Flow
Merchant Backend Payment API Bank/Card Network
│ │ │
│ POST /refund {tx_id, amt} │ │
├─────────────────────────────►│ │
│ │ Reverse settlement request │
│ ├───────────────────────────────►│
│ │ Refund confirmed │
│ │◄───────────────────────────────┤
│ refund.completed (webhook) │ │
│◄─────────────────────────────┤ │
│ Update order status │ │
Payout Flow (e.g., paying a creator/driver/vendor)
Merchant Backend Payment API Bank / Mobile Money
│ │ │
│ POST /transfer │ │
│ {amount, destination} │ │
├───────────────────────────►│ │
│ │ Disburse funds │
│ ├─────────────────────────────►│
│ │ transfer.completed webhook │
│◄───────────────────────────┤◄──────────────────────────────┤
│ Reconcile payout ledger │ │
Subscription Flow (Stripe-style, conceptually generalizable)
Customer Backend Payment API
│ │ │
│ Sign up │ │
├────────────────►│ │
│ │ Create Customer │
│ ├───────────────────►│
│ │ Attach payment method (Setup Intent)
│ ├───────────────────►│
│ │ Create Subscription │
│ ├───────────────────►│
│ │ │ Billing cycle triggers
│ │ │ invoice.payment_succeeded
│ │◄───────────────────┤ (or _failed → dunning retries)
│ │ Update entitlement │
Payment Verification Flow
┌────────────────────────────────────┐
│ NEVER trust frontend redirect alone │
└────────────────────────────────────┘
│
Frontend redirect ──► Backend ──► GET /verify/{reference} ──► Payment API
│ │
│◄─────────────── status: success ────────┤
│
Only NOW: fulfill order
8. Closing Thoughts
Strip away the branding, the dashboards, and the marketing copy, and every payment API in this article is solving the same handful of hard problems:
- How do we let two parties who don't trust each other exchange money safely? (Aggregation, PCI scope reduction, tokenization)
- How do we know a payment actually happened, when the client can lie? (Server-side verification, never trusting redirects)
- How do we react to money movement without polling forever? (Webhooks, event-driven design)
- How do we prevent a network hiccup from charging someone twice? (Idempotency)
- How do we model something as messy as "a payment" as clean software state? (Payment state machines — pending, authorized, captured, refunded) Flutterwave and IntaSend lean into African payment rails and mobile money. Paystack optimizes for developer velocity in West Africa. PesaPal owns institutional East African verticals. Stripe pushes the ceiling on what a payment API's engineering can look like — richer state, native idempotency, built-in fraud detection, and a real subscription/marketplace layer.
None of them is "the best" in the abstract. The right provider is the one whose rails match your users' money and once you understand the shared architecture underneath all of them, switching providers, or running two in parallel, stops being a rewrite and starts being a config change.
If you're building payment infrastructure for African markets specifically — say, a platform combining M-Pesa collections with a token-based creator economy — the practical pattern that emerges from this comparison is: pick a mobile-money-native provider (Flutterwave or IntaSend) for local collections/payouts, and reserve Stripe-style depth (Connect, subscriptions, Radar) for the parts of your system dealing with global cards or platform-level fund splitting. Very few products need to build all of that state-machine complexity themselves — that's exactly the point of these APIs existing.
Top comments (0)