Don’t let prepaid OTP die overnight: monitor your SMS credit balance
Robase · 2026-09-10
OTP is invisible until it fails. One common failure mode on prepaid SMS stacks is not your generate/verify code — it is an empty wallet at 2am.
If you use a prepaid OTP or transactional SMS API (Termii-style get-balance, Africa’s Talking balance checks, or Robase prepaid credits), treat credit visibility as part of auth reliability, not “billing later.”
Why prepaid wallets go quiet
Prepaid is simple: buy credits, each OTP or SMS burns some, no monthly minimum. When the balance hits zero, sends stop. Users see “couldn’t send code.” On-call sees insufficient_credits / HTTP 402 if anyone is listening.
Public write-ups on Termii and Africa’s Talking balance endpoints exist because teams learn this the hard way. The lesson is the same on any prepaid stack: watch the wallet, do not assume it still has credits because last week’s deploy worked.
What good looks like
Three signals, not one:
- Low balance — warn before users feel it.
- Exhausted — hard stop; page someone.
- Topped up — confirm the wallet recovered before you close the incident.
Polling a balance API works when that is all you have. Webhooks are better when the provider supports them: the event arrives when the threshold is crossed, including overnight.
How Robase handles it
Robase prepaid credits cover OTP and transactional SMS on the same wallet. The PHP SDK documents credit webhooks:
credit_balance.lowcredit_balance.exhaustedcredit_balance.topped_up
Sends that cannot be charged raise InsufficientCreditsException (HTTP 402 / insufficient_credits). Catch that in the OTP send path so the user sees a clear “try again shortly” and ops gets a page — not a generic 500.
match ($event->event) {
WebhookEvent::CreditLow => alertLowBalance($event->int('balance')),
WebhookEvent::CreditExhausted => pageOncall('Robase credits empty'),
WebhookEvent::CreditToppedUp => resolveIncident('Robase credits restored'),
default => null,
};
Configure endpoints in the dashboard (https://robase.dev/app). The payload data object for these events carries workspace_id, balance, and threshold. Set the low-balance threshold under Settings → Low balance alerts (leave it at 0 to disable). credit_balance.low and credit_balance.exhausted fire once per crossing and re-arm after the balance recovers above the threshold.
There is no public “get balance” REST path in this post. Use the webhooks and the 402 on send.
Ops checklist
- Subscribe to the credit webhooks before production OTP.
- Alert on low, not only on empty.
- Map HTTP 402 /
insufficient_creditsto clear UX and an ops page. - Keep a top-up runbook (Paystack or Flutterwave in the dashboard).
- Wait for
credit_balance.topped_upbefore closing the incident.
Sender ID is a different failure
An empty wallet is not the only silent OTP miss in Nigeria. A Sender ID or DND routing problem can also show as “sent” with nothing on the handset. That is a separate class: Robase’s preapproved IDs are Robase and RAlerts. See also Nigeria SMS Sender ID for OTP for the Sender ID / DND angle.
Fund the wallet, then hook the warning
Do not learn about an empty prepaid OTP wallet from a 2am login spike. Fund credits, subscribe to credit_balance.low, and catch InsufficientCreditsException on the send path before you put OTP in production. Start at https://robase.dev; API notes live on docs.