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

# Webhook events

> Sample request and field reference for every outbound, inbound and billing webhook event.

Ondial sends a `POST` request to your webhook URL each time an event you subscribed to happens. Register the URL with [Manage webhooks](https://docs.ondial.ai/api-reference/endpoint/webhooks/manage-webhooks.md) or in **API Access → Notifications**.

## Event index

| Event | Group | When it fires |
| - | - | - |
| [`call.outbound.billed`](#call-outbound-billed) | Outbound | Credits were deducted for an outbound call |
| [`whatsapp.outbound.status`](#whatsapp-outbound-status) | Outbound | A WhatsApp message to the lead was sent, delivered, read or failed |
| [`email.outbound.status`](#email-outbound-status) | Outbound | A follow-up email was sent or failed |
| [`contact.cancelled`](#contact-cancelled) | Outbound | A contact was cancelled or deleted through the API |
| [`call.inbound.started`](#call-inbound-started) | Inbound | An inbound agent answered a call |
| [`call.inbound.completed`](#call-inbound-completed) | Inbound | An inbound call ended |
| [`call.inbound.billed`](#call-inbound-billed) | Inbound | Credits were deducted for an inbound call |
| [`call.inbound.analysis`](#call-inbound-analysis) | Inbound | Post-call analysis of an inbound call is ready |
| [`whatsapp.inbound.received`](#whatsapp-inbound-received) | Inbound | The lead sent a WhatsApp message |
| [`inbound.lead.created`](#inbound-lead-created) | Inbound | An inbound caller was pushed to your connected CRM |
| [`credits.usage`](#credits-usage) | Billing | Any call debited your wallet (inbound or outbound) |

Samples for `outbound.lead.created`, `outbound.lead.updated`, `call.outbound.status`, `call.outbound.analysis` and `followup.scheduled` are in the [CRM lead bank integration](https://docs.ondial.ai/get-started/guides/crm-lead-bank-integration.md) guide.

## Request format

Every event uses the same request shape. Only `data` changes between events.

```http theme={"system"}
POST https://your-crm.example.com/ondial/webhook
Content-Type: application/json
X-Ondial-Event: call.inbound.completed
X-Ondial-Signature: 5d41402abc4b2a76b9719d911017c592...
X-Ondial-Idempotency-Key: call.inbound.completed|66f0c1d2e3f4a5b6c7d8e9f0|completed
```

```json theme={"system"}
{
  "eventType": "call.inbound.completed",
  "occurredAt": "2026-10-14T09:32:10.000Z",
  "data": { }
}
```

| Field / header | Description |
| - | - |
| `eventType` | Event name. Same value as the `X-Ondial-Event` header. |
| `occurredAt` | When Ondial sent this delivery (ISO-8601, UTC). |
| `data` | Event payload. See each event below. |
| `X-Ondial-Signature` | Hex HMAC-SHA256 of the **raw request body**, using your webhook secret. |
| `X-Ondial-Idempotency-Key` | Stable key for this delivery. Use it to ignore duplicates. |

**1. Verify the signature**

Compute HMAC-SHA256 over the raw body bytes with your webhook secret and compare it with `X-Ondial-Signature`. Don't re-serialize the JSON first.

**2. Respond quickly**

Return any `2xx` within 10 seconds. Do slow work (CRM updates) after responding.

**3. Ignore duplicates**

Store `X-Ondial-Idempotency-Key` and skip keys you've already processed.

**Retries:** a failed delivery is tried up to 3 times. Ondial stops retrying on `400`, `401`, `403`, `404`, `405`, `410` and `422`. A webhook that keeps failing is disabled automatically. Re-enable it in **API Access → Notifications**.

> [!NOTE]
>
> Outbound events (`call.outbound.*`, `whatsapp.outbound.status`, `email.outbound.status`, `contact.cancelled` and outbound `credits.usage`) are only sent for campaigns whose Source Type is **API / Custom CRM**.

> [!WARNING]
>
> Treat any field not marked as always present as optional. Fields can be `null` or missing when the value isn't known, and new fields may be added at any time.

***

## Outbound events

### call.outbound.billed

Sent when credits are deducted for an outbound call. A matching [`credits.usage`](#credits-usage) event with the same `data` is sent at the same time, so subscribe to only one of them if you don't want both.

```json theme={"system"}
{
  "eventType": "call.outbound.billed",
  "occurredAt": "2026-10-14T09:12:41.000Z",
  "data": {
    "usageId": null,
    "billingKey": "66f0aaaa1111222233334444:call:9f3c2a1b-7d4e-4b6a-9c1d-2e3f4a5b6c7d",
    "direction": "outbound",
    "callId": "9f3c2a1b-7d4e-4b6a-9c1d-2e3f4a5b6c7d",
    "conversationId": "66f0c0ffee1111222233334444",
    "callSessionId": null,
    "callSid": null,
    "campaignId": "66f0aaaa1111222233334444",
    "contactId": "66f0a1b2c3d4e5f678901234",
    "leadId": 177,
    "campaignName": null,
    "agentId": null,
    "agentName": null,
    "durationSeconds": 65,
    "fromNumber": "+919484957201",
    "toNumber": "+919727735519",
    "callerNumber": "+919484957201",
    "phoneNumber": "+919727735519",
    "phoneSuffix": "5519",
    "billingStatus": "charged",
    "creditsCharged": 0.0725,
    "balanceAfter": 2416.39,
    "occurredAt": "2026-10-14T09:12:40.512Z"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `callId` | string | Call ID. Same value as in `call.outbound.status` and `call.outbound.analysis`. |
| `conversationId` | string \| null | Ondial call log ID. |
| `campaignId` | string | Campaign the call belongs to. |
| `contactId` | string | Contact that was called. May be missing. |
| `leadId` | string or number | Your own lead id, if you sent one with the lead. Same value as in `call.outbound.status` and `call.outbound.analysis`. Left out otherwise. |
| `durationSeconds` | integer | Billed call length in seconds. |
| `fromNumber` | string | Your Ondial number that placed the call. |
| `toNumber` | string | Lead's phone number. |
| `callerNumber` / `phoneNumber` | string | Same values as `fromNumber` / `toNumber`. |
| `phoneSuffix` | string | Last 4 digits of the lead's number. |
| `billingStatus` | string | Always `charged`. |
| `creditsCharged` | number | Credits deducted for this call. |
| `balanceAfter` | number \| null | Wallet balance after the deduction. Can be `null`. |
| `billingKey` | string \| null | Stable billing key, when available. |
| `usageId` | string \| null | Credit transaction ID. Usually `null` for outbound calls. |
| `campaignName`, `agentId`, `agentName`, `callSessionId`, `callSid` | null | Not used for outbound calls. |

### whatsapp.outbound.status

Sent when a WhatsApp message to the lead changes status.

> [!WARNING]
>
> If your webhook is subscribed to `*`, this event arrives with `eventType: "whatsapp.status"` (its legacy name). If you subscribed to `whatsapp.outbound.status` by name, it arrives under that name. Handle both.

```json theme={"system"}
{
  "eventType": "whatsapp.outbound.status",
  "occurredAt": "2026-10-14T09:14:02.000Z",
  "data": {
    "contactId": "66f0a1b2c3d4e5f678901234",
    "campaignId": "66f0aaaa1111222233334444",
    "phoneSuffix": "5519",
    "status": "delivered"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `contactId` | string | Contact the message was sent to. |
| `campaignId` | string | Campaign that sent the message. |
| `phoneSuffix` | string | Last 4 characters of the lead's number. Can be empty. |
| `status` | string | `sent`, `delivered`, `read` or `failed`. |

### email.outbound.status

Sent when a follow-up email to the lead is sent or fails.

> [!WARNING]
>
> If your webhook is subscribed to `*`, this event arrives with `eventType: "email.status"` (its legacy name). If you subscribed to `email.outbound.status` by name, it arrives under that name. Handle both.

```json theme={"system"}
{
  "eventType": "email.outbound.status",
  "occurredAt": "2026-10-14T09:20:15.000Z",
  "data": {
    "contactId": "66f0a1b2c3d4e5f678901234",
    "campaignId": "66f0aaaa1111222233334444",
    "phoneSuffix": "5519",
    "status": "sent"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `contactId` | string | Contact the email was sent to. |
| `campaignId` | string | Campaign that sent the email. |
| `phoneSuffix` | string | Last 4 characters of the lead's number. Can be empty. |
| `status` | string | `sent` or `failed`. |

### contact.cancelled

Sent when you cancel a contact with [Cancel contact](https://docs.ondial.ai/api-reference/endpoint/contacts/cancel-contact.md) or delete it with [Delete contact](https://docs.ondial.ai/api-reference/endpoint/contacts/delete-contact.md). Contacts stopped from the dashboard don't send this event.

```json theme={"system"}
{
  "eventType": "contact.cancelled",
  "occurredAt": "2026-10-14T10:05:00.000Z",
  "data": {
    "contactId": "66f0a1b2c3d4e5f678901234",
    "campaignId": "66f0aaaa1111222233334444",
    "reason": "api_cancel"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `contactId` | string | Cancelled contact. |
| `campaignId` | string | Campaign the contact belonged to. |
| `reason` | string | `api_cancel` (cancelled) or `api_delete` (deleted). |

***

## Inbound events

Inbound events are sent for every inbound agent on your account. They don't depend on campaign Source Type.

### call.inbound.started

Sent when an inbound agent answers a call.

```json theme={"system"}
{
  "eventType": "call.inbound.started",
  "occurredAt": "2026-10-14T09:30:00.000Z",
  "data": {
    "direction": "inbound",
    "conversationId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callSessionId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "agentId": "66f0b1b2c3d4e5f678905678",
    "agentName": "Clinic front desk",
    "status": "active",
    "billingStatus": "pending",
    "fromNumber": "+919812345678",
    "toNumber": "+919484957201",
    "phoneSuffix": "5678"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `conversationId` | string | Inbound conversation ID. Use it to match the later `completed`, `billed` and `analysis` events. |
| `callId` / `callSessionId` | string | Same value as `conversationId`. |
| `agentId` | string | Inbound agent that took the call. |
| `agentName` | string | Inbound agent name. May be missing. |
| `campaignId` | string | Only present when the agent is linked to a campaign. |
| `status` | string | Always `active`. |
| `billingStatus` | string | Always `pending`. Billing arrives later in `call.inbound.billed`. |
| `fromNumber` | string | Caller's number. |
| `toNumber` | string | Your Ondial number that was called. |
| `phoneSuffix` | string | Last 4 digits of the caller's number. |

### call.inbound.completed

Sent once when an inbound call ends. Billing is usually still `pending` at this point; wait for [`call.inbound.billed`](#call-inbound-billed) for the final cost.

```json theme={"system"}
{
  "eventType": "call.inbound.completed",
  "occurredAt": "2026-10-14T09:32:10.000Z",
  "data": {
    "id": "66f0c1d2e3f4a5b6c7d8e9f0",
    "direction": "inbound",
    "conversationId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callSessionId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callSid": null,
    "status": "completed",
    "outcome": "resolved",
    "startedAt": "2026-10-14T09:30:00.000Z",
    "endedAt": "2026-10-14T09:32:00.000Z",
    "durationSeconds": 120,
    "fromNumber": "+919812345678",
    "toNumber": "+919484957201",
    "agentId": "66f0b1b2c3d4e5f678905678",
    "agentName": "Clinic front desk",
    "campaignId": null,
    "contactId": null,
    "billingStatus": "pending",
    "creditsCharged": null,
    "creditsError": null,
    "balanceAfter": null,
    "hasTranscript": true,
    "hasAnalysis": false,
    "summary": null,
    "phoneSuffix": "5678",
    "recordingUrl": "https://..."
  }
}
```

| Field | Type | Description |
| - | - | - |
| `id` / `conversationId` / `callId` | string | Inbound conversation ID. |
| `status` | string | Final call status, usually `completed`. |
| `outcome` | string \| null | Call outcome, when known. |
| `startedAt` / `endedAt` | string \| null | Call start and end (ISO-8601, UTC). |
| `durationSeconds` | integer | Call length in seconds. |
| `fromNumber` / `toNumber` | string | Caller's number / your Ondial number. |
| `agentId` / `agentName` | string | Inbound agent that took the call. |
| `campaignId` / `contactId` | string \| null | Linked campaign and contact, when any. |
| `billingStatus` | string | Usually `pending`. Can be `charged`, `failed` or `skipped`. |
| `hasTranscript` | boolean | Whether a transcript was saved. |
| `hasAnalysis` | boolean | Always `false` here. Analysis arrives in `call.inbound.analysis`. |
| `recordingUrl` | string \| null | Call recording link. Only included if recording links are enabled for your account. Contact support to enable it. |
| `phoneSuffix` | string | Last 4 digits of the caller's number. |

### call.inbound.billed

Sent when credits are deducted for an inbound call. A matching [`credits.usage`](#credits-usage) event with the same `data` is sent at the same time.

```json theme={"system"}
{
  "eventType": "call.inbound.billed",
  "occurredAt": "2026-10-14T09:32:40.000Z",
  "data": {
    "usageId": "66f0d1d2e3f4a5b6c7d8e9a1",
    "billingKey": "66f0b1b2c3d4e5f678905678:call:66f0c1d2e3f4a5b6c7d8e9f0",
    "direction": "inbound",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "conversationId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callSessionId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callSid": null,
    "campaignId": null,
    "campaignName": "Clinic front desk",
    "agentId": "66f0b1b2c3d4e5f678905678",
    "agentName": "Clinic front desk",
    "durationSeconds": 120,
    "fromNumber": "+919812345678",
    "toNumber": "+919484957201",
    "callerNumber": "+919812345678",
    "phoneNumber": "+919484957201",
    "billingStatus": "charged",
    "creditsCharged": 0.15,
    "balanceAfter": 2416.24,
    "occurredAt": "2026-10-14T09:32:39.870Z"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `usageId` | string \| null | Credit transaction ID. |
| `billingKey` | string \| null | Stable billing key (`<agentId>:call:<callId>`). |
| `callId` / `conversationId` | string | Inbound conversation ID from `call.inbound.started`. |
| `agentId` / `agentName` | string | Inbound agent that took the call. |
| `campaignName` | string \| null | Same value as `agentName`. |
| `durationSeconds` | integer | Billed call length in seconds. |
| `fromNumber` / `callerNumber` | string | Caller's number. |
| `toNumber` / `phoneNumber` | string | Your Ondial number. |
| `billingStatus` | string | Always `charged`. |
| `creditsCharged` | number | Credits deducted for this call. |
| `balanceAfter` | number \| null | Wallet balance after the deduction. |

### call.inbound.analysis

Sent when post-call analysis of an inbound call is ready. This event is a **notification only**: it carries the call ID, not the analysis itself. Fetch the summary, classification and next action with [Get call](https://docs.ondial.ai/api-reference/endpoint/calls/get-call.md).

```json theme={"system"}
{
  "eventType": "call.inbound.analysis",
  "occurredAt": "2026-10-14T09:33:05.000Z",
  "data": {
    "direction": "inbound",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "inboundConfigId": "66f0b1b2c3d4e5f678905678"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `direction` | string | Always `inbound`. |
| `callId` | string | Call to fetch the analysis for. |
| `inboundConfigId` | string | Inbound agent that took the call (same as `agentId` in other events). |

### whatsapp.inbound.received

Sent each time the lead replies on WhatsApp to a campaign conversation. The message text isn't included.

```json theme={"system"}
{
  "eventType": "whatsapp.inbound.received",
  "occurredAt": "2026-10-14T11:02:18.000Z",
  "data": {
    "direction": "inbound",
    "campaignId": "66f0aaaa1111222233334444",
    "contactId": "66f0a1b2c3d4e5f678901234",
    "phoneSuffix": "5519"
  }
}
```

| Field | Type | Description |
| - | - | - |
| `direction` | string | Always `inbound`. |
| `campaignId` | string | Campaign the conversation belongs to. |
| `contactId` | string | Contact who replied. May be missing. |
| `phoneSuffix` | string | Last 4 digits of the lead's number. |

### inbound.lead.created

Sent after an inbound call when Ondial successfully creates or updates the caller as a lead in your connected CRM (Zoho, HubSpot, Salesforce or Odoo). Not sent if CRM push is off or fails.

`data` contains every field from [`call.inbound.completed`](#call-inbound-completed), plus `provider` and `recordId`.

```json theme={"system"}
{
  "eventType": "inbound.lead.created",
  "occurredAt": "2026-10-14T09:32:12.000Z",
  "data": {
    "id": "66f0c1d2e3f4a5b6c7d8e9f0",
    "direction": "inbound",
    "conversationId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "status": "completed",
    "durationSeconds": 120,
    "fromNumber": "+919812345678",
    "toNumber": "+919484957201",
    "agentId": "66f0b1b2c3d4e5f678905678",
    "agentName": "Clinic front desk",
    "billingStatus": "pending",
    "phoneSuffix": "5678",
    "provider": "hubspot",
    "recordId": "48213377901"
  }
}
```

The sample above is shortened. See `call.inbound.completed` for the full field list.

| Field | Type | Description |
| - | - | - |
| `provider` | string | CRM the lead was pushed to: `zoho`, `hubspot`, `salesforce` or `odoo`. |
| `recordId` | string | Record ID in your CRM. May be missing. |

***

## Billing events

### credits.usage

Sent for every wallet debit, for both inbound and outbound calls. Use it if you want one event for all billing instead of subscribing to `call.outbound.billed` and `call.inbound.billed` separately.

`data` is exactly the same as the matching billed event:

* `direction: "outbound"` → same fields as [`call.outbound.billed`](#call-outbound-billed)
* `direction: "inbound"` → same fields as [`call.inbound.billed`](#call-inbound-billed)

```json theme={"system"}
{
  "eventType": "credits.usage",
  "occurredAt": "2026-10-14T09:32:40.000Z",
  "data": {
    "usageId": "66f0d1d2e3f4a5b6c7d8e9a1",
    "billingKey": "66f0b1b2c3d4e5f678905678:call:66f0c1d2e3f4a5b6c7d8e9f0",
    "direction": "inbound",
    "callId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "conversationId": "66f0c1d2e3f4a5b6c7d8e9f0",
    "agentId": "66f0b1b2c3d4e5f678905678",
    "agentName": "Clinic front desk",
    "durationSeconds": 120,
    "billingStatus": "charged",
    "creditsCharged": 0.15,
    "balanceAfter": 2416.24,
    "occurredAt": "2026-10-14T09:32:39.870Z"
  }
}
```

> [!NOTE]
>
> To reconcile billing, upsert on `billingKey` when it's present, otherwise on `callId` + `direction`. Outbound `usageId` is usually `null`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.