# call.ended

A phone or WhatsApp call ended; carries duration, end reason and a zero-markup cost breakdown.

### Payload notes

`durationSeconds`; `endReason` is `hangup`, `no_answer`, `rejected` or `error`; `call.billing` holds Meta cost, Telnyx cost, recording surcharge and total. Costs are pass-through, with no margin.

### When it fires

Unanswered, busy and rejected calls end here too. Branch on `endReason`; `call.failed` is for hard errors.

### Recordings

With recording enabled, the payload carries `recordingUrl`, a signed URL expiring at `recordingExpiresAt`. Fetch a fresh one with `GET /v1/calls/{id}/recording` and `call.id`.

### Missed-call follow-up

Check for `endReason: "no_answer"`, then text the caller over the linked `conversationId`.




**Payload for `call.ended`:**

- **test** `boolean`: Always true when present: only a sample sent by POST /v1/webhooks/test with an event carries it. Real deliveries never do.
- **id** (required) `string`: Stable webhook event ID: the dedupe key, also sent as the X-Zernio-Event-Id header and identical on every retry and redelivery. It identifies the event only, never an account or other resource.
- **profiles**: `WebhookProfiles` - See schema definition
- **event** (required) `string`: No description - one of: call.ended
- **call** (required) `object`: 
  - **id** `string`: No description
  - **metaCallId** `string,null`: No description
  - **accountId** `string`: No description
  - **phoneNumberId** `string`: No description
  - **direction** `string`: No description - one of: inbound, outbound
  - **from** `string`: No description
  - **to** `string`: No description
  - **startedAt** `string` (date-time): No description
  - **endedAt** `string` (date-time): No description
  - **durationSeconds** `integer`: No description
  - **endReason** `string`: No description - one of: hangup, no_answer, rejected, error
  - **hangupCause** `string,null`: Raw carrier hangup cause behind endReason (e.g. normal_clearing, call_rejected, not_found). Null when the carrier reported none.
  - **sipHangupCause** `string,null`: SIP response code that ended the call when SIP-signalled (e.g. '403', '486', '603'). endReason collapses all three to 'rejected', so this is what separates a refused destination from a busy line. Null on non-SIP legs.
  - **isVoicemail** `boolean`: True when the inbound call was handled by voicemail, whether scheduled or because the forward did not connect.
  - **callErrors** `array[object]`: Failures recorded on the call up to hangup (bridge failed, dial failed, recording error). Empty on a clean call.

- `message` is free-form diagnostic text and is not stable; do not parse it.
- `code` is 0 unless a provider code is known.

Errors the carrier reports after hangup appear only on GET /v1/calls/{id}.

    - **code** `integer`: No description
    - **message** `string`: No description
  - **recordingUrl** `string`: No description
  - **recordingExpiresAt** `string` (date-time): No description
  - **billing** `object`: 
    - **metaCostUSD** `number`: Meta per-minute charge. Billed by Meta DIRECTLY to your WhatsApp Business Account payment method (your separate Meta invoice). Zernio does NOT charge this. Display only.
    - **telnyxCostUSD** `number`: No description
    - **recordingCostUSD** `number`: No description
    - **billableCostUSD** `number`: The amount Zernio bills you = Telnyx leg + recording. Excludes Meta (billed by Meta directly).
    - **totalCostUSD** `number`: Full economic cost incl. the Meta portion you pay directly (Meta + Telnyx + recording). Display only, not the Zernio-billed amount.
- **account** (required): `InboxWebhookAccount` - See schema definition
- **timestamp** (required) `string` (date-time): UTC time at which Zernio generated this event (set once when the event payload is built, before delivery is queued). Retries and redeliveries keep the original value, so it reflects the event, not the delivery attempt.