Skip to main content
DialNexa sends webhook events to your endpoint using HTTP POST.

Quick start

  1. Register a webhook URL in the dashboard.
  2. Save your webhook secret securely.
  3. Verify every incoming signature before processing events.
  4. Parse event_type and handle the event-specific payload.

Request format

Every webhook request has this high-level structure:
{
  "event_type": "call_ended",
  "payload": {
    "call": {
      "id": "call_abc123xyz"
    }
  }
}
  • event_type: The event name used for routing.
  • payload: Event-specific data.

Supported event types

call_initiated

Sent when a call starts.
{
  "event_type": "call_initiated",
  "payload": {
    "call": {
      "id": "call_abc123xyz",
      "direction":"outbound",
      "type": "phone_call",
      "category": "batch",
      "category_id": "batch_001",
      "from_number": "12137771234",
      "to_number": "12137771235",
      "status": "initiated",
      "agent_id": "agent_987654321",
      "initiated_at": "2025-05-02T10:21:15.945Z"
    }
  }
}

call_ended with status: "hangup"

Sent when a call ends without completing. Common disconnection_reason values:
  • user_did_not_pick_up: The recipient did not answer.
  • user_busy: The line was busy.
  • invalid_phone_number: The dialed number is not valid.
  • network_failure: A network-level error occurred.
  • unknown: The reason could not be determined.
{
  "event_type": "call_ended",
  "payload": {
    "call": {
      "id": "call_abc123xyz",
      "direction": "outbound",
      "type": "phone_call",
      "status": "hangup",
      "category": "batch",
      "category_id": "batch_001",
      "from_number": "12137771234",
      "to_number": "12137771235",
      "agent_id": "agent_987654321",
      "initiated_at": "2025-05-02T10:21:15.945Z",
      "disconnection_reason": "user_did_not_pick_up"
    }
  }
}

call_ended with status: "completed"

Sent when a call completes. This payload can include transcript, summary, recording URL, and post-call analysis fields.
recording_url values are pre-signed links and expire after 7 days.
{
  "event_type": "call_ended",
  "payload": {
    "call": {
      "id": "call_abc123xyz",
      "type": "phone_call",
      "direction": "outbound",
      "status": "completed",
      "category": "batch",
      "category_id": "batch_001",
      "from_number": "12137771234",
      "to_number": "12137771235",
      "agent_id": "agent_987654321",
      "hangup_at": "2025-05-02T10:21:15.945Z",
      "duration_in_seconds": 142,
      "hangup_reason": "user_hangup",
      "transcript": "Agent: Hello, thanks for calling DialNexa support...\nUser: Hi, I need help with my account...",
      "summary": "Customer called to inquire about account billing. Agent resolved the issue and confirmed the next payment date.",
      "recording_url": "https://storage.dialnexa.com/recordings/call_abc123xyz.wav?se=2025-05-09T10%3A21%3A15Z&sig=...",
      "post_call_analysis": {
        "sentiment": "positive",
        "intent": "billing_inquiry",
        "resolution": "resolved"
      }
    }
  }
}

transfer_completed

For conversational agents using a Transfer node, this event is emitted when handoff is processed.
{
  "event_type": "transfer_completed",
  "payload": {
    "event": "transfer_completed",
    "call": {
      "id": "call_k7m2nq9xw4p8r1v3",
      "from_number": "13105551234",
      "to_number": "13105559876",
      "agent_id": "agent_8f3k2m9x7p1w4q6",
      "direction": "inbound",
      "category": "batch",
      "category_id": "batch_qyurbcy76471ud"
    },
    "call_transfer": {
      "id": "transfer_trk8xq2m4n9p1w3",
      "transferred_at": "2025-05-02T10:19:40.000Z",
      "destination": "human",
      "status": "connected",
      "destination_id": "13105554455",
      "destination_call_id": null
    }
  }
}

call_ended (transferred calls)

