Verify API
Deliver secure one-time passwords to any mobile number. Configurable length, expiry, and type — with automatic rate limiting and brute-force protection built in so you can focus on your product.
Multiple PIN Types
Choose NUMERIC, ALPHANUMERIC, or ALPHABETIC codes, 4–10 characters, to match your security requirements.
Configurable Expiry
Set expiry from 1 minute to 24 hours with automatic invalidation — no manual cleanup needed.
Built-in Rate Limiting
Automatic cooldown periods and max-attempt enforcement prevent brute force and OTP spam.
Metadata Support
Attach custom context (userId, sessionId, IP) to each OTP request for audit trails.
Resend & Cooldown
Resend OTPs after a configurable cooldown without creating duplicate active codes.
Encrypted at Rest
Codes are encrypted with AES-256-GCM (never stored in plaintext), scrubbed automatically after expiry, with brute-force protection built in.
10 min
Configurable 1–1440 min
3
Default (max 10)
30 s
Between resends
4–10
Chars (all PIN types)
Authentication
Request OTP
Call /v1/otp/request — Sendexa generates a secure code and sends it by SMS.
curl -X POST 'https://api.sendexa.co/v1/otp/request' \-H 'Content-Type: application/json' \-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \-d '{"phone": "0244123456","from": "MyBrand","message": "Your code is {code}. Valid for {amount} {duration}.","pinLength": 6,"pinType": "NUMERIC","expiry": { "amount": 5, "duration": "minutes" }}'
{"success": true,"data": {"id": "otp_abc123xyz","expiresAt": "2024-01-15T10:35:00.000Z"}}
User enters the code
Display an input in your UI. Store the idfrom step 1 — you'll need it for verification.
Verify the code
Call /v1/otp/verify with the OTP ID and the code the user entered.
curl -X POST 'https://api.sendexa.co/v1/otp/verify' \-H 'Content-Type: application/json' \-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \-d '{"phone": "0244123456","code": "482910","id": "otp_abc123xyz"}'
{"success": true,"message": "OTP verified successfully","data": { "verified": true }}
API Endpoints
/v1/otp/requestGenerate and send a new OTP. Defaults to SMS — pass "channel": "whatsapp" | "voice" | "email", or an array for fallback.
/v1/otp/verifyValidate a code submitted by the user against the active OTP — channel-agnostic, works regardless of which channel delivered it.
/v1/otp/resendRe-send the OTP after the cooldown period has elapsed, on the same channel or a new one you specify.
/v1/otp/status/:idLook up a single OTP by id — status, which channel it went out on, and channel fallback attempts if any.
/v1/otp/request/voiceStill fully supported — equivalent to "channel": "voice" on the unified endpoint above.
/v1/otp/request/emailStill fully supported — equivalent to "channel": "email" on the unified endpoint above.
/v1/otp/request/whatsappStill fully supported — equivalent to "channel": "whatsapp" on the unified endpoint above.
| Type | Length | Example | Best For |
|---|---|---|---|
NUMERIC | 4–10 digits | 482910 | Login, general verification (easiest to type) |
ALPHANUMERIC | 4–10 chars | A7B9X2K4 | Financial transactions, high-security flows |
ALPHABETIC | 4–10 letters | KPTMRB | Voice-read verification codes |
RATE_LIMIT_EXCEEDEDMax 3 OTP requests per phone per hour. There is no "active OTP already exists" error — a new request always invalidates any still-pending OTP for that phone and sends a fresh code.
IP_RATE_LIMIT_EXCEEDEDThe same IP address requested OTPs for too many distinct phone numbers within an hour.
OTP_EXPIREDThe OTP has passed its expiry time. Prompt the user to request a new code.
INVALID_PINWrong code entered. Check attemptsRemaining — lock the flow at 0 to prevent brute force.
OTP_NOT_FOUNDThe OTP ID does not exist or was already verified. Redirect to request a new OTP.
WHATSAPP_NOT_CONFIGUREDThis business has no active WhatsApp Business Account with an approved authentication template.
Use these placeholders in the message field — they are replaced at send time:
{code}The generated OTP code (required)
{amount}The expiry amount (e.g. 5)
{duration}The expiry unit (minutes / hours)
Example: "Your code is {code}. Valid for {amount} {duration}." → "Your code is 482910. Valid for 10 minutes."
| Channel | Availability | Notes |
|---|---|---|
| SMS | Works out of the box | Sendexa's own provider accounts — no setup required. |
| Voice | Works out of the box | Same as SMS — no setup required. |
| Works out of the box | Sent from Sendexa's mailer — no channel fee. | |
| Works out of the box | Uses your own connected WhatsApp Business Account with an approved authentication template if you have one — otherwise falls back to Sendexa's own shared WhatsApp number automatically, still billed to your account. |
If SMS delivery is still pending 45 seconds after the request and the business has WhatsApp configured, the same code is automatically also sent via WhatsApp — no extra request needed. For an immediate, explicit fallback instead of waiting on delivery confirmation, pass an array to channel on the request itself — see Channel & Fallback.
A flat $0.05 verification fee is charged once, only when a verification actually succeeds — not on every OTP request or resend. SMS and WhatsApp also incur a real per-message channel fee at send time (that part is charged regardless of whether the user ever completes verification, since the message still has to be delivered). Voice and Email have no separate channel fee.
| Channel | Price |
|---|---|
| SMS | $0.05 per successful verification + the real per-country SMS rate |
| $0.05 per successful verification + a per-message authentication-template fee | |
| Voice | $0.05 per successful verification — no separate channel fee |
| $0.05 per successful verification — no separate channel fee |
High-volume or enterprise pricing is negotiated directly — contact sales.
Pass an Idempotency-Key header on any /v1/otp/request* call. A retried request with the same key within 24 hours returns the original response instead of creating a second OTP — safe to use on network retries.
curl -X POST 'https://api.sendexa.co/v1/otp/request' \-H 'Content-Type: application/json' \-H 'Authorization: Basic YOUR_DASHBOARD_BASE64_TOKEN' \-H 'Idempotency-Key: 3f29-a91c-...' \-d '{ "phone": "0244123456", "from": "MyBrand" }'
Reserved test identifiers never send a real message and are never billed — the code is always 000000, on every channel:
1 555 000 0000–1 555 000 9999for SMS, Voice, and WhatsApp- Any address at
@sandbox.sendexa.testfor Email
Subscribe a webhook to any of these events from your dashboard. Every delivery is HMAC-SHA256 signed (x-webhook-signature) and wrapped in a stable envelope — { eventId, event, businessId, data, timestamp } — so you can dedupe retried deliveries on eventId. Failed deliveries are automatically retried with backoff (1 min, 5 min, 30 min) up to your webhook's configured max retries.
OTP_SENTOTP_DELIVEREDOTP_FAILEDOTP_VERIFIEDOTP_EXPIRED*889*142#When SMS is delayed or unavailable, users can dial *889*142# from any mobile phone to retrieve a pending OTP via USSD — no internet or SMS delivery required.
SMS delayed
Carrier congestion or network issues holding up delivery.
No data needed
Works on feature phones and areas with no internet access.
Same session
The OTP ID and expiry are unchanged — verify as normal.
The USSD fallback is active automatically for all OTP requests — no extra configuration required. The code is available until the session expires or the OTP is verified.
Security Best Practices
- Set
maxAmountOfValidationRetriesto 3–5 to prevent brute force attacks - Use 5-minute expiry for most flows — shorter is more secure
- Lock the UI after the attempt limit is reached; do not allow silent retries
- Pass the end-user's real
ipAddresson every request — it powers the IP-based velocity check, not just audit trails - Send an
Idempotency-Keyheader on every request so network retries never create a duplicate OTP - Never log or expose the OTP code in your application logs
- Verify server-side — never trust client-side OTP validation alone
- Build and test against the sandbox range before going live