> ## 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. Create a webhook secret and store it securely.
2. Register a webhook URL in the dashboard.
3. Return a `2xx` status code to the `verification` request DialNexa sends the moment you save the URL. Until your endpoint does this, the webhook is not saved and no events are delivered.
4. Verify every incoming signature before processing events.
5. 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

### `verification`

Sent when you register a webhook URL, and again each time you change that URL. It is the first request your endpoint will ever receive from DialNexa, and the only event that is not tied to a call.

Your endpoint must respond with a `2xx` status code within the request timeout configured on the webhook. If it responds with anything else, times out, or is unreachable, DialNexa does not save the webhook and no call events are ever delivered to it.

```json theme={null}
{
  "event_type": "verification",
  "payload": {
    "message": "This is a verification request from DialNexa to ensure your webhook URL is reachable.",
    "timestamp": "2025-05-02T10:21:15.945Z"
  }
}
```

| Field               | Description                                                 |
| ------------------- | ----------------------------------------------------------- |
| `payload.message`   | Fixed text identifying the request as a reachability check. |
| `payload.timestamp` | UTC ISO 8601 time the verification request was sent.        |

How this event differs from call events:

* `payload` is flat and contains no `call` object. Handlers that read `payload.call.id` without a guard will throw on this request, which fails verification and blocks the webhook from being saved.
* The `User-Agent` header is `DialNexa-External-Webhook-Verification/1.0`. Every other event uses `DialNexa-External-Webhook/1.0`.
* Signature verification works exactly as described in [Signature verification](#signature-verification). The `x-dialnexa-signature` header is present whenever your organization has a webhook secret, so create the secret before registering the URL if you want to test your signature check with this request.
* Verification requests are not retried and do not appear in delivery logs. Retry settings apply only to call events.

<Tip>
  The simplest endpoint that passes verification is one that returns `204` for any `POST` it can authenticate. Add event handling after that works.
</Tip>

### `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`).

  Before saving, DialNexa sends a single [`verification`](#verification) `POST` to the URL you entered to confirm the endpoint is reachable:

  ```json theme={null}
  {
    "event_type": "verification",
    "payload": {
      "message": "This is a verification request from DialNexa to ensure your webhook URL is reachable.",
      "timestamp": "2025-05-02T10:21:15.945Z"
    }
  }
  ```

  Your endpoint must return a `2xx` status code within the request timeout you configured, which defaults to 10,000 ms. On any other response, timeout, or connection failure, the dashboard shows a `Webhook verification failed` error and the webhook is not created.

  The same check runs again whenever you edit the URL of an existing webhook. Changing only the retry count or timeout does not trigger it.

  To confirm your handler before saving, replay the request yourself with the secret from step 1:

  ```bash theme={null}
  SECRET='your_webhook_secret_key'
  URL='https://your-server.com/webhook'

  TS=$(date -u +%Y-%m-%dT%H:%M:%S.%3NZ)
  BODY="{\"event_type\":\"verification\",\"payload\":{\"message\":\"This is a verification request from DialNexa to ensure your webhook URL is reachable.\",\"timestamp\":\"$TS\"}}"
  SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)

  curl -i -X POST "$URL" \
    -H 'Content-Type: application/json' \
    -H 'User-Agent: DialNexa-External-Webhook-Verification/1.0' \
    -H "x-dialnexa-signature: $SIG" \
    --max-time 10 \
    -d "$BODY"
  ```

  <Warning>
    Send the body exactly as built above. The signature covers the raw bytes, so reformatting the JSON or reordering its keys produces a different digest and your signature check will reject a request that DialNexa considers valid.
  </Warning>

  Common reasons verification fails:

  | Symptom                                                    | Cause                                                                         | Fix                                                                        |
  | ---------------------------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
  | `Webhook verification failed` with no request in your logs | URL not publicly reachable, DNS not resolving, or TLS certificate rejected    | Confirm the endpoint is reachable from the public internet over `https://` |
  | Your server logs a `500`                                   | Handler assumes `payload.call` exists                                         | Branch on `event_type` before reading call fields                          |
  | Your server logs a `401`                                   | Signature check compared against the inner `payload` instead of the full body | Hash the entire raw request body                                           |
  | Timeout error                                              | Handler does work before responding                                           | Return `2xx` first, then process asynchronously                            |
</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>
