Everything you need to integrate AkidOTP verification into your app — WhatsApp, Telegram, SMS, and Email. All endpoints, JSON shapes, and limit values reflect the current production API.
Base URL: https://akidotp.com — all paths below are relative to it.
Content type: send Content-Type: application/json on every POST.
AkidOTP is a multi-channel verification API. You verify your own end-users' phone numbers and email addresses through four channels:
Create a developer account and generate a key from the AkidOTP dashboard, or use the auth API directly:
| Action | Method + path | Auth | Body | Success |
|---|---|---|---|---|
| Sign up | POST /api/auth/signup |
— | { name, email, password, verificationToken } |
201 { success, token, developer } |
| Log in (step 1) | POST /api/auth/login |
— | { email, password } |
200 { success: true, otpSent: true } |
| Log in (step 2) | POST /api/auth/login |
— | { email, code } |
200 { success, token, developer } |
| Create key | POST /api/auth/keys |
Authorization: Bearer <JWT> |
optional { name } |
201 { success, key, apiKey } |
| List keys | GET /api/auth/keys |
Authorization: Bearer <JWT> |
— | 200 { success, keys } |
key value returned by POST /api/auth/keys.X-API-Key: YOUR_API_KEY
No other identifier is required. Your account and plan are resolved server-side from the key — you do not send a developer id or app id.
With reverse verification, you do not send a code to the user. You display a code and the user sends it to AkidOTP from their own device; receiving it from their number proves ownership.
POST /api/mobileverify/start
X-API-Key: YOUR_API_KEY
Content-Type: application/json
{ "phone": "+218912345678" }
phone (E.164, leading +) is the only required field. There is no request field
that limits the response to a single channel — the response returns deep links for every channel your
plan allows, and you choose which one to present.
Success 200 — fields present depend on your plan:
{
"success": true,
"code": "483920", // 6-digit code to show your user
"socketRoom": "9f3c…d21a", // session handle; use it to poll
"expiresIn": 120, // seconds
// If your plan allows WhatsApp:
"whatsappLink": "https://wa.me/<number>?text=483920",
"whatsappPersonalLink": "intent://…", // Android: WhatsApp personal
"whatsappBusinessLink": "intent://…", // Android: WhatsApp Business
// If your plan allows Telegram:
"telegramLink": "https://t.me/<bot>?start=483920",
// If your plan allows SMS and SMS quota remains:
"smsNumber": "<inbound number>",
"smsBody": "483920",
"smsLink": "sms:<number>?body=483920", // Android
"smsLinkIOS": "sms:<number>&body=483920", // iOS
"smsRemaining": 99 // a number, or "unlimited"
}
The session is short-lived — see expiresIn (seconds) in the response. Missing phone →
400 { success: false, error: "Phone number required" }.
whatsappLink → WhatsApp opens with the code pre-filled → the user
sends it to the AkidOTP number.telegramLink → the AkidOTP bot opens → the user taps
Start and shares their contact.smsLink / smsLinkIOS (or texts smsBody to
smsNumber) → the user sends the SMS to the AkidOTP inbound number.GET /api/mobileverify/status?room=SOCKET_ROOM
X-API-Key: YOUR_API_KEY
| State | Response |
|---|---|
| Verified | { "verified": true, "phone": "+218912345678" } |
| Pending | { "verified": false, "expired": false, "mismatch": false } |
| Wrong number replied | { "verified": false, "mismatch": true } |
| Expired / no session | { "verified": false, "expired": true } |
The verified response includes the confirmed phone. Poll roughly every 3 seconds until
verified or expired is true.
If you set a webhook URL on your API key (in the dashboard), AkidOTP POSTs a signed
notification to it when a reverse verification succeeds.
POST, HTTPS only, 10-second timeout, one automatic retry after 5
seconds. Webhook delivery never affects the verification result itself.Content-Type: application/json,
X-AkidOTP-Signature: <hex>, User-Agent: AkidOTP-Webhook/1.0.X-AkidOTP-Signature is the HMAC-SHA256 of the exact raw JSON request
body, keyed with your API key's webhook secret, hex-encoded. To verify, recompute it over the raw body and
compare (constant-time) against the header.Payloads — WhatsApp and Telegram share one shape; SMS uses a different shape, so handle both:
// WhatsApp / Telegram
{
"phone": "+218912345678",
"requestId": "9f3c…d21a",
"channel": "whatsapp",
"status": "verified",
"verifiedAt": "2026-07-05T12:34:56.000Z"
}
// SMS (verified)
{
"event": "verification.verified",
"channel": "sms",
"phone": "+218912345678",
"timestamp": "2026-07-05T12:34:56.000Z"
}
Treat success as status === "verified" or
event === "verification.verified".
Forward OTP: AkidOTP emails a 6-digit code and the user types it back. Synchronous — no webhook.
POST /api/verify/email/send
X-API-Key: YOUR_API_KEY
Content-Type: application/json
{ "phone": "+218912345678", "email": "user@example.com" }
Both phone and email are required. Success:
200 { "sent": true, "message": "Verification code sent to user@example.com" }. Missing a field →
400 { "error": "Phone and email are required" }.
POST /api/verify/email/confirm
X-API-Key: YOUR_API_KEY
Content-Type: application/json
{ "email": "user@example.com", "code": "483920" }
Success returns a token (JWT). The response contains a signed JWT in token, plus the
confirmed phone and email:
{ "verified": true, "phone": "+218912345678", "email": "user@example.com", "token": "JWT_TOKEN" }
Failure responses (all 401, distinguished by message):
| Case | Status | Body |
|---|---|---|
| Wrong code | 401 |
{ "error": "Incorrect code. Please try again." } |
| Expired code | 401 |
{ "error": "Code has expired. Please request a new one." } |
| No code on file | 401 |
{ "error": "No OTP found for this email. Please request a new code." } |
| Too many attempts | 401 |
{ "error": "Too many incorrect attempts. Please request a new code." } |
| Missing field | 400 |
{ "error": "Email and code are required" } |
Webhook? No. The email channel is check-only — it never fires a webhook. The result is returned synchronously in the confirm response.
| Setting | Value |
|---|---|
| OTP format | Short numeric code |
| OTP expiry | Short-lived — expires after a few minutes |
| Max verify attempts (email) | Limited — the code is invalidated after a few incorrect tries |
| Reverse session lifetime | Short-lived — see expiresIn in the start response |
| Resend | Brief cooldown between resends |
Rate limit (all /api/*) |
Per-IP — honor HTTP 429 and the retryAfter value |
Rate-limit response:
{ "error": "Too many requests. Please try again later.", "code": "RATE_LIMITED", "retryAfter": <seconds> }.
| Plan | Telegram | SMS | Monthly verifications | SMS / month | ||
|---|---|---|---|---|---|---|
| Free | — | — | — | ✅ | 1,000 | 0 |
| Starter | ✅ | ✅ | ✅ | ✅ | 10,000 | 50 |
| Growth | ✅ | ✅ | ✅ | ✅ | 100,000 | 200 |
| Enterprise | ✅ | ✅ | ✅ | ✅ | Unlimited | Unlimited |
The reverse channels (WhatsApp / Telegram / SMS) require Starter or higher. Email is available on
every tier. Your overall monthly verification count is enforced on every call (exceeding it
returns 429), and your SMS allowance is enforced when you call
POST /api/mobileverify/start. Email usage counts against your overall monthly limit. Upgrades take
effect immediately — plan and limits are read live on every request, so a change applies to your next call.
| Condition | Status | Example body |
|---|---|---|
| Missing API key | 401 |
{ "success": false, "error": "API key required. Include X-API-Key header." } |
| Invalid / revoked key | 401 |
{ "success": false, "error": "Invalid or revoked API key." } |
| Channel not on plan | 403 |
{ "success": false, "error": "WhatsApp verification is not available on the free plan. Upgrade to Starter or higher.", "upgrade": true }
|
| Monthly limit exceeded | 429 |
{ "success": false, "error": "Monthly verification limit reached (1,000). Upgrade your plan for more.", "currentUsage": 1000, "limit": 1000, "plan": "free", "upgrade": true }
|
| Rate limit exceeded | 429 |
{ "error": "Too many requests. Please try again later.", "code": "RATE_LIMITED", "retryAfter": <seconds> }
|
| Bad input (missing phone) | 400 |
{ "success": false, "error": "Phone number required" } |
| Invalid code (email) | 401 |
{ "error": "Incorrect code. Please try again." } |
| Expired code (email) | 401 |
{ "error": "Code has expired. Please request a new one." } |
Replace KEY, phone, and email with your values. SOCKET_ROOM and the code come from the
START response.
# 1) Start
curl -X POST https://akidotp.com/api/mobileverify/start \
-H "X-API-Key: KEY" -H "Content-Type: application/json" \
-d '{"phone":"+218912345678"}'
# 2) Show the code and open whatsappLink; the user SENDS it to AkidOTP on WhatsApp.
# 3) Poll for the result
curl "https://akidotp.com/api/mobileverify/status?room=SOCKET_ROOM" \
-H "X-API-Key: KEY"
# → { "verified": true, "phone": "+218912345678" }
curl -X POST https://akidotp.com/api/mobileverify/start \
-H "X-API-Key: KEY" -H "Content-Type: application/json" \
-d '{"phone":"+218912345678"}'
# Response includes telegramLink. User opens it, taps Start, shares contact. Then poll:
curl "https://akidotp.com/api/mobileverify/status?room=SOCKET_ROOM" -H "X-API-Key: KEY"
curl -X POST https://akidotp.com/api/mobileverify/start \
-H "X-API-Key: KEY" -H "Content-Type: application/json" \
-d '{"phone":"+218912345678"}'
# Response includes smsNumber, smsBody, smsLink / smsLinkIOS. User sends the SMS. Then poll:
curl "https://akidotp.com/api/mobileverify/status?room=SOCKET_ROOM" -H "X-API-Key: KEY"
# 1) Send the code
curl -X POST https://akidotp.com/api/verify/email/send \
-H "X-API-Key: KEY" -H "Content-Type: application/json" \
-d '{"phone":"+218912345678","email":"user@example.com"}'
# → { "sent": true, "message": "Verification code sent to user@example.com" }
# 2) Confirm the emailed 6-digit code
curl -X POST https://akidotp.com/api/verify/email/confirm \
-H "X-API-Key: KEY" -H "Content-Type: application/json" \
-d '{"email":"user@example.com","code":"483920"}'
# → { "verified": true, "phone": "…", "email": "…", "token": "JWT_TOKEN" }