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

# Post-call webhook

> The call.ended event we POST to a voice agent's webhook when a call ends.

When a call that went live ends, we send a `POST` to the agent's [webhook endpoint](https://docs.talking-avatar.dev/voice-agents/webhooks.md), signed like a [tool call](https://docs.talking-avatar.dev/api-reference/tool-call-request.md#headers).

```json theme={null}
{
  "event": "call.ended",
  "call": {
    "id": "8b0c6c5e-…",
    "agentId": "3f1e2d4a-…",
    "startedAt": "2026-10-23T09:30:00.000Z",
    "endedAt": "2026-10-23T09:34:12.000Z",
    "durationSeconds": 252,
    "endedReason": "agent ended the call"
  },
  "transcript": [
    { "role": "agent", "text": "Hi, I'm Maya. What can I help you with today?" },
    { "role": "user", "text": "Where's my order?" }
  ]
}
```

## Body

**event** `string` *required*

Always `call.ended`.

**call** `object` *required*

**properties**

**id** `string`

The call's ID, as in the dashboard and in tool calls

**agentId** `string`

The voice agent's ID

**startedAt** `string`

When the call went live (ISO 8601)

**endedAt** `string`

When it ended (ISO 8601)

**durationSeconds** `integer`

How long it was live, in seconds

**endedReason** `string`

Why it ended. See below.

**transcript** `object[]` *required*

The conversation, in order. Each line has a `role`, `agent` or `user` (the caller), and its `text`. When the caller interrupted, the agent's line has only the part they heard.

## endedReason

| Value | Meaning |
| - | - |
| `caller ended the call` | The caller clicked **End call** |
| `caller left` | The caller closed the page or lost their connection |
| `agent ended the call` | The agent used **End call** after saying goodbye |
| `time up` | The call reached the agent's maximum length |

Other values mean the connection dropped during the call.

## Delivery

* Sent once per call, only for calls that went live. Not retried.
* Reply with a `2xx` within 10 seconds.
* Each call's **Webhook** status in the dashboard shows the HTTP status you returned, or *Couldn't reach it*.

## Example receiver

```js Node.js (Express) theme={null}
import express from "express";
import { Webhook } from "standardwebhooks";

const webhook = new Webhook(process.env.AGENT_SIGNING_SECRET);
const app = express();

app.post("/agent/calls", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = webhook.verify(req.body.toString(), req.headers);
  } catch {
    return res.status(401).end();
  }
  res.status(204).end(); // answer quickly, then do the work
  await saveCall(event.call, event.transcript);
});
```

```python Python (Flask) theme={null}
import os
from flask import Flask, request
from standardwebhooks.webhooks import Webhook

webhook = Webhook(os.environ["AGENT_SIGNING_SECRET"])
app = Flask(__name__)

@app.post("/agent/calls")
def call_ended():
    try:
        event = webhook.verify(request.get_data(), dict(request.headers))
    except Exception:
        return "", 401
    save_call(event["call"], event["transcript"])
    return "", 204
```


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