OTP Verification Guide
Build phone OTP verification on SendAfrica — message templates, expiry handling, retry limits, and delivery confirmation.
One-time passwords are the most common SMS integration. This guide shows a production-safe pattern: short messages, idempotent sends, delivery tracking, and abuse controls.
#Message template
Keep OTPs inside one GSM-7 part (≤160 chars) so each verification costs exactly 1 credit, and always include an expiry:
Your OTP is 482910. It expires in 5 minutes. Do not share it.Avoid unicode in OTPs
Curly quotes or emoji flip the message to UCS-2 (70 chars/part). Stick to plain ASCII digits and punctuation for OTPs.
#Send with idempotency
Users double-tap "Resend". Use the OTP record ID as your Idempotency-Key so network retries never send duplicates:
import secrets
def send_otp(client, phone: str, otp_id: str) -> str:
code = f"{secrets.randbelow(1000000):06d}"
result = client.sms.send(
to=phone,
message=f"Your OTP is {code}. It expires in 5 minutes.",
sender="MyBrand",
)
# persist (otp_id, code, result.message_id, expires_at)
return result.message_id#Confirm delivery before revealing success
status: "sent" means the gateway accepted it — not that the phone received it. Wait for the webhook (sms.delivered) before telling the user "code sent", and fall back to polling logs if webhooks lag:
app.post("/webhooks/sendafrica", async (req, res) => {
const event = verifyAndParse(req); // HMAC check first!
if (event.type === "sms.delivered") {
await db.otps.markDelivered(event.message_id);
} else if (event.type === "sms.failed") {
await db.otps.markFailed(event.message_id);
// refund UX: let the user retry without penalty
}
res.json({ ok: true });
});#Abuse controls checklist
- Rate-limit per phone — max 3 OTPs per number per hour (use Redis counters).
- Rate-limit per IP — the platform already caps public auth routes at 30 req/min/IP; mirror it in your app.
- Expiry — 5 minutes is standard; reject expired codes with a generic error.
- Attempt cap — lock verification after 5 wrong attempts; require a fresh OTP.
- Generic errors — never reveal whether a phone number has an account.
- Balance guard — check
GET /v1/credits/balancebefore batch re-sends; handleinsufficient_creditsgracefully.
#Phone verification via the platform
For account-phone verification *inside* SendAfrica itself, use the built-in flow instead of building your own: POST /v1/auth/send-phone-otp then POST /v1/auth/verify-phone (JWT required, gated by ENABLE_PHONE_OTP). Verified phones unlock mobile-money top-ups.