## Follow-up по одному звонку

> **Метод выходит в обновлении `call 26.600.0` и доступен пока не на всех порталах Битрикс24.** Если обновление на ваш портал ещё не пришло, API вернёт `422 METHOD_NOT_YET_AVAILABLE` — это признак того, что метод на портале ещё не выпущен, а не ошибка интеграции.

`GET /v1/calls/followups/:callId`

Возвращает AI Follow-up одного завершённого звонка.

## Параметры

| Параметр | Тип | Обяз. | Описание |
|----------|-----|:-----:|---------|
| `callId` (path) | integer | да | Идентификатор звонка. Положительное целое. Где взять — [`POST /v1/calls/followups/list`](./list.md) |
| `select` (query) | string[] | нет | Какие поля вернуть. Передаётся повторяющимся параметром или списком через запятую. Словарь значений общий со списком — раздел «Поле select» страницы [Список Follow-up](./list.md) |
| `mentionFormat` (query) | string | нет | Формат `@`-упоминаний в текстовых полях: `bb`, `html` или `none`. По умолчанию — `bb` |

## Поле select

| Значение `select` | Что в ответе |
|---|---|
| Не указан | Полный объект Follow-up. Присутствуют все поля, отсутствующие данные приходят как `null` |
| Пустой массив | Только базовые метаданные: `callId`, `callType`, `initiatorId`, `startDate`, `endDate`, `durationSeconds` |
| Список полей | Только перечисленные поля и всегда `callId`. Запрошенное, но незаполненное поле приходит как `null` |

## Примеры

Запрос без `select` — возвращается весь Follow-up целиком. `mentionFormat: none` убирает разметку упоминаний, поэтому текст готов к передаче в нейросеть или к сохранению в задачу. Чтобы забрать только часть блоков, добавьте `select` — например `?select=overview.actionItems&select=evaluation.efficiencyValue`.

### curl — личный ключ

```bash
curl "https://vibecode.bitrix24.tech/v1/calls/followups/12345?mentionFormat=none" \
  -H "X-Api-Key: YOUR_API_KEY"
```

### curl — OAuth-приложение

```bash
curl "https://vibecode.bitrix24.tech/v1/calls/followups/12345?mentionFormat=none" \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN"
```

### JavaScript — личный ключ

```javascript
const params = new URLSearchParams({ mentionFormat: 'none' })

const res = await fetch(`https://vibecode.bitrix24.tech/v1/calls/followups/12345?${params}`, {
  headers: { 'X-Api-Key': 'YOUR_API_KEY' },
})

const body = await res.json()

if (!body.success) {
  // Ошибку разбираем явно: иначе на 403 или 422 код упадёт на обращении к data
  throw new Error(`${body.error.code}: ${body.error.message}`)
}

const { data } = body
const call = data.item

// Готовые AI-блоки перечислены в outcomes, неготовые приходят как null
console.log(call.outcomes, `${call.durationSeconds} сек`)
console.log(call.overview?.topic ?? 'обзор ещё не сформирован')

for (const item of call.overview?.actionItems ?? []) {
  console.log('задача:', item.actionItem, '— цитата:', item.quote)
}

// speakerAnalysis отсутствует, если разбор участников на портале недоступен
for (const speaker of call.insights?.speakerAnalysis ?? []) {
  console.log(`участник ${speaker.userId}: ${speaker.talkPercentage}% времени, оценка ${speaker.efficiencyValue}`)
}

// Критерии — карта, ключи зависят от типа встречи: обходим по ключам
for (const [code, criterion] of Object.entries(call.evaluation?.criteria ?? {})) {
  console.log(code, criterion.value ? 'выполнен' : 'не выполнен', '—', criterion.thoughts)
}
```

### JavaScript — OAuth-приложение

```javascript
const params = new URLSearchParams({ mentionFormat: 'none' })

const res = await fetch(`https://vibecode.bitrix24.tech/v1/calls/followups/12345?${params}`, {
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
  },
})

