SendAfrica logoSendAfricaDocs

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:

template
text
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:

otp.py
python
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:

delivery.ts
typescript
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/balance before batch re-sends; handle insufficient_credits gracefully.

#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.