---
title: "telephony.complete / web.complete"
description: "Неблокирајући вебхук који се испоручује када се позив заврши, са транскриптом, URL-ом снимка и метрикама."
---

Догађај завршетка покреће се након завршетка сваког позива — долазне телефоније,
одлазне телефоније, веб-позива или тест-позива (сесија микрофона у градитељу). Он је
**неблокирајући**: одговорите било којим кодом 2xx.

Догађај се доставља преко оба пута:

* **[Крајње тачке веб-хука](/sr/webhooks/endpoints)** примају
  `telephony.complete` (телефонски позиви) или `web.complete` (веб-позиви и
  тест-позиви микрофоном у градитељу) са стабилним телом документованим у наставку,
  `event_id` за сваку испоруку, временским ограничењем од 30 s и
  [поновним покушајима до 24 h](/sr/webhooks/overview).
* **[Застарели веб-хук са једним URL-ом](/api-reference/organizations#legacy-single-url-webhook)**
  прима један синхрони покушај (временско ограничење од 10 s, без поновних покушаја) са
  мало другачијим телом — погледајте
  [Разлике у застарелом телу](#legacy-payload-differences).

## Тело захтева (испоруке крајњим тачкама)

```json
{
  "data": {
    "billable_minutes": 1.25,
    "billing_total_cents": 8,
    "call_id": 987654321,
    "direction": "inbound",
    "duration_seconds": 54,
    "end_reason": "user_hangup",
    "end_time": "2026-04-20T18:25:04.822Z",
    "from_number": "+14155550199",
    "product": "spark",
    "recording_url": "https://storage.example.com/…",
    "start_time": "2026-04-20T18:24:10.113Z",
    "status": "completed",
    "to_number": "+15551234567",
    "transcripts": [ /* see Transcript format */ ],
    "transfer_number": null,
    "voice": "john"
  },
  "event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
  "type": "telephony.complete"
}
```

| Поље | Тип | Опис |
|-------|------|-------------|
| `call_id` | integer | Стабилно у сваком догађају за овај позив |
| `agent_id` | integer \| null | Агент који је обрадио позив, ако је био додељен |
| `agent_name` | string \| null | Агент који је обрадио позив, ако је био додељен |
| `direction` | string | `inbound`, `outbound`, `web`, `test`. Историјска тела могу садржати застареле вредности `mic` или `widget` |
| `from_number`, `to_number` | string | E.164. `from_number` је дословно `"web"` за веб-позиве и тест-позиве |
| `origin_domain` | string | **Само за веб/тестове** — извор странице на којој је смештен виџет (празно за сесије микрофона) |
| `start_time`, `end_time` | timestamp | ISO 8601 UTC |
| `duration_seconds` | integer \| null | Изведено из времена почетка/завршетка |
| `status` | string | `completed` или `failed` |
| `end_reason` | string | Погледајте табелу у наставку |
| `product`, `voice` | string | Конфигурација агента активна у време позива |
| `transfer_number` | string \| null | Постављено када је позив преусмерен |
| `recording_url` | string \| null | Потписани URL који истиче; преузмите га одмах. `null` када артефакт снимка није доступан |
| `billable_minutes` | number | Наплаћени минути, заокружени на најближу четвртину минута (инкременти од 15 секунди, минимум 0.25). Позиви који иду директно на говорну пошту и даље овде приказују стварно измерене минуте, али је наплата ограничена на један минут по тарифној стопи плана. |
| `billing_total_cents` | integer | Амерички центи |
| `transcripts` | array | Уноси транскрипта за сваку говорну размену; могу бити празни када транскрипт није доступан |

### Разлози завршетка

| Вредност | Значење |
|-------|---------|
| `user_hangup` | Удаљена страна је прва прекинула позив |
| `ai_hangup` | AI је намерно завршио позив |
| `ai_transfer` | AI је преусмерио позив; `transfer_number` је постављен |
| `ai_warm_transfer` | AI је завршио топло (надгледано) преусмеравање |
| `voicemail_hangup` | Откривена је говорна пошта и позив је завршен у складу са Вашим `voicemail_action` |
| `max_duration` | Позив је достигао ограничење максималног трајања |
| `superseded` | Сесија је замењена новијом |
| `unknown` | Разлог завршетка није могао бити утврђен |

## Формат транскрипта

Сваки унос у `transcripts` је један разговорни потез. Улоге су
`user` (говор позиваоца), `model` (говор агента **и** позиви алатки),
`tool` (резултати алатки) и `system` (догађаји позива, као што су
промене језика).

```json
[
  {
    "role": "user",
    "content_type": "text/plain",
    "content": "Hi, I'm calling about my appointment.",
    "start_ms": 1200,
    "end_ms":   4100,
    "audio_url": "https://storage.example.com/…"
  },
  {
    "role": "model",
    "content_type": "text/plain",
    "content": "Sure, what date works best?",
    "start_ms": 4200,
    "end_ms":   6100
  },
  {
    "role": "model",
    "content_type": "application/json",
    "content": {
      "tool_call": "search_appointments",
      "arguments": { "date": "2026-04-21" }
    }
  },
  {
    "role": "tool",
    "content_type": "application/json",
    "content": {
      "tool_name": "search_appointments",
      "response": { "available_slots": ["9:00 AM", "2:00 PM"] }
    }
  }
]
```

| Поље | Тип | Опис |
|-------|------|-------------|
| `role` | ниска | `user`, `model`, `tool` или `system` |
| `content_type` | ниска | `text/plain` за говор; `application/json` за позиве алатки, резултате алатки и системске догађаје |
| `content` | ниска \| објекат | Текст говора или структурирани објекат приказан изнад. Позиви алатки: `{"tool_call": name, "arguments": {…}}`. Резултати алатки: `{"tool_name": name, "response": {…}}` |
| `start_ms`, `end_ms` | цео број | Помераји од почетка позива, у мс. Присутно када је познато аудио-време |
| `ttfa_ms` | цео број | Време до првог звука за `model` потез, када је измерено |
| `audio_url`, `audio_urls` | ниска / низ | Потписани УРЛ-ови са истеком за аудио записа потеза, када се снима по потезу |

За потпуно структурисану историју потеза (са ознакама прекида,
упитима за потврду и сировим позицијама), користите
[`GET /v1/calls/{call_id}/history`](/api-reference/calls#get-history).

## Разлике у застарелом садржају

Застарела омотница веб-хука са једним УРЛ-ом је
`{"type": "telephony.complete" | "web.complete", "data": {…}}` без
**`event_id`**, а њен `data` се разликује од садржаја крајње тачке:

Застарели садржај о завршетку такође садржи `agent_id` и `agent_name`.

* Низ потеза је у оквиру **`history`**, а не `transcripts` (иста шема
  потеза као изнад).
* Скуп поља је сиров извештај на крају позива и може да садржи
  додатна интерна поља поред табеле изнад — непозната поља третирајте
  као информативна.
* Веб позиви (`direction: "web"`) **изостављају** `from_number` / `to_number`
  и додају `origin_domain`.
* Позиви за тестирање микрофона у градитељу пријављују се као `telephony.complete` на застарелој
  путањи (систем крајње тачке их мапира на `web.complete`).
* **Координација преноса:** када се позив заврши преносом, застарели
  веб-хук се позива синхроно и може да врати
  `{"transfer_ready": false}` како би означио да циљ преноса није
  спреман. Сваки други одговор (или одсуство застарелог веб-хука) омогућава наставак преноса. Испоруке крајњој тачки се за ово никада не проверавају.

---

## Пример обрађивача

<CodeGroup>
```python Python (FastAPI)
import hashlib
import hmac
import json
import os

from fastapi import FastAPI, HTTPException, Request

app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]

def verify(body: bytes, signature: str) -> bool:
    expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature or "")

@app.post("/thunderphone-webhook")
async def webhook(request: Request):
    body = await request.body()
    if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
        raise HTTPException(status_code=401)

    event = json.loads(body)
    if event["type"] in ("telephony.complete", "web.complete"):
        data = event["data"]
        # Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
        turns = data.get("transcripts") or data.get("history") or []
        await persist_call_record(
            call_id=data["call_id"],
            turns=turns,
            recording_url=data.get("recording_url"),
        )
        if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
            await notify_team(data.get("transfer_number"), data["call_id"])
    return {"ok": True}
```

```javascript Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;

function verify(body, signature) {
  const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
  return signature &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
      return res.sendStatus(401);
    }
    const event = JSON.parse(req.body.toString("utf8"));
    if (["telephony.complete", "web.complete"].includes(event.type)) {
      const data = event.data;
      // Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
      const turns = data.transcripts ?? data.history ?? [];
      await persistCallRecord({ ...data, turns });
      if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
        await notifyTeam(data.transfer_number, data.call_id);
      }
    }
    res.json({ ok: true });
  },
);
```
</CodeGroup>

---

## Уобичајени случајеви употребе

<CardGroup cols={2}>
  <Card title="Интеграција са CRM-ом" icon="database">
    Чувајте транскрипт сваког позива и URL снимка уз евиденцију о
    клијентима.
  </Card>
  <Card title="Аналитика" icon="chart-line">
    Прослеђујте транскрипте у цевовод за моделовање тема, издвајање
    CSAT сигнала или праћење стопе преноса.
  </Card>
  <Card title="Преглед квалитета" icon="clipboard-check">
    Отворите позиве у QA алату за људски преглед или их покрените кроз
    сопствени модел за евалуацију.
  </Card>
  <Card title="Обавештења" icon="bell">
    Обавестите члана тима приликом преноса / неуспеха.
  </Card>
</CardGroup>

---

## Повезано

<CardGroup cols={2}>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/sr/webhooks/call-incoming">
    Блокирајући пандан који се покреће на почетку позива.
  </Card>
  <Card title="Каталог догађаја" icon="list" href="/sr/webhooks/events">
    Други типови догађаја на које можете да се претплатите.
  </Card>
  <Card title="API историје позива" icon="phone" href="/api-reference/calls">
    Исти подаци су доступни преко REST-а за накнадно попуњавање / поновну репродукцију.
  </Card>
</CardGroup>
