Back to BlogTroubleshooting

EdTech Payments Keep Failing in Emerging Markets: Diagnose and Fix

Rupak Amin

Founder & Lead Engineer, RAITHub

9 min read

RAITHub ships and tests production software. See QA as a Service or talk to us.

When course payments keep failing in emerging markets, most failures are ordinary, not fraud: a short wallet balance, a phone push that timed out, or a callback that never reached your server. Diagnose by method and country, confirm every payment server-to-server before granting access, reconcile pending attempts on a schedule, and make retries safe. The silent killer is the payment that succeeded but your app never heard about.

If you would rather have RAITHub diagnose and fix the money path, see how below.

This post is for EdTech teams taking course or tuition payments across markets such as Bangladesh, India, Pakistan, Nigeria, Kenya and Indonesia, where payments are dropping. It is engineering guidance. The build-side companion, with the full provider interface, is EdTech payments in emerging markets: many gateways, one checkout; this post is the incident case, fixing payments that are failing now.

Why are my course payments failing in these markets?

Because learners pay with wallets, bank transfers and QR codes over networks that drop, not with the always-connected cards your test suite assumes. And because no single global provider covers these markets: Stripe's availability page marks India and Indonesia only as previews and does not list Bangladesh or Pakistan, so most platforms run a different local gateway per country, each with its own failure modes. First, classify what is actually failing.

FailureWhat it usually isWhere to look
Learner paid but has no accessCallback lost; status never reconciledPending attempts with no server-to-server confirmation
Learner charged twiceRetry started a second payment while the first was still pendingWhether retries check the last attempt first
Payment "fails" then succeeds minutes laterWallet push approved after your timeoutYour timeout and reconciliation window
One method fails far more than othersA specific gateway or method is flaky in that countrySuccess rate per gateway and method
Everything fails in one countryMerchant config, credentials or gateway outageThat gateway's dashboard and your keys

How do I diagnose which step is failing?

Add per-method, per-country measurement before you change anything, so you fix the real problem and can prove it improved.

  • Track success rate by gateway and method per country. If bKash succeeds and one card route fails, that is a routing decision, not a platform bug.
  • Log every state transition on an attempt. Started, redirected, callback received, status checked, access granted. The gap in that chain is your failure.
  • Separate "failed" from "unknown". A payment your app marked failed may have actually succeeded at the gateway. Treat unknown as something to reconcile, not as a final no.
  • Reproduce the timeout. Simulate a slow or missing callback in a test, because that is the case that leaks money. Testing payments and webhooks covers these paths.

How do I stop learners being charged twice?

Check the last attempt's status before you ever start a new one, and only retry if it genuinely failed. A wallet push can be approved after your app gave up waiting, so a naive "try again" button charges the learner twice. The rule is: query, then decide. A minimal safe-retry guard:

type Status = 'paid' | 'pending' | 'failed'

// Before starting a new payment, ask the gateway about the previous attempt.
export async function safePay(invoiceId: string, startNew: () => Promise<{ ref: string }>) {
  const prev = await lastAttempt(invoiceId)
  if (prev) {
    const s = await checkStatus(prev.providerId, prev.ref) // server-to-server
    if (s === 'paid') return { already: 'paid' as const }    // grant access; do not charge again
    if (s === 'pending') return { already: 'pending' as const } // wait and reconcile; do not start another
  }
  return startNew() // only reached when there is no prior attempt, or it truly failed
}

declare function lastAttempt(invoiceId: string): Promise<{ providerId: string; ref: string } | null>
declare function checkStatus(providerId: string, ref: string): Promise<Status>

Make the "grant access" step itself safe to run twice, so a confirmation that arrives by two paths unlocks the course once. That is the idempotency pattern in idempotency in API design.

How do I recover the payment that succeeded but never confirmed?

With a reconciliation job, because callbacks get lost and a learner who paid and has no access is your worst support ticket. Run a scheduled job that asks each gateway about every attempt still marked pending after a few minutes, and grant access when the gateway confirms the matching amount.

Signal to confirmWhy it matters
Status is paid at the gatewayThe browser redirect is never enough; confirm server-to-server
Amount and currency match what you recordedGuards against a wrong or partial payment unlocking access
You have not already granted access for this attemptKeeps the grant idempotent across callback and reconciliation

