When OTP codes fail or arrive late in Africa, the cause is usually one of five things: a sender ID that is not registered (or is registered as promotional) on the recipient's network, wording that trips a content filter, a badly formatted number, a retry pattern that floods the user with codes, or a temporary network incident. Start with the delivery report for the failed message: it tells you which of these you are dealing with in most cases.
Last reviewed: 28 October 2026. Rules and network practice change; check the linked sources for anything you rely on.
This is a symptom-first troubleshooting guide that applies across the continent. For market-specific detail, see our deep dives on Nigeria, Kenya and Tanzania.
Step 1: read the delivery report
An API response of "accepted" only means the platform took the message. The delivery report (DLR) is what the network says happened next. Use it to sort failures before changing anything:
| Status | What it usually means | Where to look |
|---|---|---|
| DELIVRD | The handset received it | User side: wrong number entered, SMS inbox full, message filtered by a spam app, or the user is reading an older code |
| UNDELIV | The network tried and failed | Phone off or out of coverage for the whole validity period, invalid or disconnected number, a network that rejects your sender ID |
| EXPIRED | Validity period ran out before delivery | Phone off, roaming, or congestion on the route; often a late-arrival problem rather than a lost message |
| REJECTD | The network refused the message | Sender ID not registered or not approved for this traffic type, or the content matched a filter |
| No final status | The network has not returned a receipt | Some routes do not return final receipts on every message; compare against delivery rates for the same network in your reports |
Group failures by network and by hour. A failure on one network only points to sender ID or routing; failures across all networks point to your number handling, content or account. How DLRs reach you is covered in receiving delivery reports.
Symptom: the code never arrives
Sender ID not registered, or registered for the wrong traffic
Most African networks expect a registered alphanumeric sender ID, and some separate promotional from transactional traffic. In Kenya, for example, Safaricom requires different sender IDs for promotions and alerts. An OTP sent on a promotional sender, or on a sender not yet approved for that network, can be blocked or replaced. Register your OTP sender ID as transactional in every country you verify users in (sender ID registration), and keep marketing on a separate one.
Do-Not-Disturb confusion
In Nigeria, subscribers who text STOP to 2442 block promotional messages under the NCC Do-Not-Disturb service. OTPs are transactional and should still reach them, but only when they are sent and recognised as transactional. If your OTPs fail mainly for some users in Nigeria, check the traffic class before anything else.
Content that looks like marketing or phishing
Network filters look at the text. Keep the OTP plain: the brand, the code, the validity, and a warning not to share it. Avoid links, prices, "win" or "offer" wording, and URL shorteners in OTP messages.
Your Kopa code is 482193. It expires in 5 minutes. Never share it. Kopa staff will never ask for it.
Number format
Always send in E.164: plus sign, country code, then the national number without the leading trunk zero (+2348031234567, not 08031234567 or 2340803...). Watch for countries that changed their numbering: Cote d'Ivoire moved to 10-digit numbers in 2021, so +225 07 ... keeps the 07. Validate on input with a country picker; do not guess the country from the first digits. More in phone number formats for SMS.
The number is opted out or outside your allowed countries
eSMS Verify returns 403 recipient_opted_out for numbers on your opt-out list and 403 country_not_allowed when the destination is outside your Verify app's country allow-list. Log the error code; it is faster than guessing.
Symptom: the code arrives late
A late code is worse than a missing one, because the user has usually requested another by then. Common causes are congestion at peak times, a phone that was briefly out of coverage, or an incident on one route. In July 2026 Kenya's eCitizen portal warned users of intermittent OTP SMS delays during a gateway migration and pointed them to email as a fallback, a reminder that even large services plan for this.
- Set a sensible expiry. Five minutes suits most flows. NIST guidance (SP 800-63B) treats an out-of-band authentication as invalid unless completed within 10 minutes, so do not go beyond that.
- Accept the latest code only and tell the user which one to use ("use the code in the most recent SMS").
- Measure time to delivery per network from DLR timestamps. A rise on one network is an incident to raise with support, not a reason to resend.
Symptom: the code arrives but the user cannot use it
- Autofill does not work. Android's SMS Retriever API needs your app's 11-character hash in the message. Safari and Chrome autofill on the web expect a last line in the form
@yourdomain.com #482193. Put these in your OTP template. - The message became Unicode. A curly apostrophe or long dash pasted into the template switches the SMS to UCS-2 and can push it into two segments, which some older handsets display oddly. Keep OTP templates in plain GSM-7 characters.
- The code is hard to read. Six digits, no letters, no spaces inside the code.
Symptom: the right code is rejected
Usually the user typed a code from an earlier SMS, the code expired, or the attempt limit was reached. With eSMS Verify a check returns pending (wrong code, attempts left), failed (too many attempts), or expired. Show the user a clear message for each, and offer a resend button rather than a generic "invalid code".
Retries, rate limits and fallback
Retry storms cause many "OTP not received" complaints. A user taps "resend" three times, three codes arrive minutes later out of order, and none works. Design the resend path deliberately:
- Show a countdown of 30 to 60 seconds before the resend button activates.
- Resend on the same verification so the attempt and expiry rules carry over. Verify allows up to 4 sends in total (the original plus 3 resends).
- Make the start call idempotent so a double tap sends one code. Verify accepts an
Idempotency-Keyheader for this. - Offer a fallback after the second failure: email, a call from support, or verification in your app if the user is already signed in elsewhere.
- Rate-limit per number and per IP. Verify allows 5 starts per number per hour; add your own limits at the signup form too.
Also watch for SMS pumping: bots requesting codes to numbers that generate revenue for a third party. Sudden spikes in starts to one country or one number range with almost no successful checks are the signal. Restrict your Verify app to the countries you actually serve.
Using eSMS Verify instead of building it
eSMS Verify generates the code, sends it, stores it hashed, expires it, rate-limits it and checks it, in two API calls. A send that fails at the gateway returns 502 otp_send_failed and you are not charged. Here is a minimal Node.js integration:
// Node 18+ (built-in fetch). Server side only: never expose the key in an app.
const API = "https://sms.esmsafrica.io/v1/verify";
const headers = {
"Authorization": `Bearer ${process.env.ESMS_API_KEY}`, // esms_live_... or esms_test_...
"Content-Type": "application/json",
};
export async function startOtp(phoneE164, attemptKey) {
const res = await fetch(`${API}/start`, {
method: "POST",
headers: { ...headers, "Idempotency-Key": attemptKey }, // a double tap sends one code, not two
body: JSON.stringify({ to: phoneE164, app_id: "vera_your_app_id" }),
});
const body = await res.json();
if (res.status === 429) return { retryLater: true }; // 5 starts per number per hour
if (res.status === 502) return { retry: true }; // not sent, not charged
if (!res.ok) throw new Error(body.code || res.status); // invalid_phone, recipient_opted_out...
return { id: body.verification_id, expiresAt: body.expires_at };
}
export async function resendOtp(id) {
// New code, same verification. Up to 4 sends in total.
const res = await fetch(`${API}/${id}/resend`, { method: "POST", headers });
return res.json();
}
export async function checkOtp(id, code) {
const res = await fetch(`${API}/check`, {
method: "POST",
headers,
body: JSON.stringify({ verification_id: id, code }),
});
const { status } = await res.json(); // approved | pending | failed | expired | canceled
return status === "approved";
}With a test key (esms_test_...) no SMS is sent, nothing is charged, and the response includes the code so you can automate tests. The full reference is in the Verify docs and the step-by-step build is in sending OTP SMS.
Pre-launch checklist
- Transactional sender ID approved in every country you verify users in.
- Template in GSM-7, under 160 characters, with autofill line if you need it.
- E.164 validation with a country picker on the form.
- Expiry of about 5 minutes, latest code only, clear error messages.
- Resend countdown, idempotent start, a non-SMS fallback.
- Country allow-list and alerts on unusual start volumes.
- Dashboards of delivery rate and time to deliver per network.
Frequently asked questions
Why do OTPs reach one network but not another?
Most often the sender ID is approved on one network and not yet on the other, or one network treats your traffic as promotional. Compare delivery reports by network, then check the sender ID status for that network.
How long should an OTP stay valid?
About five minutes works for most flows. NIST SP 800-63B treats an out-of-band authentication as invalid if it is not completed within 10 minutes.
Should I resend automatically if there is no delivery report?
No. A missing receipt does not mean a missing message, and automatic resends create confusion. Let the user ask for a resend after a countdown.
Does a failed OTP cost money?
With eSMS Verify, a code that fails at the gateway returns 502 otp_send_failed and is not charged. Verify pricing per country is on the pricing page.
Sources
- Kenyans.co.ke - eCitizen warns of OTP SMS delays for some mobile networks (7 July 2026)
- NCC - Do-Not-Disturb (DND) service in Nigeria
- KICTANet - Safaricom notice on promotional and transactional sender IDs (2020)
- NIST SP 800-63B - Authentication and authenticator management (out-of-band, 10-minute limit)
- Google - SMS Retriever API for Android
- Apple - Domain-bound codes for SMS autofill
- Chrome for Developers - WebOTP API
- Burkina24 - Cote d'Ivoire numbers move from 8 to 10 digits (31 January 2021)
- OWASP - Multifactor Authentication Cheat Sheet
Building or fixing a login flow? Read the OTP overview, try Verify with a test key, or create an account and send your first code today. Fintech teams should also see OTP for fintech in Nigeria.


