OTP API integration is the single most common security feature developers ship — and the one most frequently shipped wrong. A weak implementation leaks money through SMS pumping. A brittle one drops users at checkout. A rigid one fails the moment your primary delivery channel goes down.
This guide covers the full stack: gateway architecture patterns, working code examples in cURL, Python, and Node.js, multi-channel delivery, rate limiting, abuse protection, testing strategies, and production readiness. Whether you’re building your first OTP flow or hardening an existing one, every section targets a specific decision you’ll face in production.
How OTP API Integration Works
Every OTP system follows the same two-phase pattern: send and verify.
Phase 1 — Send. Your application calls the OTP provider’s generate endpoint with a phone number (or email). The provider generates a random code, hashes and stores it server-side, then delivers it to the user via SMS, voice call, WhatsApp, or USSD.
Phase 2 — Verify. The user enters the code in your application. Your backend sends the code and phone number to the provider’s verify endpoint. The provider compares the submitted code against the stored hash. Match? The user is authenticated. No match or expired? Verification fails.
This two-phase pattern means your application never stores OTP codes directly. The provider handles generation, delivery, storage, and expiry. You handle the user experience and the API calls.
Building this from scratch — code generation, hashing, expiry management, delivery infrastructure, retry logic — takes weeks and introduces ongoing maintenance. A managed OTP API abstracts all of that behind two REST endpoints. For a deeper look at why managed services win over self-build approaches, read about the benefits of plug-and-play OTP APIs.
The rest of this guide assumes you’re integrating with a provider API. The patterns are provider-agnostic — swap in any OTP service that follows the send/verify model.
OTP API Gateway Architecture Patterns
How you integrate your OTP provider determines your system’s reliability ceiling. Three OTP API gateway patterns dominate, each with different trade-offs.
Direct API Integration
The simplest pattern. Your application makes REST calls directly to a single OTP provider.
Your App → OTP Provider API → SMS/Voice/USSD Gateway → UserBest for: MVPs, low-volume applications, early-stage products.
Trade-offs: Fast to implement. Zero abstraction overhead. But you have a single point of failure — if the provider goes down or a carrier blocks delivery, your entire OTP flow breaks. No fallback, no channel switching.
Gateway Aggregator Pattern
An abstraction layer sits between your application and one or more OTP providers. The gateway routes requests by channel, region, or provider health.
Your App → OTP Gateway Layer → Provider A (SMS)
→ Provider B (Voice)
→ Provider C (WhatsApp)Best for: Production applications with reliability requirements, multi-channel delivery, regional optimization.
Trade-offs: More upfront work to build the routing layer. But you gain channel flexibility, provider independence, and the ability to optimize by region. A platform like Arkesel reduces aggregator complexity by supporting SMS, Voice, and USSD OTP delivery from a single API — fewer integrations to maintain.
Failover Chain Architecture
A primary provider handles all requests. If delivery fails or times out, the system automatically routes to a secondary provider, then a tertiary one.
Your App → Primary Provider (SMS, 30s timeout)
↓ (failure/timeout)
→ Secondary Provider (Voice, 60s timeout)
↓ (failure/timeout)
→ Tertiary Provider (WhatsApp)Best for: Mission-critical applications — fintech, e-commerce checkout, banking authentication.
Key implementation details:
- Timeout thresholds: 30 seconds for SMS, 60 seconds for voice. If no delivery confirmation arrives within the threshold, trigger failover.
- Circuit breaker pattern: Track provider error rates over a rolling window. If errors exceed a threshold (e.g., 5 failures in 60 seconds), temporarily remove that provider from the chain.
- Health checks: Ping provider status endpoints every 30-60 seconds to detect outages before they affect users.
When a failover triggers, it’s often a sign of a deeper issue. Your OTP API error troubleshooting guide should be the first stop for diagnosing whether the failure is on your side (authentication, formatting) or the provider’s.
SMS OTP API Integration: Delivery Channels Compared
Choosing the right delivery channel for your SMS OTP API integration is a technical decision with direct impact on conversion rates, security posture, and cost.
| Dimension | SMS | Voice | USSD | |
|---|---|---|---|---|
| Delivery speed | 5-7 seconds | 30-60 seconds | 2-4 seconds | Near-instant |
| Device reach | 99.9% of mobile devices | Any phone with voice capability | Requires app + internet | Any mobile device, no data needed |
| Security profile | SIM swap risk, SS7 interception | Call interception, voicemail risk | E2E encrypted, relies on device security | Session-based, no stored message, unencrypted over network |
| Industry cost range | $0.003-0.15 per message | $0.02-0.25 per call | $0.005-0.09 per message | Varies by market |
| Best use case | Default channel, universal reach | Fallback for failed SMS, accessibility | Tech-savvy users with smartphones | African markets, feature phones, no-data environments |
For current pricing on specific channels, check your provider’s rate card.
African Market Considerations
Channel selection shifts in African markets. Feature phone prevalence means SMS and USSD dominate. Data connectivity is intermittent in many regions, which rules out WhatsApp as a primary channel for broad-reach applications.
USSD stands out as a unique OTP channel. It works on every mobile device, requires zero data, and the session-based model means no OTP code is stored on the device after the interaction ends. For mobile money, banking, and financial services — where USSD is already the primary interface — delivering OTPs via the same channel reduces friction.
Arkesel delivers OTPs via SMS, Voice, and USSD from a single API — a distinct advantage for teams building across African markets where channel availability varies by region and device type.
For a deeper analysis of channel security trade-offs, including SIM swap mitigation and encryption considerations, see our OTP security best practices guide.
Implementing OTP API Integration (Code Examples)
These examples use generic REST API patterns that work with any OTP provider. Replace the placeholder URL, API key, and parameters with your provider’s specifics. For Arkesel’s implementation details, refer to the Arkesel developer documentation.
Send OTP Request
cURL:
curl -X POST https://api.your-provider.com/otp/send \
-H "Authorization: Bearer $OTP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "233241234567",
"channel": "sms",
"expiry_seconds": 300,
"message": "Your verification code is {{code}}. It expires in 5 minutes."
}'Python:
import os
import requests
def send_otp(phone_number, channel="sms", expiry=300):
response = requests.post(
"https://api.your-provider.com/otp/send",
headers={
"Authorization": f"Bearer {os.environ['OTP_API_KEY']}",
"Content-Type": "application/json"
},
json={
"phone_number": phone_number,
"channel": channel,
"expiry_seconds": expiry,
"message": "Your verification code is {{code}}. It expires in 5 minutes."
}
)
data = response.json()
if response.status_code == 200 and data.get("status") == "sent":
return {"success": True, "request_id": data["request_id"]}
return {"success": False, "error": data.get("message", "Unknown error")}Node.js:
async function sendOtp(phoneNumber, channel = "sms", expiry = 300) {
const response = await fetch("https://api.your-provider.com/otp/send", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OTP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
phone_number: phoneNumber,
channel: channel,
expiry_seconds: expiry,
message: "Your verification code is {{code}}. It expires in 5 minutes."
})
});
const data = await response.json();
if (response.ok && data.status === "sent") {
return { success: true, requestId: data.request_id };
}
return { success: false, error: data.message || "Unknown error" };
}Verify OTP Response
cURL:
curl -X POST https://api.your-provider.com/otp/verify \
-H "Authorization: Bearer $OTP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "233241234567",
"code": "482910"
}'Python:
def verify_otp(phone_number, code):
response = requests.post(
"https://api.your-provider.com/otp/verify",
headers={
"Authorization": f"Bearer {os.environ['OTP_API_KEY']}",
"Content-Type": "application/json"
},
json={
"phone_number": phone_number,
"code": code
}
)
data = response.json()
if response.status_code == 200 and data.get("status") == "verified":
return {"verified": True}
return {
"verified": False,
"reason": data.get("error_code"),
"message": data.get("message")
}Node.js:
async function verifyOtp(phoneNumber, code) {
const response = await fetch("https://api.your-provider.com/otp/verify", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.OTP_API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
phone_number: phoneNumber,
code: code
})
});
const data = await response.json();
if (response.ok && data.status === "verified") {
return { verified: true };
}
return {
verified: false,
reason: data.error_code,
message: data.message
};
}Common HTTP status codes to handle:
| Status | Meaning | Action |
|---|---|---|
| 200 | Success (check response body for send/verify result) | Process the result |
| 400 | Bad request (missing or malformed parameters) | Fix request payload |
| 401 | Authentication failure | Verify API key |
| 429 | Rate limited | Back off and retry |
| 500 | Provider error | Retry with backoff or trigger failover |
Handling Delivery Status Callbacks
Most providers support webhooks to notify your application about delivery status changes. Here’s a minimal Express.js handler:
const express = require("express");
const app = express();
app.post("/webhooks/otp-status", express.json(), (req, res) => {
const { request_id, status, phone_number, timestamp } = req.body;
switch (status) {
case "delivered":
updateOtpRecord(request_id, { delivered: true, delivered_at: timestamp });
break;
case "failed":
updateOtpRecord(request_id, { delivered: false, failed_at: timestamp });
triggerFallbackChannel(phone_number, request_id);
break;
case "expired":
updateOtpRecord(request_id, { expired: true });
break;
}
res.status(200).json({ received: true });
});Use delivery callbacks to trigger fallback channels, monitor delivery rates, and detect anomalies that signal abuse.
For a step-by-step walkthrough that covers environment setup, dependency installation, and your first working request, follow the OTP API setup tutorial. For SMS-specific integration patterns, see the Arkesel SMS API developer guide.
Rate Limiting and Abuse Protection
OTP endpoints are attack magnets. Without rate limiting and abuse detection, a single bad actor can drain your messaging budget overnight.
Endpoint Rate Limiting
Apply separate rate limits to your send and verify endpoints. They have different abuse patterns.
Recommended limits:
- Send endpoint: 2-3 requests per 60 seconds per user/IP
- Verify endpoint: 3-5 attempts per OTP, with account lockout after maximum failures
from collections import defaultdict
import time
rate_limits = defaultdict(list)
def check_rate_limit(identifier, namespace, max_requests, window_seconds):
key = f"{namespace}:{identifier}"
now = time.time()
rate_limits[key] = [
t for t in rate_limits[key] if now - t < window_seconds
]
if len(rate_limits[key]) >= max_requests:
return False
rate_limits[key].append(now)
return True
def send_otp_handler(phone_number, ip_address):
if not check_rate_limit(phone_number, "otp_send", max_requests=3, window_seconds=60):
return {"error": "Rate limited. Try again in 60 seconds."}
if not check_rate_limit(ip_address, "otp_send_ip", max_requests=5, window_seconds=60):
return {"error": "Too many requests from this IP."}
return send_otp(phone_number)Use separate rate limit namespaces for send and verify. An attacker who exhausts the send limit should not affect verification attempts for OTPs already in flight.
SMS Pumping Protection
SMS pumping is the most expensive attack on OTP systems. Bots trigger thousands of OTP sends to premium-rate numbers. You pay for every message. The attacker profits from carrier revenue share.
Detection signals:
- Send-to-verify ratio drop. Normal traffic: most sent OTPs get verified. Pumping attacks: sends spike while verifications flatline. Monitor this ratio in real time.
- Geographic anomalies. Sudden volume from countries or area codes where you have no users.
- Volume spikes. OTP send rate jumps 5-10x above your baseline without a corresponding traffic increase.
Prevention checklist:
- CAPTCHA or proof-of-work challenge before the OTP send endpoint
- Carrier lookup to filter non-mobile numbers (landlines, VoIP, premium-rate)
- Geographic restrictions — block or flag OTP sends to countries outside your active markets
- Conversion rate monitoring with automated alerts when send-to-verify ratio drops below threshold
Retry Logic with Exponential Backoff
When an OTP send fails, retry intelligently — not aggressively.
import time
import uuid
def send_otp_with_retry(phone_number, channel="sms", max_retries=3):
idempotency_key = str(uuid.uuid4())
for attempt in range(max_retries):
result = send_otp(
phone_number,
channel=channel,
idempotency_key=idempotency_key
)
if result["success"]:
return result
if result.get("error_code") in ("INVALID_NUMBER", "BLOCKED"):
return result
wait = (2 ** attempt) + (0.1 * attempt)
time.sleep(wait)
return {"success": False, "error": "Max retries exceeded"}Key principles:
- Exponential backoff: 1s, 2s, 4s between retries. Prevents hammering a provider that’s already struggling.
- Idempotency keys: Send the same unique key with each retry attempt. The provider deduplicates, preventing the user from receiving multiple OTPs for a single action.
- Non-retryable errors: Don’t retry permanent failures (invalid number, blocked sender). Only retry transient errors (timeout, server error, rate limited).
Multi-Channel OTP Fallback Strategy
A production OTP system needs a fallback plan. When SMS delivery stalls, users abandon transactions. A multi-channel fallback chain keeps verification working even when individual channels fail.
Decision flow:
1. Send OTP via primary channel (SMS)
2. Wait for delivery confirmation (30-second timeout)
3. No confirmation? → Send via secondary channel (Voice)
4. Wait for delivery confirmation (60-second timeout)
5. No confirmation? → Send via tertiary channel (WhatsApp or USSD)
6. All channels failed? → Surface error to user with "Contact support" optionImplementation considerations:
- Timeout-based fallback. SMS not delivered within 30 seconds triggers a voice call. Use delivery status webhooks (not just API response) to determine delivery success.
- User-preference routing. Let users choose their preferred OTP channel. Store the preference and use it as the primary channel for that user.
- Cost-optimized routing. Route to the least expensive channel first for low-risk operations (password reset). Reserve voice or WhatsApp for high-risk operations (financial transactions).
Fallback chain pseudocode:
async def send_otp_with_fallback(phone_number, channels=["sms", "voice", "ussd"]):
timeouts = {"sms": 30, "voice": 60, "ussd": 15}
for channel in channels:
result = await send_otp(phone_number, channel=channel)
if not result["success"]:
continue
delivered = await wait_for_delivery(
result["request_id"],
timeout=timeouts[channel]
)
if delivered:
return {"success": True, "channel": channel}
return {"success": False, "error": "All channels exhausted"}Arkesel’s platform supports SMS, Voice, and USSD from a single API, which means your fallback chain can operate within one provider integration. This eliminates the complexity of managing separate API keys, webhooks, and authentication flows for each channel.
How to Test OTP API Endpoints
OTP systems touch authentication, third-party APIs, delivery infrastructure, and money. Test each layer to avoid surprises in production.
Sandbox and Mock Testing
Most OTP providers offer sandbox environments with test phone numbers that return success without sending real messages. Use these for development and CI/CD.
For unit tests, mock the provider at the interface level:
class OtpProvider:
def send(self, phone_number, channel, message):
raise NotImplementedError
def verify(self, phone_number, code):
raise NotImplementedError
class MockOtpProvider(OtpProvider):
def __init__(self):
self.sent_codes = {}
def send(self, phone_number, channel, message):
code = "123456"
self.sent_codes[phone_number] = code
return {"success": True, "request_id": "mock-123"}
def verify(self, phone_number, code):
expected = self.sent_codes.get(phone_number)
return {"verified": code == expected}This pattern isolates your business logic from provider-specific behavior. Swap the mock for a real provider in integration tests.
Integration Test Patterns
Integration tests validate the full send-deliver-verify cycle.
What to test:
- OTP send returns a success response with a request ID
- OTP verify succeeds with the correct code
- OTP verify fails with an incorrect code
- OTP verify fails after expiry
- OTP verify fails after the code has already been used (one-time enforcement)
- Rate limiting triggers after exceeding the threshold
For CI/CD pipelines, use provider sandbox endpoints or mock responses. Validate request format, authentication headers, and response parsing — not actual message delivery.
When integration tests surface unexpected failures, cross-reference with the OTP API error troubleshooting guide for common failure modes and their fixes.
Load Testing OTP Endpoints
OTP endpoints need to handle concurrent requests during peak traffic — product launches, flash sales, pay-day banking surges.
Key metrics to measure:
- p99 latency for the send endpoint under load
- Verification throughput (requests per second before degradation)
- Rate limiter behavior under sustained load
- Provider response time degradation curve
Approach: Ramp from your baseline request rate to 10x projected peak. Monitor the degradation curve. Your send endpoint should fail gracefully (return 429 or 503) rather than timing out silently.
Tools like k6, Artillery, or Locust work well for this. Point them at your staging environment with provider sandbox endpoints enabled.
Sample k6 script for OTP send endpoint:
import http from "k6/http";
import { check } from "k6";
export const options = {
stages: [
{ duration: "30s", target: 10 },
{ duration: "1m", target: 50 },
{ duration: "30s", target: 100 },
{ duration: "1m", target: 0 },
],
};
export default function () {
const res = http.post(
"https://api.your-provider.com/otp/send",
JSON.stringify({
phone_number: `23324${Math.floor(Math.random() * 9000000) + 1000000}`,
channel: "sms",
}),
{
headers: {
"Authorization": `Bearer ${__ENV.OTP_API_KEY}`,
"Content-Type": "application/json",
},
}
);
check(res, {
"status is 200 or 429": (r) => [200, 429].includes(r.status),
"response time < 2s": (r) => r.timings.duration < 2000,
});
}Watch for the point where 200s transition to 429s — that’s your rate limiter kicking in. If you see 500s or timeouts before 429s, your infrastructure needs work.
Security Testing Checklist
- ☐ OTP expires after the configured TTL (verify endpoint rejects expired codes)
- ☐ OTP is single-use (second verify attempt with the same code fails)
- ☐ Brute force protection (account locks after N failed verification attempts)
- ☐ Replay attack prevention (same OTP cannot be reused across sessions)
- ☐ Secure storage (OTP stored as hash, not plaintext — verify with database inspection)
- ☐ HTTPS-only transmission (HTTP requests rejected or redirected)
- ☐ Timing attack resistance on verify endpoint (constant-time comparison, same response time for valid and invalid codes)
- ☐ Rate limiting enforced at both IP and user level
Choosing an OTP API Provider
The right provider depends on your delivery channels, geographic coverage, and developer experience requirements. Evaluate against these criteria.
API design quality. Look for RESTful endpoints with consistent error codes, webhook support for delivery status, and clear HTTP status codes. Avoid providers with inconsistent response formats or undocumented error states.
Multi-channel support. SMS-only is a starting point. Production applications need voice fallback at minimum. Providers that support SMS, voice, and at least one additional channel (WhatsApp, USSD, email) reduce the number of integrations you maintain.
Sandbox and testing environment. A provider without test phone numbers or a sandbox mode forces you to spend real money during development. Non-negotiable for any serious integration.
Documentation quality. Working code examples in your language, not just API specifications. Request/response samples for every endpoint. Error code reference with causes and fixes.
Geographic delivery coverage. If your users are in Africa, verify that the provider has direct carrier connections in your target countries. Direct connections mean faster delivery and higher success rates compared to routing through international aggregators.
Compliance. GDPR data handling, data residency options, and audit logging for regulated industries.
Arkesel for African Markets
For teams building products that serve African users, Arkesel delivers OTPs via SMS, Voice, and USSD from a single REST API.
What sets it apart:
- Direct carrier connections to MTN, Vodafone, and AirtelTigo — no international routing delays
- 99.9% delivery rate backed by direct network integration
- USSD OTP delivery — a channel no global provider supports, and one that reaches every mobile device without data
- Multi-API key management for separating production, staging, and development traffic
- REST API with webhook support for delivery status tracking
Explore the full API at the Arkesel developer documentation, or see how OTP fits into the broader verification platform at Arkesel Phone Number Verification.
Twilio and Vonage are strong alternatives for global coverage. Both offer multi-channel OTP with well-documented APIs. Evaluate based on your geographic requirements and channel needs.
For a deeper look at managed OTP services versus building your own, read about the benefits of plug-and-play OTP APIs.
OTP Integration Production Readiness Checklist
Before you deploy your OTP integration to production, verify every item.
Security
- ☐ OTP codes expire within 5 minutes (shorter for high-risk operations)
- ☐ Codes stored as hashes, never plaintext
- ☐ Rate limiting on both send and verify endpoints
- ☐ HTTPS enforced for all OTP-related endpoints
- ☐ Brute force lockout after 3-5 failed verification attempts
- ☐ CAPTCHA or challenge before OTP send (prevents SMS pumping)
Reliability
- ☐ Multi-channel fallback configured (minimum: SMS + voice)
- ☐ Delivery status monitoring via webhooks
- ☐ Alerting on delivery rate drops (threshold: below 90%)
- ☐ Circuit breaker pattern for provider failover
- ☐ Retry logic with exponential backoff and idempotency keys
Compliance
- ☐ Data retention policy defined (how long OTP records are stored)
- ☐ User consent captured before sending OTPs
- ☐ Regional regulations reviewed (GDPR, local telecom rules)
- ☐ Audit logging for all OTP events (send, verify, fail, expire)
Operations
- ☐ Logging for every OTP event with request IDs for tracing
- ☐ Metrics dashboard: send volume, delivery rate, verification rate, latency
- ☐ Cost monitoring with budget alerts
- ☐ SMS pumping detection alerts (send-to-verify ratio, geographic anomalies)
- ☐ Provider SLA monitoring (uptime, response time)
For detailed implementation guidance on the security items, see our OTP security best practices guide.
Frequently Asked Questions
What is the difference between an OTP API gateway and direct integration?
Direct integration connects your application to a single OTP provider via REST API calls. An OTP API gateway adds an abstraction layer that routes requests across multiple providers based on channel, region, or provider health. Direct integration is faster to build but creates a single point of failure. A gateway pattern adds reliability through provider redundancy and channel fallback.
How do you prevent SMS pumping attacks on OTP endpoints?
Monitor your send-to-verify ratio in real time — pumping attacks show OTP sends spiking while verifications flatline. Implement CAPTCHA or proof-of-work challenges before the send endpoint, use carrier lookup to block non-mobile numbers, apply geographic restrictions to limit sends to your active markets, and set automated alerts when the conversion ratio drops below your baseline.
What timeout should I set for OTP delivery before triggering a fallback?
Set 30 seconds for SMS delivery and 60 seconds for voice calls. Use delivery status webhooks (not just API response codes) to confirm delivery. If no delivery confirmation arrives within the threshold, trigger the next channel in your fallback chain automatically.
Start Building
You now have the architecture patterns, code examples, and production checklist to ship a reliable OTP integration. The difference between a tutorial-grade implementation and a production-grade one is everything after the first successful API call: rate limiting, abuse protection, fallback channels, and monitoring.
Explore the Arkesel developer documentation to see how SMS, Voice, and USSD OTP work within a single API. When you’re ready, create an Arkesel account and start testing in minutes.