Only mark an invoice paid after that server-to-server check matches the amount and currency you stored. Never trust the browser redirect alone; it is the single most common cause of both lost access and double grants.

How do I make the retry easy for the learner?

Because many failures are a short balance or a dropped network, the recovery is often just a clean second try, on the channel the learner uses.

  • Send one retry link by SMS, email or a messaging app, tied to the same invoice with a fresh attempt. PadhAI, the AI tutoring platform RAITHub built, reaches learners over WhatsApp and Telegram as well as the web; the channel patterns are in teaching over WhatsApp and Telegram.
  • Offer a different method on the second try. If a card failed, suggest a wallet or QR.
  • Keep the enrolment in a clear state. Enrolled, paid and active are separate; a learner can be enrolled with one payment pending, and support needs to see exactly that. If you sell to several schools or institutes, keep each one's payments and learners isolated, as in tenant isolation for EdTech done right.
  • Move flaky methods down the list. If one method fails far more often in a country, reorder it, using the per-method success data.

How long does it take to fix a failing payment path?

Adding server-to-server confirmation, a safe-retry guard and a reconciliation job to one existing gateway is often one to two weeks, with the sandbox tests to prove the timeout and double-charge cases. Each additional gateway adds its own confirm, reconcile and refund paths. The main risk of doing it yourself is testing only the happy path; the failures that lose money are late callbacks and double charges, which only appear under real traffic unless you simulate them deliberately.

How RAITHub would fix this

Scope:

  • A diagnosis of the money path: success rate per gateway and method per country, and the gap in each attempt's state chain.
  • Server-to-server confirmation before access is granted, matching amount and currency.
  • A safe-retry guard that checks the last attempt before starting a new one, so no learner is charged twice.
  • A reconciliation job that recovers payments whose callback never arrived, granting access idempotently.
  • Per-method, per-country success reporting, and automated tests over late callbacks and double-charge cases.

Timeline: fixing and hardening an existing checkout is a 2–4 week code rescue engagement; a multi-country payment layer is backend work in the 6–12 week backend and API range. You receive: automated tests and CI covering success, failure, cancel and late-callback paths, handover docs and reconciliation runbooks, and full IP under an NDA signed before detailed discussion. Payment and learner data stay in your own cloud account; development uses synthetic data. Card data stays on each gateway's hosted page; confirm your card-security obligations with your acquirer. Next step: a free 15-minute technical audit, then a written fixed quote; RAITHub publishes no rates. See what RAITHub builds for education on the EdTech industry page, and book the free audit with your gateways, your countries and a sample of the failing attempts.

Frequently asked questions

Why do my EdTech payments keep failing in emerging markets?

Mostly ordinary reasons: a short wallet balance, a push notification that timed out, or a callback that never reached your server over a flaky network. Classify failures by method and country, and separate genuinely failed payments from unknown ones that need reconciling.

A learner paid but has no access. What happened?

Almost always the confirmation callback was lost and nothing reconciled it. Run a scheduled job that asks each gateway about pending attempts, and grant access when the gateway confirms the matching amount server-to-server. Never rely on the browser redirect to mark an invoice paid.

How do I stop charging a learner twice?

Check the previous attempt's status at the gateway before starting a new payment, and retry only if it truly failed. A wallet push can be approved after your app stops waiting, so a naive retry doubles the charge. Make granting access itself safe to run twice.

Can I rely on the gateway's webhook alone?

No. Webhooks get delayed or lost, so treat them as one signal and confirm every payment with a server-to-server status check that matches the amount and currency you recorded. A reconciliation job catches the payments whose webhook never arrived.

Why does one payment method fail more than the others?

Because gateways and methods vary in reliability by country. Track success rate per gateway and method per country; if one route fails far more often, move it down the order and offer a different method on the learner's second try.

How do I recover a learner after a failed payment?

Send one retry link tied to the same invoice on the channel they use, offer a different payment method, and keep enrolled, paid and active as separate states so support can see a learner enrolled with a payment still pending. Most recoveries are just a clean second attempt.

edtech payment failuresmobile moneyfailed payment recoverypayment reconciliationbkash nagadwebhook callbacks

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.