Skip to main content

Send and Verify OTP in Sandbox

Use AfriRoute Verify to add phone verification without building OTP storage, retry controls, fraud rules, or SMS/voice fallback yourself.

This tutorial intentionally keeps OTP infrastructure inside AfriRoute. Your application starts a verification, asks the user for the code, then checks the code and creates its own authenticated session only after AfriRoute returns approved.

What You'll Build​

  • Start a managed verification challenge for an E.164 phone number.
  • Let AfriRoute deliver the code over SMS, with optional voice fallback.
  • Check the user-submitted code.
  • Create your own application session after successful verification.
  • Handle common errors without exposing OTP internals.

Estimated time: 10 minutes

Prerequisites​

  • AfriRoute account: Sign up
  • API key with verification permissions
  • A backend route in your own app
  • Phone numbers in E.164 format, for example +251912345678

Verification Flow​

User enters phone number
-> your backend calls AfriRoute Verify start
-> AfriRoute sends the code and enforces delivery/risk controls
-> user enters the code
-> your backend calls AfriRoute Verify check
-> your app creates its own session if status is approved

Step 1: Start a Verification​

Call AfriRoute from your backend. Do not call verification endpoints directly from browser code with your secret API key.

start-verification.js
import Afriroute from "@afriroute/sdk";

const afriroute = new Afriroute({
apiKey: process.env.AFRIROUTE_API_KEY,
});

export async function startPhoneVerification(phone) {
assertE164(phone);

return afriroute.verify.start({
phone,
channel: "sms",
fallbackChannel: "voice",
locale: "en",
});
}

function assertE164(phone) {
if (!/^\+[1-9]\d{7,14}$/.test(phone)) {
throw new Error("Phone number must be in E.164 format.");
}
}

Example response:

{
"verificationId": "ver_01JQ7Z9G6PMV8G1E0F8N6T3Q5K",
"status": "pending",
"expiresAt": "2026-06-01T18:25:00Z",
"channel": "sms"
}

Store only verificationId, the normalized phone number, and the status in your application. Never store or log the OTP code.

Step 2: Check the Code​

check-verification.js
export async function checkPhoneVerification({ verificationId, code }) {
if (!/^\d{6}$/.test(code)) {
throw new Error("Verification code must be six digits.");
}

const result = await afriroute.verify.check({
verificationId,
code,
});

if (result.status !== "approved") {
return {
ok: false,
reason: result.status,
};
}

// Create your app session here using your normal auth system:
// - OIDC/JWT
// - server-side session cookie
// - your identity provider
return {
ok: true,
phone: result.phone,
};
}

Example approved response:

{
"verificationId": "ver_01JQ7Z9G6PMV8G1E0F8N6T3Q5K",
"status": "approved",
"phone": "+251912345678",
"approvedAt": "2026-06-01T18:22:10Z"
}

Frontend Example​

The frontend should only talk to your backend. It should never hold an AfriRoute API key.

verify-phone-form.js
async function startVerification(phone) {
const response = await fetch("/api/verify/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ phone }),
});

if (!response.ok) throw new Error("Could not start verification.");
return response.json();
}

async function checkVerification(verificationId, code) {
const response = await fetch("/api/verify/check", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ verificationId, code }),
});

if (!response.ok) throw new Error("Verification failed.");
return response.json();
}

What AfriRoute Handles​

AfriRoute Verify is designed to keep the unsafe parts out of your application code:

  • OTP generation and expiration
  • SMS delivery and optional voice fallback
  • Code attempt limits
  • Request throttling and abuse controls
  • Delivery provider selection
  • Verification audit events
  • Normalized verification status

What Your App Still Handles​

  • E.164 phone-number validation before calling AfriRoute
  • CAPTCHA or bot protection before high-volume start requests
  • Your own authenticated user/session after approval
  • Account linking rules, for example whether one phone can be reused
  • Audit logs that do not include OTP values

Do Not Build OTP Storage Yourself​

Avoid these patterns in production documentation and applications:

  • new Map() / {} for OTP storage
  • plaintext OTP database columns
  • logging OTP values
  • random-number APIs that are not cryptographically secure
  • fake session tokens such as hashes of phone numbers and timestamps
  • per-process rate limits that disappear on restart

If you cannot use AfriRoute Verify and must build a low-level verifier, use Redis or a database, hash OTPs, enforce distributed rate limits, validate E.164 phone numbers, add bot protection, and issue sessions through your real auth system. That is an advanced architecture topic, not the recommended quickstart path.

Error Handling​

ErrorMeaningWhat to do
PHONE_INVALIDPhone is not E.164 or is not routableAsk the user to correct the number
VERIFY_RATE_LIMITEDToo many verification startsShow a retry time and do not immediately retry
VERIFY_EXPIREDCode expiredStart a new verification
VERIFY_CODE_INVALIDCode does not matchLet the user retry until attempts are exhausted
VERIFY_MAX_ATTEMPTSToo many failed checksStart over after the cooldown

Production Checklist​

  • Use AfriRoute Verify instead of custom OTP infrastructure.
  • Keep API keys on the server.
  • Validate phone numbers in E.164 format.
  • Add CAPTCHA or equivalent bot protection before start requests.
  • Never log OTP values.
  • Create your app session only after status: "approved".
  • Subscribe to verification webhooks where auditability matters.
  • Monitor rate-limit and fraud signals.

Next Steps​