Pleep Docs

Исходящие вебхуки

Pleep вызывает ваш эндпоинт, когда лид меняет этап в воронке, сделка меняет стадию в вашей CRM или заканчивается звонок голосового агента

Исходящие вебхуки позволяют не опрашивать API, а получать события в момент, когда они происходят. Pleep отправляет POST с JSON на ваш HTTPS-эндпоинт и подписывает каждый запрос.

Это не то же самое, что входящие вебхуки

Входящие вебхуки — когда внешняя система вызывает Pleep, чтобы разбудить агента в диалоге. Здесь наоборот: Pleep вызывает вас.

Настройка

Откройте Настройки → Разработчикам, раздел «Исходящие вебхуки». Укажите название, HTTPS-адрес и события, на которые подписываетесь. После сохранения один раз показывается секрет — сохраните его, повторно он не отображается.

Там же видно последние 20 попыток доставки с кодом ответа, телом и текстом ошибки, а эндпоинт можно временно выключить или сменить ему секрет.

События

СобытиеКогда срабатывает
lead.status_changedЛид перешёл на другой этап воронки Pleep. Срабатывает независимо от того, кто его передвинул: AI-агент, оператор в интерфейсе, сценарий или вызов set-status
crm.status_changedСделка или лид сменил стадию во внешней CRM: amoCRM или Битрикс24
call.completedЗакончился звонок голосового агента, входящий или исходящий. Приходит и тогда, когда человек не взял трубку

crm.status_changed требует подключённой внешней CRM

Это событие транслирует изменения из amoCRM или Битрикс24. Если вы ведёте воронку внутри Pleep и внешняя CRM не подключена, событие не сработает никогда. Для воронки Pleep подписывайтесь на lead.status_changed.

Формат запроса

Заголовки

ЗаголовокОписание
X-Pleep-EventИмя события
X-Pleep-DeliveryИдентификатор события, совпадает с полем id в теле
X-Pleep-TimestampВремя отправки, unix-секунды
X-Pleep-SignatureПодпись вида sha256=<hex>

Конверт

Тело любого события имеет одинаковую обёртку, полезная нагрузка лежит в data.

ПолеТипОписание
idstringИдентификатор события. Не меняется между повторными попытками — используйте его для дедупликации
eventstringИмя события
occurred_atstringВремя события (ISO 8601)
application_idstringИдентификатор приложения
dataobjectПолезная нагрузка, зависит от события

lead.status_changed

{
  "id": "6f1c9a2e-4b55-4a1e-9f0d-2b7c1d3e4f50",
  "event": "lead.status_changed",
  "occurred_at": "2026-08-21T10:00:00.000Z",
  "application_id": "7dbbac93-00db-46a7-be99-9fc45d3a705d",
  "data": {
    "thread_id": "thread_0765c2bd-980d-4c65-aeea-d04069646be5",
    "assistant_id": "6cc47669-8659-40ad-9d4d-bf914323120d",
    "phone": "77051234567",
    "channel": "WABA_COEXISTENCE",
    "from": { "id": "7f3a1c20-…", "name": "Визит назначен", "kind": "active" },
    "to":   { "id": "9c8b7a60-…", "name": "Продажа", "kind": "success" },
    "changed_by": "AI"
  }
}
ПолеТипОписание
thread_idstringИдентификатор диалога, тот же что в get-threads и в ссылке на диалог в мониторе
phonestring | nullНомер клиента, если у канала он есть
fromobject | nullЭтап, с которого ушёл лид. null, если этапа не было
toobject | nullНовый этап. null, если этап сняли
changed_bystring | nullКто передвинул: AI, OPERATOR, SCENARIO, SYSTEM или VERIFIED_OUTCOME

crm.status_changed

{
  "id": "8a2d7c11-9e33-4c22-b810-5f6a7b8c9d01",
  "event": "crm.status_changed",
  "occurred_at": "2026-08-21T10:05:00.000Z",
  "application_id": "7dbbac93-00db-46a7-be99-9fc45d3a705d",
  "data": {
    "thread_id": "thread_0765c2bd-980d-4c65-aeea-d04069646be5",
    "assistant_id": "6cc47669-8659-40ad-9d4d-bf914323120d",
    "provider": "amocrm",
    "entity_type": "lead",
    "entity_id": "4242",
    "pipeline_id": "77",
    "from_status_id": "141",
    "to_status_id": "142",
    "contact_phone": "77051234567"
  }
}