const body = await res.json()

if (!body.success) {
  // Ошибку разбираем явно: иначе на 403 или 422 код упадёт на обращении к data
  throw new Error(`${body.error.code}: ${body.error.message}`)
}

const { data } = body
console.log(data.item)
```

## Поля ответа

| Поле | Тип | Описание |
|------|-----|---------|
| `success` | boolean | Всегда `true` при успехе |
| `data.item` | object | Follow-up звонка. Поля — в таблице ниже |

Поля объекта `item`:

| Поле | Тип | Описание |
|------|-----|---------|
| `item.callId` | number | Идентификатор звонка |
| `item.callType` | number | Вид звонка: `1` — мгновенный, `2` — конференция, `3` — большая комната |
| `item.initiatorId` | number | Инициатор звонка. Карточка сотрудника — `GET /v1/users/:id` |
| `item.startDate` | string | Начало звонка, ISO 8601 в UTC |
| `item.endDate` | string \| null | Окончание звонка, ISO 8601 в UTC |
| `item.durationSeconds` | number | Длительность звонка в секундах |
| `item.uuid` | string | Идентификатор сессии звонка |
| `item.language` | string | Код языка транскрипции |
| `item.version` | number | Версия схемы AI-данных |
| `item.participants` | object[] | Участники звонка |
| `item.outcomes` | string[] | Готовые AI-блоки этого звонка |
| `item.createdAt` | string | Время последней AI-записи |
| `item.tracks` | object[] | Записи звонка |
| `item.transcription` | object \| null | Транскрипция разговора |
| `item.overview` | object \| null | Обзор встречи |
| `item.summary` | object \| null | Краткое изложение по фрагментам |
| `item.evaluation` | object \| null | Оценка эффективности встречи |
| `item.insights` | object \| null | Аналитические выводы и разбор участников |

### Участники — `participants[]`

| Поле | Тип | Описание |
|------|-----|---------|
| `userId` | number | Идентификатор сотрудника |
| `name` | string | Имя сотрудника |
| `avatar` | string | Ссылка на аватар |
| `workPosition` | string | Должность |
| `talkedSeconds` | number | Сколько секунд участник говорил |

### Записи звонка — `tracks[]`

| Поле | Тип | Описание |
|------|-----|---------|
| `trackId` | number | Идентификатор записи |
| `type` | string | Вид дорожки, например `mixed_audio` |
| `duration` | number | Длительность записи в секундах |
| `fileName` | string | Имя файла |
| `mimeType` | string | MIME-тип файла |
| `url` | string | Ссылка на скачивание |
| `dateCreate` | string | Когда запись создана, ISO 8601 |

### Транскрипция — `transcription`

| Поле | Тип | Описание |
|------|-----|---------|
| `language` | string | Код языка расшифровки |
| `segments[]` | object[] | Реплики в хронологическом порядке |
| `segments[].userId` | number | Кто говорит |
| `segments[].userName` | string | Имя говорящего |
| `segments[].start` | number | Начало реплики, секунды от начала звонка |
| `segments[].end` | number | Конец реплики, секунды от начала звонка |
| `segments[].text` | string | Текст реплики |

### Обзор встречи — `overview`

| Поле | Тип | Описание |
|------|-----|---------|
| `topic` | string | Тема встречи |
| `detailedTakeaways` | string | Развёрнутые выводы |
| `meetingType` | object | Тип встречи: `typeTag` — код, `title` — название, `explanation` — обоснование |
| `agenda` | object | Повестка: `explanation` — формулировка, `quote` — цитата из разговора |
| `agreements[]` | object[] | Договорённости: `agreement` — формулировка, `quote` — цитата |
| `actionItems[]` | object[] | Задачи: `actionItem` — формулировка, `quote` — цитата |
| `meetings[]` | object[] | Назначенные встречи: `meeting` — формулировка, `quote` — цитата |

### Краткое изложение — `summary`

| Поле | Тип | Описание |
|------|-----|---------|
| `segments[]` | object[] | Фрагменты разговора |
| `segments[].start` | number | Начало фрагмента, секунды |
| `segments[].end` | number | Конец фрагмента, секунды |
| `segments[].title` | string | Заголовок фрагмента |
| `segments[].summary` | string | Изложение фрагмента |

### Аналитические выводы — `insights`

| Поле | Тип | Описание |
|------|-----|---------|
| `speakerEvaluationAvailable` | boolean | Доступен ли разбор участников на портале |
| `speakerAnalysis[]` | object[] | Разбор участников, по убыванию `talkPercentage` |
| `speakerAnalysis[].userId` | number | Идентификатор сотрудника. Карточка — `GET /v1/users/:id` |
| `speakerAnalysis[].detailedInsight` | string | Вывод по участнику |
| `speakerAnalysis[].efficiencyValue` | number | Оценка эффективности участника, от 0 до 100 |
| `speakerAnalysis[].evaluationCriteria` | string | Критерий, по которому дана оценка |
| `speakerAnalysis[].talkPercentage` | number | Доля времени, которую участник говорил, в процентах |
| `speakerAnalysis[].duration` | number | Сколько секунд участник говорил |
| `speakerAnalysis[].durationFormat` | string | То же время в виде `ММ:СС` |
| `meetingStrengths[]` | object[] | Сильные стороны: `strengthTitle` и `strengthExplanation` |
| `meetingWeaknesses[]` | object[] | Слабые стороны: `weaknessTitle` и `weaknessExplanation` |
| `speechStyleInfluence` | string | Как манера речи повлияла на встречу |
| `engagementLevel` | string | Вовлечённость участников |
| `areasOfResponsibility` | string | Кто за что отвечает по итогам |
| `finalRecommendations` | string | Рекомендации к следующей встрече |

### Оценка эффективности — `evaluation`

| Поле | Тип | Описание |
|------|-----|---------|
| `efficiencyValue` | number | Общая эффективность встречи, от 0 до 100 |
| `calendar.overhead` | boolean | Была ли встреча избыточной по времени |
| `criteria` | object | Карта критериев. Ключ — код критерия, значение — объект с полями ниже |
| `criteria.<код>.value` | boolean | Выполнен ли критерий |
| `criteria.<код>.title` | string | Название критерия |
| `criteria.<код>.criteria` | string | Формулировка критерия |
| `criteria.<код>.thoughts` | string | Обоснование оценки |

## Пример ответа

```json
{
  "success": true,
  "data": {
    "item": {
      "callId": 12345,
      "callType": 1,
      "initiatorId": 7,
      "startDate": "2026-01-15T10:00:00+00:00",
      "endDate": "2026-01-15T10:42:00+00:00",
      "durationSeconds": 2520,
      "uuid": "bb085e5d-5160-4a63-9ac4-152248046c39",
      "language": "ru",
      "version": 3,
      "participants": [
        { "userId": 7, "name": "Иван Петров", "workPosition": "Руководитель проекта", "talkedSeconds": 600 }
      ],
      "outcomes": ["transcription", "overview", "summary", "insights", "evaluation"],
      "createdAt": "2026-01-15T11:05:00+00:00",
      "tracks": [
        { "trackId": 100, "type": "mixed_audio", "duration": 2520, "fileName": "call_12345.wav", "mimeType": "audio/wav", "url": "https://example.bitrix24.ru/disk/call_12345.wav", "dateCreate": "2026-01-15T11:00:00+00:00" }
      ],
      "transcription": {
        "language": "ru",
        "segments": [
          { "userId": 7, "userName": "Иван Петров", "start": 12, "end": 25, "text": "Давайте зафиксируем объём спринта. Что берём в работу?" },
          { "userId": 42, "userName": "Мария Иванова", "start": 26, "end": 48, "text": "Предлагаю оставить только импорт каталога, остальное не успеем." },
          { "userId": 7, "userName": "Иван Петров", "start": 49, "end": 70, "text": "Согласен. Тогда прототип показываем в пятницу." }
        ]
      },
      "overview": {
        "topic": "Планирование спринта",
        "detailedTakeaways": "Команда сократила объём спринта до импорта каталога и договорилась показать прототип в пятницу.",
        "meetingType": {
          "typeTag": "planning",
          "title": "Планирование",
          "explanation": "Участники распределяли задачи и сроки на ближайшую итерацию."
        },
        "agenda": {
          "explanation": "Определить объём спринта и сроки демонстрации.",
          "quote": "Давайте зафиксируем объём спринта."
        },
        "agreements": [
          { "agreement": "В спринт берём только импорт каталога", "quote": "Предлагаю оставить только импорт каталога, остальное не успеем." }
        ],
        "actionItems": [
          { "actionItem": "Подготовить прототип к пятнице", "quote": "Тогда прототип показываем в пятницу." }
        ],
        "meetings": [
          { "meeting": "Демонстрация прототипа в пятницу", "quote": "Тогда прототип показываем в пятницу." }
        ]
      },
      "summary": {
        "segments": [
          { "start": 0, "end": 600, "title": "Объём спринта", "summary": "Обсудили, что успеет команда, и сократили набор задач до импорта каталога." },
          { "start": 601, "end": 2520, "title": "Сроки и демонстрация", "summary": "Договорились показать прототип в пятницу и вернуться к остальным задачам в следующем спринте." }
        ]
      },
      "insights": {
        "speakerEvaluationAvailable": true,
        "speakerAnalysis": [
          {
            "userId": 42,
            "detailedInsight": "Участник предложил сократить объём и обосновал это оценкой сроков.",
            "efficiencyValue": 82,
            "evaluationCriteria": "Конструктивность предложений",
            "talkPercentage": 66,
            "duration": 1200,
            "durationFormat": "20:00"
          },
          {
            "userId": 7,
            "detailedInsight": "Участник вёл встречу и зафиксировал договорённости.",
            "efficiencyValue": 74,
            "evaluationCriteria": "Управление обсуждением",
            "talkPercentage": 34,
            "duration": 600,
            "durationFormat": "10:00"
          }
        ],
        "meetingStrengths": [
          { "strengthTitle": "Чёткий итог", "strengthExplanation": "Встреча завершилась зафиксированным решением и сроком." }
        ],
        "meetingWeaknesses": [
          { "weaknessTitle": "Неравное участие", "weaknessExplanation": "Две трети времени говорил один участник." }
        ],
        "speechStyleInfluence": "Спокойный темп речи помог быстро прийти к решению.",
        "engagementLevel": "Высокая вовлечённость обоих участников.",
        "areasOfResponsibility": "Импорт каталога закреплён за Марией Ивановой.",
        "finalRecommendations": "Заранее рассылать повестку, чтобы сократить обсуждение объёма."
      },
      "evaluation": {
        "efficiencyValue": 75,
        "calendar": { "overhead": false },
        "criteria": {
          "agenda_defined": {
            "value": true,
            "criteria": "Повестка обозначена в начале встречи",
            "title": "Повестка",
            "thoughts": "Ведущий сформулировал цель первой репликой."
          },
          "decisions_made": {
            "value": true,
            "criteria": "Приняты решения по обсуждаемым вопросам",
            "title": "Решения",
            "thoughts": "Объём спринта и срок демонстрации зафиксированы."
          },
          "all_participants_involved": {
            "value": false,
            "criteria": "Все участники вовлечены в обсуждение",
            "title": "Вовлечённость",
            "thoughts": "Распределение реплик — 66 на 34 процента."
          }
        }
      }
    }
  }
}
```

Ключи в `evaluation.criteria` — коды критериев. Их набор зависит от типа встречи, поэтому обходите карту по ключам, а не по фиксированному списку.

## Пример ответа при ошибке

422 — звонок не найден или его Follow-up недоступен:

```json
{
  "success": false,
  "error": {
    "code": "BITRIX_ERROR",
    "message": "Нет доступа к данным Follow-up"
  }
}
```

Текст `error.message` приходит от Битрикс24, поэтому его формулировка и язык зависят от языка портала. Ветвитесь по `error.code`, а не по тексту сообщения.

## Ошибки

| HTTP | Код | Описание |
|------|-----|---------|
| 400 | `INVALID_PARAMS` | `callId` в пути — не положительное целое число. Проверяется до обращения к Битрикс24 |
| 400 | `INVALID_PARAMS` | Значение query-параметра вне списка допустимых — например `mentionFormat`. Если Битрикс24 отклонил значение по полю, ответ дополнительно содержит массив `error.validation` с именем поля |
| 401 | `MISSING_API_KEY` | Не передан заголовок `X-Api-Key` |
| 401 | `TOKEN_MISSING` | У ключа нет токенов портала. Ключ OAuth-приложения требует заголовок `Authorization: Bearer` |
| 403 | `SCOPE_DENIED` | У ключа нет скоупа `call` |
| 403 | `BITRIX_ACCESS_DENIED` | Битрикс24 отказал в доступе. Частый случай — набор скоупов, с которым ключ обращается к порталу, не содержит `call`. Полный разбор причин и что делать по типу ключа — [Ошибки](/docs/errors#bitrix_access_denied-403) |
| 422 | `METHOD_NOT_YET_AVAILABLE` | Обновление `call 26.600.0` на портале ещё не выпущено. Ответ содержит поле `error.release` со значением `call 26.600.0` |
| 422 | `BITRIX_ERROR` | Запрос отклонён Битрикс24. Звонка с таким `callId` нет либо к его Follow-up нет доступа — сообщение «Нет доступа к данным Follow-up». Причина конкретного отказа — в `error.message` |
| 429 | `RATE_LIMITED` | Превышена частота запросов на стороне Битрикс24 |
| 429 | `QUEUE_OVERFLOW`, `QUEUE_TIMEOUT` | Очередь запросов портала переполнена или запрос не дождался очереди. Заголовок `Retry-After` подсказывает задержку перед повтором |
| 503 | `BITRIX_TIMEOUT` | Битрикс24 принял запрос, но не ответил за 15 секунд. Для чтения повтор безопасен |
| 502 | `BITRIX_UNAVAILABLE` | Битрикс24 недоступен |

Полный список общих ошибок API — [Ошибки](/docs/errors).

## Известные особенности

**Отличить несуществующий звонок от отказа в доступе по ответу нельзя.** Оба случая приходят одинаковым ответом. [Список Follow-up](./list.md) тоже не даёт ответа: в него попадают только звонки с готовым Follow-up и только те, к которым у сотрудника есть доступ, — отсутствие звонка в списке не означает, что звонка нет.

**Звонки без AI-обработки.** Если звонок завершён, но Follow-up по нему не сформирован, метод возвращает объект с метаданными: AI-блоки приходят как `null`, а `outcomes` — пустым массивом.

**Разбор участников доступен не на каждом портале.** Если на портале он недоступен, блок `insights` приходит с `speakerEvaluationAvailable: false` и пустым разбором участников. Это не ошибка запроса — структура ответа не меняется.

**Доступ ограничен правами сотрудника.** Follow-up доступен тому, кто участвовал в звонке или состоит в связанном чате — в том числе если его добавили в чат уже после разговора. Администратор портала видит Follow-up любого звонка портала.

## Смотрите также

- [Список Follow-up](/docs/calls/followup/list)
- [Follow-up звонков](/docs/calls/followup)
- [Звонки](/docs/calls)
- [Ошибки](/docs/errors)
