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.

  1. Your server calls POST /start with the number.
  2. You show the user callNumber and the instruction text.
  3. You poll GET /status/:id — or receive our webhook — until it reads verified.

POST /start

Endpoint
POST https://api.nacaller.com/api/verify/start
Body fieldNotes
phoneRequired. 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.
referenceOptional. Your own id for this attempt (a user id, say), up to 120 characters. Echoed back in status and the webhook.

Response 200:

Response
{
  "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.

StatusWhen
400Not a valid 10-digit Indian mobile number.
401Missing or invalid key.
402No verification credits left.
429Over 120 starts a minute on this key, or 5 starts for this number in 10 minutes.
503No 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:

Suggested copy
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

Endpoint
GET https://api.nacaller.com/api/verify/status/<requestId>
Response
{
  "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

curl
# 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"
Node (fetch, server-side)
// 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, ... }
}
Python (requests)
# 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 | expired

Things 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 requestId against 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.

Call meStart free