thread_id может быть null

Лид, заведённый прямо в CRM руками или импортом, ещё ни разу вам не писал, поэтому диалога у него нет. Это штатная ситуация, а не ошибка: в таком случае thread_id равен null, а единственная зацепка за человека — contact_phone. Обработчик должен это учитывать.

call.completed

Приходит после каждого звонка голосового агента. Исходящего: через make-call, из сценария или рассылки. И входящего. По телефону и в WhatsApp. Одно событие на звонок, в том числе когда человек не взял трубку, было занято или он сбросил вызов.

{
  "id": "3b0e6f4a-2c1d-4e8f-9a7b-5c6d7e8f9012",
  "event": "call.completed",
  "occurred_at": "2026-11-08T10:01:12.000Z",
  "application_id": "7dbbac93-00db-46a7-be99-9fc45d3a705d",
  "data": {
    "thread_id": "thread_0765c2bd-980d-4c65-aeea-d04069646be5",
    "assistant_id": "6cc47669-8659-40ad-9d4d-bf914323120d",
    "voice_agent_id": "1e2d3c4b-5a69-4788-9a0b-c1d2e3f4a5b6",
    "conversation_id": "custom_convai_5d0c9e1a",
    "phone": "77051234567",
    "direction": "outbound",
    "status": "answered",
    "duration_seconds": 47,
    "started_at": "2026-11-08T10:00:15.000Z",
    "ended_at": "2026-11-08T10:01:12.000Z",
    "end_reason": null,
    "hangup_cause": null,
    "transcript": [
      { "role": "agent", "text": "Здравствуйте! Это Мария из школы английского. Вам удобно говорить?" },
      { "role": "caller", "text": "Да, слушаю." },
      { "role": "agent", "text": "Вы оставляли заявку на курс. Когда вам удобно прийти на пробный урок?" },
      { "role": "caller", "text": "В субботу утром." }
    ],
    "source": "api"
  }
}

Звонок, на который не ответили:

{
  "id": "c41d2e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
  "event": "call.completed",
  "occurred_at": "2026-11-08T10:11:42.000Z",
  "application_id": "7dbbac93-00db-46a7-be99-9fc45d3a705d",
  "data": {
    "thread_id": "thread_0765c2bd-980d-4c65-aeea-d04069646be5",
    "assistant_id": "6cc47669-8659-40ad-9d4d-bf914323120d",
    "voice_agent_id": "1e2d3c4b-5a69-4788-9a0b-c1d2e3f4a5b6",
    "conversation_id": null,
    "phone": "77051234567",
    "direction": "outbound",
    "status": "busy",
    "duration_seconds": 0,
    "started_at": "2026-11-08T10:11:31.000Z",
    "ended_at": "2026-11-08T10:11:42.000Z",
    "end_reason": "line_hangup",
    "hangup_cause": 17,
    "transcript": [],
    "source": "broadcast"
  }
}
ПолеТипОписание
thread_idstringДиалог, в котором сохранился звонок. Тот же идентификатор, что в ответе make-call, в get-threads и в ссылке на диалог в мониторе
assistant_idstringАссистент, которому принадлежит голосовой агент
voice_agent_idstring | nullГолосовой агент, который звонил или принял звонок. Тот же, что voice_agent_id в make-call
conversation_idstring | nullИдентификатор разговора. null, если звонок не соединился
phonestring | nullНомер собеседника, только цифры, без +
directionstringoutbound: звонил агент. inbound: звонили агенту
statusstringЧем закончился звонок, значения ниже
duration_secondsnumberСколько длился разговор, в секундах. 0, если трубку не взяли
started_atstringКогда начался набор (исходящий) или агент ответил (входящий), ISO 8601 UTC. У исходящего с source: "other" это может быть момент, когда человек взял трубку
ended_atstringКогда звонок закончился, ISO 8601 UTC. Точность около секунды: это момент, когда конец звонка дошёл до Pleep
end_reasonstring | nullЧто закончило звонок, если это известно, значения ниже. null у обычного разговора: кто положил трубку, линия не сообщает
hangup_causenumber | nullКод причины от оператора связи, если звонок закончила сеть. Например, 17 занято, 19 нет ответа. Те же коды, что в make-call
transcriptarrayРазговор так, как он виден в мониторе: реплики { "role": "agent" | "caller", "text": "..." } по порядку. Пустой, если трубку не взяли
sourcestringКто начал звонок, значения ниже
statusЧто значит
answeredЧеловек взял трубку. Сюда же попадает звонок, который сбросили в первые секунды
no_answerЗвонок шёл, но трубку не взяли
busyЗанято
rejectedВызов отклонён. В WhatsApp это значит, что человек сбросил вызов. По телефону так же отвечает и сеть, когда не пропускает вызов, и телефон мог не зазвонить
voicemailОтветил автоответчик, агент молча положил трубку
failedСеть не смогла соединить звонок, или линия отказалась его набирать

