Verify users in minutes
AXDOX Verify confirms a user owns a phone number or email by sending a one-time code — over WhatsApp, SMS, or Email, automatically. Two API calls. About 10 lines of code.
Introduction
You call two endpoints from your app's backend: one to send a code, one to check it. AXDOX generates the code, delivers it through the best available channel, and tells you if the user entered it correctly. That's the whole product.
How it works
Your app's server talks to AXDOX. Your users only receive the code — they never talk to AXDOX directly.
POST /otp/sendPOST /otp/verifyhttps://api.axdox.in — not your own site.Quick start
test key while building.Send a code — pick your language (the choice applies to every sample on this page):
curl -X POST https://api.axdox.in/api/v1/otp/send \
-H "Authorization: Bearer axk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+14155552671" }'Then check the code the user typed — see Verify an OTP.
Full example — verify at signup
The complete flow: add two routes on your own server — one starts verification, one confirms it. Your frontend only ever calls your own routes.
// app/api/signup/start/route.ts
export async function POST(req: Request) {
const { phone } = await req.json();
const r = await fetch("https://api.axdox.in/api/v1/otp/send", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.AXDOX_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ to: phone }),
});
return Response.json({ request_id: (await r.json()).request_id });
}
// app/api/signup/confirm/route.ts
export async function POST(req: Request) {
const { request_id, code } = await req.json();
const r = await fetch("https://api.axdox.in/api/v1/otp/verify", {
method: "POST",
headers: { Authorization: "Bearer " + process.env.AXDOX_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ request_id, code }),
});
const { verified } = await r.json();
return Response.json({ ok: verified }, { status: verified ? 200 : 400 });
}Authentication
Send your API key in the Authorization header as a Bearer token. Keys are axk_test_… (building) or axk_live_… (production).
Authorization: Bearer axk_live_YOUR_KEYSend an OTP
/api/v1/otp/sendcurl -X POST https://api.axdox.in/api/v1/otp/send \
-H "Authorization: Bearer axk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+14155552671" }'| Field | Type | Description |
|---|---|---|
| to | string, required | Phone in E.164 (+14155552671) or an email address. |
| email_fallback | string | Email used as final fallback when to is a phone. |
| channel | string | Force one: whatsapp, sms, email (default: auto). |
| metadata | object | Optional key/values stored with the request. |
{
"request_id": "8f3c1e2a-...",
"status": "pending",
"channel": "whatsapp",
"to": "+14*****2671",
"expires_at": "2026-01-01T10:05:00Z"
}Verify an OTP
/api/v1/otp/verifycurl -X POST https://api.axdox.in/api/v1/otp/verify \
-H "Authorization: Bearer axk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "request_id": "8f3c...", "code": "482913" }'{ "status": "approved", "verified": true }
// status: approved | denied | expired | already_verifiedCodes are single-use, expire (5 min default), and lock after a few wrong tries — configurable in Settings.
Check status
/api/v1/otp/status?request_id=…curl "https://api.axdox.in/api/v1/otp/status?request_id=8f3c..." \
-H "Authorization: Bearer axk_live_YOUR_KEY"{ "request_id": "8f3c...", "status": "verified", "channel": "whatsapp",
"attempts": 1, "expires_at": "...", "verified_at": "..." }Channels & fallback
AXDOX tries channels in order and stops at the first that delivers:
Change the order per project in Settings, or force one with the channel field. Add email_fallback to also cover email for phone recipients.
SDKs (optional)
You can call the REST API directly in any language (see samples above). For convenience we also ship thin SDKs for these languages:
// app/api/send-code/route.ts
import Axdox from "@axdox/verify";
const axdox = new Axdox({ apiKey: process.env.AXDOX_KEY });
export async function POST(req: Request) {
const { phone } = await req.json();
const { request_id } = await axdox.send({ to: phone });
return Response.json({ request_id });
}Errors
Errors return a stable code to branch on, plus a request_id for support.
{ "error": { "code": "rate_limited", "message": "..." }, "request_id": "..." }| Code | HTTP | Meaning |
|---|---|---|
| unauthorized | 401 | Missing or invalid API key. |
| invalid_recipient | 400 | Phone/email is malformed. |
| rate_limited | 429 | Too many requests for this recipient/IP. |
| expired | 410 | The code expired. |
| max_attempts_exceeded | 429 | Too many wrong guesses. |
| all_channels_failed | 502 | No channel could deliver. |
Rate limits
To stop abuse and SMS-pumping fraud, sends are limited per recipient and per IP, with a resend cooldown. Hitting a limit returns 429 rate_limited — wait and retry. Limits are configurable per project.
Webhooks
Get notified on your own server the moment a verification finishes — no polling. Register an endpoint in the dashboard under Webhooks.
POST with a JSON body. Every request is signed so you can confirm it came from AXDOX.| Event | Fires when |
|---|---|
| verification.completed | The user entered the correct code. |
| verification.failed | Expired, or too many wrong attempts. |
{
"type": "verification.completed",
"request_id": "8f3c...",
"channel": "whatsapp",
"to": "+14*****2671",
"verified_at": "2026-01-01T10:04:12Z"
}Verify the signature
Each request includes an X-AXDOX-Signature header — an HMAC-SHA256 of the raw body using your webhook secret. Always check it before trusting the event.
import crypto from "crypto";
app.post("/webhooks/axdox", express.raw({ type: "*/*" }), (req, res) => {
const signature = req.headers["x-axdox-signature"];
const expected = crypto
.createHmac("sha256", process.env.AXDOX_WEBHOOK_SECRET)
.update(req.body) // the raw request body
.digest("hex");
if (signature !== expected) return res.status(401).end();
const event = JSON.parse(req.body);
// event.type === "verification.completed" | "verification.failed"
res.json({ received: true });
});FAQ
Which URL do I call — mine or AXDOX's?
https://api.axdox.in). Your own website's address is irrelevant — your server just makes HTTP calls to AXDOX.Where does my API key go?
Do my users see "AXDOX"?
Do I generate or store the code myself?
/verify.Which channel gets used?
/send response tells you which was used.Test vs live keys?
axk_test_ while building, then switch to axk_live_ in production. Same code, different key.Going live
axk_test_ key with an axk_live_ key.