Fraud prevention
Reduce SMS toll fraud with GCC phone validation
SMS pumping (artificially inflated traffic) abuses OTP and sign-up forms to generate paid SMS to numbers attackers profit from. Validating numbers before you send, restricting destinations, and rate-limiting sends removes most of that exposure.
What the API does and does not tell you:
/api/v1/phone/validate covers GCC numbers (AE, SA, KW, BH, OM, QA) and returns validity, type (mobile, landline, unknown), and, where the prefix maps to a known operator, carrier with type mno or mvno. Carrier is based on the original number allocation and may differ after porting. It does not classify VoIP, premium-rate, or shared-cost numbers, so combine it with the rate limits below.1. Know the response
javascript
// GET /api/v1/phone/validate?phone=%2B971501234567 (scope: validation:phone){ "data": { "phone": "+971501234567", "valid": true, "formats": { "e164": "+971501234567", "international": "...", "local": "..." }, "country": { "code": "AE", "name": "United Arab Emirates", "prefix": "+971", ... }, "type": "mobile", // "mobile" | "landline" | "unknown" "nationalNumber": "501234567", "carrier": { // present when the prefix maps to a known operator "name": "e& (Etisalat)", "mcc": "424", "mnc": "02", "type": "mno" // "mno" or "mvno" (e.g. Lebara on Zain in KSA) }, "portingNote": "Carrier is based on original number allocation. Number may have been ported to a different carrier.", "reason": "Valid phone number" }}2. Gate every OTP send on the server
Send only to valid mobile numbers in countries you serve. For OTP flows, failing closed when validation is unavailable is usually the right trade-off; offer an alternative such as email verification instead.
javascript
// Server-side OTP gate (Node.js 20+). Never call KhaleejiAPI from the browser.const KHALEEJI_API_KEY = process.env.KHALEEJI_API_KEY const ALLOWED_COUNTRIES = new Set(["AE", "SA"]) // only countries you actually serve export async function canSendOtp(rawPhone) { const url = new URL("/api/v1/phone/validate", "https://khaleejiapi.dev") url.searchParams.set("phone", rawPhone) let data try { const res = await fetch(url, { headers: { Authorization: `Bearer ${KHALEEJI_API_KEY}` }, signal: AbortSignal.timeout(3000), }) if (!res.ok) return { send: false, reason: "validation_unavailable" } data = (await res.json()).data } catch { // For OTP, fail closed: a pumping attack is cheaper to stop than to refund. return { send: false, reason: "validation_unavailable" } } if (!data?.valid) return { send: false, reason: "invalid_number" } if (data.type !== "mobile") return { send: false, reason: "not_mobile" } // landlines cannot receive SMS if (!ALLOWED_COUNTRIES.has(data.country.code)) return { send: false, reason: "country_not_served" } return { send: true, e164: data.formats.e164, carrier: data.carrier?.name ?? null }}3. Rate-limit by number and by client
Validation cannot stop an attacker cycling through real, valid mobile numbers. Per-number and per-IP limits, plus a bot challenge on the form, close that gap.
javascript
// Per-number and per-IP limits stop repeated sends even to valid numbers.// Example with any Redis-compatible store (ioredis API).async function allowSend(redis, e164, ip) { const [perNumber, perIp] = await Promise.all([ redis.incr(`otp:num:${e164}`), redis.incr(`otp:ip:${ip}`), ]) await Promise.all([ redis.expire(`otp:num:${e164}`, 3600, "NX"), redis.expire(`otp:ip:${ip}`, 3600, "NX"), ]) return perNumber <= 3 && perIp <= 10 // tune to your traffic}4. Use carrier data for routing and monitoring
- Log
carrier.nameandcountry.codeper send; a sudden spike to one operator or country is a common pumping signal. - If your SMS provider supports per-operator routes,
carrier.mcc/carrier.mncidentify the original network. Treat it as a hint, because ported numbers keep their original prefix. - Alert on OTP sends that never complete verification; a high unverified ratio indicates abuse.