1. Аспро.Cloud
  2. Центр поддержки Аспро.Cloud
  3. Разработка приложений Аспро.Cloud
  4. Как создать свое приложение для модуля «Коммуникации»

Как создать свое приложение для модуля «Коммуникации»


Контакт-центр MiniApp — внешний API

Документация для разработчиков, реализующих собственное приложение, которое подключается к Аспро.Cloud как канал коммуникации (MiniApp) в модуле «Контакт-центр».

1. Обзор

Что такое MiniApp-канал

MiniApp — это тип канала в Контакт-центре Аспро.Cloud, позволяющий вашему приложению (мессенджеру, чат-виджету, корпоративному порталу и т. п.) обмениваться сообщениями с менеджерами Аспро.Cloud.

Для MiniApp вы реализуете обе стороны коммуникации сами:

  • Входящий канал — ваш сервер шлёт HTTP-запросы в Аспро.Cloud, когда пользователь пишет/редактирует/удаляет сообщение.
  • Исходящий канал — Аспро.Cloud шлёт HTTP-запросы на ваш сервер, когда менеджер отвечает или инициирует чат.

Что нужно сделать

  1. Создать в Аспро.Cloud внешнее приложение и описать манифест с местом встройки «Подключение к сервису коммуникаций».
  2. Реализовать у себя страницу подключения — она открывается в iframe внутри Аспро.Cloud в момент, когда менеджер настраивает интеграцию.
  3. Реализовать у себя приёмник исходящих вебхуков от Аспро.Cloud.
  4. Подключаться к 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

Полный путь от нуля до работающей интеграции:

  1. Зарегистрируйте приложение в Аспро.Cloud (страница /module/miniapps/cabinet), добавьте место встройки contactcenter.service.wizard с URL вашей страницы подключения. Дайте приложению доступ к модулю «Коммуникации». → §4
  2. Реализуйте iframe-страницу подключения. На неё Аспро.Cloud делает POST с данными аккаунта и пользовательским OAuth2-токеном. Подключите JS SDK Аспро.Cloud для удобной работы с iframe. → §5
  3. Реализуйте приёмник исходящих вебхуков на своём сервере по постоянному URL (этот URL вы укажете при создании канала). → §9
  4. Создайте канал коммуникации через POST /api/v1/module/contactcenter/bot/create от имени пользователя, который открыл iframe. → §7
  5. Обменивайтесь сообщениями:
    • Когда пользователь пишет — вы шлёте message.new.personal на /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}.
    • Когда менеджер пишет — Аспро.Cloud делает POST на ваш webhook_url, вы доставляете и подтверждаете через message.completed.personal.

4. Создание приложения в Аспро.Cloud

4.1 Регистрация в кабинете

  1. Зайдите на https://{your-domain}/module/miniapps/cabinet.
  2. Нажмите «Создать приложение», заполните основные поля и сохраните.
  3. Откройте созданное приложение и перейдите в редактирование манифеста.

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: если он непредсказуемый, утечка маловероятна.

Рекомендации:

  • Дополнительно проверяйте, что приходящий в payload channel_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-ответе результата фактической доставки сообщения.

Поток обработки выглядит так:

  1. Аспро.Cloud делает POST на ваш webhook_url.
  2. Вы отвечаете 2xx сразу, как только приняли запрос в обработку. Ответ означает только «принято».
  3. Дальше вы доставляете сообщение пользователю на своей стороне (это может занять любое время).
  4. По итогу вы шлёте встречный входящий вебхук в Аспро.Cloud:
    • message.completed.personal (§8.6) — если доставка успешна;
    • error (§8.7) — если обработать не удалось.

Без встречного хука сообщение менеджера в Аспро.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 → успех. Счётчик неудач сбрасывается.
  • 400499 → логический отказ (ваш сервер ответил, но отказался обработать). Канал не деактивируется, но соответствующее сообщение менеджера будет помечено как «не доставлено». 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-исходящий)

Что делать:

  1. Ответьте 200 сразу.
  2. Найдите чат по external_chat_id, доставьте сообщение пользователю.
  3. После доставки — отправьте message.completed.personal (§8.6) с inner_message_id и своим external_message_id.
  4. При ошибке — отправьте 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), чтобы первое сообщение было атрибутировано менеджеру-инициатору

Что делать:

  1. Ответьте 200 сразу.
  2. Найдите/создайте пользователя на своей стороне по полям to (приоритеты — на ваше усмотрение: phoneemailothername).
  3. Откройте чат с этим пользователем, получите ваш external_chat_id.
  4. Доставьте message.text пользователю.
  5. Пришлите message.new.personal с этим external_chat_id, direction: 1 (echo от менеджера) и event_id из этого chat.init.personal — так чат будет создан в Аспро.Cloud, а первое сообщение отобразится от менеджера-инициатора (без event_id оно отобразится со стороны контакта). В последующих message.new.personal этого чата event_id не нужен.
  6. При ошибке — отправьте 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

Телефон

email

Email

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

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 не появляется — что проверить?

  1. Канал активен (active = 1)?
  2. URL запроса корректен: /external/rest/contactcenter/bot/hook_miniapp/{account_id}/{bot_id}?
  3. Ответ 200 { "success": true }? 400 — payload не валиден, 404 — не тот канал/сообщение, 409 — канал деактивирован, 410 — канал удалён.
  4. external_message_id уникален в пределах треда? Дубликаты no-op.

Q: Менеджер пишет, но мой webhook не вызывается.

  1. webhook_url задан в канале? Проверьте через API-запрос на чтение канала.
  2. URL отвечает за разумное время (несколько секунд) и кодом 2xx?
  3. URL — HTTPS, публично доступный?
  4. Не превышен ли порог авто-деактивации? Если сервер отвечал ошибками 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.

Предыдущая статья Как создать свое приложение для модуля «Телефония»
Следующая статья Частые вопросы о Маркетплейсе Аспро.Cloud