Most OTPs that "never arrive" in Kenya are delayed or filtered, not lost: they go out on the wrong kind of sender ID, carry content that looks promotional, are resent so often they get throttled, or hit a temporary route problem nobody is monitoring. Verification that works plans for all four, with a transactional sender, plain OTP text, sensible resend limits, delivery monitoring and a fallback channel.

Last reviewed: 27 October 2026

Kenyan users have seen this first-hand. On 7 July 2026 eCitizen, the government services portal, told users it was experiencing intermittent OTP SMS delivery delays to some numbers during an SMS gateway migration, and advised them to choose the "Send code to my Email" option instead, as reported by Kenyans.co.ke. The response is a good model: say what is happening and offer another channel.

Why OTPs fail: the common causes

1. The code is on a promotional sender ID

Safaricom separates promotional and transactional sender IDs. Promotional traffic can only be delivered between 08:00 and 18:00 and must carry an opt-out. If your OTPs share an ID that has been classified as promotional, a user logging in at 21:00 waits for a code that is held back or never sent. Register a transactional ID for codes and alerts. The full rules are in our guide to promotional vs transactional sender IDs on Safaricom.

2. The message looks like marketing

An OTP that says "Welcome! Get 20% off your first order with code 482913" reads like a promotion to people and to filters. Keep OTP text predictable and dull: brand, code, expiry, warning. No links, no offers, no emojis that force Unicode encoding and split the message.

3. Users press "resend" again and again

When a code takes 20 seconds, users press resend, and each press sends a new code, so the first code they finally receive is already invalid. Rapid repeat sends to the same number can also trigger rate limiting on your own side or the network's. Enforce a cooldown and make every resent code work, or keep the same code valid until expiry.

4. The number is wrong

Leading zeros, missing country codes and typed spaces cause a surprising share of failures. Normalise every number to the international format +2547XXXXXXXX or +2541XXXXXXXX before sending, and validate length before you call the API.

5. A temporary route or network issue

Sometimes the problem is upstream: congestion, maintenance or a migration like eCitizen's. You cannot prevent these, but you can detect them in minutes if you watch delivery reports, and you can switch users to a fallback channel.

6. The phone cannot receive

Switched off, out of coverage, inbox full, SIM swapped or a number that has changed hands. Delivery reports tell you which of these you are dealing with, and some (a recent SIM swap on a banking app) are security signals in their own right.

Designing verification that arrives

Design choiceRecommendation
Sender IDRegistered transactional ID on Safaricom, Airtel and Telkom
Code length6 digits
Expiry5 to 10 minutes, stated in the message
Resend cooldown30 to 60 seconds, shown as a countdown
Resend limit3 to 5 per number per hour, then fallback
Check attempts5 wrong guesses, then a new code is required
FallbackEmail, a voice call if you have one, or an in-app code for logged-in devices
MonitoringDLR webhooks with an alert when delivery drops

Write the message for people and for phones

A good Kenyan OTP looks like this:

"Your Pesafy code is 482913. It expires in 5 minutes. Pesafy staff will never ask for this code."

  • The brand comes first so users can spot fake messages.
  • The code is near the start, so it shows in the notification preview.
  • It stays under 160 GSM characters, so it travels as one segment.
  • The warning helps users resist social-engineering calls, which are common in Kenya.

On Android you can also append the app hash required by Google's SMS Retriever API, so the code fills in automatically without the user reading it.

Resends that help instead of hurt

Show a countdown before the resend button activates. After two failed attempts, offer an alternative: "Send code by email" or "Call me with the code". After the limit, ask the user to try again later and log the event. A burst of resend requests from many numbers is also a classic sign of SMS pumping fraud, so rate-limit per IP and per device as well as per number.

Monitor delivery, not just API responses

An HTTP 200 from an SMS API means the message was accepted, not delivered. Subscribe to delivery reports and track, per network, the share of OTPs delivered and how long they took. Alert someone when that share drops. That is how you notice a problem in five minutes instead of from a wave of support tickets. Our guide to receiving delivery reports shows the webhook setup.

Always have a fallback

eCitizen's email option is the right instinct. For a fintech, the fallback could be email, a voice call that reads the code aloud, or approval in the app on an already-trusted device. Decide in advance which actions are risky enough that you will not fall back to a weaker channel, for example raising a withdrawal limit.

Let eSMS Verify handle the hard parts

You can build all of this on top of a plain SMS API, but every piece (code generation, storage, expiry, attempt counting, rate limits) is a place for security bugs. eSMS Verify is a managed OTP API: eSMS generates the code, sends it on transactional routing, rate-limits retries and checks the answer. Your server makes two calls.

// Start a verification: eSMS generates and sends the code
const start = await fetch('https://sms.esmsafrica.io/v1/verify/start', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ESMS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ to: '+254712345678', app_id: 'vera_yourappid' }),
});
const { verification_id } = await start.json();

// Later, when the user types the code
const check = await fetch('https://sms.esmsafrica.io/v1/verify/check', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.ESMS_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ verification_id, code: userInput }),
});
const result = await check.json(); // approved or not

Use an esms_test_ key to simulate the flow without charge. Full parameters are in the Verify documentation, and the OTP sending guide covers the plain-SMS approach if you prefer to manage codes yourself.

A checklist before launch

  1. Transactional sender ID registered on every network you serve.
  2. Numbers normalised to +254 format and validated.
  3. OTP text with brand, code, expiry and warning, under 160 characters, no links or offers.
  4. Resend cooldown, per-number and per-IP limits, and a capped number of check attempts.
  5. DLR webhook connected, with an alert on falling delivery rates.
  6. A fallback channel and a status message ready for when SMS is slow.
  7. Support staff able to look up a user's last OTP and its delivery status.

Industry details for fintechs and wallets are on our Kenya use-case pages, and OTP SMS in Kenya covers coverage and pricing.

Frequently asked questions

Why is my OTP not received on Safaricom at night?

The most common reason is that the code is going out on a promotional sender ID, which Safaricom restricts to 08:00 to 18:00. Move OTPs to a registered transactional ID. If the problem continues, check delivery reports for the failing numbers to see whether they are switched off, unreachable or rejected.

How long should an OTP be valid?

Five to ten minutes suits most logins and sign-ups. Shorter windows punish users when a message is slow, while longer ones give attackers more time. State the expiry in the message so users know whether to wait or request a new code.

Should I include a link in an OTP SMS?

No. Links make an OTP look like phishing and can lead to filtering. Users in Kenya are taught to distrust texts with links that ask them to act. Keep the code, the expiry and a warning, nothing else.

Does an OTP cost more than a normal SMS?

Through the SMS API, an OTP is billed like any other SMS, currently KES 1.08 ($0.0083) per Kenyan message at the basic tier. Verify and volume rates are listed on the pricing page.

Sources

Want codes that arrive? Read the Verify documentation, then create a free account and run your first verification with a test key today.