NaCaller API Reference New
NaCaller Voice OTP API
We call the number and read a 6-digit code aloud, twice. Your user types it into your app and you check it. It is the second method on the same API as Miss OTP— same key, same base URL, same webhook — for the users who can’t or won’t give a missed call.
- Your server calls
POST /startwithmethod: "voice". - The user answers, hears the code, and types it in.
- Your server calls
POST /checkwith the id and the code.
The recommended pattern
Missed call first — it is cheaper and there is nothing to type. Then, a few seconds in, a secondary button: “Can’t give a missed call? Get a call instead”. That button starts a voice verification and swaps the screen for a code input.
POST /start (method: voice)
POST https://api.nacaller.com/api/verify/start| Body field | Notes |
|---|---|
phone | Required. A 10-digit Indian mobile, as for Miss OTP. |
method | "voice". Leave it out (or send "missed_call") for Miss OTP. |
language | Optional. "en" (English), "hi" (Hindi) or "te" (Telugu) — the language the code is read in. |
reference | Optional. Your own id for this attempt, echoed back. |
Response 200 — we are ringing the number now:
{
"id": "cm0x9a1b40007",
"method": "voice",
"status": "calling",
"expiresAt": "2026-09-11T06:22:04.000Z"
}The verification is valid for 10 minutes from /start. While the phone is ringing, /status reports pending; the code only exists once the user picks up, so a /check before then returns 400 “not answered yet” without using an attempt. Show the user something like:
We're calling <phone> now.
Enter the 6-digit code you hear.Give the input inputmode="numeric" and autocomplete="one-time-code".
POST /check
POST https://api.nacaller.com/api/verify/check
{ "id": "<id from /start>", "code": "123456" }| Status | Body |
|---|---|
200 | { "verified": true, "id": "cm0x9a1b40007", "phone": "9876543210", "reference": "user_123" } |
400 | Wrong code: { "verified": false, "error": "…", "attemptsLeft": 3 }. 5 attempts per verification. |
410 | The code has expired. Offer to call again. |
Checking is idempotent once verified: sending the right code again returns the same 200, so a double-submitted form does no harm.
GET /status/:id
The same endpoint as Miss OTP, and the response now includes method ("voice" or "missed_call"). Voice verifications also fire your webhook, with method in the payload.
Limits and errors
| Status | When |
|---|---|
400 | Invalid number, unknown method or language, a code that isn’t 6 digits, a wrong code on /check, or a /check before the call was answered. |
401 | Missing or invalid key. |
402 | Out of voice credits. |
403 | This number can’t be verified. |
404 | Unknown verification id (ids are scoped to your key). |
410 | Code expired. |
429 | More than 3 voice calls to one number in an hour, or 10 in a day. |
502 | The call could not be placed. Start again. |
503 | Voice OTP is briefly unavailable. Fall back to a missed call. |
Billing
One credit per answeredcall. If the user doesn’t pick up, it costs you nothing. You get 5 free calls per account to start; after that, voice credits come in prepaid packs — see Voice OTP pricing.
Compliance
- Only place a call when the user has asked for one — the fallback button is the request.
- The call says the code and nothing else — there is no message of yours to add.
- Not for regulated lenders, banks or insurers yet.If you are regulated by the RBI, SEBI or IRDAI, TRAI rules require your OTP calls to come from a 1600-series number, so Voice OTP isn’t for you yet.
Code
# 1 — start: we ring the number and read the code
curl -X POST https://api.nacaller.com/api/verify/start \
-H "Authorization: Bearer $NACALLER_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"9876543210","method":"voice","language":"en","reference":"user_123"}'
# 2 — check the code the user typed
curl -X POST https://api.nacaller.com/api/verify/check \
-H "Authorization: Bearer $NACALLER_KEY" \
-H "Content-Type: application/json" \
-d '{"id":"VERIFICATION_ID","code":"123456"}'// Server-side only — NACALLER_KEY must never reach the browser.
const BASE = "https://api.nacaller.com/api/verify";
const headers = {
Authorization: `Bearer ${process.env.NACALLER_KEY}`,
"Content-Type": "application/json",
};
export async function startVoiceCall(phone, userId, language = "en") {
const res = await fetch(`${BASE}/start`, {
method: "POST", headers,
body: JSON.stringify({ phone, method: "voice", language, reference: userId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error); // 400, 401, 402, 429
return data; // { id, method: "voice", status: "calling", expiresAt }
}
export async function checkCode(id, code) {
const res = await fetch(`${BASE}/check`, {
method: "POST", headers, body: JSON.stringify({ id, code }),
});
const data = await res.json();
if (res.ok) return data; // { verified: true, id, phone, reference }
if (res.status === 410) return { verified: false, expired: true, error: data.error };
return data; // 400 → { verified: false, error, attemptsLeft }
}# Server-side only — NACALLER_KEY must never reach the browser.
import os, requests
BASE = "https://api.nacaller.com/api/verify"
AUTH = {"Authorization": f"Bearer {os.environ['NACALLER_KEY']}"}
def start_voice_call(phone: str, user_id: str, language: str = "en") -> dict:
r = requests.post(f"{BASE}/start", headers=AUTH, timeout=10, json={
"phone": phone, "method": "voice", "language": language, "reference": user_id,
})
data = r.json()
if not r.ok:
raise RuntimeError(data["error"]) # 400, 401, 402, 429
return data # id, method="voice", status="calling", expiresAt
def check_code(verification_id: str, code: str) -> dict:
r = requests.post(f"{BASE}/check", headers=AUTH, timeout=10,
json={"id": verification_id, "code": code})
data = r.json()
if r.status_code == 410:
return {"verified": False, "expired": True, "error": data.get("error")}
return data # 200 → verified True; 400 → verified False, error, attemptsLeftStuck on the NaCaller API? Ask us.
A developer reads every message and replies by email, usually within one business day.