Completed transferred calls can include an additional call_transfer object for transfer metadata.
{
  "event_type": "call_ended",
  "payload": {
    "call": {
      "id": "call_k7m2nq9xw4p8r1v3",
      "type": "phone_call",
      "direction": "outbound",
      "status": "completed",
      "category": "batch",
      "category_id": "batch_qyurbcy76471ud",
      "from_number": "13105551234",
      "to_number": "13105559876",
      "agent_id": "agent_8f3k2m9x7p1w4q6",
      "hangup_at": "2025-05-02T10:21:15.945Z",
      "duration_in_seconds": 142,
      "hangup_reason": "user_hangup",
      "transcript": {
        "Agent": "Hello, thanks for calling DialNexa support. How can I help you today?",
        "User": "Hi, I need to speak with someone about my last invoice."
      },
      "summary": "Customer requested a billing specialist. Call was transferred to the support queue; issue addressed after transfer.",
      "recording_url": "https://storage.dialnexa.com/recordings/call_k7m2nq9xw4p8r1v3.wav?se=2025-05-09T10%3A21%3A15Z&sig=a7f3c2e8910b4d6e8f0a2c4d6e8f0a2b4c6d8e0f2a4b6c8d0e2f4a6b8c0d2e4f6",
      "post_call_analysis": {
        "sentiment": "neutral",
        "intent": "billing_escalation",
        "resolution": "transferred"
      }
    },
    "call_transfer": {
      "id": "transfer_trk8xq2m4n9p1w3",
      "transferred_at": "2025-05-02T10:19:40.000Z",
      "destination": "human",
      "status": "connected",
      "destination_id": "13105554455",
      "duration_in_seconds": 62,
      "destination_call_id": null
    }
  }
}

Signature verification

Every webhook POST includes an x-dialnexa-signature header. DialNexa computes this value as an HMAC-SHA256 hex digest over the entire request body (the raw JSON bytes sent in the POST) using your webhook secret as the key.
Sign and verify the full request body exactly as received. Do not hash only the inner payload object, and do not re-serialize a parsed JSON object unless you can guarantee byte-for-byte parity with what DialNexa sent.

Verification checklist

  1. Read the x-dialnexa-signature header.
  2. Read the raw request body as bytes (before JSON parsing).
  3. Compute HMAC_SHA256(raw_request_body, WEBHOOK_SECRET).
  4. Compare expected vs received signatures using a constant-time comparison.
  5. Reject on mismatch, then parse JSON and handle event_type and payload.

Node.js example

import crypto from "crypto";
import express, { Request, Response } from "express";

const app = express();

/**
 * Verifies a DialNexa webhook signature against the raw request body.
 * Uses a constant-time compare to avoid timing attacks.
 */
export function verifyDialNexaSignature(
  rawBody: string | Buffer,
  receivedSignature: string,
  webhookSecret: string
) {
  const bodyBuffer =
    typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : rawBody;

  const expectedSignature = crypto
    .createHmac("sha256", webhookSecret)
    .update(bodyBuffer)
    .digest("hex");

  const expectedBuffer = Buffer.from(expectedSignature, "utf8");
  const receivedBuffer = Buffer.from(receivedSignature || "", "utf8");

  if (expectedBuffer.length !== receivedBuffer.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuffer, receivedBuffer);
}

app.post(
  "/webhook",
  express.raw({ type: "application/json" }),
  (req: Request, res: Response) => {
    const receivedSignature = req.headers["x-dialnexa-signature"] as string;
    const rawBody = req.body as Buffer;

    if (!verifyDialNexaSignature(rawBody, receivedSignature, process.env.DIALNEXA_WEBHOOK_SECRET!)) {
      return res.status(401).send("Invalid signature");
    }

    const body = JSON.parse(rawBody.toString("utf8"));
    // Handle body.event_type and body.payload
    return res.sendStatus(204);
  }
);

Python example

import hashlib
import hmac
import json
import os

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()


def verify_dialnexa_signature(
    raw_body: bytes, received_signature: str, webhook_secret: str
) -> bool:
    """Verify DialNexa webhook signature for the full request body."""
    expected_signature = hmac.new(
        webhook_secret.encode(),
        raw_body,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected_signature, received_signature or "")


@app.post("/webhook")
async def handle_webhook(request: Request):
    raw_body = await request.body()
    received_signature = request.headers.get("x-dialnexa-signature")

    if not verify_dialnexa_signature(
        raw_body,
        received_signature or "",
        os.environ["DIALNEXA_WEBHOOK_SECRET"],
    ):
        return JSONResponse(status_code=401, content={"message": "Unauthorized"})

    body = json.loads(raw_body.decode("utf-8"))
    # Handle body["event_type"] and body["payload"]
    return JSONResponse(status_code=204)

Secret rotation

Rotating your webhook secret updates all webhooks in your organization immediately. The old secret is deactivated and the new secret takes effect right away.
  • Update your webhook server configuration to use the new secret before or immediately after rotation.
  • Keep deployment steps ready to avoid signature mismatches during rollout.
  • Validate signature checks in staging before rotating in production.

Register Webhook

All webhook configuration is managed through the DialNexa dashboard, no API calls required.