Как создать свое приложение для модуля «Коммуникации»
Контакт-центр MiniApp — внешний API
Документация для разработчиков, реализующих собственное приложение, которое подключается к Аспро.Cloud как канал коммуникации (MiniApp) в модуле «Контакт-центр».
1. Обзор
Что такое MiniApp-канал
MiniApp — это тип канала в Контакт-центре Аспро.Cloud, позволяющий вашему приложению (мессенджеру, чат-виджету, корпоративному порталу и т. п.) обмениваться сообщениями с менеджерами Аспро.Cloud.
Для MiniApp вы реализуете обе стороны коммуникации сами:
- Входящий канал — ваш сервер шлёт HTTP-запросы в Аспро.Cloud, когда пользователь пишет/редактирует/удаляет сообщение.
- Исходящий канал — Аспро.Cloud шлёт HTTP-запросы на ваш сервер, когда менеджер отвечает или инициирует чат.
Что нужно сделать
- Создать в Аспро.Cloud внешнее приложение и описать манифест с местом встройки «Подключение к сервису коммуникаций».
- Реализовать у себя страницу подключения — она открывается в iframe внутри Аспро.Cloud в момент, когда менеджер настраивает интеграцию.
- Реализовать у себя приёмник исходящих вебхуков от Аспро.Cloud.
- Подключаться к API Аспро.Cloud для создания/обновления канала, отправки входящих вебхуков, отчёта об ошибках.
Спецификация хуков (OpenAPI 3.1)
Этот документ описывает интеграцию целиком. Полные машиночитаемые схемы payload'ов хуков приложены к статье двумя файлами:
incoming_events.yaml— входящие события (от вашего приложения в Аспро.Cloud).outgoing_hooks.yaml— исходящие хуки (от Аспро.Cloud в ваше приложение).
Открыть yaml в удобном для чтения виде можно через онлайн-сервис ReDoc — https://redocly.github.io/redoc/ (загрузите скачанный файл в форме на странице).
2. Архитектура и принципы интеграции
Между сторонами есть три канала взаимодействия:
Канал | Кто инициирует | Назначение |
|---|---|---|
iframe-страница подключения | Аспро.Cloud открывает её в iframe, делает POST | Пользователь настраивает интеграцию у себя в Cloud; ваше приложение получает контекст пользователя (включая `auth.access_token`) |
API Аспро.Cloud | Ваш сервер | Создание/обновление/удаление канала, отправка входящих вебхуков, отчёт об ошибках |
Исходящие вебхуки | Аспро.Cloud | Доставка сообщений менеджера, инициация чатов, события жизненного цикла канала |
Все API/вебхук-сообщения передают JSON формата:
{ "method": "<имя метода>", "payload": { /* поля */ } }
Глоссарий
Термин | Описание |
|---|---|
Внешнее приложение | Ваш код — серверная часть + iframe-страница подключения |
Манифест | Конфигурация вашего приложения в Аспро.Cloud: места встройки, права доступа, URL'ы |
Место встройки (placement) | Точка в UI Аспро.Cloud, в которой открывается ваша страница. Для контакт-центра это `contactcenter.service.wizard` («Подключение к сервису коммуникаций») |
`app.id` | UUID вашего приложения в Аспро.Cloud. Приходит в POST на iframe |
`account.id` | Числовой ID аккаунта Аспро.Cloud, в котором подключают канал. Приходит в POST на iframe |
`domain` | Домен аккаунта Аспро.Cloud, например `my.aspro.cloud`. Приходит в POST на iframe; используется как base URL для API-запросов |
`auth.access_token` | OAuth2-токен пользователя, открывшего iframe. Им вы вызываете API от имени этого пользователя |
Канал коммуникации (бот) | Конкретная подключенная интеграция: ваш `bot_token`. Создаётся через API |
`bot_id` (uuid канала на стороне CRM) | UUID канала, возвращается из `/contactcenter/bot/create`. Используется в URL входящих вебхуков |
`bot_inner_id` | Числовой id канала в Аспро.Cloud (тот же `id` из ответа `/contactcenter/bot/create`). Приходит в POST на iframe при редактировании. Нужен для update/delete канала через API |
`bot_token` | Идентификатор интеграции на вашей стороне, например ID канала в вашей системе. Вы задаёте его при создании канала, Аспро.Cloud возвращает его в каждом исходящем хуке как `channel_id` |
`manifest_placement_id` | Идентификатор секции `contactcenter.service.wizard` в манифесте (значение поля `id` секции), через которую был подключён канал. Совпадает с `placement.id` из POST на iframe. Используется Аспро.Cloud, чтобы открыть редактирование канала именно на той странице, через которую он был подключён |
`event_id` | Идентификатор события исходящего вебхука. Нужен чтобы отправить ответный `error`, если не удалось обработать запрос, так же можно его использовать для дедупликации событий от CRM |
`external_chat_id` / `external_message_id` / `external_user_id` | Идентификаторы чата / сообщения / пользователя в вашей системе |
`inner_message_id` | Идентификатор сообщения в Аспро.Cloud. Приходит в исходящем `message.new.personal`, должен быть возвращён в `message.completed.personal` |
3. Quick Start
Полный путь от нуля до работающей интеграции:
- Зарегистрируйте приложение в Аспро.Cloud (страница
/module/miniapps/cabinet), добавьте место встройкиcontactcenter.service.wizardс URL вашей страницы подключения. Дайте приложению доступ к модулю «Коммуникации». → §4 - Реализуйте iframe-страницу подключения. На неё Аспро.Cloud делает POST с данными аккаунта и пользовательским OAuth2-токеном. Подключите JS SDK Аспро.Cloud для удобной работы с iframe. → §5
- Реализуйте приёмник исходящих вебхуков на своём сервере по постоянному URL (этот URL вы укажете при создании канала). → §9
- Создайте канал коммуникации через
POST /api/v1/module/contactcenter/bot/createот имени пользователя, который открыл iframe. → §7 - Обменивайтесь сообщениями:
- Когда пользователь пишет — вы шлёте
message.new.personalна/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}. - Когда менеджер пишет — Аспро.Cloud делает POST на ваш
webhook_url, вы доставляете и подтверждаете черезmessage.completed.personal.
- Когда пользователь пишет — вы шлёте
4. Создание приложения в Аспро.Cloud
4.1 Регистрация в кабинете
- Зайдите на
https://{your-domain}/module/miniapps/cabinet. - Нажмите «Создать приложение», заполните основные поля и сохраните.
- Откройте созданное приложение и перейдите в редактирование манифеста.
4.2 Добавление места встройки
Добавьте место встройки «Подключение к сервису коммуникаций» (contactcenter.service.wizard).
Поля места встройки:
Поле | Описание |
|---|---|
id | Стабильный идентификатор места встройки внутри вашего манифеста (≤ 32 символа). **Не меняйте его между версиями манифеста** — он используется как ключ привязки уже подключённых каналов: приходит в POST на iframe как `placement.id` ([§5.1](#51-что-приходит-в-post)), и должен передаваться в `manifest_placement_id` при создании канала через API ([§7.2](#72-payload)). Если у вашего приложения **несколько** мест встройки `contactcenter.service.wizard` — каждое из них должно иметь свой уникальный `id`. |
Заголовок | Название, которое менеджер увидит в списке возможных каналов в Контакт-центре. Также используется как название канала по умолчанию, если при создании канала в payload не передан `name` |
Иконка (src) | Публичная HTTPS-ссылка на иконку канала. Аспро.Cloud отображает её рядом с названием подключённого канала во всех местах UI (список каналов, лента чатов, шапка диалога, история CRM). Иконку можно менять в новых версиях манифеста — Аспро.Cloud автоматически обновит её у всех подключённых каналов после публикации |
Действие | Как откроется страница подключения: модальное окно, боковая панель, новая вкладка |
title | Заголовок открытой страницы (если выбрано модальное окно или боковая панель) |
Размер | Размер модального окна / боковой панели |
iframe (src) | URL вашей страницы подключения. Аспро.Cloud будет открывать её внутри своего интерфейса |
4.3 Права доступа
В настройках манифеста выдайте приложению доступ к API тех модулей, к которым оно будет обращаться:
- «Коммуникации» — обязательно, иначе создать/обновить канал через API не получится.
- «Пользователи» / «Сотрудники» — если ваше приложение хранит данные о пользователе у себя и будет запрашивать их через
/api/v1/module/core/user/get(см. §5.2).
Без выданного доступа API вернёт ошибку авторизации.
После сохранения манифеста на странице /_module/contactcenter/view/index?_ref=menu_app&tab=connected_bots, при клике на кнопку «Подключить», ваша интеграция появится в списке возможных каналов. По клику Аспро.Cloud откроет ваш iframe (src).
5. Место встройки: iframe-страница подключения
Когда менеджер кликает на пункт вашей интеграции в списке каналов, Аспро.Cloud открывает в iframe URL, который вы указали в iframe (src), и отправляет на него POST-запрос с контекстом.
5.1 Что приходит в POST
{
"domain": "my.aspro.cloud",
"language": "ru",
"placement": {
"id": "123abc",
"code": "contactcenter.service.wizard"
},
"https": "1",
"account": {
"id": "123456"
},
"app": {
"id": "e9277716-2840-11f1-9ce9-4a62868fa1db",
"version": "1.0.0"
},
"auth": {
"expires_at": "2026-03-27 17:29:33",
"access_token": "...",
"refresh_token": "..."
},
"ratelimit": {
"limit": "16"
},
"bot_id": "<uuid канала, если открывается редактирование>",
"bot_inner_id": "<числовой id канала, если открывается редактирование>"
}
Поле | Описание |
|---|---|
domain | Домен аккаунта Аспро.Cloud. Используйте как base URL: `https://{domain}/api/v1/...` |
language | Язык интерфейса пользователя |
placement.id | ID конкретного места встройки из вашего манифеста (тот, который вы задали в поле `id` секции `placements`). Сохраните это значение — его нужно передать в `manifest_placement_id` при создании канала ([§7.2](#72-payload)), чтобы Аспро.Cloud запомнил, через какую именно секцию манифеста подключён канал. Особенно важно, если у приложения несколько мест встройки `contactcenter.service.wizard` |
placement.code | Код места встройки. Для контакт-центра — всегда `contactcenter.service.wizard` |
account.id | Числовой ID аккаунта Аспро.Cloud. Используется в URL входящих вебхуков |
app.id | UUID вашего приложения |
app.version | Текущая версия манифеста (актуально для публичных приложений) |
auth.access_token | OAuth2-токен пользователя, открывшего iframe |
auth.refresh_token | Refresh-токен пользователя |
auth.expires_at | Время истечения access_token |
ratelimit.limit | Лимит запросов к API |
bot_id | UUID существующего канала. Если поле есть — это страница редактирования. Если нет — страница создания нового канала |
bot_inner_id | Числовой id существующего канала в Аспро.Cloud (приходит только на странице редактирования, вместе с `bot_id`). Это тот же `id`, что вернулся из `/contactcenter/bot/create` ([§7.3](#73-ответ)). Используйте его для обновления/удаления канала через API ([§7.4](#74-редактирование)). На странице создания нового канала поля нет |
5.2 Свежесть токенов и данные пользователя
Данные auth.* всегда актуальны на момент открытия iframe — Аспро.Cloud сам проверяет и обновляет токены перед отрисовкой страницы.
Если ваше приложение хранит данные пользователя у себя, выполните запрос к /api/v1/module/core/user/get после получения POST и обновите свою копию:
GET https://{domain}/api/v1/module/core/user/get
Authorization: Bearer {auth.access_token}
Для работы этого endpoint в манифесте должен быть выдан соответствующий доступ (§4.3).
5.3 JS SDK Аспро.Cloud
Для работы из iframe (вызов нативных диалогов, тостов, закрытие фрейма, чтение контекста) подключите официальный JS SDK:
https://aspro.cloud/miniapp-jssdk/getting-started/install
SDK даёт удобный доступ к domain, app.id, текущему пользователю, а также к UI-примитивам Аспро.Cloud.
6. Аутентификация
6.1 Запросы от вашего приложения в Аспро.Cloud
API (создание/обновление канала, получение пользователя):
заголовок Authorization: Bearer {auth.access_token}. Токен — пользовательский, выдаётся в момент открытия iframe. Запрос выполняется от имени этого пользователя.
Входящие вебхуки (/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}):
аутентификация по секретному bot_id (uuid канала на стороне CRM) в URL. bot_id сгенерирован сервером, неизменяем, является секретом — храните его как API-ключ. account_id это ID вашего портала Аспро.Cloud, который приходит в POST в iframe.
6.2 Запросы от Аспро.Cloud к вашему приложению
Исходящие вебхуки приходят на ваш webhook_url без подписи и без Bearer-токена. Подтверждением подлинности служит сам URL: если он непредсказуемый, утечка маловероятна.
Рекомендации:
- Дополнительно проверяйте, что приходящий в
payloadchannel_idсовпадает с вашимbot_tokenинтеграции. - Используйте HTTPS со своей стороны.
7. Создание канала коммуникации через API
7.1 Endpoint
POST https://{domain}/api/v1/module/contactcenter/bot/create
Authorization: Bearer {auth.access_token}
Content-Type: application/json
где {domain} — domain из POST на iframe.
7.2 Payload
{
"name": "<имя канала, заданное менеджером>",
"webhook_url": "https://<your-app>/webhook/aspro",
"bot_token": "<ваш идентификатор интеграции>",
"manifest_placement_id": "<placement.id из POST на iframe>",
"active": 1,
"can_write_first": true
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
name | string | нет | Название канала, отображается менеджеру в Контакт-центре. Обычно задаётся менеджером на странице подключения. Если не передано — Аспро.Cloud подставит заголовок места встройки из манифеста |
webhook_url | string | да | HTTPS-URL вашего сервера для приёма исходящих вебхуков |
bot_token | string | да | Идентификатор интеграции на вашей стороне. Возвращается в каждом исходящем вебхуке как `channel_id` — используется, чтобы вы могли определить, к какому каналу относится событие, особенно если у вас несколько каналов на одном `webhook_url` |
manifest_placement_id | string | да | Идентификатор места встройки манифеста, через которое подключается этот канал. Берётся из `placement.id` POST'а на iframe ([§5.1](#51-что-приходит-в-post)). Длина ≤ 32 символа. Значение должно совпадать с одной из секций `contactcenter.service.wizard` в текущем манифесте — иначе API вернёт `422`. См. также [§7.5](#75-валидация-manifest_placement_id) |
active | int / bool | нет | `1` — активен (по умолчанию), `0` — выключен. Деактивированный канал не принимает входящие хуки (отвечает `409`) |
can_write_first | boolean | нет | `true` — у менеджера доступна кнопка «Написать первому». По умолчанию `false` |
7.3 Ответ
{
"data": {
"id": 123,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"name": "Мой канал",
"webhook_url": "https://<your-app>/webhook/aspro",
"bot_token": "...",
"active": true
}
}
uuid— этоbot_id, который вы используете в URL входящих вебхуков. Это секрет.id— внутренний числовой ID канала для последующего обновления/удаления.
Сохраните uuid и id в БД вашего приложения, связав с вашей внутренней интеграцией.
7.4 Редактирование
Если в POST на iframe пришёл bot_id — это страница редактирования. Найдите канал по bot_id (uuid) на своей стороне и обновите его поля через update-метод API (name/webhook_url/active/can_write_first). Для самого API-запроса обновления используйте числовой идентификатор канала bot_inner_id, который приходит в том же POST на iframe (§5.1) — это тот же id, что вернулся из /contactcenter/bot/create.
Поля manifest_placement_id, type и ref_id существующего канала изменить нельзя — Аспро.Cloud игнорирует их в payload запроса на обновление. Чтобы сменить место встройки у канала, удалите его и подключите заново.
7.5 Валидация manifest_placement_id
Аспро.Cloud валидирует поле при создании канала. Если передано значение, которого нет среди секций contactcenter.service.wizard в текущем манифесте, либо если в манифесте этих секций вообще нет — API возвращает HTTP 422 с локализованным описанием ошибки в поле errors.manifest_placement_id.
Передавайте именно то значение, которое пришло вам в placement.id POST'а на iframe (§5.1) — оно гарантированно соответствует секции манифеста, через которую пользователь открыл подключение.
7.6 Удаление канала
Канал можно удалить через API. Если при удалении у канала был задан webhook_url, Аспро.Cloud отправит вам исходящий хук bot.deleted — это хороший момент очистить связанные данные у себя.
8. Входящие вебхуки (ваше приложение → Аспро.Cloud)
Полные схемы payload'ов всех методов этого раздела — в приложенном к статье файле incoming_events.yaml (OpenAPI 3.1).8.1 Endpoint и общая структура
POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}
Content-Type: application/json
где:
{domain}—domainаккаунта (вы сохранили его при создании канала);{account_id}—account.idиз POST на iframe;{bot_id}—uuidканала, который вернул/contactcenter/bot/create.
Тело: { "method": "<...>", "payload": { ... } }.
8.2 Коды ответа
HTTP | Тело | Что означает |
|---|---|---|
200 | `{"success": true}` | Событие принято |
400 | `{"success": false, "message": "<описание>"}` | Невалидный payload |
404 | `{"success": false, "message": "Not found"}` | Сообщение не найдено или принадлежит другому каналу |
409 | `{"success": false, "error": "...", "message": "..."}` | Канал деактивирован или аккаунт заблокирован |
410 | пусто | Канал или аккаунт удалён |
500 | `{"success": false, "message": "Internal error"}` | Временная ошибка Аспро.Cloud |
409 и 410 — альтернативный путь обнаружения деактивации/удаления: если по какой-то причине вы не получилиbot.deactivated/bot.deleted, эти коды служат страховкой.
8.3 message.new.personal — новое сообщение из внешнего канала
Когда вызывать: пользователь в вашем приложении написал в чат, или нужно зафиксировать сообщение менеджера, отправленное вне Аспро.Cloud (echo).
{
"method": "message.new.personal",
"payload": {
"text": "Здравствуйте, по заказу №42…",
"send_date": 1710752400,
"external_message_id": "msg_123",
"external_user_id": "user_456",
"external_chat_id": "chat_789",
"direction": 0,
"event_id": null,
"attachments": [],
"user_data": {
"name": "Иван Иванов",
"username": "ivan",
"phone": "+79001234567",
"email": "ivan@example.com",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/ivan"
}
}
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
text | string | нет (по умолчанию `""`) | Текст сообщения |
send_date | int | нет (по умолчанию `time()`) | Unix-таймштамп (секунды) |
external_message_id | string | да | ID сообщения в вашей системе. Используется для дедупликации |
external_user_id | string | да | ID пользователя-собеседника в вашей системе |
external_chat_id | string | да | ID чата в вашей системе |
direction | int | нет | `0` — входящее (клиент → менеджер), значение по умолчанию. `1` — исходящее (echo сообщения менеджера, отправленного вне Аспро.Cloud) |
event_id | string | нет | Только для echo (`direction=1`) **первого** сообщения чата, инициированного менеджером (см. [§9.5](#95-chatinitpersonal--менеджер-инициирует-чат)): верните сюда `event_id` из соответствующего `chat.init.personal`. По нему Аспро.Cloud привязывает первое сообщение к менеджеру-инициатору (иначе оно отобразится со стороны контакта). Для обычных входящих и последующих echo не нужен. |
attachments | array | нет | Список вложений, см. [§10.2](#102-attachment-входящий) |
user_data | object | нет | Данные собеседника, см. [§10.1](#101-userdata) |
user_data содержит данные собеседника, а не отправителя. В исходящих сообщениях (direction = 1) — это всё равно клиент (тот, кому пишут), а не менеджер.
external_user_id — это id собеседника, не отправителя. Для исходящих echo сообщений это всё равно id клиента, не id менеджера.
Идемпотентность: сообщение дедуплицируется по external_message_id в пределах одного треда. Повторный запрос с тем же ID — no-op (200 OK, без побочных эффектов).
8.4 message.edit.personal — редактирование сообщения
{
"method": "message.edit.personal",
"payload": {
"external_message_id": "msg_123",
"edited_date": 1710752400,
"new_text": "Исправленный текст"
}
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
external_message_id | string | да | ID редактируемого сообщения |
edited_date | int | нет (по умолчанию `time()`) | Unix-таймштамп редактирования |
new_text | string | нет (по умолчанию `""`) | Новый текст |
Если сообщение не найдено или принадлежит другому каналу — 404.
8.5 message.delete.personal — удаление сообщения
{
"method": "message.delete.personal",
"payload": {
"external_message_id": "msg_123",
"deleted_date": 1710752400
}
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
external_message_id | string | да | ID удаляемого сообщения |
deleted_date | int | нет | Unix-таймштамп удаления |
Удаление мягкое — сообщение скрывается у менеджера. Удалить можно только сообщения своего канала; попытка удалить чужое вернёт 404.
8.6 message.completed.personal — подтверждение доставки
Отправляется после того, как сообщение из исходящего хука §9.4 message.new.personal реально доставлено в ваш канал и получило в нём external_message_id.
Без этого события сообщение менеджера остаётся в Аспро.Cloud в статусе «отправляется».
{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "ext_msg_555"
}
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
inner_message_id | int | да | ID сообщения в Аспро.Cloud (приходит в исходящем `message.new.personal`) |
external_message_id | string | да | ID сообщения в вашей системе |
event_id | string | нет | `event_id` из исходного исходящего `message.new.personal`. Можно вернуть для однородности; в текущей версии Аспро.Cloud принимает, но не использует (корреляция идёт по `inner_message_id`). Зарезервировано под будущую обработку подтверждения отправки. |
8.7 error — не удалось обработать исходящий хук
Отправляется, если ваше приложение не смогло обработать исходящий вебхук §9.4 message.new.personal или §9.5 chat.init.personal (например, чат закрыт, пользователь заблокировал бота, во внешнем канале нет такого получателя).
{
"method": "error",
"payload": {
"event_id": "<значение event_id из исходящего хука>",
"message": "User not found"
}
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
event_id | string | да | Значение `event_id` из исходного исходящего вебхука — возвращается ровно как пришло, без изменений |
message | string | нет | Свободный текст для лога |
event_id — непрозрачный токен. Аспро.Cloud использует его, чтобы понять, к какому именно событию относится ошибка, и корректно обновить статус сообщения/чата. Не парсите и не модифицируйте его.
Эффект:
- Если
event_idсоответствует исходящемуmessage.new.personal— сообщение менеджера переводится в статус «не доставлено» (иконка ошибки в UI). - Если соответствует
chat.init.personal— менеджер получает уведомление о неудачной инициации чата.
event_id имеет ограниченный срок жизни на стороне Аспро.Cloud — отправляйте error сразу, как только обнаружили проблему. Хранить event_id у себя дольше обработки текущего запроса не требуется.
8.8 message.read.personal (зарезервирован)
Метод принимается сервером, но в текущей версии не обрабатывается — это no-op. Не стройте бизнес-логику на read-receipts: статус прочтения нигде не сохраняется и менеджеру не отображается.
9. Исходящие вебхуки (Аспро.Cloud → ваше приложение)
Полные схемы payload'ов всех хуков этого раздела — в приложенном к статье файле outgoing_hooks.yaml (OpenAPI 3.1).9.1 Принцип: хук = триггер, доставка асинхронна
Исходящий вебхук — это сигнал-триггер: «менеджер написал, отправь во внешний канал», «менеджер инициирует чат, открой его у себя» и т. п. Аспро.Cloud не ждёт в HTTP-ответе результата фактической доставки сообщения.
Поток обработки выглядит так:
- Аспро.Cloud делает POST на ваш
webhook_url. - Вы отвечаете
2xxсразу, как только приняли запрос в обработку. Ответ означает только «принято». - Дальше вы доставляете сообщение пользователю на своей стороне (это может занять любое время).
- По итогу вы шлёте встречный входящий вебхук в Аспро.Cloud:
Без встречного хука сообщение менеджера в Аспро.Cloud остаётся в статусе «отправляется» (для message.new.personal) или не превратится в реальный тред (для chat.init.personal).
9.2 Общие параметры запроса
- Endpoint: ваш
webhook_url, заданный при создании канала. - Метод:
POST,Content-Type: application/json. - Тело:
{ "method": "<...>", "payload": { ... } }. - Ответ: любой
2xx(200/201/202) трактуется как успех — событие принято в обработку. Тело ответа сервер не парсит.
9.3 Retry и таймауты
При не-2xx ответе или сетевой ошибке Аспро.Cloud выполняет до 3 попыток с экспоненциальным backoff [500, 1000, 2000] мс:
Попытка | Когда |
|---|---|
1 | Сразу |
2 | Через 0.5 с после неудачи попытки 1 |
3 | Через 1 с после неудачи попытки 2 |
После 3-й неудачной попытки счётчик неудач канала инкрементируется. На 10-й неудаче подряд канал автоматически деактивируется, и вы получите хук bot.deactivated с reason: "auto_unreachable".
Классификация ответов:
200/201/202→ успех. Счётчик неудач сбрасывается.400–499→ логический отказ (ваш сервер ответил, но отказался обработать). Канал не деактивируется, но соответствующее сообщение менеджера будет помечено как «не доставлено». Retry не выполняется.5xx, таймауты, DNS/TCP/TLS-ошибки → инфраструктурный сбой. Будет выполнено до 3 попыток с backoff; счётчик неудач инкрементируется.- Редиректы (
3xx) запрещены — отвечайте200напрямую.
Тайм-аут отдельного запроса — порядка десятков секунд. Делайте обработку быстрой, тяжёлую работу выносите в фоновую задачу.
9.4 message.new.personal — менеджер отправил сообщение
{
"method": "message.new.personal",
"payload": {
"channel_id": "<ваш bot_token>",
"inner_message_id": "9001",
"external_chat_id": "<id чата в вашем канале>",
"text": "Добрый день! Чем могу помочь?",
"timestamp": 1710752400,
"event_id": "<непрозрачный токен>",
"attachments": []
}
}
Поле | Тип | Описание |
|---|---|---|
channel_id | string | Ваш `bot_token`, который вы передали при создании канала. Используйте, чтобы определить интеграцию, если у вас несколько каналов на одном `webhook_url` |
inner_message_id | string | ID сообщения в Аспро.Cloud. Сохраните — он понадобится для `message.completed.personal` |
external_chat_id | string | ID чата в вашей системе (вы прислали его в `message.new.personal`) |
text | string | Текст сообщения |
timestamp | int | Unix-таймштамп отправки |
event_id | string | Непрозрачный токен события. Нужен только если вы будете отправлять ответный `error` |
attachments | array | Список вложений, см. [§10.3](#103-attachment-исходящий) |
Что делать:
- Ответьте
200сразу. - Найдите чат по
external_chat_id, доставьте сообщение пользователю. - После доставки — отправьте
message.completed.personal(§8.6) сinner_message_idи своимexternal_message_id. - При ошибке — отправьте
error(§8.7) с переданнымevent_id.
9.5 chat.init.personal — менеджер инициирует чат
Менеджер нажал «Написать первому». Чат на стороне Аспро.Cloud ещё не создан — он появится, когда ваше приложение пришлёт message.new.personal с новым external_chat_id.
Доступен только если у канала can_write_first = true.
{
"method": "chat.init.personal",
"payload": {
"channel_id": "<ваш bot_token>",
"to": {
"phone": "+79001234567",
"email": "user@example.com",
"name": "Иван Иванов",
"other": null
},
"message": { "text": "Добрый день!" },
"event_id": "<непрозрачный токен>"
}
}
Поле | Тип | Описание |
|---|---|---|
channel_id | string | Ваш `bot_token` |
to | object | Получатель — см. [§10.4](#104-to) |
message.text | string | Текст первого сообщения |
event_id | string | Непрозрачный токен события. **Сохраните его** — его нужно вернуть в ответном `message.new.personal` (шаг 5), чтобы первое сообщение было атрибутировано менеджеру-инициатору |
Что делать:
- Ответьте
200сразу. - Найдите/создайте пользователя на своей стороне по полям
to(приоритеты — на ваше усмотрение:phone→email→other→name). - Откройте чат с этим пользователем, получите ваш
external_chat_id. - Доставьте
message.textпользователю. - Пришлите
message.new.personalс этимexternal_chat_id,direction: 1(echo от менеджера) иevent_idиз этогоchat.init.personal— так чат будет создан в Аспро.Cloud, а первое сообщение отобразится от менеджера-инициатора (безevent_idоно отобразится со стороны контакта). В последующихmessage.new.personalэтого чатаevent_idне нужен. - При ошибке — отправьте
errorс переданнымevent_id.
9.6 bot.activated / bot.deactivated / bot.deleted
События жизненного цикла канала. Fire-and-forget — действие уже совершено на стороне Аспро.Cloud, ваш ответ нужен только для логов. Возвращайте 2xx.
{
"method": "bot.deactivated",
"payload": {
"channel_id": "<ваш bot_token>",
"reason": "manual"
}
}
{
"method": "bot.activated",
"payload": { "channel_id": "<ваш bot_token>" }
}
{
"method": "bot.deleted",
"payload": { "channel_id": "<ваш bot_token>" }
}
Возможные значения reason у bot.deactivated:
Значение | Описание |
|---|---|
manual | Администратор аккаунта вручную выключил канал |
auto_unreachable | Аспро.Cloud деактивировал канал автоматически из-за стабильно недоступного `webhook_url` (10 неуспехов подряд). После того как ваш сервер снова будет доступен, администратор должен включить канал вручную |
subscription_expired | Истёк тариф аккаунта Аспро.Cloud |
10. Объекты данных
10.1 UserData
Используется в §8.3 message.new.personal. Все поля опциональны.
{
"name": "Иван Иванов",
"username": "ivan",
"phone": "+79001234567",
"email": "ivan@example.com",
"avatar_url": "https://example.com/avatar.jpg",
"public_link": "https://example.com/ivan"
}
Поле | Описание |
|---|---|
name | Полное имя. Разбивается на first/last по первому пробелу |
username | Никнейм/логин в вашей системе |
phone | Телефон |
avatar_url | URL аватара (будет скачан) |
public_link | Публичная ссылка на профиль пользователя в вашем приложении |
10.2 Attachment (входящий)
Используется в §8.3.
{
"id": "att_1",
"type": "photo",
"url": "https://files.example.com/att_1.jpg",
"filename": "screenshot.jpg",
"size": 245678,
"metadata": null
}
Поле | Тип | Обязательное | Описание |
|---|---|---|---|
id | string | да | Уникальный ID вложения в вашей системе |
url | string | да | HTTPS URL для скачивания. Должен быть доступен серверу Аспро.Cloud |
filename | string | да | Имя файла с расширением |
type | string | нет (по умолчанию `file`) | Тип: `photo`, `file`, `video`, `audio`, `location` |
size | int | нет | Размер в байтах. Используется для pre-check лимита |
metadata | object | нет | Произвольный JSON-объект |
Файл скачивается сразу после получения вебхука. Если URL временный — он должен оставаться валидным хотя бы несколько секунд после отправки вебхука.
10.3 Attachment (исходящий)
Используется в §9.4. Структура совпадает с входящим: id, type, url, filename, size, metadata.
id— ID файла в Аспро.Cloud (приходит строкой).typeопределяется по MIME:image/*→photo,video/*→video,audio/*→audio, остальное →file.url— одноразовая ссылка для скачивания. Скачивайте сразу, не сохраняйте URL надолго.
10.4 To
Используется в §9.5 chat.init.personal. Хотя бы одно поле должно быть непустым.
{
"phone": "+79001234567",
"email": "user@example.com",
"name": "Иван Иванов",
"other": "custom_user_id"
}
Поле | Описание |
|---|---|
phone | Телефон получателя в международном формате |
Email получателя | |
name | Имя получателя (когда телефон/email недоступны) |
other | Произвольный идентификатор в вашей системе |
11. Лимиты
Параметр | Значение |
|---|---|
Максимум MiniApp-каналов на аккаунт | 10 |
Количество вложений в одном сообщении | 10 |
Максимальный размер файла | 50 МБ |
Максимальный размер изображения | 50 МБ |
Попыток доставки исходящего вебхука | 3 |
Backoff между попытками | 0.5 с / 1 с / 2 с |
Порог авто-деактивации канала | 10 неуспехов подряд |
Допустимая схема webhook_url | только https |
12. Обработка ошибок и идемпотентность
12.1 Идемпотентность ваших запросов
message.new.personalдедуплицируется поexternal_message_idв пределах треда. Безопасно ретраить — дубликат вернёт200 OKи не создаст второго сообщения.message.completed.personalидемпотентен: после первого подтверждения повторные запросы для того жеinner_message_idвернут200.message.edit.personalиmessage.delete.personalне строго идемпотентны (повторныйeditперепишет текст, повторныйdeleteвернёт404). Не ретраите без причины.
12.2 Идемпотентность исходящих вебхуков
Аспро.Cloud не гарантирует exactly-once: один и тот же вебхук может прийти несколько раз (например, ваш сервер ответил 200, но ответ потерялся в сети — последует retry).
Постройте обработку идемпотентно. В каждом исходящем хуке передаётся уникальный event_id — двух разных хуков с одинаковым event_id не бывает, поэтому его удобно использовать как ключ дедупликации: запомните уже обработанные event_id и при повторе с тем же значением просто верните 200.
Для message.new.personal дополнительно можно ориентироваться на inner_message_id: если такое сообщение уже доставлено пользователю, повторите ответ 200 без повторной доставки.
12.3 Канал деактивирован — что происходит
- Все входящие хуки на
/external/rest/contactcenter/bot/hook_miniapp/...возвращают409 Conflict. - Исходящие хуки на ваш
webhook_urlне отправляются.
Активировать канал может только администратор аккаунта (через UI Контакт-центра или через вашу страницу подключения). После активации вы получите хук bot.activated.
12.4 Cross-bot ошибки 404
Если вы пытаетесь редактировать/удалять/подтверждать сообщение, которое принадлежит другому каналу в том же аккаунте, API вернёт 404. Это защита от того, чтобы один MiniApp не мог влиять на сообщения другого.
13. Полный сценарий end-to-end
13.1 Подключение канала менеджером
1. Менеджер открывает Контакт-центр → «Подключить канал» → выбирает вашу интеграцию
│
▼
2. Аспро.Cloud открывает iframe с вашей страницей подключения и шлёт на неё POST:
{ domain, account.id, app.id, auth.access_token, ... }
│
▼
3. Ваша страница подключения:
- запрашивает у менеджера название канала / прочие настройки
- POST https://{domain}/api/v1/module/contactcenter/bot/create
с Bearer {auth.access_token}
{ name, webhook_url, bot_token, manifest_placement_id: placement.id, active: 1 }
(manifest_placement_id берётся из placement.id того же POST на iframe — §5.1)
│
▼
4. Аспро.Cloud возвращает: { data: { id, uuid, ... } }
│
▼
5. Сохраняете uuid (=bot_id) и id у себя. Закрываете iframe через JS SDK.
13.2 Пользователь пишет, менеджер отвечает
1. Пользователь у вас пишет «Привет»
│
▼
2. POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}
{
"method": "message.new.personal",
"payload": {
"external_message_id": "msg_001",
"external_chat_id": "chat_42",
"external_user_id": "user_42",
"text": "Привет",
"send_date": 1710752400,
"user_data": { "name": "Иван", "phone": "+79001234567" },
"attachments": []
}
}
← 200 { "success": true }
│
▼
3. Менеджер пишет «Здравствуйте! Чем помочь?»
│
▼
4. Аспро.Cloud → POST <ваш webhook_url>
{
"method": "message.new.personal",
"payload": {
"channel_id": "<ваш bot_token>",
"inner_message_id": "9001",
"external_chat_id": "chat_42",
"text": "Здравствуйте! Чем помочь?",
"timestamp": 1710752700,
"event_id": "<токен>",
"attachments": []
}
}
← 200 (вы отвечаете сразу)
│
▼
5. Вы доставляете сообщение пользователю, получаете свой ID msg_xyz_789
│
▼
6. POST https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}
{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "msg_xyz_789"
}
}
← 200 { "success": true }
│
▼
7. У менеджера статус сообщения меняется «отправляется» → «доставлено».
13.3 Менеджер пишет первым
1. Менеджер нажимает «Написать первому» (доступно при can_write_first = true)
│
▼
2. Аспро.Cloud → POST <ваш webhook_url>
{
"method": "chat.init.personal",
"payload": {
"channel_id": "<ваш bot_token>",
"to": { "phone": "+79001234567" },
"message": { "text": "Видел вашу заявку…" },
"event_id": "<токен>"
}
}
← 200
│
▼
3. Вы находите пользователя по телефону, открываете чат external_chat_id=chat_99,
доставляете сообщение
│
│ если не удалось — POST .../hook_miniapp/...
│ { "method": "error", "payload": { "event_id": "<токен>", "message": "user not found" } }
│
▼
4. POST .../hook_miniapp/...
{
"method": "message.new.personal",
"payload": {
"external_message_id": "msg_init_1",
"external_chat_id": "chat_99",
"external_user_id": "user_42",
"text": "Видел вашу заявку…",
"send_date": 1710752800,
"user_data": { ... },
"direction": 1
}
}
← 200
│
▼
5. В Аспро.Cloud создан тред с external_chat_id=chat_99. Менеджер видит чат.
14. Примеры кода
В примерах ниже используются placeholder'ы:
{domain}—my.aspro.cloud(значение из POST на iframe);{account_id}—123456;{bot_id}—550e8400-e29b-41d4-a716-446655440000(uuid канала);{access_token}— пользовательский OAuth2-токен.
14.1 curl: создать канал
curl -X POST 'https://{domain}/api/v1/module/contactcenter/bot/create' \
-H 'Authorization: Bearer {access_token}' \
-H 'Content-Type: application/json' \
-d '{
"name": "Мой канал",
"webhook_url": "https://example.com/aspro-webhook/8f7a3c",
"bot_token": "my-integration-id-42",
"manifest_placement_id": "my_wizard_v1",
"active": 1,
"can_write_first": true
}'
manifest_placement_id берётся из placement.id POST'а на iframe (§5.1).
14.2 curl: отправить новое сообщение
curl -X POST 'https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}' \
-H 'Content-Type: application/json' \
-d '{
"method": "message.new.personal",
"payload": {
"text": "Привет!",
"send_date": 1710752400,
"external_message_id": "msg_001",
"external_user_id": "user_42",
"external_chat_id": "chat_42",
"attachments": [],
"user_data": { "name": "Иван Иванов", "phone": "+79001234567" }
}
}'
14.3 curl: подтвердить доставку
curl -X POST 'https://{domain}/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}' \
-H 'Content-Type: application/json' \
-d '{
"method": "message.completed.personal",
"payload": {
"inner_message_id": 9001,
"external_message_id": "msg_xyz_789"
}
}'
14.4 Node.js: приёмник вебхуков и клиент
import express from 'express';
import axios from 'axios';
const DOMAIN = 'my.aspro.cloud';
const ACCOUNT_ID = 123456;
const BOT_ID = '550e8400-e29b-41d4-a716-446655440000';
const BOT_TOKEN = 'my-integration-id-42';
const hookUrl = `https://${DOMAIN}/external/rest/contactcenter/bot/hook_miniapp/${ACCOUNT_ID}/${BOT_ID}`;
const app = express();
app.use(express.json({ limit: '10mb' }));
// --- Приёмник исходящих вебхуков от Аспро.Cloud ---
app.post('/aspro-webhook', async (req, res) => {
const { method, payload } = req.body;
if (payload?.channel_id && payload.channel_id !== BOT_TOKEN) {
return res.status(400).json({ error: 'unknown channel' });
}
// Отвечаем 200 сразу, обработку запускаем асинхронно.
res.status(200).json({ ok: true });
try {
switch (method) {
case 'message.new.personal': await deliverManagerMessage(payload); break;
case 'chat.init.personal': await openManagerInitiatedChat(payload); break;
case 'bot.activated':
case 'bot.deactivated':
case 'bot.deleted':
console.log(`Lifecycle: ${method}`, payload);
break;
default:
console.warn('Unknown method', method);
}
} catch (err) {
if (payload?.event_id) {
await reportError(payload.event_id, err.message).catch(() => {});
}
}
});
app.listen(3000);
// --- Доставка сообщения менеджера пользователю + подтверждение ---
async function deliverManagerMessage(payload) {
const { inner_message_id, external_chat_id, text, attachments } = payload;
const externalMessageId = await sendToUser(external_chat_id, text, attachments);
await axios.post(hookUrl, {
method: 'message.completed.personal',
payload: { inner_message_id, external_message_id: externalMessageId },
});
}
// --- Инициация чата менеджером ---
async function openManagerInitiatedChat(payload) {
const { to, message, event_id } = payload;
const user = await findUserByContact(to);
if (!user) throw new Error('user not found');
const chatId = await createOrOpenChat(user.id);
await sendToUser(chatId, message.text);
// Echo в Аспро.Cloud — без этого тред не создастся
await axios.post(hookUrl, {
method: 'message.new.personal',
payload: {
external_message_id: `init_${Date.now()}`,
external_chat_id: chatId,
external_user_id: user.id,
text: message.text,
send_date: Math.floor(Date.now() / 1000),
user_data: { name: user.name, phone: user.phone, email: user.email },
attachments: [],
direction: 1,
},
});
}
// --- Отчёт об ошибке доставки ---
async function reportError(eventId, message) {
await axios.post(hookUrl, {
method: 'error',
payload: { event_id: eventId, message },
});
}
// --- Когда конечный пользователь пишет менеджеру ---
async function userSentMessage({ userId, chatId, messageId, text, userData }) {
await axios.post(hookUrl, {
method: 'message.new.personal',
payload: {
text,
send_date: Math.floor(Date.now() / 1000),
external_message_id: messageId,
external_user_id: userId,
external_chat_id: chatId,
attachments: [],
user_data: userData,
},
});
}
// Заглушки — реализуйте под свой стек
async function sendToUser(chatId, text, attachments) { /* ... */ return `local_${Date.now()}`; }
async function findUserByContact(to) { /* ... */ }
async function createOrOpenChat(userId) { /* ... */ }
14.5 PHP: минимальный клиент
<?php
class AsproMiniAppClient
{
public function __construct(
private string $domain,
private int $accountId,
private string $botId
) {}
public function sendUserMessage(
string $externalMessageId,
string $externalChatId,
string $externalUserId,
string $text,
array $userData = [],
array $attachments = []
): void {
$this->post([
'method' => 'message.new.personal',
'payload' => [
'text' => $text,
'send_date' => time(),
'external_message_id' => $externalMessageId,
'external_user_id' => $externalUserId,
'external_chat_id' => $externalChatId,
'attachments' => $attachments,
'user_data' => $userData,
],
]);
}
public function confirmDelivery(int $innerMessageId, string $externalMessageId): void
{
$this->post([
'method' => 'message.completed.personal',
'payload' => [
'inner_message_id' => $innerMessageId,
'external_message_id' => $externalMessageId,
],
]);
}
public function reportError(string $eventId, string $message): void
{
$this->post([
'method' => 'error',
'payload' => ['event_id' => $eventId, 'message' => $message],
]);
}
private function post(array $body): void
{
$url = sprintf(
'https://%s/external/rest/contactcenter/bot/hook_miniapp/%d/%s',
$this->domain,
$this->accountId,
$this->botId
);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_TIMEOUT => 10,
]);
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code < 200 || $code >= 300) {
throw new RuntimeException("Aspro hook failed: HTTP $code, body: $response");
}
}
}
$client = new AsproMiniAppClient('my.aspro.cloud', 123456, '550e8400-e29b-41d4-a716-446655440000');
$client->sendUserMessage(
'msg_001',
'chat_42',
'user_42',
'Привет!',
['name' => 'Иван Иванов', 'phone' => '+79001234567']
);
15. FAQ
Q: Можно ли использовать один webhook_url для нескольких каналов?
Да. Различайте их по channel_id (= bot_token, заданный при создании каждого канала).
Q: Что передавать в bot_token — какой-то фиксированный секрет?
Это идентификатор интеграции на вашей стороне, не секрет. Удобно использовать собственный UUID или ID канала в вашей БД. Главное — чтобы он был уникальным в пределах вашего приложения и неизменным.
Q: Зачем нужно manifest_placement_id?
Аспро.Cloud сохраняет этот id у канала, чтобы при следующем открытии редактирования iframe открылся именно на той странице, через которую пользователь подключил канал — а не на «первой попавшейся» секции contactcenter.service.wizard из манифеста. Это особенно важно, если у вашего приложения несколько секций (разные «варианты подключения»), но в любом случае поле обязательное — берите его из placement.id POST'а на iframe.
Q: Можно ли передавать в manifest_placement_id собственный произвольный id?
Нет. Аспро.Cloud валидирует значение по списку секций contactcenter.service.wizard из текущего манифеста — id должен совпадать с одним из них (поле id секции в манифесте). Произвольный id → 422 UNKNOWN_PLACEMENT.
Q: Я переименовал секцию в манифесте (id изменился). Что будет со старыми подключёнными каналами?
У них в БД хранится прежний id. После публикации нового манифеста подключённые каналы продолжат работать (входящие/исходящие вебхуки не зависят от placement), но при открытии iframe редактирования Аспро.Cloud не найдёт сохранённый id в новом манифесте и сделает fallback на первую секцию contactcenter.service.wizard. Не меняйте id секций между версиями — это структурный ключ.
Q: Что передавать в user_data при echo сообщения менеджера (direction=1)?
Данные собеседника (клиента), а не менеджера. CRM использует их для связи с профилем клиента.
Q: Что передавать в external_user_id при исходящем сообщении менеджера с другого устройства?
ID собеседника, не отправителя. Сообщение в любом случае ассоциируется с тем чатом, где общается этот собеседник.
Q: Сообщение в Аспро.Cloud не появляется — что проверить?
- Канал активен (
active = 1)? - URL запроса корректен:
/external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}? - Ответ
200 { "success": true }?400— payload не валиден,404— не тот канал/сообщение,409— канал деактивирован,410— канал удалён. external_message_idуникален в пределах треда? Дубликаты no-op.
Q: Менеджер пишет, но мой webhook не вызывается.
webhook_urlзадан в канале? Проверьте через API-запрос на чтение канала.- URL отвечает за разумное время (несколько секунд) и кодом
2xx? - URL — HTTPS, публично доступный?
- Не превышен ли порог авто-деактивации? Если сервер отвечал ошибками 10 раз — канал выключен и ждёт ручной активации.
Q: Что делать, если я получил дубликат исходящего вебхука?
Постройте обработку идемпотентно: для message.new.personal ориентируйтесь на event_id — если событие уже обработано, верните 200 OK без повторных действий.
Q: Можно ли изменить webhook_url существующего канала?
Да, через API-запрос на обновление канала с новым webhook_url.
Q: Где взять account_id клиента?
В POST на iframe в поле account.id. Это публичный идентификатор аккаунта, передаётся в URL входящих вебхуков.
Q: Где взять domain клиента?
В POST на iframe в поле domain. Используйте его как base URL для всех API-запросов в этот аккаунт.
Q: Поддерживается ли read-receipt («сообщение прочитано»)?
Нет. Метод message.read.personal зарезервирован, но не обрабатывается в текущей версии — см. §8.8.
- attach_file incoming_events.yaml (40.4КБ)
- attach_file outgoing_hooks.yaml (19.31КБ)