Skip to content
nitrodialerDocs
Browse the docs

Docs / Developers

Webhooks

Receive signed call.completed, lead.created and other events, and verify signatures in Node.js or Python.

Webhooks tell your systems when something happens in NitroDialer — a call finished, a lead arrived, someone asked not to be called — without polling the API.

Set up an endpoint

  1. In Settings → Webhooks, select Add endpoint.
  2. Enter an https:// URL that's reachable from the internet, and choose the events to receive.
  3. Copy the signing secret (whsec_…). It's shown once.
  4. Select Send test to send a signed ping event and see what your endpoint answered.

Events

EventSent when
call.completedAn agent saved an outcome for a call. Includes the outcome, talk time, notes, and the Dialpad AI summary and recording link when available.
lead.createdA new lead entered a campaign through the API or the speed-to-lead webhook.
lead.dncA queued lead was stopped because its number was added to the internal do-not-call list.
campaign.completedEvery lead in a campaign is done, exhausted or on the do-not-call list.

Request format

NitroDialer sends a POST with a JSON body:

JSON
{
  "id": "evt_1042",
  "event": "call.completed",
  "created_at": "2026-10-27T14:14:40.000Z",
  "data": {
    "id": "c41d9a6e-7b2f-4e8a-9f10-3d2c1b0a9f88",
    "campaign": { "id": "8f0b5c2e-6c1a-4f43-9d7e-2b8c1f0a9e11", "name": "Heat pump rebate — web leads" },
    "lead": { "id": "2b7e4f55-0d3c-4a11-8a2f-9c6d5e4b3a21", "first_name": "Daniel", "last_name": "Okafor", "company": null, "crm_object": "contacts", "crm_record_id": "51234" },
    "agent": { "id": "0e6f7a8b-1c2d-4e3f-8a9b-0c1d2e3f4a5b", "name": "Priya Raman" },
    "phone": "+15125550142",
    "started_at": "2026-10-27T14:03:12.000Z",
    "connected_at": "2026-10-27T14:03:30.000Z",
    "ended_at": "2026-10-27T14:14:12.000Z",
    "talk_seconds": 642,
    "disposition": { "key": "appointment", "label": "Appointment set", "category": "contact_positive" },
    "notes": "Gas furnace, 19 years old. Booked Thursday 10am.",
    "ai_summary": "Homeowner wants to replace an aging furnace; in-home assessment booked.",
    "recording_url": "https://dialpad.com/r/abc123"
  }
}

Headers:

HeaderValue
Content-Typeapplication/json
NitroDialer-EventThe event name, e.g. call.completed
NitroDialer-Signaturet=<unix seconds>,v1=<signature>
User-AgentNitroDialer-Webhooks/1.0

The id is unique per delivery. Store it and ignore repeats — retries reuse the same id.

Verify the signature

The signature is a hex HMAC-SHA256 of "<t>.<raw request body>" using your endpoint's signing secret. Always verify it against the raw body, before parsing JSON, and reject timestamps more than 5 minutes old to stop replays.

Node.js

JavaScript
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.NITRODIALER_WEBHOOK_SECRET; // whsec_…

app.post("/webhooks/nitrodialer", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("NitroDialer-Signature") ?? "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.status(400).send("stale");

  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "", "hex");
  const ok = given.length === 32 && crypto.timingSafeEqual(given, Buffer.from(expected, "hex"));
  if (!ok) return res.status(400).send("bad signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // …handle event.event / event.data, de-duplicating on event.id
  res.sendStatus(200);
});

Python

Python
import hashlib, hmac, json, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["NITRODIALER_WEBHOOK_SECRET"].encode()  # whsec_…

@app.post("/webhooks/nitrodialer")
def nitrodialer():
    header = request.headers.get("NitroDialer-Signature", "")
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t = int(parts.get("t", "0"))
    if abs(time.time() - t) > 300:
        abort(400)
    body = request.get_data()  # raw bytes
    expected = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")):
        abort(400)
    event = json.loads(body)
    # …handle event["event"] / event["data"], de-duplicating on event["id"]
    return "", 200

Responding and retries

Answer with any 2xx status within 10 seconds. Do slow work after responding.

Anything else — a non-2xx status, a timeout, a connection error — is retried with exponential backoff, for 6 attempts in total. After the last attempt the delivery is marked failed. Each endpoint's recent deliveries, with the status and response we got, are listed in Settings → Webhooks.

Deliveries can arrive out of order and, rarely, more than once. Use created_at for ordering and id for de-duplication.

Turning an endpoint off

Switch an endpoint off in Settings → Webhooks to pause deliveries without losing its secret. Deleting it stops deliveries for good.