Skip to main content
This page covers dashboard webhooks: webhooks registered in the dashboard, attached to an agent, and signed with the x-dialnexa-signature header. Subscriptions created through the API with POST /v1/user-webhooks are a different system with a flat payload and an x-nexa-signature: sha256=<hex> header; see the v1 webhooks overview.
DialNexa sends webhook events to your endpoint using HTTP POST.

Quick start

  1. Create a webhook secret and store it securely. A webhook cannot be registered until the workspace has an active webhook secret.
  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:
  • 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.
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. Registering a webhook requires an active webhook secret, so this request is always signed with it.
  • Verification requests are not retried and do not appear in delivery logs. Retry settings apply only to call events.
The simplest endpoint that passes verification is one that returns 204 for any POST it can authenticate. Add event handling after that works.

call_initiated

Sent when a call starts.

call_ended with status: "hangup"

Sent when a call ends without completing, for the reasons marked hangup in Hangup reasons. A call that ends without connecting for any other reason does not produce this event. The reason is in hangup_reason. It is always present on this event, and is also sent as disconnection_reason, a deprecated alias carrying the identical value. Read hangup_reason; treat disconnection_reason as legacy. See Hangup reasons for every value and what it means.

call_ended with status: "completed"

Sent when a call completes. This payload can include transcript, summary, recording URL, and post-call analysis fields. Unlike call_initiated and the hangup variant, its payload also carries "event": "call_ended" next to call; transfer_completed carries "event": "transfer_completed" the same way. transcript is always a string. When the transcript is a list of messages, the string is that list encoded as JSON with system messages removed, so parse it with JSON.parse before reading turns. It is an empty string when no transcript was recorded. On this event hangup_reason tells you who ended the conversation, rather than why the call failed. See Hangup reasons. Note it is not accompanied by a disconnection_reason alias; that field appears only on the hangup variant above.
recording_url values are pre-signed links and expire after 7 days.

transfer_completed

Sent when the call engine reports a phone transfer attempt. call_transfer.status is successful or failed. Warm transfers handled through a transfer session do not send this event.

call_ended (transferred calls)

Completed transferred calls can include an additional call_transfer object for transfer metadata.

Call category

payload.call.category tells you how the call was started, and category_id carries the related ID. The batch value differs by event, so match both spellings: Automatic retry attempts of a batch call are sent as single_outbound with category_id null. To relate a retry to its batch, use the original call you recorded (payload.call.id) and that call’s batch. agent_id is the agent ID with the agent_ prefix. On call_initiated and the hangup variant it can be the bare string agent_ when the telephony data for the call carries no agent ID. On transfer_completed it is an empty string in that case.

Hangup reasons

Every call_ended event carries hangup_reason. On the completed variant it is the reason stored on the call and can be null when none was recorded. What it describes depends on the variant:
  • status: "completed": the call connected and a conversation happened, so the reason says who hung up first.
  • status: "hangup": the call never reached a conversation, so the reason says why it did not connect.
The Sent on column below tells you which variant each value arrives on. New reasons may be added over time, so treat a value you do not recognize as valid rather than failing on it.

Custom notes

Every call event carries a notes field inside payload.call. It returns whatever JSON object you attached to the call when you created it, unchanged, and is null when you attached nothing. Use it to carry your own identifiers through the call and back, so your handler can match an event to a record on your side without keeping a lookup table of DialNexa call IDs.

Setting notes

How you attach notes depends on how the call is placed: In a recipient or lead file, the notes column is reserved: it becomes the call’s notes rather than a dynamic variable. A cell holding a JSON object is stored as that object. Plain text is wrapped as {"text": "..."}. A cell that starts with { or [ but is not valid JSON fails the upload with a 400 rather than being stored as text, so a malformed object is never silently flattened.
Retries do not inherit notes. A retry call log is created fresh and copies the parent’s tags and metadata but not its notes, so webhook events for a retry report notes: null even when the original attempt carried them. Match retries to your records through payload.call.id and the original call instead.
notes and metadata are separate fields with separate jobs. metadata is injected into the agent’s prompt as {{variables}} and shapes what the agent says; notes is inert storage that only your webhook handler sees. Data the agent must not read belongs in notes; though since notes are sent to your endpoint on every event for the call, they are not a place for credentials or secrets either.

Signature verification

Every webhook POST includes an x-dialnexa-signature header. A webhook cannot be registered without an active webhook secret, so every request is signed. 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.

Secret rotation and the grace window

Rotating a webhook secret from the dashboard replaces its key in place; see Webhook Secrets for the dashboard side. By default the previous key stays valid for 24 hours, and during that window every delivery carries two signature headers: When the window ends, x-dialnexa-signature switches to the new key and the extra header disappears. Rotating with the expire-now option skips the window entirely, and the old key stops working at once. Write your receiver to hold a list of configured keys and accept a delivery when any configured key verifies any signature header present. Add the new key alongside the old one when you rotate, then remove the old key after the window ends. A receiver written this way never rejects a valid event across a rotation. Verification requests sent when a webhook is saved are always signed with the current key only.

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

Python example

Secret rotation

Rotation applies to one signing secret and only changes the webhooks that use it; it does not change whether any webhook is active. See Secret rotation and the grace window for how the old and new secrets overlap.
  • 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.