Key Takeways
- Every OTP API integration comes down to two REST endpoints: send (returns verification ID) and validate (accepts ID and code, returns verified status).
- Production-quality integration handles 5 specific error categories: invalid number, code mismatch, expired, rate-limited, fraud-detected.
- Use webhooks for asynchronous delivery status rather than polling, and always verify the signature header.
- 12-item production checklist before flipping the feature flag: secrets, libphonenumber, SMS Retriever, WebOTP, country dropdown, resend timer, multi-channel fallback, signed webhooks, alerting, app-layer rate limits, STOP handling, runbook.
- Pick the language your auth service is written in — REST is REST, SDKs are mostly cosmetic.
If you're a developer evaluating an OTP API for a US-targeted application in 2026, you don't want yet another marketing page, you want code that works. This guide is a hands-on walkthrough of integrating a phone number verification API into a production application: REST endpoint reference, working code samples in Node.js, Python, PHP, and Java, error handling, webhook handling, sandbox testing, and a production checklist before you flip the feature flag.
All examples use VerifyNow's REST endpoints, but the patterns transfer to most modern OTP APIs (Twilio Verify, Vonage Verify, Sinch Verify) with minimal changes. Where US-specific concerns apply (10DLC routing, TCPA opt-in, fraud protection), we'll call them out inline.
The Two Endpoints You Need
Every OTP verification flow comes down to two REST calls, regardless of provider:
- POST /verification/send: generates an OTP, picks the optimal channel (SMS, WhatsApp, voice), and sends the code to the user's phone. Returns a verification ID for the next call.
- POST /verification/validate: accepts the verification ID and the code the user entered. Returns success/failure plus an error code on failure.
Everything else (channel preferences, retry policies, fraud-protection toggles) is configuration on the send call. Most providers expose 8–12 optional parameters; the defaults are sensible for ~80% of use cases.
Node.js: Send and Verify OTP
// Install: npm install axios
const axios = require('axios');
const API_BASE = 'https://cpaas.messagecentral.com/verification/v3';
const API_KEY = process.env.MC_API_KEY;
async function sendOtp(phoneNumber, channel = 'SMS') {
const response = await axios.post(`${API_BASE}/send`, {
countryCode: '1',
mobileNumber: phoneNumber,
flowType: channel, // 'SMS' | 'WHATSAPP' | 'VOICE'
otpLength: 6,
}, {
headers: { 'authToken': API_KEY }
});
return response.data.data.verificationId;
}
async function verifyOtp(verificationId, code) {
const response = await axios.post(`${API_BASE}/validate`, {
verificationId,
code,
}, {
headers: { 'authToken': API_KEY }
});
return response.data.data.verificationStatus === 'VERIFIED';
}
// Usage in your auth flow
const verificationId = await sendOtp('5551234567');
// ... user enters code ...
const verified = await verifyOtp(verificationId, '482917');
Python: Send and Verify OTP
# Install: pip install requests
import os
import requests
API_BASE = 'https://cpaas.messagecentral.com/verification/v3'
API_KEY = os.environ['MC_API_KEY']
def send_otp(phone_number, channel='SMS'):
response = requests.post(
f'{API_BASE}/send',
json={
'countryCode': '1',
'mobileNumber': phone_number,
'flowType': channel,
'otpLength': 6,
},
headers={'authToken': API_KEY}
)
response.raise_for_status()
return response.json()['data']['verificationId']
def verify_otp(verification_id, code):
response = requests.post(
f'{API_BASE}/validate',
json={
'verificationId': verification_id,
'code': code,
},
headers={'authToken': API_KEY}
)
response.raise_for_status()
return response.json()['data']['verificationStatus'] == 'VERIFIED'
PHP: Send and Verify OTP
<?php
// Using cURL — no external dependencies needed.
define('API_BASE', 'https://cpaas.messagecentral.com/verification/v3');
define('API_KEY', getenv('MC_API_KEY'));
function sendOtp($phoneNumber, $channel = 'SMS') {
$ch = curl_init(API_BASE . '/send');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'authToken: ' . API_KEY,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'countryCode' => '1',
'mobileNumber' => $phoneNumber,
'flowType' => $channel,
'otpLength' => 6,
]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response['data']['verificationId'];
}
function verifyOtp($verificationId, $code) {
$ch = curl_init(API_BASE . '/validate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'authToken: ' . API_KEY,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'verificationId' => $verificationId,
'code' => $code,
]),
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
return $response['data']['verificationStatus'] === 'VERIFIED';
}
Java: Send and Verify OTP
// Using java.net.http (JDK 11+).
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
public class VerifyNowClient {
private static final String API_BASE = "https://cpaas.messagecentral.com/verification/v3";
private static final String API_KEY = System.getenv("MC_API_KEY");
private static final HttpClient client = HttpClient.newHttpClient();
private static final ObjectMapper mapper = new ObjectMapper();
public static String sendOtp(String phoneNumber, String channel) throws Exception {
String body = mapper.writeValueAsString(java.util.Map.of(
"countryCode", "1",
"mobileNumber", phoneNumber,
"flowType", channel,
"otpLength", 6
));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/send"))
.header("authToken", API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
return mapper.readTree(resp.body()).get("data").get("verificationId").asText();
}
public static boolean verifyOtp(String verificationId, String code) throws Exception {
String body = mapper.writeValueAsString(java.util.Map.of(
"verificationId", verificationId,
"code", code
));
HttpRequest req = HttpRequest.newBuilder()
.uri(URI.create(API_BASE + "/validate"))
.header("authToken", API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
return "VERIFIED".equals(mapper.readTree(resp.body())
.get("data").get("verificationStatus").asText());
}
}
Error Handling You Actually Need
The errors that matter in production are not the ones in the happy path. Five you must handle:
Invalid phone number format
Returns HTTP 400 with error code INVALID_NUMBER. Use Google's libphonenumber to normalize before calling the API.
Code mismatch
User entered the wrong OTP. Returns HTTP 200 with verificationStatus: "FAILED". Allow up to 3–5 attempts before invalidating the verification ID.
Code expired
User waited too long. Returns verificationStatus: "EXPIRED". Surface a "Resend code" CTA in your UI.
Rate-limited
Same phone number requested too many OTPs. Returns HTTP 429. Don't retry immediately — back off exponentially.
SMS pumping detected
Returns HTTP 403 with error code FRAUD_DETECTED. Don't retry. Flag the request in your fraud-detection pipeline.
Webhooks for Delivery Status
For production deployments, listen for asynchronous delivery callbacks rather than polling. Configure a webhook URL in your provider dashboard, then handle:
// Express.js webhook handler example
app.post('/webhooks/verifynow', (req, res) => {
const { verificationId, deliveryStatus, channel, latencyMs } = req.body;
// Persist for analytics
db.deliveryEvents.insert({
verificationId,
deliveryStatus, // 'DELIVERED' | 'FAILED' | 'PENDING'
channel, // 'SMS' | 'WHATSAPP' | 'VOICE'
latencyMs,
timestamp: new Date(),
});
// Optional: trigger fallback to alternate channel on failure
if (deliveryStatus === 'FAILED' && channel === 'SMS') {
sendOtpFallback(verificationId, 'WHATSAPP');
}
res.status(200).end();
});
Always verify webhook authenticity using the signature header your provider sends — never trust an unsigned webhook in production.
Sandbox Testing Before Production
Before flipping the feature flag, validate end-to-end on the sandbox:
- Test phone numbers: most OTP providers in the US offer reserved test numbers that always succeed/fail/timeout deterministically.
- Network testing: verify your firewall allows outbound HTTPS to the provider's API endpoints (typically port 443).
- Rate-limit testing: deliberately trigger a per-number rate limit and confirm your error handling surfaces a useful message.
- Latency testing: measure round-trip time for send and verify under your typical load.
- Webhook delivery testing: use a service like webhook.site to inspect callbacks before pointing them at production.
VerifyNow's sandbox uses free test credits with no credit card, so you can run the full integration in pre-production without committing to a contract.
Production Checklist Before Launch
Twelve items to verify before flipping the feature flag:
- API keys stored in secrets manager (not source control)
- libphonenumber installed and used for client-side normalization
- SMS Retriever API integrated on Android (skips manual code entry)
- WebOTP API integrated on web (autofill from notification)
- Country-code dropdown defaults to user's geo-IP
- "Resend code" button hidden for first 30 seconds, then revealed
- Multi-channel fallback enabled (SMS → WhatsApp → Voice)
- Webhook handler authenticates signature header
- Failed-delivery alerting wired to Slack/PagerDuty
- Per-IP and per-number rate limiting at your application layer (not just the provider's)
- STOP-keyword handling tested (TCPA-compliance)
- Production runbook for "OTP delivery is broken" scenarios written and stored
FAQs
Which language has the best SDK for OTP APIs?
Most providers offer official SDKs in Node, Python, Java, PHP, Go, and Ruby with feature parity. The differences between SDKs are mostly cosmetic — REST is REST. Pick the language your team writes the auth service in. If your team writes in Rust or .NET, calling REST endpoints directly with the language's HTTP client (as in the Java example above) is straightforward.
How do I handle OTP fallback to a different channel on failure?
Two patterns: (a) configure automatic provider-side fallback by listing channels in priority order on the send call, and the provider tries each in sequence on delivery failure; (b) listen for delivery webhooks and trigger a new send call with a different channel from your application. The first is simpler; the second gives you more control. VerifyNow supports both patterns.
Should I implement the OTP UI on web with WebOTP?
Yes, where browsers support it. Google's WebOTP API auto-fills the code from an SMS notification on Chrome and Edge. The OTP message must be formatted with a special pattern (e.g., "Your code is 123456 #abc.example.com #482917") for browsers to recognize it. Falls back gracefully to manual entry on unsupported browsers.
Get a Sandbox Key in Under a Minute
The fastest way to validate any of the code above is to run it against a real sandbox. VerifyNow for USA gives you free test credits with no credit card, REST endpoints documented end-to-end, and SDKs in 6 languages. Most teams ship their first OTP integration within a few hours of signup.

.svg%20(1).png)



