AXDOX
Docs
API v1Base URL: https://api.axdox.in

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.

Fast to add
Two endpoints: send & verify.
Any language
REST API + SDKs. 9 code samples.
Secure by default
Hashing, expiry, rate limits, fraud guards.

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.

Your users never see "AXDOX" — they just receive a code. You only need your API key and these two endpoints.

How it works

Your app's server talks to AXDOX. Your users only receive the code — they never talk to AXDOX directly.

User enters phone/email on your site
Your server → POST /otp/send
AXDOX sends the code → WhatsApp, else SMS, else Email
User types code → your server → POST /otp/verify
AXDOX replies ✓ verified — you log the user in
You call our URL. Requests go to https://api.axdox.in — not your own site.
Your key identifies you. Keep it on your server, never in the browser.

Quick start

1
Create a project
Sign in to the dashboard and create a project.
2
Get your API key
API Keys → Create key, copy the secret (shown once). Use a test key while building.
3
Call two endpoints from your backend
Send a code, then check it. That's the whole integration.

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).

header
Authorization: Bearer axk_live_YOUR_KEY
Keep API keys on your server only — never in frontend/browser code, or anyone could read and use them.

Send an OTP

POST/api/v1/otp/send
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" }'
Parameters
FieldTypeDescription
tostring, requiredPhone in E.164 (+14155552671) or an email address.
email_fallbackstringEmail used as final fallback when to is a phone.
channelstringForce one: whatsapp, sms, email (default: auto).
metadataobjectOptional key/values stored with the request.
Response
200 · application/json
{
  "request_id": "8f3c1e2a-...",
  "status": "pending",
  "channel": "whatsapp",
  "to": "+14*****2671",
  "expires_at": "2026-01-01T10:05:00Z"
}

Verify an OTP

POST/api/v1/otp/verify
curl -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" }'
Response
200 · application/json
{ "status": "approved", "verified": true }
// status: approved | denied | expired | already_verified

Codes are single-use, expire (5 min default), and lock after a few wrong tries — configurable in Settings.

Check status

GET/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"
Response
200 · application/json
{ "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:

1 · WhatsApp 2 · SMS 3 · Email

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 });
}
Don't see your language here? Just use the REST examples — an SDK is only a small wrapper around the same two HTTP calls.

Errors

Errors return a stable code to branch on, plus a request_id for support.

error shape
{ "error": { "code": "rate_limited", "message": "..." }, "request_id": "..." }
CodeHTTPMeaning
unauthorized401Missing or invalid API key.
invalid_recipient400Phone/email is malformed.
rate_limited429Too many requests for this recipient/IP.
expired410The code expired.
max_attempts_exceeded429Too many wrong guesses.
all_channels_failed502No 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.

Events are delivered as an HTTPS POST with a JSON body. Every request is signed so you can confirm it came from AXDOX.
Events
EventFires when
verification.completedThe user entered the correct code.
verification.failedExpired, or too many wrong attempts.
Example payload
POST to your endpoint
{
  "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?
Always AXDOX's URL (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?
On your server only (an environment variable). Never in frontend/browser code.
Do my users see "AXDOX"?
No. They just receive a code on WhatsApp/SMS/email. AXDOX is invisible to them.
Do I generate or store the code myself?
No. AXDOX generates it, stores it securely, and checks it. You only pass the code the user typed to /verify.
Which channel gets used?
Automatic: WhatsApp first, then SMS, then Email. The /send response tells you which was used.
Test vs live keys?
Use axk_test_ while building, then switch to axk_live_ in production. Same code, different key.

Going live

1
Swap to a live key
Replace your axk_test_ key with an axk_live_ key.
2
Lock down countries
Restrict allowed countries in Settings to the markets you serve.
3
Protect your keys
Keep them server-side; rotate immediately if exposed.
4
Watch the numbers
Monitor delivery & conversion in Analytics.
Ready to integrate?
Grab an API key and send your first code.
Get API keys