Developers
OTP SMS for Ugandan Fintech and SACCOs: 5 Things to Get Right
How to ship secure, reliable OTP delivery for Ugandan fintech apps and SACCOs. Latency targets, code length, expiry windows, brute-force defense, and fallback patterns.
OTP SMS is one of the most demanding workloads on a Ugandan SMS API. A delayed code costs you a signup. A leaked code costs you money. A brute force attempt costs you trust. Here's what to get right.
1. Latency: target sub-5 seconds
Median delivery on MTN Uganda is 3-5 seconds end-to-end. On Airtel it's 4-7. Anything significantly slower means:
- Users assume the message didn't arrive and request another (doubling your cost and confusing your retry logic).
- Conversion drops measurably — sign-up funnels see 5-10% completion loss for every 5 seconds of OTP wait.
What to do: use a provider with prioritized OTP routing (Wesendall's default), keep your message under 160 characters (one SMS unit), and avoid USD-billed international providers that route through extra hops.
2. Code length: 6 digits is the sweet spot
- 4 digits: 10,000 possibilities. Brute-forceable in seconds without rate limiting.
- 6 digits: 1,000,000 possibilities. Rate-limit to 5 attempts and a brute force becomes statistically irrelevant.
- 8 digits: 100,000,000 possibilities. Overkill for most flows; users mistype them more often.
Use 6 digits with rate limiting.
3. Expiry window: 5-10 minutes
- Too short (under 2 minutes): users can't get back to the form fast enough, abandon flow.
- Too long (over 30 minutes): every leaked code remains valid for long enough to be useful to an attacker.
Default to 10 minutes; reduce to 5 for high-value flows (fund withdrawals, password resets).
4. Rate limiting: per-phone, per-IP, per-account
Three independent windows:
- max 1 OTP send per 30 seconds (per phone)
- max 5 OTP sends per hour (per phone)
- max 10 OTP attempts per hour (per IP)
- max 5 wrong attempts before locking the account
This stops the most common abuses: account takeover via OTP spam, brute-force code guessing, and SMS-pumping attacks where an attacker abuses your free signup to charge your wallet.
5. Fallback channel
When SMS fails for legitimate users (bad network, foreign roaming, opt-out), have a fallback:
- Voice OTP — robotic call that reads the code. Works on every phone.
- Email OTP — if you have the email on file.
- Human support — call your support line, verify identity manually.
Implementing OTP via Wesendall
The API is intentionally simple. You generate the code (it never leaves your control), put it in the message body, and ship:
async function sendOtp(phone, code) {
const credentials = Buffer.from(
`${process.env.WESENDALL_KEY}:${process.env.WESENDALL_SECRET}`
).toString("base64");
const response = await fetch(
"https://www.wesendall.com/api/v1/sms/send",
{
method: "POST",
headers: {
Authorization: `Basic ${credentials}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
walletId: process.env.WESENDALL_WALLET_ID,
message: `Your code is ${code}. Valid for 10 minutes. Never share it.`,
recipient: phone,
}),
}
);
if (!response.ok) {
throw new Error("OTP send failed");
}
return response.json();
}
Refunds on failed sends happen automatically — your wallet isn't drained by retry storms.
What to log
For every OTP send:
- Phone number (or hash if you're privacy-conscious)
- Timestamp
- Wesendall transaction ID (returned in the response)
- Outcome: sent / delivered / failed
- Latency (delivery webhook timestamp - send timestamp)
This gives you the evidence you need when investigating fraud claims or explaining a regulator audit.
Cost math
At UGX 35/SMS, 1,000 OTP sends/month costs UGX 35,000. For a fintech shipping 50,000 OTPs/month, you're at UGX 1.75M — about USD 460. Cheap insurance for the trust you're building.
Get started
- Create a Wesendall account and top up with Mobile Money.
- Generate an API key on the API page.
- Wire the snippet above into your auth flow.
- Ship OTPs in under an hour.
For the full API walkthrough, see our SMS API developer guide.