> ## Documentation Index
> Fetch the complete documentation index at: https://dialnexa.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Webhook events, payload examples, and signature verification for DialNexa integrations.

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:

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

<Note>
  `recording_url` values are pre-signed links and expire after 7 days.
</Note>

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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.

<Warning>
  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.
</Warning>

### 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

```ts theme={null}
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

```python theme={null}
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.

<StepSection number={1} title="Create a webhook secret">
  Navigate to **Dashboard > Keys > Add Key** and select **Webhook Secret**. Give it a descriptive name (for example, `Production Secret`) and save.

  Store the generated secret securely. You will need it to verify incoming webhook signatures on your server.
</StepSection>

<StepSection number={2} title="Register your webhook URL">
  Go to the **Webhooks** tab in your dashboard and add your endpoint URL (for example, `https://your-server.com/webhook`).

  DialNexa sends a verification `POST` to your URL. Your endpoint must return a `2xx` status code to confirm the endpoint is reachable.
</StepSection>

<StepSection number={3} title="Attach webhook to an agent">
  When creating or editing an agent in the **Agent creation** tab, select the webhook you registered in the previous step. Events are only delivered for agents with an attached webhook.
</StepSection>
