NaCaller API Reference
NaCaller Miss OTP API
Your user gives a missed call to a number you show them, from the phone they are claiming. The network hands us their caller ID; we match it to your pending request and flip it to verified. They can cut the call as soon as it rings — we hang up anyway, so it stays a free missed call.
- Your server calls
POST /startwith the number. - You show the user
callNumberand theinstructiontext. - You poll
GET /status/:id— or receive our webhook — until it readsverified.
POST /start
POST https://api.nacaller.com/api/verify/start| Body field | Notes |
|---|---|
phone | Required. We keep the last 10 digits, so +91 98765 43210 and 9876543210 both work. Must be an Indian mobile (starts 6–9); anything else is a 400 straight away rather than a wait that could never succeed. |
reference | Optional. Your own id for this attempt (a user id, say), up to 120 characters. Echoed back in status and the webhook. |
Response 200:
{
"requestId": "cm0x7f3k20001",
"phone": "9876543210",
"callNumber": "<the number your user should dial>",
"status": "pending",
"expiresAt": "2026-09-11T06:17:04.000Z",
"expiresIn": 300,
"instruction": "Give a missed call to <callNumber> from 9876543210. You can cut the call as soon as it rings."
}The request waits 5 minutes for the call (expiresIn is in seconds). After that it reads expired and the user has to start again.
| Status | When |
|---|---|
400 | Not a valid 10-digit Indian mobile number. |
401 | Missing or invalid key. |
402 | No verification credits left. |
429 | Over 120 starts a minute on this key, or 5 starts for this number in 10 minutes. |
503 | No verification line is configured for your site yet — ask us to assign one. |
What to show your user
Use the instruction string as it comes back, or write your own around callNumber:
Give a missed call to <callNumber> from <phone>.
You can cut the call as soon as it rings.- Show the number large, and make it a
tel:link so a tap dials it on a phone. - Show a countdown to
expiresAt, and a “Try again” button once it passes. - Offer a fallback for users who can’t give a missed call — Voice OTP rings them with a code instead.
GET /status/:id
GET https://api.nacaller.com/api/verify/status/<requestId>{
"requestId": "cm0x7f3k20001",
"phone": "9876543210",
"reference": "user_123",
"status": "verified",
"verified": true,
"verifiedAt": "2026-09-11T06:12:09.412Z",
"expiresAt": "2026-09-11T06:17:04.000Z"
}status is pending, verified or expired; verified is the same answer as a boolean. A request can only be read with the key of the site that started it — any other id returns 404. Status reads are limited to 600 a minute per key.
Polling or webhook
Polling is the simplest thing that works: while the user is on the screen, call status every 2–3 seconds and stop on verified or expired. The webhook saves you the loop: set an HTTPS callback URL for your site in the dashboard and we POST the moment the number verifies, signed so you can prove it came from us. We send it once and do not retry, so a screen the user is watching should still poll. Details: Webhooks.
Code
# 1 — start
curl -X POST https://api.nacaller.com/api/verify/start \
-H "Authorization: Bearer $NACALLER_KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"9876543210","reference":"user_123"}'
# 2 — check (repeat every 2–3 s until status is verified or expired)
curl https://api.nacaller.com/api/verify/status/REQUEST_ID \
-H "Authorization: Bearer $NACALLER_KEY"// Server-side only — NACALLER_KEY must never reach the browser.
const BASE = "https://api.nacaller.com/api/verify";
const auth = { Authorization: `Bearer ${process.env.NACALLER_KEY}` };
export async function startMissedCall(phone, userId) {
const res = await fetch(`${BASE}/start`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ phone, reference: userId }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error); // 400, 401, 402, 429, 503
return data; // { requestId, callNumber, instruction, expiresAt, ... }
}
export async function missedCallStatus(requestId) {
const res = await fetch(`${BASE}/status/${encodeURIComponent(requestId)}`, { headers: auth });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
return data; // { status: "pending" | "verified" | "expired", verified, ... }
}# 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_missed_call(phone: str, user_id: str) -> dict:
r = requests.post(f"{BASE}/start", headers=AUTH,
json={"phone": phone, "reference": user_id}, timeout=10)
data = r.json()
if not r.ok:
raise RuntimeError(data["error"]) # 400, 401, 402, 429, 503
return data # requestId, callNumber, instruction, expiresAt, ...
def missed_call_status(request_id: str) -> dict:
r = requests.get(f"{BASE}/status/{request_id}", headers=AUTH, timeout=10)
data = r.json()
if not r.ok:
raise RuntimeError(data["error"])
return data # status: pending | verified | expiredThings worth knowing
- Billing. A credit is spent only when a number verifies. A request nobody calls costs nothing.
- The right handset. The match is on caller ID. A call from any other phone verifies nothing.
- One call, one verification. A call can verify one pending request, once; it cannot be replayed.
- Your user’s own verification. Save the
requestIdagainst the signed-in user on your server, and mark the phone verified there — never trust a browser telling you it verified.
Next: Voice OTP for the fallback, or see what it costs.
Stuck on the NaCaller API? Ask us.
A developer reads every message and replies by email, usually within one business day.