NaCaller API Reference
NaCaller Webhooks
Two directions. Verification callbacks are us telling your server a number just verified. The Instant Leads webhook is you (or Zapier, or your form) sending us a lead for the AI agent to call.
Verification callback
Set a callback URL for each site in the dashboard, on the Verifyscreen (Miss OTP & Voice OTP). It must start with https://. When a number verifies, we POST this to it:
POST <your callback URL>
Content-Type: application/json
X-MissOTP-Signature: 5f2b… (hex HMAC-SHA256 of the raw body)
{
"event": "verification.verified",
"requestId": "cm0x…",
"phone": "9876543210",
"reference": "user_123",
"verifiedAt": "2026-09-11T06:12:09.412Z",
"timestamp": 1789107129412
}| Field | Notes |
|---|---|
event | Always verification.verified. |
requestId | The id /start returned. |
phone | The verified number, 10 digits. |
reference | Your reference from /start, or null. |
verifiedAt | When it verified, ISO 8601, UTC. |
timestamp | When we sent this, in milliseconds since the epoch. It sits inside the signed body, so you can reject replays. |
method | Voice OTP verifications carry "voice" here. |
Checking the signature
The X-MissOTP-Signature header is a hex-encoded HMAC-SHA256 of the raw request body, keyed with your site’s webhook signing secret. Reveal the secret on the same dashboard card and store it as NACALLER_WEBHOOK_SECRET. To verify:
- Read the body as raw bytes, before any JSON parsing. Re-serialising parsed JSON will not match.
- Compute
HMAC-SHA256(secret, rawBody)as lowercase hex. - Compare it with the header in constant time. No match: reply
401and ignore it. - Parse the body and reject a
timestampolder than a few minutes.
// Express. The signature covers the EXACT bytes we sent, so read the raw body —
// JSON.stringify(req.body) will not reproduce it byte for byte.
import express from "express";
import crypto from "node:crypto";
const app = express();
app.post("/webhooks/nacaller", express.raw({ type: "application/json" }), (req, res) => {
const sent = String(req.get("X-MissOTP-Signature") || "");
const expected = crypto
.createHmac("sha256", process.env.NACALLER_WEBHOOK_SECRET)
.update(req.body) // a Buffer: the raw body
.digest("hex");
const ok = sent.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString("utf8"));
if (Date.now() - event.timestamp > 5 * 60 * 1000) return res.status(400).end(); // replay
// event.requestId is verified — mark that user's phone as verified here.
res.sendStatus(204);
});# Flask. Verify against request.get_data() — the raw bytes, not parsed JSON.
import hashlib, hmac, json, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["NACALLER_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/nacaller")
def nacaller_webhook():
raw = request.get_data()
expected = hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
sent = request.headers.get("X-MissOTP-Signature", "")
if not hmac.compare_digest(sent, expected):
abort(401)
event = json.loads(raw)
if time.time() * 1000 - event["timestamp"] > 5 * 60 * 1000:
abort(400) # too old — treat as a replay
# event["requestId"] is verified — mark that user's phone as verified here.
return "", 204Delivery
- We send each callback once. There are no retries, and we don’t act on your response code.
- We wait 8 seconds for a response, then give up. Reply
2xxquickly and do slow work afterwards. - Because a callback can be lost to a network blip, treat it as a shortcut, not the only path: a screen the user is watching should still poll status.
Instant Leads webhook (inbound)
POST a lead here and NaCaller’s AI agent calls them — from a website form, a CRM, or a Facebook or Instagram lead ad through Zapier, Pabbly or Make. Each lead source in your dashboard has its own URL:
POST https://api.nacaller.com/api/hooks/lead/<source token>There is no auth header: the token in the URL is the credential, so keep the URL private. If it leaks, issue a new one from the dashboard, under Instant Leads → Connections. Send JSON or form-encoded fields.
| Field | Notes |
|---|---|
phone | Required. A 10-digit Indian mobile; we keep the last 10 digits. |
name | Optional. Used by the agent on the call. |
email | Optional. |
campaign | Optional attribution. Also accepted as campaign_name. |
ad | Also ad_name. |
adset | Also adset_name. |
form | Also form_name. |
ad_id / form_id | Meta ids, if you have them. |
platform | Where the lead came from, e.g. Website. |
ref | Your id for this lead (also leadgen_id). The same ref again within an hour is treated as a retry, not a new lead. |
Attribution values are trimmed and capped in length. Any other fields are ignored.
curl -X POST https://api.nacaller.com/api/hooks/lead/YOUR_SOURCE_TOKEN \
-H "Content-Type: application/json" \
-d '{
"phone": "9876543210",
"name": "Ravi Kumar",
"email": "ravi@example.com",
"campaign": "Diwali offer",
"platform": "Website",
"ref": "form-8841"
}'Response 200:
{ "ok": true, "leadId": "cm0y…", "called": false, "callAt": "2026-09-11T06:13:40.000Z", "reason": "queued" }| Field | Meaning |
|---|---|
called | true if the call was placed right away. |
callAt | When the call is queued for. By default a source waits 90 seconds, and only calls inside its calling window (9 am to 9 pm IST unless you change it). |
duplicate | true when the ref was seen in the last hour. Nothing new is queued. |
reason | queued, opted-out (this person asked not to be called, so we never will), no-agent (no live agent on your account to take the call), or call-failed. |
| Status | When |
|---|---|
400 | Missing or invalid phone number. |
404 | Unknown or disabled token (the same answer for both). |
429 | More than 30 requests a minute to one URL, or the source’s daily lead cap (500 by default) is reached. |
500 | Our fault. Safe to retry — send a ref so a retry can’t queue a second call. |
More on the product side: Instant Leads and Integrations.
Stuck on the NaCaller API? Ask us.
A developer reads every message and replies by email, usually within one business day.