Значения совпадают с call_status в ответе make-call: answered там называется connected, а not_connected и network_error здесь приходят как failed.

end_reasonЧто значит
voicemailАгент положил трубку, услышав автоответчик
ring_timeoutНикто не взял трубку, пока шли гудки
line_hangupЗвонок закончила сеть до соединения, код в hangup_cause
sourceКто начал звонок
apiЗапрос make-call, в том числе запланированный на потом через scheduled_at
scenarioДействие «Позвонить» в сценарии
broadcastРассылка, в том числе созданная через API
inboundВходящий звонок
otherОстальное, например звонок, который AI-агент сделал из переписки или запланировал сам

Как связать событие с запросом make-call

В ответе make-call приходит thread_id. Событие call.completed по этому звонку придёт с тем же thread_id и source: "api". conversation_id при звонке через телефонию Pleep известен только после соединения, поэтому для связи используйте thread_id.

Когда события нет

Когда Pleep не стал звонить (закончились минуты, человек отказался от звонков, нерабочее время, агент не настроен), звонка не было и call.completed не придёт. make-call в этом случае сразу отвечает ошибкой, а в итогах рассылки видна причина.

Если линия не сообщила вовремя, чем кончился набор (make-call ответил 504 CALL_RESULT_UNKNOWN), событие придёт, только если звонок соединился.

Если собственный номер агента подключён по-старому, через ElevenLabs, по звонку, где трубку не взяли, события не будет.

Проверка подписи

Подпись считается как HMAC-SHA256 от строки ${timestamp}.${тело}, где timestamp — значение заголовка X-Pleep-Timestamp, а тело — сырые байты запроса.

Проверяйте по сырому телу

Если распарсить JSON и собрать его обратно, порядок ключей и пробелы изменятся, и подпись не сойдётся. Это самая частая ошибка при подключении. Считайте подпись до разбора тела.

Время подписывается вместе с телом, поэтому перехваченный запрос нельзя переиграть позже: сверьте X-Pleep-Timestamp со своими часами и отклоняйте слишком старые.

const crypto = require('node:crypto');
 
function verify(rawBody, headers, secret) {
  const ts = headers['x-pleep-timestamp'];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
 
  const mac = crypto
    .createHmac('sha256', secret)
    .update(`${ts}.${rawBody}`)
    .digest('hex');
 
  const expected = Buffer.from(`sha256=${mac}`);
  const received = Buffer.from(headers['x-pleep-signature'] ?? '');
  if (expected.length !== received.length) return false;
  return crypto.timingSafeEqual(expected, received);
}

Повторные попытки

На любой ответ вне диапазона 2xx, а также на таймаут или сетевую ошибку, Pleep повторит доставку. Всего до 5 попыток с нарастающей паузой. Таймаут одного запроса — 10 секунд.

Отвечайте 2xx сразу, а тяжёлую обработку уносите в свою очередь. Обработчик должен быть идемпотентным по полю id: при повторе оно то же самое.

Каждая попытка попадает в журнал доставок в интерфейсе, так что видно и код ответа, и тело ошибки с вашей стороны.

Требования к эндпоинту

  • Только https://. Адреса на http://, localhost и в приватных диапазонах отклоняются.
  • Адрес проверяется не только при сохранении, но и перед каждой отправкой.
  • Выключенный эндпоинт не вызывается: попытка отмечается в журнале как skipped_inactive.

On this page