NaCaller API Reference
Get every finished call into your own system
When a call ends, NaCaller can send you what it left behind: the outcome, the details your campaign asked the agent to find out, the recording link and the transcript. Two ways, and you can use both at once:
- A webhook: we POST JSON to your server. Best for a CRM, a database or an automation tool.
- A Google Sheet: every call becomes a row. No code at all.
Building with an AI coding tool?
Paste this prompt into Cursor, Lovable, Bolt, Replit, v0 or Claude Code
Add an endpoint to this app that receives finished-call results from NaCaller.
Full reference: https://nacaller.com/docs/call-results — plain-text summary: https://nacaller.com/llms-full.txt
WHAT NACALLER DOES
- When a call ends, NaCaller waits about 45 seconds (so the transcript and outcome are ready) and then POSTs JSON to my HTTPS URL.
- Body: { event: "call.completed", call: { id, direction, status, outcome, outcomeReason, startedAt, completedAt, durationSeconds, to, from, recordingUrl, transcript, captured, tags }, contact: { id, name, phone } | null, campaign: { id, name } | null, agent: { id, name } | null }.
- "captured" is an object of the fields my campaign asked the agent to find out (for example { course: "B.Tech CSE" }); it can be empty.
- Header X-NaCaller-Signature: "sha256=" + hex HMAC-SHA256 of the RAW request body, keyed with the signing secret from NaCaller → Settings. Store that secret in NACALLER_CALL_SECRET.
BUILD
1. A POST route at a public HTTPS URL that reads the RAW body (not parsed JSON).
2. Verify the signature with a constant-time compare. Reject with 401 if it does not match.
3. De-duplicate on call.id (a retry after a failure can deliver the same call twice).
4. Save what I need (outcome, captured, recordingUrl, transcript), then reply with any 2xx within 8 seconds. Anything else is retried 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours later, then given up.
RULES
- The URL must be https and a public host. Redirects are not followed. Localhost does not work: use a tunnel while developing.
- Never log the signing secret. Do not invent other NaCaller endpoints or fields.
Write the code, then list the env vars and the URL I must paste into NaCaller → Settings → "Send call results".The webhook
1. Switch it on
- Sign in and open Settings, then the card Send call results to your CRM or sheet.
- Paste your HTTPS URL and save. A signing secret appears: store it as
NACALLER_CALL_SECRETon your server. - Press Send a sample call to check it reaches you.
The URL must be https and a public host. Redirects are not followed, and localhost does not work, so use a tunnel while you build.
2. What we send
A POST with a JSON body, about 45 seconds after the call ends so the transcript and outcome are ready. Calls that were never answered are sent sooner and carry a status such as No Answer.
{
"event": "call.completed",
"call": {
"id": "clx9k2m0a0001",
"direction": "outbound",
"status": "Completed",
"outcome": "interested",
"outcomeReason": "Asked to book a counselling slot on Saturday.",
"startedAt": "2026-10-03T07:12:04.000Z",
"completedAt": "2026-10-03T07:14:31.000Z",
"durationSeconds": 147,
"to": "919876543210",
"from": null,
"recordingUrl": "https://…",
"transcript": "Agent: Namaskaram! …\nCustomer: …",
"captured": { "course": "B.Tech CSE", "hostel": "Needed, Guntur" },
"tags": ["wants site visit"]
},
"contact": { "id": "cld8…", "name": "Ravi", "phone": "9876543210" },
"campaign": { "id": "clc7…", "name": "October admissions" },
"agent": { "id": "cla6…", "name": "Priya" }
}| Field | Meaning |
|---|---|
event | Always call.completed. |
call.id | A stable id. Use it to de-duplicate. |
call.direction | Whether we called out (outbound) or answered a call (inbound). |
call.status | How the call ended, for example Completed, Busy or No Answer. |
call.outcome | Our reading of the call, for example interested, callback, not_interested or do_not_call. null when it could not be judged. |
call.outcomeReason | One sentence on why. |
call.durationSeconds | Connected time in seconds. 0 if never answered. |
call.to / call.from | The numbers involved, digits only. from is set on inbound calls. |
call.recordingUrl | A link to the recording, or null. |
call.transcript | The whole conversation as text, or null. |
call.captured | An object of the details your campaign asked the agent to find out, such as { "course": "B.Tech CSE" }. Empty if none were set. Choose them when you create a campaign. |
call.tags | Short labels the analysis added. |
contact / campaign / agent | Who was called, which campaign it belongs to and which agent spoke. Each is null when it does not apply. |
3. Check that it is really us
Every request carries X-NaCaller-Signature: sha256=<hex>, the HMAC-SHA256 of the raw request body keyed with your signing secret. Compare in constant time against the raw bytes, not re-serialised JSON. This is a different header from the verification callback, so do not reuse that check.
// Express. Verify against the RAW body, not re-serialised JSON.
import crypto from "node:crypto";
function verify(raw, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(raw).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(header || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
app.post("/nacaller/calls", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body, req.get("X-NaCaller-Signature"), process.env.NACALLER_CALL_SECRET)) return res.sendStatus(401);
const evt = JSON.parse(req.body); // evt.event === "call.completed"
// A retry can deliver the same call twice: de-duplicate on evt.call.id.
saveCall(evt); // your code
res.sendStatus(200); // any 2xx means delivered
});# Flask. Verify against the RAW body, not re-serialised JSON.
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
def verify(raw: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
@app.post("/nacaller/calls")
def calls():
if not verify(request.get_data(), request.headers.get("X-NaCaller-Signature"), os.environ["NACALLER_CALL_SECRET"]):
abort(401)
evt = request.get_json() # evt["event"] == "call.completed"
# A retry can deliver the same call twice: de-duplicate on evt["call"]["id"].
save_call(evt) # your code
return "", 2004. Reply, and what happens if you cannot
- Answer with any
2xxwithin 8 seconds. That counts as delivered. - Anything else (an error code, a timeout, a network failure) is retried 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours later, then given up. That is 6 attempts in all.
- Because a retry can follow a response that was slow rather than failed, the same call can arrive twice. De-duplicate on
call.id. - Settings shows how many deliveries are waiting or have been given up, with the latest error.
Google Sheets
Every finished call is added as a new row in a sheet you own. There is no Google sign-in, and we can only touch a sheet you have deliberately shared with us.
- Open Settings, then the card Google Sheets.
- Share your sheet with the address shown there, as an Editor.
- Paste the sheet link and press Connect. Use Add a sample row to check.
If the Google Sheets card is not in your Settings, it is not switched on for your account yet. Ask us and we will enable it.
We add a header row if the sheet is empty, then one row per call with these columns:
Date (IST) Contact Phone Direction Status Outcome Why Duration (sec) Campaign Agent Captured Tags Recording TranscriptCaptured holds all your campaign’s fields in one cell, for example course: B.Tech CSE · hostel: Needed, because every campaign asks different questions and a sheet’s columns are fixed once written. Long transcripts are cut to fit a cell. A sheet that cannot be reached is retried on the same schedule as the webhook.
Related
- Verification callbacks and the Instant Leads hook
- Build with an AI coding tool: prompts and ground rules
Stuck on the NaCaller API? Ask us.
A developer reads every message and replies by email, usually within one business day.