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

# Webhook events

> Reference for the three call lifecycle webhook events: call_started, call_ended, and call_analyzed. Includes exact payload shapes and field-level notes.

Verbeo delivers real-time webhook events to the URL configured in your agent's `config.settings.webhookUrl`. When a call is placed or received using that agent, Verbeo sends HTTP POST requests at key points in the call lifecycle. All requests use `Content-Type: application/json`.

There are three call lifecycle events. Verbeo delivers them in lifecycle order, holding a later event while an earlier one for the same call is still being retried (see [Delivery order](#delivery-order)). After a long outage on your side they can still arrive out of order — treat each event as self-contained.

| Event | When it fires |
| - | - |
| `call_started` | Immediately after the call is answered |
| `call_ended` | Immediately when the call disconnects |
| `call_analyzed` | Once the transcript, summary and post-call analysis are ready — usually within seconds of the call ending. If generating them fails it can take up to 3 hours, and it is still sent only once (see [Timing](#timing)) |

Every event payload includes `call_id` and `agent_id` so a single webhook consumer can correlate all three events for the same call.

<Tip>
  Treat every incoming webhook as idempotent. Network issues or retries can cause the same event to be delivered more than once. Use the combination of `call_id` and `event` as your deduplication key.
</Tip>

## Configuring your webhook URL

Set the `webhookUrl` field in your agent's configuration with [Update assistant](/api-reference/assistants/update). Only the webhook changes:

```bash theme={null}
curl -X PATCH "https://api.verbeo.ai/v1/assistant/YOUR_ASSISTANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "config": {
      "settings": {
        "webhookUrl": "https://your-server.example.com/verbeo-events"
      }
    }
  }'
```

The URL must be publicly reachable. Verbeo refuses webhook URLs that point at a private, local or cloud metadata address — for example `localhost`, `127.0.0.1`, `10.x.x.x`, `192.168.x.x` or `169.254.169.254` — and sends no events to them. To receive events on a server running on your own machine, expose it through a public tunnel. Use the final address as your webhook URL: Verbeo follows a `307` or `308` redirect once, and no other redirects (see [Retries](#retries)).

***

## `call_started`

Fires once, immediately after the call is answered. Use this event to start any real-time tracking or alerting for active calls.

### Payload

```json theme={null}
{
  "event": "call_started",
  "call_id": "b7a3c1e4-94f2-4c9d-a8e0-5d9f6c2a1b34",
  "agent_id": "agent_f3a2b1c4e5d6",
  "timestamp": "2026-04-17T11:03:15.000Z",
  "payload": {
    "status": "started",
    "start_time": "2026-04-17T11:03:15.000Z",
    "call_type": "outbound",
    "from_number": "+34912345678",
    "to_number": "+14155551234",
    "dynamic_variables": {
      "customer_name": "Juan Pérez",
      "plan": "premium"
    }
  }
}
```

### Fields

<ResponseField name="event" type="string" required>
  Always `"call_started"`.
</ResponseField>

<ResponseField name="call_id" type="string" required>
  Verbeo UUID for this call. Use this to correlate with `call_ended` and `call_analyzed` events.
</ResponseField>

<ResponseField name="agent_id" type="string" required>
  UUID of the agent handling the call.
</ResponseField>

<ResponseField name="timestamp" type="string" required>
  ISO 8601 timestamp of when the event was emitted.
</ResponseField>

<ResponseField name="payload.status" type="string" required>
  `"started"`.
</ResponseField>

<ResponseField name="payload.start_time" type="string" required>
  ISO 8601 timestamp of when the call was answered.
</ResponseField>

<ResponseField name="payload.call_type" type="string" required>
  `"inbound"`, `"outbound"`, or `"web"`.
</ResponseField>

<ResponseField name="payload.from_number" type="string">
  Originating phone number in E.164 format. `null` for web calls.
</ResponseField>

<ResponseField name="payload.to_number" type="string">
  Destination phone number in E.164 format. `null` for web calls.
</ResponseField>

<ResponseField name="payload.dynamic_variables" type="object">
  Key-value pairs passed at call initiation. Empty object `{}` if none were provided.
</ResponseField>

***

## `call_ended`

Fires once, immediately when the call disconnects. The transcript and analysis data are not yet available at this point — those arrive with `call_analyzed`.

### Payload

```json theme={null}
{
  "event": "call_ended",
  "call_id": "b7a3c1e4-94f2-4c9d-a8e0-5d9f6c2a1b34",
  "agent_id": "agent_f3a2b1c4e5d6",
  "timestamp": "2026-04-17T11:06:42.000Z",
  "payload": {
    "status": "completed",
    "call_type": "outbound",
    "from_number": "+34912345678",
    "to_number": "+14155551234",
    "start_time": "2026-04-17T11:03:15.000Z",
    "end_time": "2026-04-17T11:06:42.000Z",
    "duration": 207,
    "cost": 12,
    "disconnection_reason": "user_hangup",
    "dynamic_variables": {
      "customer_name": "Juan Pérez",
      "plan": "premium"
    }
  }
}
```

### Fields

<ResponseField name="event" type="string" required>
  Always `"call_ended"`.
</ResponseField>

<ResponseField name="payload.status" type="string" required>
  Final call status, typically `"completed"`.
</ResponseField>

<ResponseField name="payload.start_time" type="string" required>
  ISO 8601 timestamp of when the call was answered.
</ResponseField>

<ResponseField name="payload.end_time" type="string" required>
  ISO 8601 timestamp of when the call disconnected.
</ResponseField>

<ResponseField name="payload.duration" type="number" required>
  Call duration in seconds.
</ResponseField>

<ResponseField name="payload.cost" type="number" required>
  Cost in cents, derived from your workspace's usage rate per minute.
</ResponseField>

<ResponseField name="payload.disconnection_reason" type="string" required>
  How the call ended. Common values: `completed`, `user_hangup`, `agent_hangup`, `timeout`, `error`. This is an open set — handle values you don't recognize gracefully.
</ResponseField>

***

## `call_analyzed`

Fires once, when transcription, the summary and AI post-call analysis are complete — usually within seconds of the call ending (see [Timing](#timing)). It is sent for every completed call. This event carries the full call record including the conversation transcript, AI summary, structured analysis fields, and recording URL.

### Payload

```json theme={null}
{
  "event": "call_analyzed",
  "call_id": "b7a3c1e4-94f2-4c9d-a8e0-5d9f6c2a1b34",
  "agent_id": "agent_f3a2b1c4e5d6",
  "timestamp": "2026-04-17T11:07:05.000Z",
  "payload": {
    "status": "analyzed",
    "data": {
      "id": "b7a3c1e4-94f2-4c9d-a8e0-5d9f6c2a1b34",
      "workspace_id": "789e0123-e45b-67c8-d901-234567890123",
      "agent_id": "agent_f3a2b1c4e5d6",
      "call_type": "outbound",
      "from_number": "+34912345678",
      "to_number": "+14155551234",
      "status": "completed",
      "duration": 207,
      "started_at": "2026-04-17T11:03:15.000Z",
      "ended_at": "2026-04-17T11:06:42.000Z",
      "disconnection_reason": "user_hangup",
      "recording_url": "https://recordings.verbeo.ai/b7a3c1e4_20260417_110315.wav",
      "recording_sid": "RE9f8e7d6c5b4a3928170655e4d3c2b1a0",
      "call_sid": "CA1a2b3c4d5e6f70819a2b3c4d5e6f7081",
      "cost": 12,
      "created_at": "2026-04-17T11:03:14.000Z",
      "updated_at": "2026-04-17T11:07:05.000Z",
      "transcript": [
        {
          "role": "agent",
          "content": "Hello, how can I help you today?",
          "timestamp": "2026-04-17T11:03:18.000Z"
        },
        {
          "role": "user",
          "content": "I'd like to check on my order.",
          "timestamp": "2026-04-17T11:03:22.123Z"
        },
        {
          "role": "tool",
          "tool_name": "check_order_status",
          "query": "{\"order_id\":\"ORD-12345\"}",
          "status_code": 200,
          "result": {"status": "shipped", "eta": "tomorrow"},
          "error": null,
          "documents": [],
          "timestamp": "2026-04-17T11:03:45.200Z"
        },
        {
          "role": "agent",
          "content": "Your order ships tomorrow.",
          "timestamp": "2026-04-17T11:03:50.000Z"
        }
      ],
      "transcript_clean": [
        {
          "role": "agent",
          "content": "Hello, how can I help you today?",
          "timestamp": "2026-04-17T11:03:18.000Z"
        },
        {
          "role": "user",
          "content": "I'd like to check on my order.",
          "timestamp": "2026-04-17T11:03:22.123Z"
        },
        {
          "role": "agent",
          "content": "Your order ships tomorrow.",
          "timestamp": "2026-04-17T11:03:50.000Z"
        }
      ],
      "summary": "Customer asked about order ORD-12345. Agent confirmed it ships tomorrow.",
      "analysis_data": {
        "issue_resolved": true,
        "follow_up_required": false
      },
      "dynamic_variables": {
        "customer_name": "Juan Pérez",
        "plan": "premium"
      },
      "tools": [
        {
          "tool_name": "check_order_status",
          "tool_type": "custom",
          "description": "check_order_status executed (HTTP 200)",
          "args": {"order_id": "ORD-12345"},
          "result": {"status": "shipped", "eta": "tomorrow"},
          "error": null,
          "executed_at": "2026-04-17T11:03:45.200Z"
        }
      ]
    }
  }
}
```

### Fields

<ResponseField name="payload.status" type="string" required>
  Always `"analyzed"`.
</ResponseField>

<ResponseField name="payload.data" type="object" required>
  Complete call record.

  <Expandable title="data properties">
    <ResponseField name="payload.data.transcript" type="object[]" required>
      Interleaved array of all turns in chronological order, including agent turns, user turns, and tool invocations. Each entry has `role`, `content` (or tool fields), and `timestamp`.
    </ResponseField>

    <ResponseField name="payload.data.transcript_clean" type="object[]" required>
      Same as `transcript` but with tool entries removed. Contains only `agent` and `user` turns — useful for rendering a readable conversation view.
    </ResponseField>

    <ResponseField name="payload.data.summary" type="string" required>
      AI-generated natural language summary of the call. For a call that reached voicemail, or where the contact never spoke, it is a fixed sentence saying so. `null` when the call never connected, and in the rare case described in [Timing](#timing).
    </ResponseField>

    <ResponseField name="payload.data.analysis_data" type="object" required>
      Structured fields extracted by post-call analysis. The keys match the field names you configured on the agent. `null` when post-call analysis is disabled or no field could be extracted, and in the rare case described in [Timing](#timing).
    </ResponseField>

    <ResponseField name="payload.data.recording_url" type="string">
      URL to the call recording audio file. Download and store the recording in your own system if you need it long-term, as URLs may expire.
    </ResponseField>

    <ResponseField name="payload.data.cost" type="number">
      Cost in cents.
    </ResponseField>

    <ResponseField name="payload.data.recording_sid" type="string">
      Provider recording identifier, when a recording exists.
    </ResponseField>

    <ResponseField name="payload.data.call_sid" type="string">
      Telephony provider call identifier. `null` for web calls.
    </ResponseField>

    <ResponseField name="payload.data.created_at" type="string">
      ISO 8601 timestamp of when the call record was created.
    </ResponseField>

    <ResponseField name="payload.data.updated_at" type="string">
      ISO 8601 timestamp of the last update to the call record.
    </ResponseField>

    <ResponseField name="payload.data.tools" type="object[]">
      Detailed log of each tool invocation during the call, including the arguments passed and the result or error returned.
    </ResponseField>

    <ResponseField name="payload.data.dynamic_variables" type="object">
      Dynamic variables that were active during this call.
    </ResponseField>
  </Expandable>
</ResponseField>

***

## Delivery

### Timing

* `call_started` and `call_ended` are sent immediately.
* `call_analyzed` is sent once the transcript, summary and post-call analysis are ready — usually within seconds of the call ending.
* If generating them fails (for example, the AI model is unavailable), Verbeo keeps retrying in the background and `call_analyzed` waits. It can take up to 3 hours, and it is still sent only once. In the rare case that they are still missing after 3 hours, it is sent with what is available, so `summary` or `analysis_data` can be `null`.
* Once Verbeo has recorded a `2xx` from your endpoint for `call_analyzed`, it never sends it again on its own. It is sent again only when someone in your workspace clicks **Retry** on the call's **Webhooks** tab in the dashboard (see [Retry a delivery](/guides/webhooks#retry-a-delivery)), or uses **Enhance Transcript** on the call, which sends it with the improved transcript.

Retries and the delivery order below can add to these times when your endpoint is failing.

### Delivery order

Verbeo delivers each call's events in lifecycle order: `call_started`, then `call_ended`, then `call_analyzed`. While an earlier event is still being retried, the later ones wait — but only until 30 minutes after the call ended. After that they are sent anyway, and so are resends from the dashboard, so if your endpoint is down for longer, events can arrive out of order. Each event is self-contained: you never need an earlier event to process a later one.

### Retries

A delivery succeeds when your endpoint returns any `2xx` status within 12 seconds. Any other outcome — a non-`2xx` status, a timeout, a connection error or a redirect that isn't followed — is retried:

* **Up to 12 automatic attempts** per event. A resend from the dashboard starts a fresh series of up to 12.
* **Exponential backoff.** The wait after each failed attempt doubles — 2, 4, 8, 16, 32, 64, 128 and 256 minutes — and is then capped at 6 hours. The 12th and final attempt happens about 26 hours after the first.
* **`Retry-After` is honoured.** When a `429` or `503` response includes a `Retry-After` header (in seconds or as an HTTP date), Verbeo waits that long instead, clamped between 1 minute and 6 hours.
* **Only `307` and `308` redirects are followed, and only once.** On a `307` or `308`, Verbeo sends the same request, with the same body, to the address in the `Location` header. Any other `3xx` (such as `301` or `302`), or a second redirect from that address, is not followed and counts as a failed attempt. Configure your webhook URL as the final address.

After the 12th failed attempt Verbeo stops retrying the event. You can still resend it from the dashboard once your endpoint is fixed. See [Monitor and retry deliveries](/guides/webhooks#monitor-and-retry-deliveries-in-the-dashboard), where every attempt's full request and response is shown.

***

## Responding to webhook events

Your webhook endpoint must return an HTTP `2xx` status code within a reasonable timeout — Verbeo waits 12 seconds (see [Retries](#retries)). No specific response body is required for call lifecycle events.

```javascript theme={null}
// Express.js example
app.post("/verbeo-events", (req, res) => {
  const { event, call_id, agent_id, payload } = req.body;

  if (event === "call_analyzed") {
    const { summary, analysis_data, transcript_clean } = payload.data;
    // Process the completed call data
    saveCallRecord({ call_id, summary, analysis_data, transcript_clean });
  }

  res.sendStatus(200);
});
```

<Warning>
  Schema changes to webhook payloads are additive. New fields may appear in future releases without notice. Fields documented here will not be removed or renamed without a breaking-change notice.
</Warning>


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