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:

What we send
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
}
FieldNotes
eventAlways verification.verified.
requestIdThe id /start returned.
phoneThe verified number, 10 digits.
referenceYour reference from /start, or null.
verifiedAtWhen it verified, ISO 8601, UTC.
timestampWhen we sent this, in milliseconds since the epoch. It sits inside the signed body, so you can reject replays.
methodVoice 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:

  1. Read the body as raw bytes, before any JSON parsing. Re-serialising parsed JSON will not match.
  2. Compute HMAC-SHA256(secret, rawBody) as lowercase hex.
  3. Compare it with the header in constant time. No match: reply 401 and ignore it.
  4. Parse the body and reject a timestamp older than a few minutes.
Node (Express)
// 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);
});
Python (Flask)
# 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 "", 204

Delivery

  • 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 2xx quickly 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:

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

FieldNotes
phoneRequired. A 10-digit Indian mobile; we keep the last 10 digits.
nameOptional. Used by the agent on the call.
emailOptional.
campaignOptional attribution. Also accepted as campaign_name.
adAlso ad_name.
adsetAlso adset_name.
formAlso form_name.
ad_id / form_idMeta ids, if you have them.
platformWhere the lead came from, e.g. Website.
refYour 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
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:

Response
{ "ok": true, "leadId": "cm0y…", "called": false, "callAt": "2026-09-11T06:13:40.000Z", "reason": "queued" }
FieldMeaning
calledtrue if the call was placed right away.
callAtWhen 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).
duplicatetrue when the ref was seen in the last hour. Nothing new is queued.
reasonqueued, 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.
StatusWhen
400Missing or invalid phone number.
404Unknown or disabled token (the same answer for both).
429More than 30 requests a minute to one URL, or the source’s daily lead cap (500 by default) is reached.
500Our 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.

Call meStart free