Phone Number Verification
Confirm that a customer controls a phone number by sending a one-time passcode (OTP) over SMS, voice, or WhatsApp. Every verification returns carrier metadata and a risk assessment so you can detect VoIP, disposable, and high-risk numbers during onboarding.
🚀 Quick Start
curl -X POST https://api.afriroute.ai/api/v1/identity/phone/send-otp \
-H "Authorization: Bearer $AFRIROUTE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "+251911234567",
"method": "sms"
}'
📡 Endpoints
Send OTP
POST /v1/identity/phone/send-otp
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
phone_number | string | Yes | Recipient number in E.164 format |
method | string | No | sms (default), voice, or whatsapp |
code_length | integer | No | OTP length, 4–8 digits (default 6) |
expiry_seconds | integer | No | Code lifetime in seconds (default 300) |
sender_id | string | No | Alphanumeric sender ID for the SMS channel |
Verify OTP
POST /v1/identity/phone/verify-otp
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
otp_id | string | Yes | The otp_id returned by send-otp |
code | string | Yes | The code entered by the user |
💻 Code Samples
Node.js — Send then verify
const send = await fetch('https://api.afriroute.ai/api/v1/identity/phone/send-otp', {
method: 'POST',
headers: { 'Authorization': 'Bearer $AFRIROUTE_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ phone_number: '+251911234567', method: 'sms' })
});
const { otp_id } = await send.json();
// ...collect code from the user...
const verify = await fetch('https://api.afriroute.ai/api/v1/identity/phone/verify-otp', {
method: 'POST',
headers: { 'Authorization': 'Bearer $AFRIROUTE_API_KEY', 'Content-Type': 'application/json' },
body: JSON.stringify({ otp_id, code: '123456' })
});
console.log(await verify.json());
Python
import requests
base = 'https://api.afriroute.ai/api/v1/identity/phone'
headers = {'Authorization': 'Bearer $AFRIROUTE_API_KEY'}
otp_id = requests.post(f'{base}/send-otp',
headers=headers, json={'phone_number': '+251911234567', 'method': 'sms'}
).json()['otp_id']
res = requests.post(f'{base}/verify-otp',
headers=headers, json={'otp_id': otp_id, 'code': '123456'}
)
print(res.json()['verified'])
📊 Responses
Send OTP
{
"otp_id": "otp_xyz789",
"phone_number": "+251911234567",
"method": "sms",
"sent_at": "2026-05-10T14:30:00Z",
"expires_at": "2026-05-10T14:35:00Z"
}
Verify OTP
{
"verified": true,
"phone_number": "+251911234567",
"carrier": "Ethio Telecom",
"country": "ET",
"line_type": "mobile",
"risk_assessment": {
"risk_score": 5,
"risk_level": "low",
"voip_detected": false,
"temporary_number": false,
"known_fraud": false
}
}
💡 Best Practices
- Rate-limit per number — cap OTP sends to roughly 3 per number per hour to control cost and abuse.
- Use voice fallback when an SMS is not confirmed within 60 seconds.
- Inspect
risk_assessment— block or step up review whenvoip_detectedortemporary_numberistrue. - Never log the OTP code in plaintext; store only the
otp_idand verification outcome. - Localize the message template to the recipient's country for higher delivery rates.
⚠️ Error Handling
| Code | HTTP | Description |
|---|---|---|
INVALID_PHONE | 400 | Number is not valid E.164 |
OTP_EXPIRED | 422 | The code has passed expires_at |
OTP_INVALID | 422 | The submitted code does not match |
OTP_MAX_ATTEMPTS | 429 | Too many incorrect attempts for this otp_id |
UNDELIVERABLE | 422 | Carrier could not deliver to the number |
See the error code reference for the full list.
📚 Related Resources
Last Updated: May 2026 | Need help? [email protected]