# Meliora API: полная документация > Публичный API Meliora v1: чтение кампаний, лидов, переписки и ленты событий по ключу организации. Только чтение, ранний доступ. Базовый адрес: https://api.melioralab.io/v1. Аутентификация: заголовок `Authorization: Bearer <ключ>`, ключ никогда не передается в адресе запроса. Сайт: https://docs.melioralab.io/. Спецификация: https://docs.melioralab.io/openapi.json. Оглавление: https://docs.melioralab.io/llms.txt. --- ## API Meliora Читайте кампании, лиды, сохраненную переписку и события вашего кабинета Meliora из CRM или своей системы. Версия v1, ранний доступ. Промпт для ИИ-агента: https://docs.melioralab.io/agent-prompt.txt ### Что можно сделать API отдает данные кабинета в JSON по ключу организации. Типичные задачи: - **Связать ответы с CRM.** Лента событий сообщает о первом сообщении лиду и о его ответе, а поле `fields.record_id` в карточке лида связывает его с записью CRM. - **Показать прогресс кампаний.** Статусы кампаний и лидов, состояние доставки и причина ожидания: например, окно отправки или дневная норма аккаунта. - **Забрать переписку.** Сохраненные сообщения с лидом: шаги цепочки, ручные сообщения менеджеров и ответы. - **Сверить данные.** Списки кампаний и лидов с фильтрами по пространству, кампании, статусу и нику. ### Маршруты Все маршруты в v1 только читают данные, метод GET. | Маршрут | Что возвращает | Право | |---|---|---| | [`GET /v1/me`](https://docs.melioralab.io/reference/api-context/#getMe) | Получить сведения о ключе | любое | | [`GET /v1/workspaces`](https://docs.melioralab.io/reference/workspaces/#listWorkspaces) | Список пространств | любое | | [`GET /v1/campaigns`](https://docs.melioralab.io/reference/campaigns/#listCampaigns) | Список кампаний | `campaigns:read` | | [`GET /v1/campaigns/{id}`](https://docs.melioralab.io/reference/campaigns/#getCampaign) | Получить кампанию | `campaigns:read` | | [`GET /v1/leads`](https://docs.melioralab.io/reference/leads/#listLeads) | Список лидов | `leads:read` | | [`GET /v1/leads/{id}`](https://docs.melioralab.io/reference/leads/#getLead) | Получить лида | `leads:read` | | [`GET /v1/leads/{id}/messages`](https://docs.melioralab.io/reference/messages/#listLeadMessages) | Переписка с лидом | `messages:read` | | [`GET /v1/events`](https://docs.melioralab.io/reference/events/#listEvents) | Лента событий | `events:read` | ### Как устроен API - Базовый адрес: `https://api.melioralab.io/v1`. - Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Ключ принадлежит кабинету, а не сотруднику, и продолжает работать, если создавший его сотрудник ушел. - Ответы в JSON UTF-8, имена полей в `snake_case`, идентификаторы в формате UUID, время в ISO 8601 по UTC с суффиксом `Z`. - Ключ видит только то, что разрешено его правами и охватом. Объект вне охвата для ключа не существует и дает 404. - Запросы выполняются с вашего сервера. Браузерные запросы с чужих сайтов не поддерживаются. ### Ранний доступ API находится в раннем доступе. Ключи выдаются по запросу: напишите в [поддержку](https://docs.melioralab.io/support/), и мы подключим ваш кабинет и выпустим ключ. Чтение доступно на тарифе Pro и выше. Что важно знать заранее: - **Только чтение.** Принять контакты, запустить или остановить цепочку через API пока нельзя. Это делается в кабинете. - **Архив переписки может быть неполным.** API отдает то, что сохранено. Полнота указана в `meta.completeness`. - **Лента хранит события 30 дней.** Опрашивайте ее регулярно, а после долгого простоя сверяйте карточки по инструкции из руководства [Опрос событий](https://docs.melioralab.io/events-polling/). - **Часть значений статусов пока не приходит.** Подробности в руководстве [Статусы лида и доставки](https://docs.melioralab.io/lead-states/). ### С чего начать 1. [Быстрый старт](https://docs.melioralab.io/quickstart/): первый запрос за 5 минут. 2. [Аутентификация и ключи](https://docs.melioralab.io/authentication/): права, охват, ротация и отзыв. 3. [Опрос событий](https://docs.melioralab.io/events-polling/): надежный цикл для интеграции с CRM. 4. [Справочник API](https://docs.melioralab.io/reference/): все поля, параметры и примеры ответов. 5. [ИИ-агенты и MCP](https://docs.melioralab.io/ai-agents/): как дать документацию Claude, ChatGPT, Cursor или Claude Code. Спецификация в формате OpenAPI 3.1 доступна для скачивания: [openapi.json](https://docs.melioralab.io/openapi.json). Ее можно импортировать в Postman, Insomnia или генератор клиентов. --- ## Быстрый старт Пять шагов от ключа до ленты событий. Понадобится терминал с curl, Node.js 18+ или Python 3. Промпт для ИИ-агента: https://docs.melioralab.io/agent-prompt.txt #### Получите ключ В раннем доступе ключ выпускаем мы: напишите в [поддержку](https://docs.melioralab.io/support/), какие права и какой охват нужны. Для первого знакомства хватит прав «Кампании», «Лиды» и «События». Если в вашем кабинете открыт раздел **Настройки › Интеграции › API**, ключ может создать владелец или администратор кнопкой «Создать ключ». Ключ показывается один раз. Сохраните его сразу: восстановить его нельзя, только перевыпустить. #### Сохраните ключ в окружении Не вставляйте ключ в код и не передавайте его в адресе запроса. Положите его в переменную окружения: ```bash export MELIORA_API_KEY="mel_live_..." ``` #### Проверьте доступ Запрос `GET /v1/me` возвращает организацию, права и лимиты ключа. Он доступен с любым набором прав. ```bash curl https://api.melioralab.io/v1/me \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` ```js const res = await fetch('https://api.melioralab.io/v1/me', { headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` }, }); console.log(res.status, await res.json()); ``` ```python import os import requests resp = requests.get( "https://api.melioralab.io/v1/me", headers={"Authorization": f"Bearer {os.environ['MELIORA_API_KEY']}"}, timeout=15, ) print(resp.status_code, resp.json()) ``` В ответе проверьте `scopes` и `scope`: это права и охват ключа. Если пришла ошибка, ее код и что делать описаны в руководстве [Ошибки](https://docs.melioralab.io/errors/). Чаще всего это `invalid_api_key`: ключ скопирован не полностью или с лишним пробелом. #### Получите кампании и лидов Список кампаний возвращается страницами, от старых к новым: ```bash curl -G https://api.melioralab.io/v1/campaigns \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d status=active ``` Возьмите `id` нужной кампании и запросите ее лидов: ```bash curl -G https://api.melioralab.io/v1/leads \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d campaign_id=4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14 \ -d limit=100 ``` Если в ответе `has_more: true`, передайте значение `next_cursor` в параметре `cursor` следующего запроса. Подробнее в руководстве [Пагинация](https://docs.melioralab.io/pagination/). #### Подпишитесь на события Чтобы узнавать о новых сообщениях и ответах без обхода всех лидов, читайте ленту событий. Первый запрос с `start=now` возвращает пустую страницу и курсор, с которого начнется лента: ```bash curl -G https://api.melioralab.io/v1/events \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d start=now ``` Сохраните `next_cursor` и раз в минуту запрашивайте `GET /v1/events?cursor=...`. Каждый ответ содержит новый курсор, даже если событий нет: сохраняйте его всегда. ### Что дальше - [Опрос событий](https://docs.melioralab.io/events-polling/): цикл, который не теряет и не дублирует события, с примерами на Node.js и Python. - [Связь с CRM](https://docs.melioralab.io/crm-mapping/): как сопоставить лида с записью CRM через `record_id`. - [Лимиты и повторы](https://docs.melioralab.io/rate-limits/): как обрабатывать 429 и 503. --- ## Аутентификация и ключи Каждый запрос подписывается ключом организации. Ключ определяет, что именно можно читать: права, пространства, кампании и тексты сообщений. ### Как передать ключ Передавайте ключ в заголовке `Authorization` со схемой `Bearer`: ```http GET /v1/me HTTP/1.1 Host: api.melioralab.io Authorization: Bearer mel_live_AbCdEfGhIjKlMnOpQrStUvWxYz012345 ``` Ключ имеет вид `mel_live_` и 32 латинские буквы или цифры. Cookie кабинета и другие способы входа ключ не заменяют. Запрос без ключа или с неверным ключом получает 401 `invalid_api_key`. После 20 ответов 401 за 5 минут IP-адрес блокируется на 15 минут, поэтому не повторяйте запросы с неверным ключом в цикле. Ключ в адресе запроса не принимается: параметр вроде `?api_key=...` дает 400 `unknown_parameter`. Если ключ однажды попал в адрес, журнал прокси или историю браузера, перевыпустите его. ### Получить доступ API в раннем доступе, ключи выдаются по запросу. Напишите в [поддержку](https://docs.melioralab.io/support/), какие права, охват, срок и IP-адреса нужны: мы подключим кабинет, выпустим ключ и поможем с первым запросом. Полный ключ передается один раз. Когда в кабинете открыт раздел API, ключами управляет **владелец или администратор**, у которого есть доступ ко всем пространствам и кампаниям кабинета. Остальные участники видят раздел только для просмотра. 1. Откройте **Настройки › Интеграции**, блок **API**. 2. Если доступ выключен, нажмите «Включить API». 3. Нажмите «Создать ключ», задайте имя, права, охват и при необходимости срок и IP-адреса. 4. Скопируйте ключ из окна «Ключ будет показан один раз». Чтение через API доступно на тарифе Pro и выше. Если тариф ниже, подписка не действует или доступ кабинета приостановлен, запросы получают 403 с кодом причины: `api_not_available`, `subscription_inactive` или `organization_suspended`. В кабинете может быть до 10 действующих ключей. В это число входят ключи в подготовке и старые ключи на время перекрытия при перевыпуске. ### Права Права выбираются при создании ключа и меняются кнопкой «Изменить». | Право | В кабинете | Что открывает | |---|---|---| | `campaigns:read` | Кампании | `GET /v1/campaigns`, `GET /v1/campaigns/{id}` | | `leads:read` | Лиды | `GET /v1/leads`, `GET /v1/leads/{id}` | | `messages:read` | Переписка | `GET /v1/leads/{id}/messages` и тексты в событиях | | `events:read` | События | `GET /v1/events` | `GET /v1/me` и `GET /v1/workspaces` доступны ключу с любым набором прав. **Тексты сообщений** выдаются, только если у ключа есть право `messages:read` **и** включена галочка «Текст сообщений». Она выключена по умолчанию. Без нее `GET /v1/leads/{id}/messages` и запрос событий с `include_text=true` получают 403 `scope_required`. ### Охват Охват ограничивает, какие данные видит ключ: - **Пространства:** все существующие или выбранные, до 50. - **Кампании:** все кампании выбранных пространств или выбранные, до 500. - **Включать будущие:** при охвате «все» ключ увидит и пространства или кампании, созданные позже. Если галочку снять, охват фиксируется списком того, что есть на момент сохранения. Объект вне охвата для ключа не существует: запрос к нему дает 404 `not_found`, так же как запрос к несуществующему объекту. Текущий охват ключа можно прочитать в поле `scope` ответа [`GET /v1/me`](https://docs.melioralab.io/reference/api-context/). ### Срок действия и IP-адреса - **Срок действия** задается по UTC. После него ключ получает 401 `invalid_api_key`. Пустое поле означает ключ без срока. Срок виден в поле `expires_at` ответа `GET /v1/me`. - **Разрешенные IP** задаются адресами или подсетями в формате CIDR, IPv4 или IPv6, до 50 записей. Запрос с другого адреса получает 403 `ip_not_allowed`. Пустое поле разрешает любые адреса. ### Подготовка нового ключа Сразу после создания или перевыпуска ключ может быть в состоянии «Подготовка». В это время запросы получают 503 `integration_initializing` с заголовком `Retry-After`. Повторите запрос через указанное время. Подготовка длится не дольше 15 минут. Если за это время ключ не подготовился, его состояние станет «Не подготовлен», и запросы получат 401: выпустите ключ заново или напишите в поддержку. ### Перевыпуск Кнопка «Перевыпустить» выдает новый секрет с теми же правами и охватом. Выберите срок перекрытия: | Перекрытие | Что происходит со старым ключом | |---|---| | Без перекрытия | Отзывается сразу | | Одни сутки | Работает еще 24 часа | | Семь суток | Работает еще 7 дней | Перекрытие позволяет обновить ключ на всех серверах без простоя. Курсоры, выданные старому ключу, продолжают работать с новым, если права и охват не менялись. ### Изменение и отзыв - **Изменение** прав, охвата или галочки «Текст сообщений» вступает в силу сразу. Курсоры, выданные до изменения, после него дают 400 `cursor_mismatch`: начните список заново, а для ленты событий выполните [восстановление](https://docs.melioralab.io/events-polling/#vosstanovlenie). - **Отзыв** останавливает ключ сразу и навсегда. Отменить отзыв нельзя. - **Выключить API** можно для всего кабинета одной кнопкой в том же разделе. Пока доступ выключен, все ключи получают 403 `api_disabled`. ### Журнал Кнопка «Журнал» у ключа показывает последние запросы за 14 суток: время, маршрут, статус, код ошибки и длительность. По `Request-Id` из журнала поддержка найдет нужный запрос. ### Хранение ключа Ключ дает доступ к данным вашего кабинета. Обращайтесь с ним как с паролем: - храните в менеджере секретов или переменных окружения сервера, не в репозитории; - вызывайте API только с сервера: ключ не должен попадать в браузер или мобильное приложение; - не пишите ключ в журналы; для поиска используйте его начало и последние 4 знака, которые видны в кабинете; - выдавайте каждой интеграции свой ключ с минимальными правами и охватом; - при подозрении на утечку сразу отзовите ключ или перевыпустите без перекрытия. --- ## Ошибки Успешный ответ всегда имеет статус 200. Ошибка приходит с кодом 4xx или 5xx и телом в формате RFC 9457. ### Формат Ошибка возвращается с заголовком `Content-Type: application/problem+json` и телом из восьми полей: ```json { "type": "urn:meliora:problem:invalid_parameter", "title": "Invalid parameter", "status": 400, "detail": "Проверьте значение параметра", "instance": "urn:request:0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20", "code": "invalid_parameter", "param": "limit", "request_id": "0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20" } ``` | Поле | Описание | |---|---| | `type` | URI типа ошибки: `urn:meliora:problem:` и код. | | `title` | Короткое название на английском. | | `status` | HTTP-статус, тот же, что у ответа. | | `detail` | Пояснение для человека. Язык зависит от заголовка `Accept-Language`. | | `instance` | URI запроса: `urn:request:` и идентификатор запроса. | | `code` | Машинный код ошибки. | | `param` | Параметр, к которому относится ошибка, или `null`. | | `request_id` | Идентификатор запроса, тот же, что в заголовке `Request-Id`. | ### Отказы до проверки ключа Часть запросов отклоняется еще до проверки ключа: превышен лимит с одного IP-адреса, неизвестный путь, тело в запросе, перегрузка. У таких ответов те же поля `status`, `code`, `param` и `request_id`, но `type` равен `about:blank`, а `title` и `detail` совпадают с `code`: ```json { "type": "about:blank", "title": "rate_limit_exceeded", "status": 429, "code": "rate_limit_exceeded", "detail": "rate_limit_exceeded", "instance": "urn:request:0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20", "request_id": "0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20", "param": null } ``` Поэтому опирайтесь на `status` и `code`, а не на `type` и `title`. ### Как обрабатывать - Ветвите логику по `status` и `code`. Текст `detail` может меняться, не разбирайте его в коде. - Если пришел незнакомый `code`, обработайте ошибку по HTTP-статусу: 4xx означает проблему в запросе или доступе, 5xx временную проблему на нашей стороне. - При ошибке в параметре `param` содержит его имя: `limit`, `status`, `cursor`, `include_text` и так далее. - Ошибка никогда не продвигает курсор. После повтора запрос с тем же курсором вернет те же данные. ### Язык пояснения По умолчанию `detail` на русском. Чтобы получать английский текст, передайте заголовок `Accept-Language: en`. Поля `code` и `title` от языка не зависят. ### Request-Id Каждый ответ, включая ошибки, содержит заголовок `Request-Id` с UUID запроса. Записывайте его в журнал рядом с маршрутом и временем. В обращении в [поддержку](https://docs.melioralab.io/support/) достаточно `Request-Id`: по нему мы найдем запрос. Сам ключ не присылайте. ### Коды ошибок | Статус | Код | Что случилось | |---|---|---| | 400 | `invalid_parameter` | Значение параметра не подходит по формату, диапазону или сочетанию. Имя параметра в `param`. | | 400 | `unknown_parameter` | Маршрут не принимает такой параметр. Ключ API в адресе тоже дает эту ошибку. | | 400 | `cursor_mismatch` | Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. | | 401 | `invalid_api_key` | Ключа нет, он неверен, отозван или истек его срок. | | 403 | `scope_required` | Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». | | 403 | `api_disabled` | Доступ по API выключен в настройках кабинета. | | 403 | `api_not_available` | Тариф кабинета не включает API. Чтение доступно с тарифа Pro. | | 403 | `subscription_inactive` | Подписка кабинета не действует. | | 403 | `module_not_available` | В кабинете недоступен модуль рассылок. | | 403 | `organization_suspended` | Доступ кабинета приостановлен. | | 403 | `ip_not_allowed` | Запрос пришел с IP-адреса, которого нет в списке разрешенных у ключа. | | 404 | `not_found` | Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. | | 405 | `method_not_allowed` | Разрешен только метод GET. Внешний адрес отвечает на другие методы 404 без тела, поэтому этот код снаружи не встречается. | | 410 | `cursor_expired` | Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. | | 429 | `rate_limit_exceeded` | Исчерпан лимит запросов ключа, организации или IP-адреса. Повторите через `Retry-After` секунд. | | 503 | `service_unavailable` | Данные временно не прочитаны. Повторите позже. | | 503 | `server_busy` | Сервис перегружен. Повторите через `Retry-After` секунд. | | 503 | `integration_initializing` | Новый ключ еще готовится. Повторите позже. | ### Что повторять | Статус | Повторять? | Как | |---|---|---| | 400 | Нет | Исправьте параметр из `param`. Для `cursor_mismatch` начните список заново. | | 401, 403 | Нет | Проверьте ключ, права и настройки кабинета. | | 404 | Нет | Объект не существует или вне охвата ключа. | | 410 | Нет | Курсор истек: начните список заново или [восстановите ленту](https://docs.melioralab.io/events-polling/#vosstanovlenie). | | 429 | Да | Подождите `Retry-After` секунд, добавьте случайный разброс. | | 503 | Да | Подождите `Retry-After` секунд, при повторных отказах увеличивайте паузу. | Подробнее о паузах между повторами в руководстве [Лимиты и повторы](https://docs.melioralab.io/rate-limits/). ### Особые случаи - **Неизвестный путь** и объект с идентификатором не в формате UUID дают 404 `not_found`. Путь пишется точно как в справочнике: завершающий `/` или закодированные символы в пути тоже дают 404. - **Методы кроме GET**, включая `HEAD` и `OPTIONS`, и адреса вне `/v1/` получают 404 без тела. Код `method_not_allowed` снаружи не встречается. - **Тело в GET-запросе**, адрес длиннее 8 КиБ или заголовки больше 8 КиБ дают 400 `invalid_parameter` с `param` равным `body`, `uri` или `headers`. - **Повторенный параметр** (`?status=new&status=replied`) и **пустое значение** (`?status=`) дают 400 `invalid_parameter`. - **Долгий запрос** прерывается через 10 секунд с ответом 503 `service_unavailable`. --- ## Пагинация Списки возвращаются страницами. Переход к следующей странице выполняется по курсору, без номеров страниц и смещений. ### Ответ со списком Все списки имеют одинаковую оболочку: ```json { "object": "list", "data": [ { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31" } ], "has_more": true, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU" } ``` | Поле | Описание | |---|---| | `object` | Всегда `list`. | | `data` | Объекты текущей страницы. | | `has_more` | `true`, если после этой страницы есть еще данные. | | `next_cursor` | Курсор следующей страницы. `null`, если страниц больше нет. | | `meta` | Дополнительные сведения. Есть у переписки и ленты событий. | Общего числа записей (`total`) и смещения (`offset`) нет. ### Параметры - `limit`: сколько объектов вернуть. По умолчанию 50, от 1 до 100. У ленты событий до 200. - `cursor`: значение `next_cursor` из предыдущего ответа. Порядок записей фиксирован: от старых к новым по времени создания, при равном времени по `id`. Параметра сортировки нет. Новые записи попадают в конец списка. ### Как листать 1. Отправьте первый запрос с нужными фильтрами. 2. Обработайте `data`. 3. Если `has_more` равен `true`, повторите запрос с теми же фильтрами и `cursor=`. 4. Остановитесь, когда `has_more` равен `false`. ```js async function listAll(path, params = {}) { const items = []; let cursor = null; do { const url = new URL(`https://api.melioralab.io/v1${path}`); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); if (cursor) url.searchParams.set('cursor', cursor); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` }, }); if (!res.ok) throw new Error(`Meliora API: ${res.status} ${res.headers.get('Request-Id')}`); const page = await res.json(); items.push(...page.data); cursor = page.has_more ? page.next_cursor : null; } while (cursor); return items; } const leads = await listAll('/leads', { campaign_id: '4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14', limit: 100 }); ``` ```python import os import requests def list_all(path, params=None): session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['MELIORA_API_KEY']}" params = dict(params or {}) items = [] while True: resp = session.get(f"https://api.melioralab.io/v1{path}", params=params, timeout=15) resp.raise_for_status() page = resp.json() items.extend(page["data"]) if not page["has_more"]: return items params["cursor"] = page["next_cursor"] leads = list_all("/leads", {"campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "limit": 100}) ``` ### Правила курсора - **Курсор непрозрачен.** Не разбирайте и не собирайте его сами, только передавайте обратно. - **Курсор привязан к запросу.** Фильтры следующего запроса должны совпадать с первым. Если фильтры другие, ключ получил другие права или охват, ответ будет 400 `cursor_mismatch`. - **`limit` можно менять** между страницами, это не ошибка. - **Курсор списка живет 15 минут.** Просроченный курсор дает 410 `cursor_expired`: начните обход заново с первой страницы. Курсор ленты событий живет 30 суток, см. [Опрос событий](https://docs.melioralab.io/events-polling/). - **Страница может быть короче `limit`.** Ответ не превышает 1 МиБ, поэтому длинная страница сокращается, а невыданные записи придут на следующей. Ориентируйтесь на `has_more`, а не на число записей. ### Списки не снимок Список читается постранично, и между запросами данные могут меняться: лиды добавляются, статусы обновляются. Записи не пропадают и не повторяются из-за смещений, но статус лида на первой странице мог измениться к моменту чтения последней. Если нужно согласованное состояние, сочетайте обход списка с [лентой событий](https://docs.melioralab.io/events-polling/): события после якоря покажут, что изменилось. --- ## Лимиты и повторы Лимиты считаются в весовых единицах: тяжелые запросы расходуют больше. Превышение дает 429, перегрузка или временная недоступность 503. Оба ответа говорят, когда повторить. ### Вес запроса | Маршрут | Вес | |---|---| | `GET /v1/me` | 1 | | `GET /v1/workspaces` | 1 | | `GET /v1/campaigns` | 2 | | `GET /v1/campaigns/{id}` | 1 | | `GET /v1/leads` | 2 | | `GET /v1/leads/{id}` | 1 | | `GET /v1/leads/{id}/messages` | 3 | | `GET /v1/events` | 2, с `include_text=true` 3 | ### Лимиты по тарифам Лимиты действуют одновременно на ключ и на всю организацию, по всем ее ключам вместе. | Окно | Pro | Premium | Enterprise | |---|---|---|---| | Ключ, в секунду | 5 | 10 | 10 | | Ключ, в минуту | 300 | 600 | 600 | | Организация, в секунду | 10 | 25 | 25 | | Организация, в минуту | 600 | 1500 | 1500 | | Одновременных запросов на ключ | 5 | 5 | 5 | | Одновременных запросов на организацию | 10 | 10 | 10 | Пример: на тарифе Pro один ключ может сделать 2 запроса списка лидов в секунду (вес 2, лимит 5) и до 150 таких запросов в минуту. Действующие лимиты ключа возвращает [`GET /v1/me`](https://docs.melioralab.io/reference/api-context/) в поле `limits`. Кроме того, API ограничивает запросы с одного IP-адреса: в среднем 20 в секунду, кратковременно до 40. Если с одного адреса за 5 минут пришло 20 ответов 401, адрес блокируется на 15 минут: все его запросы получают 429 с `Retry-After`. Проверьте ключ, прежде чем повторять запросы с ошибкой 401. Иногда, например во время работ на нашей стороне, лимиты временно снижаются. Тогда в `limits.mode` приходит `emergency`, а в полях лимитов меньшие значения. Код, который читает заголовки ответа, подстроится сам. ### Заголовки Успешные ответы и ответы 429 по лимитам ключа и организации содержат состояние лимитов. Ответ 429 по лимиту IP-адреса приходит без этих заголовков, только с `Retry-After`. ```http RateLimit-Policy: "key_second";q=5;w=1, "key_minute";q=300;w=60, "org_second";q=10;w=1, "org_minute";q=600;w=60 RateLimit: "key_second";r=3;t=1, "key_minute";r=291;t=60, "org_second";r=8;t=1, "org_minute";r=583;t=60 RateLimit-Limit: 5 RateLimit-Remaining: 3 RateLimit-Reset: 1 ``` | Заголовок | Значение | |---|---| | `RateLimit-Policy` | Окна: имя, лимит `q` в весовых единицах и длина окна `w` в секундах. | | `RateLimit` | По каждому окну остаток `r` и секунды до сброса `t`. | | `RateLimit-Limit` | Лимит окна, которое ближе всего к исчерпанию. | | `RateLimit-Remaining` | Остаток в этом окне. | | `RateLimit-Reset` | Секунд до сброса этого окна. | ### Ответ 429 Если вес запроса больше остатка или превышено число одновременных запросов, приходит 429 `rate_limit_exceeded` с заголовком `Retry-After` в секундах. Запрос не выполнен. Подождите `Retry-After` секунд и добавьте случайный разброс, чтобы несколько процессов не повторили запрос одновременно. ### Ответ 503 503 означает, что запрос сейчас не выполнен. Код уточняет причину: | Код | Причина | `Retry-After` | |---|---|---| | `server_busy` | Сервис перегружен | 1 секунда | | `service_unavailable` | Данные временно не прочитаны | 5 секунд | | `integration_initializing` | Новый ключ еще готовится | 5 секунд | Повторяйте с растущей паузой: начните с `Retry-After`, затем удваивайте до 60 секунд. Если 503 приходит дольше нескольких минут, напишите в [поддержку](https://docs.melioralab.io/support/) и приложите `Request-Id`. ### Повтор безопасен Все маршруты v1 только читают данные, поэтому любой запрос можно повторить без побочных эффектов. Ошибка не продвигает курсор: повтор с тем же курсором вернет ту же страницу. ```js const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); async function meliora(path, params = {}, maxAttempts = 6) { const url = new URL(`https://api.melioralab.io/v1${path}`); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); for (let attempt = 1; ; attempt++) { let res; try { res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.MELIORA_API_KEY}` }, signal: AbortSignal.timeout(15_000), }); } catch (networkError) { if (attempt >= maxAttempts) throw networkError; await sleep(Math.min(60, 2 ** attempt) * 1000); continue; } if (res.ok) return res.json(); const problem = await res.json().catch(() => ({})); if ((res.status === 429 || res.status === 503) && attempt < maxAttempts) { const base = Number(res.headers.get('Retry-After')) || 5; const wait = Math.min(60, base * (res.status === 503 ? 2 ** (attempt - 1) : 1)); await sleep((wait + Math.random() * Math.max(1, wait * 0.25)) * 1000); continue; } const error = new Error(`Meliora API ${res.status} ${problem.code ?? ''}`); Object.assign(error, { status: res.status, code: problem.code, requestId: res.headers.get('Request-Id') }); throw error; } } ``` ```python import os import random import time import requests session = requests.Session() session.headers["Authorization"] = f"Bearer {os.environ['MELIORA_API_KEY']}" class MelioraError(Exception): def __init__(self, status, code, request_id): super().__init__(f"Meliora API {status} {code}") self.status, self.code, self.request_id = status, code, request_id def meliora(path, params=None, max_attempts=6): for attempt in range(1, max_attempts + 1): try: resp = session.get(f"https://api.melioralab.io/v1{path}", params=params, timeout=15) except requests.RequestException: if attempt == max_attempts: raise time.sleep(min(60, 2 ** attempt)) continue if resp.ok: return resp.json() if resp.status_code in (429, 503) and attempt < max_attempts: base = int(resp.headers.get("Retry-After", "5")) wait = min(60, base * (2 ** (attempt - 1) if resp.status_code == 503 else 1)) time.sleep(wait + random.uniform(0, max(1, wait * 0.25))) continue try: code = resp.json().get("code") except ValueError: code = None raise MelioraError(resp.status_code, code, resp.headers.get("Request-Id")) raise RuntimeError("unreachable") ``` ### Как не упираться в лимиты - Опрашивайте [ленту событий](https://docs.melioralab.io/events-polling/) раз в минуту вместо регулярного обхода всех лидов. - Используйте `limit=100` для списков и до 200 для ленты: меньше запросов на тот же объем. - Фильтруйте на нашей стороне: `campaign_id`, `status`, `username` дешевле, чем выгрузка всего списка. - Не запускайте больше 5 параллельных запросов на ключ, лишние получат 429. - Запрашивайте тексты только там, где они нужны: события с `include_text=true` и переписка весят 3. --- ## Версии API Версия указана в адресе: /v1. Внутри версии ответы меняются только совместимо, а несовместимые изменения выходят в новой версии с отдельным адресом. ### Номера версий - **Версия API** указана в пути: `https://api.melioralab.io/v1`. Ее же возвращает `GET /v1/me` в поле `api_version`. - **Версия формата события** указана в каждом событии в поле `api_version`, сейчас `2026-10-18`. Она меняется, если меняется состав полей события. Значение совпадает с версией формата тех же событий в вебхуках кабинета. ### Совместимые изменения Эти изменения могут появиться в v1 без новой версии: - новые маршруты; - новые необязательные параметры запроса; - новые поля в ответах и во вложенных объектах; - новые типы событий в ленте; - новые коды ошибок с уже существующими HTTP-статусами; - изменение текста `detail` в ошибках. ### Несовместимые изменения Эти изменения в v1 не делаются: удаление или переименование полей, изменение типа поля, требование нового обязательного параметра, изменение смысла существующих значений. Если такое понадобится, мы выпустим новую версию с отдельным адресом и заранее сообщим об этом и о сроках перехода в [журнале изменений](https://docs.melioralab.io/changelog/). ### Как писать устойчивый клиент - **Пропускайте неизвестные поля** в ответах, не падайте на них. - **Обрабатывайте неизвестное значение** перечисления как `unknown`. Мы стараемся не расширять перечисления v1, но такой код ничего не стоит. - **Пропускайте события незнакомого типа**, но сохраняйте курсор страницы, на которой они пришли. - **Обрабатывайте незнакомый код ошибки** по HTTP-статусу. - **Не разбирайте формат идентификаторов** и курсоров: `id` сообщения и `next_cursor` непрозрачны. ### Журнал изменений Все изменения API записываются в [журнал изменений](https://docs.melioralab.io/changelog/). Спецификация [openapi.json](https://docs.melioralab.io/openapi.json) обновляется вместе с ними. --- ## Опрос событий Лента событий сообщает о двух фактах: лиду отправлено первое сообщение и лид ответил. Цикл ниже не теряет и не дублирует события, даже после сбоев и простоев. ### Как устроена лента `GET /v1/events` возвращает события в порядке их появления в ленте. Типов два: | Тип | Когда появляется | |---|---| | `lead.first_message_sent` | Лиду отправлено первое сообщение, в том числе вручную. | | `lead.replied` | Лид ответил. | Свойства ленты: - **Порядок по появлению, а не по времени факта.** Поле `timestamp` показывает, когда произошел факт. Событие с более ранним `timestamp` может прийти позже соседнего. Не используйте `timestamp` как курсор. - **Доставка не реже одного раза.** После сбоя страница может прийти повторно. Ключ дедупликации: `event.id` вида `evt_` и 32 шестнадцатеричных знака. - **Идентификаторы совпадают с вебхуками.** Если в кабинете настроены вебхуки, у того же факта тот же `id`. Ленту и вебхуки можно использовать вместе, дедуплицируя по `id`. - **Курсор есть всегда.** `next_cursor` никогда не равен `null`, даже у пустой страницы. Каждый ответ выдает новый курсор, его нужно сохранить. - **Хранение 30 дней.** События старше 30 дней из ленты уходят. Курсор живет 30 суток с момента выдачи и продлевается каждым запросом. Что важно знать о данных события: - `lead.replied` приходит на каждый засчитанный ответ, у одного лида их может быть несколько. - `data.reply.automation_stopped` сейчас всегда `null`: остановку цепочки событие не подтверждает. Поле `data.reply.is_first_reply` сейчас не передается. - `data.first_message_sent_at` у события ответа часто равен `null`. Время первого сообщения надежнее брать из события `lead.first_message_sent` или из карточки лида. - `data.lead.fields.record_id` сейчас равен `null`, `record_id_status` равен `unknown`. Как связать событие с CRM, описано в руководстве [Связь с CRM](https://docs.melioralab.io/crm-mapping/#record-id). - `data.lead.username` приходит в нижнем регистре. ### Первый запуск Есть два способа начать: - **С текущего момента.** Запрос с `start=now` возвращает пустую страницу и курсор. Лента начнется с событий, которые появятся после него. Прошлое состояние заберите обходом [кампаний](https://docs.melioralab.io/reference/campaigns/) и [лидов](https://docs.melioralab.io/reference/leads/). - **С доступной истории.** Запрос без `cursor` и без `start` начинает с самого раннего события, которое хранит лента, но не раньше 30 дней и не раньше подключения кабинета к сбору событий. Для интеграции с CRM обычно удобнее первый способ: сначала получить курсор, затем сверить карточки, затем читать ленту от курсора. Так ни одно изменение во время сверки не потеряется. ### Надежный цикл 1. Загрузите сохраненный курсор. Если его нет, выполните первый запуск. 2. Запросите `GET /v1/events?cursor=<курсор>&limit=100`. 3. В **одной транзакции** вашей базы примените события страницы и сохраните новый `next_cursor`. Пропускайте события, `id` которых уже применены. 4. Если `has_more` равен `true`, сразу запросите следующую страницу. 5. Если `has_more` равен `false`, подождите минуту и повторите с шага 2. Почему транзакция: если процесс упадет между применением событий и сохранением курсора, после перезапуска он повторит ту же страницу, а дедупликация по `id` не даст применить событие дважды. Если сохранить курсор раньше, чем применить события, при сбое они потеряются. > **Внимание.** Не меняйте фильтры при опросе по сохраненному курсору. Курсор привязан к `workspace_id`, `campaign_id`, `type` и `include_text` первого запроса. Другой набор фильтров даст 400 `cursor_mismatch`. Для другого набора заведите отдельный курсор. ### Ошибки при опросе | Ответ | Что делать | |---|---| | 429, 503 | Повторите тот же запрос с тем же курсором после `Retry-After`. Курсор не продвигается. | | Сеть, таймаут | Повторите с тем же курсором. | | 410 `cursor_expired` | Курсор старше 30 суток. Выполните восстановление. | | 400 `cursor_mismatch` | Изменились права или охват ключа либо фильтры. Выполните восстановление. | | 401, 403 | Ключ отозван, истек или доступ выключен. Остановите опрос и проверьте ключ. | ### Восстановление после простоя Восстановление нужно после ответа 410 или `cursor_mismatch`, после простоя дольше 30 дней и когда `meta.coverage.status` сообщает о возможной неполноте. 1. Отметьте у себя разрыв: время, когда поток прервался. 2. Получите новый якорь: `GET /v1/events?start=now`. Сохраните его курсор только после шага 3. 3. Обойдите текущие [кампании](https://docs.melioralab.io/reference/campaigns/) и [лидов](https://docs.melioralab.io/reference/leads/) и обновите карточки в своей системе. Поля `first_message_sent_at`, `first_reply_at` и `status` лида покажут, что произошло за время разрыва. 4. Сохраните курсор якоря и продолжайте обычный цикл. События, которые появятся во время обхода, придут в ленте после якоря. Совместите их с карточками монотонно: не откатывайте статус назад и не заменяйте более позднее время более ранним. ### Полнота ленты Каждая страница содержит `meta`: - `meta.available_from`: с какого момента в ленте есть события. - `meta.coverage.status`: `complete`, `partial` или `unknown`. В раннем доступе обычно приходит `unknown` или `partial`: полнота ленты не гарантируется. Для важных процессов периодически, например раз в сутки, сверяйте карточки лидов. - `meta.coverage.gaps`: интервалы, за которые события могли собраться не полностью. После такого интервала сверьте лидов затронутых кампаний. - `meta.collection.state`: состояние сбора. `degraded` означает, что данные могут быть неполными. > **Важно.** Если лента остается пустой, хотя в кабинете идут отправки и ответы, напишите в [поддержку](https://docs.melioralab.io/support/): в раннем доступе ленту событий для кабинета подключаем мы. ### Тексты сообщений С параметром `include_text=true` событие содержит текст: у `lead.first_message_sent` в `data.first_message`, у `lead.replied` в `data.reply`. Нужны право `messages:read` и включенный у ключа «Текст сообщений». Текст есть, только если сохранен и его принадлежность переписке доказана; иначе `text` равен `null`, а причина в `text_unavailable_reason`. Запрос с текстом весит 3 вместо 2. ### Пример на Node.js Функции хранилища замените своими. `meliora()` с повторами при 429 и 503 описана в руководстве [Лимиты и повторы](https://docs.melioralab.io/rate-limits/#povtor-bezopasen). ```js const POLL_INTERVAL_MS = 60_000; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); export async function runEventLoop(db) { let cursor = await db.loadCursor(); if (!cursor) cursor = await resync(db); for (;;) { let page; try { page = await meliora('/events', { cursor, limit: 100 }); } catch (err) { if (err.status === 410 || err.code === 'cursor_mismatch') { cursor = await resync(db); continue; } throw err; // 401 и 403: остановиться и проверить ключ } await db.transaction(async (tx) => { for (const event of page.data) { if (await tx.isApplied(event.id)) continue; if (event.type === 'lead.first_message_sent' || event.type === 'lead.replied') { await applyToCrm(tx, event); } await tx.markApplied(event.id); // незнакомый тип тоже отмечаем } await tx.saveCursor(page.next_cursor); }); cursor = page.next_cursor; if (!page.has_more) await sleep(POLL_INTERVAL_MS); } } async function resync(db) { await db.recordGap(new Date()); const anchor = await meliora('/events', { start: 'now' }); await reconcileLeads(db); // обход /v1/leads и обновление карточек await db.saveCursor(anchor.next_cursor); return anchor.next_cursor; } ``` ### Пример на Python ```python import time from datetime import datetime, timezone POLL_INTERVAL = 60 def run_event_loop(db): cursor = db.load_cursor() or resync(db) while True: try: page = meliora("/events", {"cursor": cursor, "limit": 100}) except MelioraError as err: if err.status == 410 or err.code == "cursor_mismatch": cursor = resync(db) continue raise # 401 и 403: остановиться и проверить ключ with db.transaction() as tx: for event in page["data"]: if tx.is_applied(event["id"]): continue if event["type"] in ("lead.first_message_sent", "lead.replied"): apply_to_crm(tx, event) tx.mark_applied(event["id"]) # незнакомый тип тоже отмечаем tx.save_cursor(page["next_cursor"]) cursor = page["next_cursor"] if not page["has_more"]: time.sleep(POLL_INTERVAL) def resync(db): db.record_gap(datetime.now(timezone.utc)) anchor = meliora("/events", {"start": "now"}) reconcile_leads(db) # обход /v1/leads и обновление карточек db.save_cursor(anchor["next_cursor"]) return anchor["next_cursor"] ``` Функция `meliora()` и класс `MelioraError` приведены в руководстве [Лимиты и повторы](https://docs.melioralab.io/rate-limits/#povtor-bezopasen). Как сопоставить событие с записью CRM, описано в руководстве [Связь с CRM](https://docs.melioralab.io/crm-mapping/). ### Как часто опрашивать Раз в минуту достаточно для большинства интеграций, это 2 весовые единицы в минуту из лимита ключа. Реже тоже можно, например раз в час: курсор живет 30 суток, а лента хранит события 30 дней. Пока `has_more` равен `true`, забирайте страницы подряд без паузы. Более частый опрос не ускоряет появление событий: они попадают в ленту по мере сбора данных, и время этой задержки не гарантируется. --- ## Связь с CRM Надежная связь строится на идентификаторе записи вашей CRM, который вы передаете вместе с контактами. API возвращает его в карточке лида, а события связываются с лидом по его id. ### Поле record_id Передайте идентификатор записи CRM при загрузке контактов в кампанию. В файле контактов добавьте колонку `record_id` и в окне загрузки выберите для нее «Своя колонка» с именем `record_id`. Значение до 128 знаков, без переводов строк. API вернет его в `fields.record_id` у [лида](https://docs.melioralab.io/reference/leads/#lead-fields): в `GET /v1/leads` и `GET /v1/leads/{id}`. > **Внимание.** В событиях ленты `record_id` сейчас не передается: `data.lead.fields.record_id` равен `null`, а `record_id_status` равен `unknown`. Связывайте событие по `data.lead.id`: возьмите `record_id` из `GET /v1/leads/{id}` и сохраните у себя пару `lead.id` и `record_id`, чтобы не запрашивать лида на каждое событие. ```json { "fields": { "record_id": "crm-10482", "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "valid" } ``` ### Статус record_id Прежде чем использовать `record_id`, проверьте `record_id_status`: | Значение | Что означает | Что делать | |---|---|---| | `valid` | Одно непротиворечивое значение. | Ищите запись CRM по `fields.record_id`. | | `missing` | Значения нет. | Сопоставьте вручную или по своим правилам. | | `conflict` | В карточке контакта два разных значения. `fields.record_id` равен `null`. | Исправьте карточку, не выбирайте значение сами. | | `unknown` | Надежность значения не подтверждена, например у старых данных. `fields.record_id` равен `null`. | Сверьте лида по `GET /v1/leads/{id}` или вручную. | `valid` означает, что значение корректно в данных Meliora. Существует ли такая запись в вашей CRM, проверяет ваша интеграция. У событий `record_id_status` сейчас всегда `unknown`, см. предупреждение выше. ### Почему не ник Ник Telegram (`username`) не подходит как ключ связи: - человек может сменить или удалить ник, тогда `username` станет другим или `null`; - один контакт в двух кампаниях дает **два лида** с разными `id` и одинаковым ником; - у разных людей в разное время может быть один и тот же ник. Фильтр `GET /v1/leads?username=...` удобен для поиска и проверки, но не для автоматического сопоставления. Поле `external_id` в v1 всегда `null`: оно зарезервировано для следующих версий. ### Какие поля выдаются В `fields` попадают только перечисленные ключи, если они есть у контакта: `record_id`, `first_name`, `last_name`, `company`, `position`, `website`, `source`, `utm_source`, `utm_medium`, `utm_campaign`. Ключ `record_id` есть всегда, остальные только при наличии значения. Не выдаются: телефон, email, другие свои колонки и служебные данные. Колонка «Имя и фамилия» тоже не попадает в `fields`: для API загружайте имя и фамилию отдельными колонками «Имя» и «Фамилия». Значения длиннее 1024 знаков сокращаются, весь объект не больше 8 КиБ. Если что-то сокращено или скрыто из-за размера, `fields_truncated` равен `true`. ### Статусы в CRM Пример соответствия для воронки CRM: | Событие или поле | Статус в CRM | |---|---| | `lead.first_message_sent` или непустой `first_message_sent_at` | В работе | | `lead.replied` или непустой `first_reply_at` | Связались | Событие `lead.replied` приходит на каждый засчитанный ответ, поэтому у одного лида их может быть несколько. Обновляйте статус **только вперед**: «Связались» не должен откатываться в «В работе», даже если событие первого сообщения пришло позже ответа. Времена касания и ответа объединяйте так же: сохраняйте самое раннее известное время, не заменяйте его более поздним. ```js const RANK = { IN_PROGRESS: 1, CONNECTED: 2 }; // record_id берем из карточки лида и запоминаем: в событиях он не передается. async function crmIdFor(tx, leadId) { const known = await tx.getCrmId(leadId); if (known !== undefined) return known; const lead = await meliora(`/leads/${leadId}`); const crmId = lead.record_id_status === 'valid' ? lead.fields.record_id : null; await tx.rememberCrmId(leadId, crmId); return crmId; } async function applyToCrm(tx, event) { const leadId = event.data.lead.id; const crmId = await crmIdFor(tx, leadId); if (!crmId) { await tx.queueForReview(event.id, leadId); // ручная проверка return; } const target = event.type === 'lead.replied' ? 'CONNECTED' : 'IN_PROGRESS'; const record = await tx.crm.get(crmId); if (!record) return tx.queueForReview(event.id, leadId); if ((RANK[record.status] ?? 0) < RANK[target]) await tx.crm.setStatus(crmId, target); if (event.type === 'lead.replied') { await tx.crm.addNote(crmId, `Ответ в Telegram, ${event.data.reply.replied_at}`); } } ``` ```python RANK = {"IN_PROGRESS": 1, "CONNECTED": 2} def crm_id_for(tx, lead_id): # record_id берем из карточки лида и запоминаем: в событиях он не передается. known = tx.get_crm_id(lead_id) if known is not None: return known or None lead = meliora(f"/leads/{lead_id}") crm_id = lead["fields"]["record_id"] if lead["record_id_status"] == "valid" else None tx.remember_crm_id(lead_id, crm_id or "") return crm_id def apply_to_crm(tx, event): lead_id = event["data"]["lead"]["id"] crm_id = crm_id_for(tx, lead_id) if not crm_id: tx.queue_for_review(event["id"], lead_id) # ручная проверка return target = "CONNECTED" if event["type"] == "lead.replied" else "IN_PROGRESS" record = tx.crm.get(crm_id) if record is None: tx.queue_for_review(event["id"], lead_id) return if RANK.get(record.status, 0) < RANK[target]: tx.crm.set_status(crm_id, target) if event["type"] == "lead.replied": tx.crm.add_note(crm_id, f"Ответ в Telegram, {event['data']['reply']['replied_at']}") ``` ### Переписка в CRM Тексты сообщений можно получить двумя способами: - в событиях с `include_text=true`: текст первого сообщения и текст ответа; - из [переписки](https://docs.melioralab.io/reference/messages/) `GET /v1/leads/{id}/messages`: все сохраненные сообщения с лидом. Что важно знать об архиве переписки: - Архив может быть неполным: переписка до подключения API сохранена частично, сообщения, отправленные с телефона или из Telegram напрямую, в него не попадают. Сейчас `meta.completeness` всегда сообщает о неполноте. - Правки и удаления сообщений в Telegram не отслеживаются: в копии остается первоначальный текст. - Вложения передаются только видом в `media_kind`, без файлов. Если исходящее содержало вложение, его текста в копии может не быть. - Сообщение может появиться в архиве позже и с более ранним временем, чем уже прочитанные. Поэтому читайте переписку лида целиком, все страницы, и объединяйте с уже сохраненной по `id`. - При `text_unavailable: true` текст не выдается, причина в `text_unavailable_reason`. Не создавайте в CRM пустые заметки вместо таких сообщений. Читайте переписку тех лидов, по которым пришло событие или изменился статус, а не всех подряд: запрос весит 3. ### Первичная загрузка Чтобы связать уже идущие кампании: 1. Получите курсор ленты: `GET /v1/events?start=now`. 2. Обойдите `GET /v1/leads` нужных кампаний, сопоставьте лидов с записями CRM по `record_id` и сохраните пары `lead.id` и `record_id` для обработки событий. 3. Перенесите статусы по `first_message_sent_at` и `first_reply_at`. 4. Начните опрос ленты от курсора из шага 1. Подробности цикла в руководстве [Опрос событий](https://docs.melioralab.io/events-polling/). --- ## Статусы лида и доставки У лида четыре поля состояния. status говорит, где лид в кампании; delivery_state и automation_state говорят, что происходит с отправкой; reason объясняет, почему цепочка ждет. ### Как читать состояния - `status`: итог участия лида в кампании. Меняется редко: написали, ответил, отписался. - `delivery_state`: судьба ближайшего неотправленного сообщения цепочки, а если его нет, последнего отправленного. - `automation_state`: идет ли цепочка для этого лида. - `reason`: почему цепочка ждет и когда ожидается следующая попытка, если это известно. Если данных недостаточно или источники противоречат друг другу, API отдает `unknown`, а не угадывает значение. Это нормальное состояние, а не ошибка. ### Статус лида | `status` | Что означает | |---|---| | `new` | Лид добавлен в кампанию, сообщений еще не было. | | `contacted` | Первое сообщение отправлено, ответа пока нет. | | `completed` | Цепочка пройдена, ответа нет. | | `replied` | Лид ответил. | | `blocked` | Писать этому контакту нельзя или не стали: например, получателя не существует, он не принимает сообщения, ему уже писали или он в стоп-листе. | | `failed` | Отправка не удалась. | | `suspended` | Работа с лидом временно приостановлена, например пока отдыхает аккаунт отправителя. | | `deleted` | Лид удален из кампании. | | `opted_out` | Лид попросил больше не писать. | | `unknown` | Статус не удалось определить. | ### Состояние доставки | `delivery_state` | Что означает | Сейчас | |---|---|---| | `queued` | Сообщение в очереди на отправку. | Приходит | | `held` | Отправка удержана, пока не будет решения в кабинете. | Приходит | | `sent` | Отправка подтверждена, следующих сообщений не ожидается или они еще не запланированы. | Приходит | | `unknown` | Исход неизвестен: например, попытка отправки не подтверждена или данных недостаточно. | Приходит | | `waiting_queue` | Следующее сообщение ожидается, но еще не поставлено в очередь. | Пока не приходит | | `failed` | Отправка окончательно не удалась, повтор не назначен. | Пока не приходит | `unknown` не означает неудачу. Не отправляйте лиду сообщение повторно своими средствами только потому, что пришел `unknown`: сообщение могло дойти. ### Состояние цепочки | `automation_state` | Что означает | Сейчас | |---|---|---| | `running` | Цепочка идет: кампания активна, отправка разрешена. Окно отправки, дневная норма и паузы Telegram лишь откладывают следующее сообщение. | Приходит | | `paused` | Кампания на паузе, еще не запущена или остановилась из-за подписки. | Приходит | | `held` | Цепочка удержана, пока не будет решения в кабинете. | Приходит | | `stopped` | Цепочка остановлена: лид ответил, отписался, его остановили, либо кампания остановлена или в архиве. | Приходит | | `unknown` | Состояние не удалось определить. | Приходит | | `completed` | Все шаги цепочки выполнены. | Пока не приходит | У лидов, для которых сейчас нет запланированного сообщения, например у ответивших или прошедших цепочку, `automation_state` пока часто приходит как `unknown`. Итог участия смотрите в `status`. > **Важно.** Значения, отмеченные «Пока не приходит», есть в схеме, но в раннем доступе не выдаются: такие лиды приходят с `unknown`. Фильтр `GET /v1/leads?delivery_state=failed` или `waiting_queue` сейчас возвращает пустой список. Когда значения начнут приходить, мы сообщим в [журнале изменений](https://docs.melioralab.io/changelog/). ### Причина ожидания Объект `reason` есть у лида и у кампании: ```json { "reason_code": "telegram_wait", "reason_observed_at": "2026-10-27T08:41:07.000Z", "next_attempt_at": "2026-10-27T09:26:00.000Z", "requires_customer_action": false } ``` | `reason_code` | Что означает | Сейчас | |---|---|---| | `none` | Цепочка ничего не ждет. | Приходит | | `daily_limit` | Аккаунт отправителя исчерпал дневную норму. | Приходит | | `queue_pending` | Сообщение ждет своей очереди на отправку. | Приходит | | `telegram_wait` | Telegram попросил аккаунт подождать перед следующей отправкой. | Приходит | | `account_unavailable` | Аккаунт отправителя недоступен. | Приходит | | `account_restricted` | Telegram ограничил аккаунт отправителя. | Приходит | | `campaign_paused` | Кампания на паузе или еще не запущена. | Приходит | | `subscription_inactive` | Подписка не действует или закончились кредиты. | Приходит | | `manual_hold` | Отправка удержана до решения в кабинете. | Приходит | | `automation_stopped` | Цепочка остановлена. | Приходит | | `unknown` | Причина неизвестна. | Приходит | | `schedule_window` | Ждет окна отправки по расписанию кампании. | Пока не приходит | | `service_unavailable` | Причину сейчас не удалось прочитать. | Пока не приходит | - `next_attempt_at` заполнен, только если срок известен. Иначе `null`. - `requires_customer_action: true` означает, что цепочка не продолжится без действий в кабинете: например, нужно запустить кампанию или продлить подписку. - Лимиты API (ответ 429) не влияют на `reason`: это ограничения ваших запросов, а не отправки сообщений. ### Статус кампании | `status` | Что означает | |---|---| | `draft` | Черновик. | | `scheduled` | Запуск запланирован. | | `launching` | Кампания запускается. | | `active` | Кампания идет. | | `paused` | На паузе. | | `paused_no_credits` | На паузе: закончились кредиты. | | `stopped` | Остановлена. | | `completed` | Завершена. | | `archived` | В архиве. | | `failed` | Ошибка запуска или работы. | | `unknown` | Статус не удалось определить. | ### Времена - `first_message_sent_at`: когда отправлено первое сообщение. Если отправка не подтверждена, поле равно `null`, даже при статусе `contacted`. - `first_reply_at`: когда пришел первый ответ. - `created_at`: когда лид добавлен в кампанию. Не меняется. - Все времена приходят по UTC с миллисекундами, например `2026-10-26T07:30:00.000Z`. - `observed_at`: когда сервер прочитал данные для этого ответа. Это не время последнего изменения лида. --- ## ИИ-агенты и MCP Документация подготовлена для ИИ-помощников: у каждой страницы есть Markdown, вся документация собрана в один файл, а готовый промпт задает агенту правила интеграции. Промпт для ИИ-агента: https://docs.melioralab.io/agent-prompt.txt ### Файлы для агентов | Файл | Что внутри | |---|---| | [llms.txt](https://docs.melioralab.io/llms.txt) | Оглавление: ссылки на Markdown каждой страницы с кратким описанием. | | [llms-full.txt](https://docs.melioralab.io/llms-full.txt) | Вся документация одним Markdown-файлом: руководства и справочник. | | [openapi.json](https://docs.melioralab.io/openapi.json) | Спецификация OpenAPI 3.1: маршруты, параметры, схемы ответов и примеры. | | `/md/<страница>.md` | Markdown отдельной страницы, например [/md/events-polling.md](https://docs.melioralab.io/md/events-polling.md) или [/md/reference/leads.md](https://docs.melioralab.io/md/reference/leads.md). | | [agent-prompt.txt](https://docs.melioralab.io/agent-prompt.txt) | Промпт для ИИ-агента текстом. | На каждой странице руководства и справочника справа от заголовка есть меню «Копировать страницу»: скопировать Markdown страницы, открыть его отдельно или открыть страницу в ChatGPT или Claude с готовым вопросом. ### Как подключить документацию #### Claude, ChatGPT и другие чаты Вставьте в чат [промпт](https://docs.melioralab.io/agent-prompt.txt) кнопкой «Скопировать промпт». Если у помощника есть доступ в интернет, он сам прочитает `llms-full.txt` и `openapi.json`. Если доступа нет, приложите файл [llms-full.txt](https://docs.melioralab.io/llms-full.txt) к сообщению. Для вопроса по одной странице откройте меню «Копировать страницу» и выберите «Открыть в ChatGPT» или «Открыть в Claude». #### Cursor Добавьте документацию как источник: в настройках документации Cursor укажите адрес `https://docs.melioralab.io/llms-full.txt`, затем ссылайтесь на нее в чате через `@`. Промпт можно сохранить в правилах проекта, в папке `.cursor/rules`. #### Claude Code и другие агенты в терминале Сохраните промпт в файл инструкций проекта, например `CLAUDE.md` или `AGENTS.md`, или передайте его в начале сессии. Агент с доступом к сети прочитает документацию по ссылкам из промпта. Без сети скачайте файлы в проект: ```bash curl -o docs/meliora-llms-full.txt https://docs.melioralab.io/llms-full.txt curl -o docs/meliora-openapi.json https://docs.melioralab.io/openapi.json ``` ### Ключ и агент Агенту нужен ключ, только если он сам выполняет запросы к API. Чтобы прочитать документацию и написать код, ключ не нужен. > **Внимание.** Не вставляйте ключ в чат с моделью и не пишите его в промпт. Переписка с моделью может храниться у поставщика, а ключ дает доступ к данным вашего кабинета. Если агент должен выполнять запросы: 1. Выпустите для него **отдельный ключ**, не тот, что работает в вашей интеграции. 2. Дайте **минимальные права**: обычно хватает «Кампании» и «Лиды». Текст сообщений включайте, только если он действительно нужен. 3. Ограничьте **охват** одной тестовой кампанией. 4. Задайте **срок действия**, например на неделю, и при возможности список разрешенных IP. 5. Передайте ключ через **переменную окружения** `MELIORA_API_KEY` в среде, где агент запускает код. Промпт требует от агента брать ключ оттуда и не выводить его. 6. После работы **отзовите ключ**. Подробнее о правах, охвате и отзыве в руководстве [Аутентификация и ключи](https://docs.melioralab.io/authentication/). ### Сервер MCP Собственного сервера MCP у Meliora пока нет: он есть в планах на странице [Что дальше](https://docs.melioralab.io/roadmap/). Агенты с поддержкой MCP могут работать с документацией через свой инструмент чтения веб-страниц или файлов: дайте им ссылки на `llms-full.txt` и `openapi.json` или промпт. Сторонние серверы MCP, которые выдают себя за сервер Meliora, к нам отношения не имеют. Не передавайте им ключи. --- ## Журнал изменений Здесь появляются все изменения API: новые маршруты, поля, значения и исправления. ### Ошибка 410 у списков Текст `detail` у ошибки `cursor_expired` теперь подходит и спискам, и ленте событий: для списка начните заново без `cursor`, для ленты выполните восстановление. Код и статус не изменились. ### v1, ранний доступ Первая версия публичного API. Только чтение по ключу организации. - Маршруты: `GET /v1/me`, `GET /v1/workspaces`, `GET /v1/campaigns`, `GET /v1/campaigns/{id}`, `GET /v1/leads`, `GET /v1/leads/{id}`, `GET /v1/leads/{id}/messages`, `GET /v1/events`. - Права ключа: `campaigns:read`, `leads:read`, `messages:read`, `events:read`; отдельная галочка «Текст сообщений». - Охват ключа по пространствам и кампаниям, срок действия, список разрешенных IP, перевыпуск с перекрытием, отзыв и журнал запросов. - Лента событий `lead.first_message_sent` и `lead.replied` с курсором на 30 суток, параметрами `start=now` и `include_text`. - Ошибки в формате RFC 9457 с заголовком `Request-Id`, лимиты в весовых единицах с заголовками `RateLimit`. - Спецификация OpenAPI 3.1. - Материалы для ИИ-агентов: `llms.txt`, `llms-full.txt`, Markdown каждой страницы, промпт для агента и меню «Копировать страницу». Известные ограничения раннего доступа: - `delivery_state` со значениями `waiting_queue` и `failed` и `automation_state` со значением `completed` пока не выдаются. - Полнота архива переписки и ленты событий не гарантируется: смотрите `meta.completeness` и `meta.coverage`. - Записи через API нет. Планы описаны на странице [Что дальше](https://docs.melioralab.io/roadmap/). --- ## Что дальше Первая версия API только читает данные. Ниже то, над чем мы работаем дальше. Сроков нет, состав и порядок могут измениться. ### Запись - Добавление контактов в кампанию через API, с идентификатором вашей системы в поле `external_id`. - Остановка цепочки для отдельного лида. - Кампании, первое сообщение которых передается через API. ### События - Новые типы событий: отправка каждого сообщения цепочки, ручное сообщение менеджера, остановка лида. ### Позже - Ответы лидам через API. - Управление кампаниями и стоп-листом. - Тестовая среда для отладки интеграций. - Собственный сервер MCP, чтобы ИИ-агенты работали с API Meliora напрямую. Пока агентам помогают [файлы для агентов и промпт](https://docs.melioralab.io/ai-agents/). Если вам нужна возможность, которой нет в списке, или важен порядок, напишите в [поддержку](https://docs.melioralab.io/support/). Запросы клиентов раннего доступа влияют на план. Пока записи через API нет, контакты загружаются и кампании запускаются в кабинете Meliora. --- ## Поддержка Пишите в Telegram, отвечаем по рабочим вопросам API, ключам и подключению раннего доступа. ### Контакт Telegram: [@meliora_support](https://t.me/meliora_support). ### Что приложить Чтобы мы быстрее нашли причину, укажите: - `Request-Id` из заголовка ответа или поля `request_id` ошибки; - время запроса с часовым поясом; - маршрут и параметры без ключа; - статус и `code` из ответа; - имя ключа или его начало и последние 4 знака, как они показаны в кабинете. > **Внимание.** Никогда не присылайте ключ целиком: ни в поддержку, ни в общие чаты. Если ключ попал в переписку, отзовите его в кабинете и выпустите новый. ### Частые вопросы #### В настройках нет раздела API В раннем доступе ключи выпускаем мы по запросу, раздел в кабинете может быть закрыт. Напишите нам: подключим кабинет и выпустим ключ с нужными правами. #### Пришел 403 api_not_available Чтение через API доступно на тарифе Pro и выше. Смените тариф в кабинете или напишите нам. #### Лента событий пустая Проверьте, что ключ получил право «События», а запрос не начат с `start=now` до появления событий. Если отправки и ответы в кабинете идут, а лента остается пустой, напишите нам: в раннем доступе ленту событий для кабинета подключаем мы. #### Нет текста сообщений Нужны право «Переписка» и включенная галочка «Текст сообщений» у ключа. Если текст недоступен у отдельного сообщения, причина указана в `text_unavailable_reason`. --- ## Справочник API Все маршруты, поля и примеры ответов API Meliora v1. API Meliora v1 отдает данные кабинета в JSON. Все маршруты используют метод GET и только читают данные. Справочник собран из спецификации [openapi.json](https://docs.melioralab.io/openapi.json). ### Базовый адрес Все пути в справочнике указаны от адреса `https://api.melioralab.io`. Запросы выполняются по HTTPS с вашего сервера. ### Аутентификация Передавайте ключ организации в заголовке `Authorization: Bearer mel_live_...`. Права и охват ключа описаны в руководстве [Аутентификация и ключи](https://docs.melioralab.io/authentication/). ### Формат данных - JSON в UTF-8, имена полей в `snake_case`. - Идентификаторы в формате UUID. Идентификатор сообщения и курсоры непрозрачные строки. - Время в ISO 8601 по UTC с миллисекундами и суффиксом `Z`: `2026-10-26T07:30:00.000Z`. - У каждого объекта есть поле `object` с его типом. - Неизвестное значение передается как `unknown` или `null`, а не пропуском поля. ### Маршруты | Маршрут | Что возвращает | Право | |---|---|---| | [`GET /v1/me`](https://docs.melioralab.io/reference/api-context/#getMe) | Получить сведения о ключе | любое | | [`GET /v1/workspaces`](https://docs.melioralab.io/reference/workspaces/#listWorkspaces) | Список пространств | любое | | [`GET /v1/campaigns`](https://docs.melioralab.io/reference/campaigns/#listCampaigns) | Список кампаний | `campaigns:read` | | [`GET /v1/campaigns/{id}`](https://docs.melioralab.io/reference/campaigns/#getCampaign) | Получить кампанию | `campaigns:read` | | [`GET /v1/leads`](https://docs.melioralab.io/reference/leads/#listLeads) | Список лидов | `leads:read` | | [`GET /v1/leads/{id}`](https://docs.melioralab.io/reference/leads/#getLead) | Получить лида | `leads:read` | | [`GET /v1/leads/{id}/messages`](https://docs.melioralab.io/reference/messages/#listLeadMessages) | Переписка с лидом | `messages:read` | | [`GET /v1/events`](https://docs.melioralab.io/reference/events/#listEvents) | Лента событий | `events:read` | ### Заголовки ответа | Заголовок | Описание | |---|---| | `Request-Id` | UUID запроса. Есть в каждом ответе, включая ошибки. | | `Cache-Control` | Всегда `no-store`: ответы не кешируются. | | `RateLimit-Policy`, `RateLimit` | Окна лимитов и остатки. См. [Лимиты и повторы](https://docs.melioralab.io/rate-limits/#zagolovki). | | `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset` | Лимит, остаток и время до сброса ближайшего к исчерпанию окна. | | `Retry-After` | Секунд до повтора. Только у ответов 429 и 503. | ### Списки и ошибки Списки возвращаются страницами по курсору, см. [Пагинация](https://docs.melioralab.io/pagination/). Ошибки приходят в формате `application/problem+json`, объект описан ниже, коды в руководстве [Ошибки](https://docs.melioralab.io/errors/). ### Объект Problem Ошибка в формате RFC 9457 (`application/problem+json`). | Поле | Тип | Описание | |---|---|---| | `type` | string | URI типа ошибки: `urn:meliora:problem:`. | | `title` | string | Короткое название ошибки на английском. | | `status` | integer | HTTP-статус. | | `detail` | string | Пояснение на русском или английском, по заголовку `Accept-Language`. Не разбирайте этот текст в коде. | | `instance` | string | URI запроса: `urn:request:`. | | `code` | string | Машинный код ошибки. Обрабатывайте ошибки по `status` и `code`. | | `param` | string или null | Параметр, к которому относится ошибка, или `null`. | | `request_id` | string (uuid) | Идентификатор запроса, тот же, что в заголовке `Request-Id`. | ```json { "type": "urn:meliora:problem:invalid_parameter", "title": "Invalid parameter", "status": 400, "detail": "Проверьте значение параметра", "instance": "urn:request:0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20", "code": "invalid_parameter", "param": "limit", "request_id": "0f3c7a95-2e8b-4d16-b9a4-6c5e1d8f7b20" } ``` --- ## Контекст ключа Какой ключ выполняет запрос, что он видит и какие лимиты у него сейчас. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/me`: Получить сведения о ключе ### Объект ApiContext Сведения о ключе, которым выполнен запрос: организация, права, охват, лимиты и состояние сбора. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `api_context`. | | `key_id` | string (uuid) | Идентификатор ключа. Секрет ключа в ответах не возвращается. | | `organization` | object | Организация (кабинет), от имени которой действует ключ. | | `organization.id` | string (uuid) | Идентификатор организации. | | `organization.name` | string | Название организации. | | `scopes` | array of string | Права чтения, выданные ключу. | | `scope` | object | Охват ключа: какие пространства и кампании он видит. | | `scope.workspace_mode` | string | `all`: все пространства кабинета; `selected`: только перечисленные. Значения: `all`, `selected`. | | `scope.workspace_ids` | array of string | Выбранные пространства. Пустой массив при `workspace_mode=all`. | | `scope.workspace_include_future` | boolean | Попадают ли в охват пространства, созданные после выпуска ключа. | | `scope.campaign_mode` | string | `all`: все кампании выбранных пространств; `selected`: только перечисленные. Значения: `all`, `selected`. | | `scope.campaign_ids` | array of string | Выбранные кампании. Пустой массив при `campaign_mode=all`. | | `scope.campaign_include_future` | boolean | Попадают ли в охват кампании, созданные после выпуска ключа. | | `scope.scope_version` | integer | Номер версии охвата. Растет при каждом изменении охвата или прав ключа. | | `api_version` | string | Версия API, всегда `v1`. | | `expires_at` | string (date-time) или null | Когда ключ перестанет действовать. `null`, если срок не задан. | | `limits` | object | Лимиты запросов, действующие для ключа сейчас. | | `limits.key_second` | integer | Весовых единиц в секунду на ключ. | | `limits.key_minute` | integer | Весовых единиц в минуту на ключ. | | `limits.org_second` | integer | Весовых единиц в секунду на организацию, по всем ключам вместе. | | `limits.org_minute` | integer | Весовых единиц в минуту на организацию, по всем ключам вместе. | | `limits.key_concurrent` | integer | Одновременных запросов на ключ. | | `limits.org_concurrent` | integer | Одновременных запросов на организацию. | | `limits.max_response_bytes` | string | Наибольший размер ответа в байтах (1 МиБ). Длинные страницы сокращаются, данные не теряются. Всегда `1048576`. | | `limits.mode` | string | `normal`: обычный режим; `emergency`: временно сниженные лимиты. Значения: `normal`, `emergency`. | | `collection` | object | Состояние сбора сохраненной переписки для API. | | `collection.state` | string | `running`: сбор идет; `paused`: приостановлен в кабинете; `preparing`: готовится после включения; `degraded`: данные могут быть неполными. Значения: `running`, `paused`, `preparing`, `degraded`. | | `collection.enabled_at` | string (date-time) или null | Когда сбор был включен. `null`, если еще не включен. | | `collection.pause_pending` | boolean | Пауза или возобновление запрошены в кабинете, но еще не применены. | Пример: ```json { "object": "api_context", "key_id": "9b1e7c42-5a6d-4f83-9e0b-2d7c4a8f1e65", "organization": { "id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "name": "Пример" }, "scopes": [ "campaigns:read", "events:read", "leads:read", "messages:read" ], "scope": { "workspace_mode": "all", "workspace_ids": [], "workspace_include_future": true, "campaign_mode": "all", "campaign_ids": [], "campaign_include_future": true, "scope_version": 1 }, "api_version": "v1", "expires_at": null, "limits": { "key_second": 5, "key_minute": 300, "org_second": 10, "org_minute": 600, "key_concurrent": 5, "org_concurrent": 10, "max_response_bytes": 1048576, "mode": "normal" }, "collection": { "state": "running", "enabled_at": "2026-10-20T08:00:00.000Z", "pause_pending": false } } ``` ### Получить сведения о ключе `GET /v1/me` Возвращает организацию, права, охват и лимиты ключа. Первый запрос для проверки подключения. Доступен ключу с любым набором прав. Право: любое право чтения. Вес запроса: 1. #### Параметры Параметров нет. #### Ответ Объект `ApiContext`. #### Особые ошибки - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl https://api.melioralab.io/v1/me \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` #### Пример ответа ```json { "object": "api_context", "key_id": "9b1e7c42-5a6d-4f83-9e0b-2d7c4a8f1e65", "organization": { "id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "name": "Пример" }, "scopes": [ "campaigns:read", "events:read", "leads:read", "messages:read" ], "scope": { "workspace_mode": "all", "workspace_ids": [], "workspace_include_future": true, "campaign_mode": "all", "campaign_ids": [], "campaign_include_future": true, "scope_version": 1 }, "api_version": "v1", "expires_at": null, "limits": { "key_second": 5, "key_minute": 300, "org_second": 10, "org_minute": 600, "key_concurrent": 5, "org_concurrent": 10, "max_response_bytes": 1048576, "mode": "normal" }, "collection": { "state": "running", "enabled_at": "2026-10-20T08:00:00.000Z", "pause_pending": false } } ``` --- ## Пространства Пространства: отдельные рабочие области кабинета. У каждого свои кампании, аккаунты и контакты. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/workspaces`: Список пространств ### Объект Workspace Пространство: отдельная рабочая область кабинета со своими кампаниями. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `workspace`. | | `id` | string (uuid) | Идентификатор пространства. | | `name` | string | Название пространства. | | `created_at` | string (date-time) или null | Когда пространство создано. Может быть `null` у старых пространств. | Пример: ```json { "object": "workspace", "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство", "created_at": "2026-09-01T12:00:00.000Z" } ``` ### Список пространств `GET /v1/workspaces` Пространства в охвате ключа, от старых к новым. Доступен ключу с любым набором прав. Право: любое право чтения. Вес запроса: 1. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `limit` | строка запроса | integer | нет | Сколько объектов вернуть на странице. От 1 до 100. По умолчанию `50`. | | `cursor` | строка запроса | string | нет | Значение `next_cursor` из предыдущего ответа. Остальные фильтры должны совпадать с первым запросом. | #### Ответ Список: `object`, `data`, `has_more`, `next_cursor`. Пагинация: https://docs.melioralab.io/md/pagination.md #### Особые ошибки - `cursor_mismatch` (400): Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. - `cursor_expired` (410): Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl https://api.melioralab.io/v1/workspaces \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` #### Пример ответа ```json { "object": "list", "data": [ { "object": "workspace", "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство", "created_at": "2026-09-01T12:00:00.000Z" }, { "object": "workspace", "id": "7a2c5e81-3d9f-4b6a-8c40-1e5f7a9b2d68", "name": "Тестовые рассылки", "created_at": "2026-10-02T09:30:00.000Z" } ], "has_more": false, "next_cursor": null } ``` --- ## Кампании Кампания: цепочка сообщений, которую аккаунты Telegram отправляют контактам по расписанию. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/campaigns`: Список кампаний - `GET /v1/campaigns/{id}`: Получить кампанию ### Объект Campaign Кампания: цепочка сообщений, которую аккаунты Telegram отправляют контактам. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `campaign`. | | `id` | string (uuid) | Идентификатор кампании. | | `workspace_id` | string (uuid) | Пространство, которому принадлежит кампания. | | `name` | string | Название кампании. | | `status` | string | Статус кампании. Неизвестное значение приходит как `unknown`. Значения: `draft`, `scheduled`, `active`, `paused`, `stopped`, `completed`, `archived`, `launching`, `failed`, `paused_no_credits`, `unknown`. | | `first_message_mode` | string | Откуда берется первое сообщение. В v1 всегда `template`: из шаблона кампании. | | `accepts_api_leads` | boolean | Принимает ли кампания контакты через API. В v1 всегда `false`. | | `reason` | object | Почему кампания сейчас не отправляет сообщения, если это известно. | | `reason.reason_code` | string | Код причины. Значения: `none`, `schedule_window`, `daily_limit`, `queue_pending`, `telegram_wait`, `account_unavailable`, `account_restricted`, `campaign_paused`, `subscription_inactive`, `manual_hold`, `automation_stopped`, `service_unavailable`, `unknown`. | | `reason.reason_observed_at` | string (date-time) или null | Когда причина была зафиксирована. `null`, если неизвестно. | | `reason.next_attempt_at` | string (date-time) или null | Когда ожидается следующая попытка. `null`, если срок неизвестен. | | `reason.requires_customer_action` | boolean или null | Нужны ли действия в кабинете, чтобы отправка продолжилась. `null`, если неизвестно. | | `created_at` | string (date-time) | Когда кампания создана. | | `observed_at` | string (date-time) | Время чтения данных сервером. | Пример: ```json { "object": "campaign", "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Пример кампании", "status": "active", "first_message_mode": "template", "accepts_api_leads": false, "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-20T08:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ``` ### Список кампаний `GET /v1/campaigns` Кампании в охвате ключа, от старых к новым по `created_at`. Право: `campaigns:read`. Вес запроса: 2. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `limit` | строка запроса | integer | нет | Сколько объектов вернуть на странице. От 1 до 100. По умолчанию `50`. | | `cursor` | строка запроса | string | нет | Значение `next_cursor` из предыдущего ответа. Остальные фильтры должны совпадать с первым запросом. | | `workspace_id` | строка запроса | string (uuid) | нет | Только объекты этого пространства. | | `status` | строка запроса | string | нет | Только кампании с этим статусом. Значения: `draft`, `scheduled`, `active`, `paused`, `stopped`, `completed`, `archived`, `launching`, `failed`, `paused_no_credits`, `unknown`. | #### Ответ Список: `object`, `data`, `has_more`, `next_cursor`. Пагинация: https://docs.melioralab.io/md/pagination.md #### Особые ошибки - `cursor_mismatch` (400): Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. - `cursor_expired` (410): Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl -G https://api.melioralab.io/v1/campaigns \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d workspace_id=3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43 \ -d status=active ``` #### Пример ответа ```json { "object": "list", "data": [ { "object": "campaign", "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Пример кампании", "status": "active", "first_message_mode": "template", "accepts_api_leads": false, "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-20T08:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" }, { "object": "campaign", "id": "6e3a9c15-2b7d-4f80-9a64-3c1d8e5f7b29", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Повторное касание", "status": "paused", "first_message_mode": "template", "accepts_api_leads": false, "reason": { "reason_code": "campaign_paused", "reason_observed_at": "2026-10-25T16:40:00.000Z", "next_attempt_at": null, "requires_customer_action": true }, "created_at": "2026-10-22T11:15:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ], "has_more": true, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU" } ``` ### Получить кампанию `GET /v1/campaigns/{id}` Одна кампания по идентификатору. Кампания вне охвата ключа дает 404, как несуществующая. Право: `campaigns:read`. Вес запроса: 1. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `id` | путь | string (uuid) | да | Идентификатор кампании. | #### Ответ Объект `Campaign`. #### Особые ошибки - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl https://api.melioralab.io/v1/campaigns/4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14 \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` #### Пример ответа ```json { "object": "campaign", "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Пример кампании", "status": "active", "first_message_mode": "template", "accepts_api_leads": false, "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-20T08:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ``` --- ## Лиды Лид: участие одного контакта в одной кампании. Если один человек есть в двух кампаниях, это два лида с разными `id`. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/leads`: Список лидов - `GET /v1/leads/{id}`: Получить лида ### Объект Lead Лид: участие одного контакта в одной кампании. Один и тот же человек в двух кампаниях дает два разных лида. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `lead`. | | `id` | string (uuid) | Идентификатор лида (участия в кампании). | | `workspace_id` | string (uuid) | Пространство кампании. | | `campaign_id` | string (uuid) | Кампания, в которой участвует контакт. | | `username` | string или null | Ник Telegram без `@`. Может быть `null`. Не уникален: используйте `fields.record_id` для связи с CRM. | | `status` | string | Статус лида в кампании. Значения: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. | | `external_id` | null | Зарезервировано для следующих версий. В v1 всегда `null`. | | `fields` | object | Разрешенные поля карточки контакта. | | `fields.record_id` | string или null | Идентификатор записи во внешней CRM, переданный при загрузке контактов. `null`, если `record_id_status` не `valid`. Может отсутствовать. | | `fields.first_name` | string или null | Имя. Может отсутствовать. | | `fields.last_name` | string или null | Фамилия. Может отсутствовать. | | `fields.company` | string или null | Компания. Может отсутствовать. | | `fields.position` | string или null | Должность. Может отсутствовать. | | `fields.website` | string или null | Сайт. Может отсутствовать. | | `fields.source` | string или null | Источник контакта. Может отсутствовать. | | `fields.utm_source` | string или null | Метка `utm_source`. Может отсутствовать. | | `fields.utm_medium` | string или null | Метка `utm_medium`. Может отсутствовать. | | `fields.utm_campaign` | string или null | Метка `utm_campaign`. Может отсутствовать. | | `fields_truncated` | boolean | `true`, если часть значений полей сокращена или скрыта из-за ограничений размера. | | `record_id_status` | string | Насколько надежен `fields.record_id`: `valid`, `missing`, `conflict` или `unknown`. Значения: `valid`, `missing`, `conflict`, `unknown`. | | `first_contact_account` | object или null | Аккаунт, который отправил первое сообщение. `null`, если неизвестно. | | `first_contact_account.id` | string (uuid) | Идентификатор аккаунта. | | `first_contact_account.username` | string или null | Ник аккаунта без `@`. Может быть `null`. | | `first_contact_account.name` | string или null | Отображаемое имя аккаунта. Может быть `null`. | | `first_message_sent_at` | string (date-time) или null | Когда отправлено первое сообщение. `null`, если отправка не подтверждена. | | `first_reply_at` | string (date-time) или null | Когда пришел первый ответ. `null`, если ответа не было. | | `delivery_state` | string | Состояние доставки ближайшего или последнего сообщения цепочки. Значения: `waiting_queue`, `queued`, `held`, `sent`, `failed`, `unknown`. | | `automation_state` | string | Состояние автоматической цепочки для этого лида. У лидов без запланированного сообщения, например ответивших, сейчас часто `unknown`. Значения: `running`, `paused`, `held`, `stopped`, `completed`, `unknown`. | | `reason` | object | Почему цепочка ждет, если это известно. | | `reason.reason_code` | string | Код причины. Значения: `none`, `schedule_window`, `daily_limit`, `queue_pending`, `telegram_wait`, `account_unavailable`, `account_restricted`, `campaign_paused`, `subscription_inactive`, `manual_hold`, `automation_stopped`, `service_unavailable`, `unknown`. | | `reason.reason_observed_at` | string (date-time) или null | Когда причина была зафиксирована. `null`, если неизвестно. | | `reason.next_attempt_at` | string (date-time) или null | Когда ожидается следующая попытка. `null`, если срок неизвестен. | | `reason.requires_customer_action` | boolean или null | Нужны ли действия в кабинете, чтобы отправка продолжилась. `null`, если неизвестно. | | `created_at` | string (date-time) | Когда контакт добавлен в кампанию. | | `observed_at` | string (date-time) | Время чтения данных сервером. | Пример: ```json { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "username": "example_user", "status": "replied", "external_id": null, "fields": { "record_id": "crm-10482", "first_name": "Анна", "company": "ООО Пример", "position": "Руководитель отдела продаж", "utm_source": "partner" }, "fields_truncated": false, "record_id_status": "valid", "first_contact_account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z", "first_reply_at": "2026-10-26T09:12:00.000Z", "delivery_state": "sent", "automation_state": "unknown", "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-25T14:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ``` ### Список лидов `GET /v1/leads` Лиды (участия контактов в кампаниях) в охвате ключа, от старых к новым по `created_at`. Право: `leads:read`. Вес запроса: 2. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `limit` | строка запроса | integer | нет | Сколько объектов вернуть на странице. От 1 до 100. По умолчанию `50`. | | `cursor` | строка запроса | string | нет | Значение `next_cursor` из предыдущего ответа. Остальные фильтры должны совпадать с первым запросом. | | `workspace_id` | строка запроса | string (uuid) | нет | Только объекты этого пространства. | | `campaign_id` | строка запроса | string (uuid) | нет | Только объекты этой кампании. | | `username` | строка запроса | string | нет | Точный ник Telegram. Знак `@` в начале и регистр не важны. | | `status` | строка запроса | string | нет | Только лиды с этим статусом. Значения: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. | | `delivery_state` | строка запроса | string | нет | Только лиды с этим состоянием доставки. Значения `waiting_queue` и `failed` сейчас не выдаются, фильтр по ним вернет пустой список. Значения: `waiting_queue`, `queued`, `held`, `sent`, `failed`, `unknown`. | #### Ответ Список: `object`, `data`, `has_more`, `next_cursor`. Пагинация: https://docs.melioralab.io/md/pagination.md #### Особые ошибки - `cursor_mismatch` (400): Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. - `cursor_expired` (410): Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl -G https://api.melioralab.io/v1/leads \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d campaign_id=4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14 \ -d limit=50 ``` #### Пример ответа ```json { "object": "list", "data": [ { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "username": "example_user", "status": "replied", "external_id": null, "fields": { "record_id": "crm-10482", "first_name": "Анна", "company": "ООО Пример", "position": "Руководитель отдела продаж", "utm_source": "partner" }, "fields_truncated": false, "record_id_status": "valid", "first_contact_account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z", "first_reply_at": "2026-10-26T09:12:00.000Z", "delivery_state": "sent", "automation_state": "unknown", "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-25T14:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" }, { "object": "lead", "id": "8c1e5a37-9d2f-4b74-a6e8-2f9b3c7d5a40", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "username": "sample_contact", "status": "contacted", "external_id": null, "fields": { "record_id": null, "first_name": "Игорь" }, "fields_truncated": false, "record_id_status": "missing", "first_contact_account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:42:00.000Z", "first_reply_at": null, "delivery_state": "queued", "automation_state": "running", "reason": { "reason_code": "queue_pending", "reason_observed_at": "2026-10-27T08:58:10.000Z", "next_attempt_at": "2026-10-27T10:00:00.000Z", "requires_customer_action": false }, "created_at": "2026-10-25T14:00:01.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ], "has_more": true, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU" } ``` ### Получить лида `GET /v1/leads/{id}` Один лид по идентификатору. Лид вне охвата ключа дает 404. Право: `leads:read`. Вес запроса: 1. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `id` | путь | string (uuid) | да | Идентификатор лида. | #### Ответ Объект `Lead`. #### Особые ошибки - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31 \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` #### Пример ответа ```json { "object": "lead", "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "workspace_id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "campaign_id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "username": "example_user", "status": "replied", "external_id": null, "fields": { "record_id": "crm-10482", "first_name": "Анна", "company": "ООО Пример", "position": "Руководитель отдела продаж", "utm_source": "partner" }, "fields_truncated": false, "record_id_status": "valid", "first_contact_account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z", "first_reply_at": "2026-10-26T09:12:00.000Z", "delivery_state": "sent", "automation_state": "unknown", "reason": { "reason_code": "none", "reason_observed_at": null, "next_attempt_at": null, "requires_customer_action": null }, "created_at": "2026-10-25T14:00:00.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } ``` --- ## Переписка Сохраненные сообщения переписки аккаунта с лидом: исходящие шаги цепочки, ручные сообщения и ответы. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/leads/{id}/messages`: Переписка с лидом ### Объект Message Сообщение из сохраненной переписки с лидом. Архив может быть неполным: см. `meta.completeness`. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `message`. | | `id` | string | Непрозрачный устойчивый идентификатор сообщения. Не разбирайте его формат. | | `direction` | string | `outbound`: от аккаунта к лиду; `inbound`: от лида. Значения: `outbound`, `inbound`. | | `author` | string | `campaign`: шаг цепочки; `manager`: ручное сообщение; `lead`: ответ лида; `unknown`: не определено. Значения: `campaign`, `manager`, `lead`, `unknown`. | | `sent_at` | string (date-time) или null | Когда сообщение отправлено или получено. `null`, если неизвестно. | | `state` | string | `sent`: отправка подтверждена; `received`: входящее сохранено; `unknown`: исход не подтвержден. Значения: `sent`, `received`, `unknown`. | | `media_kind` | string | Вид вложения. Файлы и ссылки на них не выдаются. Значения: `none`, `photo`, `video`, `voice`, `audio`, `document`, `sticker`, `animation`, `video_note`, `contact`, `location`, `poll`, `other`, `unknown`. | | `text` | string или null | Сохраненный текст, до 4000 знаков. `null`, если текст недоступен. | | `text_truncated` | boolean или null | `true`: текст сокращен; `false`: текст полный; `null`: полнота неизвестна. | | `text_unavailable` | boolean | `true`, если текст не выдается. Причина в `text_unavailable_reason`. | | `text_unavailable_reason` | string или null | `not_saved`: текст не сохранен; `ownership_unproven`: принадлежность переписке не доказана; `source_conflict`: источники расходятся; `purged`: текст удален по сроку хранения. Значения: `not_saved`, `ownership_unproven`, `source_conflict`, `purged`. | Пример: ```json { "object": "message", "id": "m_6b2e9f403a7c4d158e921c5a7f3b9d08", "direction": "outbound", "author": "campaign", "sent_at": "2026-10-26T07:30:00.000Z", "state": "sent", "media_kind": "none", "text": "Добрый день, Анна! Пишу из Примера: подскажите, кто у вас отвечает за аутрич?", "text_truncated": false, "text_unavailable": false, "text_unavailable_reason": null } ``` ### Переписка с лидом `GET /v1/leads/{id}/messages` Сохраненные сообщения переписки с лидом, от ранних к поздним. Нужны право `messages:read` и включенный у ключа «Текст сообщений». Архив может быть неполным: смотрите `meta.completeness`. Право: `messages:read` и включенный у ключа «Текст сообщений». Вес запроса: 3. #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `id` | путь | string (uuid) | да | Идентификатор лида. | | `limit` | строка запроса | integer | нет | Сколько объектов вернуть на странице. От 1 до 100. По умолчанию `50`. | | `cursor` | строка запроса | string | нет | Значение `next_cursor` из предыдущего ответа. Остальные фильтры должны совпадать с первым запросом. | #### Ответ Список: `object`, `data`, `has_more`, `next_cursor`, `meta`. Пагинация: https://docs.melioralab.io/md/pagination.md Поле `meta`: | Поле | Тип | Описание | |---|---|---| | `meta.completeness` | object | Насколько полон сохраненный архив переписки с лидом. Сейчас полнота не вычисляется: `overall` и `inbound` приходят как `partial`, `outbound` как `unknown`. | | `meta.completeness.overall` | string | Общая полнота: `complete`, `partial` или `unknown`. Значения: `complete`, `partial`, `unknown`. | | `meta.completeness.inbound` | string | Полнота входящих сообщений. Значения: `complete`, `partial`, `unknown`. | | `meta.completeness.outbound` | string | Полнота исходящих сообщений. Значения: `complete`, `partial`, `unknown`. | | `meta.completeness.available_from` | string (date-time) или null | Начало периода, за который есть данные. `null`, если неизвестно. | | `meta.completeness.available_to` | string (date-time) или null | Конец периода, за который есть данные. `null`, если неизвестно. | | `meta.completeness.observed_at` | string (date-time) | Время чтения данных сервером. | #### Особые ошибки - `cursor_mismatch` (400): Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. - `cursor_expired` (410): Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl https://api.melioralab.io/v1/leads/5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31/messages \ -H "Authorization: Bearer $MELIORA_API_KEY" ``` #### Пример ответа ```json { "object": "list", "data": [ { "object": "message", "id": "m_6b2e9f403a7c4d158e921c5a7f3b9d08", "direction": "outbound", "author": "campaign", "sent_at": "2026-10-26T07:30:00.000Z", "state": "sent", "media_kind": "none", "text": "Добрый день, Анна! Пишу из Примера: подскажите, кто у вас отвечает за аутрич?", "text_truncated": false, "text_unavailable": false, "text_unavailable_reason": null }, { "object": "message", "id": "m_c7a1d3e59b2f4a68b4d05e8f2a6c1b97", "direction": "inbound", "author": "lead", "sent_at": "2026-10-26T09:12:00.000Z", "state": "received", "media_kind": "none", "text": "Здравствуйте! Это я, давайте созвонимся завтра.", "text_truncated": null, "text_unavailable": false, "text_unavailable_reason": null }, { "object": "message", "id": "m_a3f8c6e27d1b4e949c058b2d4f7a6e13", "direction": "inbound", "author": "lead", "sent_at": "2026-10-26T09:13:30.000Z", "state": "received", "media_kind": "photo", "text": null, "text_truncated": null, "text_unavailable": true, "text_unavailable_reason": "not_saved" } ], "has_more": false, "next_cursor": null, "meta": { "completeness": { "overall": "partial", "inbound": "partial", "outbound": "unknown", "available_from": "2026-10-26T07:30:00.000Z", "available_to": "2026-10-27T09:15:42.000Z", "observed_at": "2026-10-27T09:15:42.000Z" } } } ``` --- ## События Лента событий: первое сообщение лиду и ответ лида. Читайте ее по курсору, чтобы узнавать об изменениях без обхода всех лидов. Базовый адрес: https://api.melioralab.io/v1. Ключ передается в заголовке `Authorization: Bearer mel_live_...`. Маршруты: - `GET /v1/events`: Лента событий ### Событие lead.first_message_sent Лиду отправлено первое сообщение. `data.first_message` есть только при `include_text=true`. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `event`. | | `id` | string | Идентификатор события: `evt_` и 32 шестнадцатеричных знака. Ключ дедупликации. | | `api_version` | string | Версия формата события. Всегда `"2026-10-18"`. | | `timestamp` | string (date-time) | Когда произошел факт. | | `test` | boolean | Всегда `false`. | | `type` | string | Тип события. Всегда `"lead.first_message_sent"`. | | `data` | object | Данные события. | | `data.organization_id` | string (uuid) | Организация, в которой произошло событие. | | `data.workspace` | object | Пространство на момент события. | | `data.workspace.id` | string (uuid) | Идентификатор. | | `data.workspace.name` | string или null | Название. Может быть `null`. | | `data.campaign` | object или null | Кампания на момент события. Может быть `null`. | | `data.campaign.id` | string (uuid) | Идентификатор. | | `data.campaign.name` | string или null | Название. Может быть `null`. | | `data.lead` | object | Снимок лида. | | `data.lead.id` | string (uuid) | Идентификатор лида. | | `data.lead.username` | string или null | Ник Telegram без `@`. Может быть `null`. | | `data.lead.status` | string | Статус лида на момент события. Значения: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. | | `data.lead.fields` | object | Поля карточки на момент события. | | `data.lead.fields.record_id` | string или null | Идентификатор записи во внешней CRM, переданный при загрузке контактов. `null`, если `record_id_status` не `valid`. Может отсутствовать. | | `data.lead.fields.first_name` | string или null | Имя. Может отсутствовать. | | `data.lead.fields.last_name` | string или null | Фамилия. Может отсутствовать. | | `data.lead.fields.company` | string или null | Компания. Может отсутствовать. | | `data.lead.fields.position` | string или null | Должность. Может отсутствовать. | | `data.lead.fields.website` | string или null | Сайт. Может отсутствовать. | | `data.lead.fields.source` | string или null | Источник контакта. Может отсутствовать. | | `data.lead.fields.utm_source` | string или null | Метка `utm_source`. Может отсутствовать. | | `data.lead.fields.utm_medium` | string или null | Метка `utm_medium`. Может отсутствовать. | | `data.lead.fields.utm_campaign` | string или null | Метка `utm_campaign`. Может отсутствовать. | | `data.lead.fields_truncated` | boolean | `true`, если часть значений сокращена. | | `data.lead.record_id_status` | string | Надежность `fields.record_id`. В v1 у событий приходит `unknown`, а `fields.record_id` равен `null`: берите `record_id` из `GET /v1/leads/{id}` по `data.lead.id`. Значения: `valid`, `missing`, `conflict`, `unknown`. | | `data.account` | object или null | Аккаунт, который вел переписку. `null`, если неизвестно. | | `data.account.id` | string (uuid) | Идентификатор аккаунта. | | `data.account.username` | string или null | Ник аккаунта без `@`. Может быть `null`. | | `data.account.name` | string или null | Отображаемое имя аккаунта. Может быть `null`. | | `data.first_message_sent_at` | string (date-time) или null | Когда отправлено первое сообщение. `null`, если отправка не подтверждена; у события ответа это частый случай. | | `data.first_message` | object | Текст первого сообщения. Только при `include_text=true`. Может отсутствовать. | | `data.first_message.text` | string или null | Сохраненный текст, до 4000 знаков. `null`, если текст недоступен. | | `data.first_message.text_truncated` | boolean или null | `true`: текст сокращен; `false`: текст полный; `null`: полнота неизвестна. | | `data.first_message.text_unavailable` | boolean | `true`, если текст не выдается. | | `data.first_message.text_unavailable_reason` | string или null | Причина недоступности текста или `null`. Значения: `not_saved`, `ownership_unproven`, `source_conflict`, `purged`. | Пример: ```json { "object": "event", "id": "evt_3b9e2c7a1f4d4e8a9b6c0d2e5f7a8c91", "type": "lead.first_message_sent", "api_version": "2026-10-18", "timestamp": "2026-10-26T07:30:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "contacted", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z" } } ``` ### Событие lead.replied Лид ответил. Приходит на каждый засчитанный ответ, у одного лида их может быть несколько. Поля текста в `data.reply` есть только при `include_text=true`. | Поле | Тип | Описание | |---|---|---| | `object` | string | Тип объекта, всегда `event`. | | `id` | string | Идентификатор события: `evt_` и 32 шестнадцатеричных знака. Ключ дедупликации. | | `api_version` | string | Версия формата события. Всегда `"2026-10-18"`. | | `timestamp` | string (date-time) | Когда произошел факт. | | `test` | boolean | Всегда `false`. | | `type` | string | Тип события. Всегда `"lead.replied"`. | | `data` | object | Данные события. | | `data.organization_id` | string (uuid) | Организация, в которой произошло событие. | | `data.workspace` | object | Пространство на момент события. | | `data.workspace.id` | string (uuid) | Идентификатор. | | `data.workspace.name` | string или null | Название. Может быть `null`. | | `data.campaign` | object или null | Кампания на момент события. Может быть `null`. | | `data.campaign.id` | string (uuid) | Идентификатор. | | `data.campaign.name` | string или null | Название. Может быть `null`. | | `data.lead` | object | Снимок лида. | | `data.lead.id` | string (uuid) | Идентификатор лида. | | `data.lead.username` | string или null | Ник Telegram без `@`. Может быть `null`. | | `data.lead.status` | string | Статус лида на момент события. Значения: `new`, `contacted`, `completed`, `replied`, `blocked`, `failed`, `suspended`, `deleted`, `opted_out`, `unknown`. | | `data.lead.fields` | object | Поля карточки на момент события. | | `data.lead.fields.record_id` | string или null | Идентификатор записи во внешней CRM, переданный при загрузке контактов. `null`, если `record_id_status` не `valid`. Может отсутствовать. | | `data.lead.fields.first_name` | string или null | Имя. Может отсутствовать. | | `data.lead.fields.last_name` | string или null | Фамилия. Может отсутствовать. | | `data.lead.fields.company` | string или null | Компания. Может отсутствовать. | | `data.lead.fields.position` | string или null | Должность. Может отсутствовать. | | `data.lead.fields.website` | string или null | Сайт. Может отсутствовать. | | `data.lead.fields.source` | string или null | Источник контакта. Может отсутствовать. | | `data.lead.fields.utm_source` | string или null | Метка `utm_source`. Может отсутствовать. | | `data.lead.fields.utm_medium` | string или null | Метка `utm_medium`. Может отсутствовать. | | `data.lead.fields.utm_campaign` | string или null | Метка `utm_campaign`. Может отсутствовать. | | `data.lead.fields_truncated` | boolean | `true`, если часть значений сокращена. | | `data.lead.record_id_status` | string | Надежность `fields.record_id`. В v1 у событий приходит `unknown`, а `fields.record_id` равен `null`: берите `record_id` из `GET /v1/leads/{id}` по `data.lead.id`. Значения: `valid`, `missing`, `conflict`, `unknown`. | | `data.account` | object или null | Аккаунт, который вел переписку. `null`, если неизвестно. | | `data.account.id` | string (uuid) | Идентификатор аккаунта. | | `data.account.username` | string или null | Ник аккаунта без `@`. Может быть `null`. | | `data.account.name` | string или null | Отображаемое имя аккаунта. Может быть `null`. | | `data.first_message_sent_at` | string (date-time) или null | Когда отправлено первое сообщение. `null`, если отправка не подтверждена; у события ответа это частый случай. | | `data.reply` | object | Ответ лида. | | `data.reply.id` | string (uuid) | Идентификатор ответа. | | `data.reply.replied_at` | string (date-time) | Когда пришел ответ. | | `data.reply.automation_stopped` | boolean или null | Остановлена ли цепочка после ответа. `null`, если остановка не подтверждена. Сейчас всегда `null`. | | `data.reply.is_first_reply` | boolean | Первый ли это ответ лида в кампании. Сейчас не передается. Может отсутствовать. | | `data.reply.text` | string или null | Текст ответа. Только при `include_text=true`. Может отсутствовать. | | `data.reply.text_truncated` | boolean или null | Признак сокращения текста. Только при `include_text=true`. Может отсутствовать. | | `data.reply.text_unavailable` | boolean | Текст недоступен. Только при `include_text=true`. Может отсутствовать. | | `data.reply.text_unavailable_reason` | string или null | Причина недоступности текста. Только при `include_text=true`. Значения: `not_saved`, `ownership_unproven`, `source_conflict`, `purged`. Может отсутствовать. | Пример: ```json { "object": "event", "id": "evt_8d4f1a6c2e9b4c7d8a3e5f0b1c6d9e27", "type": "lead.replied", "api_version": "2026-10-18", "timestamp": "2026-10-26T09:12:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "replied", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": null, "reply": { "id": "e4b7a2d9-6c1f-4e38-9a5b-0d8c3f7e1a62", "replied_at": "2026-10-26T09:12:00.000Z", "automation_stopped": null } } } ``` ### Лента событий `GET /v1/events` События в порядке появления: первое сообщение лиду и ответ лида. Опрашивайте ленту по курсору. `next_cursor` есть всегда, даже у пустой страницы. С `include_text=true` вес запроса 3. Право: `events:read`. Вес запроса: 2 (3 с `include_text=true`). #### Параметры | Параметр | Где | Тип | Обязательный | Описание | |---|---|---|---|---| | `limit` | строка запроса | integer | нет | Сколько объектов вернуть на странице. От 1 до 200. По умолчанию `50`. | | `cursor` | строка запроса | string | нет | Значение `next_cursor` из предыдущего ответа. Остальные фильтры должны совпадать с первым запросом. | | `workspace_id` | строка запроса | string (uuid) | нет | Только объекты этого пространства. | | `campaign_id` | строка запроса | string (uuid) | нет | Только объекты этой кампании. | | `type` | строка запроса | string | нет | Только события этого типа. Значения: `lead.first_message_sent`, `lead.replied`. | | `include_text` | строка запроса | string | нет | Добавить тексты сообщений в события. Нужны право `messages:read` и включенный «Текст сообщений» у ключа. Значения: `true`, `false`. По умолчанию `false`. | | `start` | строка запроса | string | нет | `now`: начать ленту с текущего момента. Возвращает пустую страницу и курсор. Нельзя вместе с `cursor`. Значения: `now`. | #### Ответ Список: `object`, `data`, `has_more`, `next_cursor`, `meta`. Пагинация: https://docs.melioralab.io/md/pagination.md Поле `meta`: | Поле | Тип | Описание | |---|---|---| | `meta.available_from` | string (date-time) или null | С какого момента в ленте есть события. `null`, если неизвестно. | | `meta.retention_days` | string | Сколько суток лента хранит события. Всегда `30`. | | `meta.collection` | object | Состояние сбора сохраненной переписки для API. | | `meta.collection.state` | string | `running`: сбор идет; `paused`: приостановлен в кабинете; `preparing`: готовится после включения; `degraded`: данные могут быть неполными. Значения: `running`, `paused`, `preparing`, `degraded`. | | `meta.collection.enabled_at` | string (date-time) или null | Когда сбор был включен. `null`, если еще не включен. | | `meta.collection.pause_pending` | boolean | Пауза или возобновление запрошены в кабинете, но еще не применены. | | `meta.coverage` | object | Полнота ленты событий. | | `meta.coverage.status` | string | `complete`, `partial` или `unknown`. Значения: `complete`, `partial`, `unknown`. | | `meta.coverage.gaps` | array of object | До 50 интервалов возможной неполноты. | | `meta.coverage.gaps[].id` | string (uuid) | Идентификатор интервала. | | `meta.coverage.gaps[].from` | string (date-time) | Начало интервала. | | `meta.coverage.gaps[].to` | string (date-time) или null | Конец интервала. `null`, если интервал еще не закрыт. | | `meta.coverage.gaps[].state` | string | `open`, `recovering`, `recovered`, `unrecoverable` или `unknown`. Значения: `open`, `recovering`, `recovered`, `unrecoverable`, `unknown`. | | `meta.coverage.gaps_truncated` | boolean | `true`, если интервалов больше 50. | | `meta.coverage.incomplete_from` | string (date-time) или null | Самая ранняя граница скрытых неполных интервалов. `null`, если таких нет. | | `meta.observed_at` | string (date-time) | Время чтения данных сервером. | #### Особые ошибки - `cursor_mismatch` (400): Курсор поврежден, выдан для другого маршрута или фильтров, либо права и охват ключа изменились. - `scope_required` (403): Ключ не имеет права на это чтение. Для текстов нужен также включенный «Текст сообщений». - `not_found` (404): Объект не найден или находится вне охвата ключа. Неизвестный путь тоже дает 404. - `cursor_expired` (410): Срок курсора истек: 15 минут для списков, 30 суток для ленты событий. Общие ошибки: https://docs.melioralab.io/md/errors.md #### Пример запроса ```bash curl -G https://api.melioralab.io/v1/events \ -H "Authorization: Bearer $MELIORA_API_KEY" \ -d limit=100 ``` #### Пример ответа: Ответ ```json { "object": "list", "data": [ { "object": "event", "id": "evt_3b9e2c7a1f4d4e8a9b6c0d2e5f7a8c91", "type": "lead.first_message_sent", "api_version": "2026-10-18", "timestamp": "2026-10-26T07:30:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "contacted", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z" } }, { "object": "event", "id": "evt_8d4f1a6c2e9b4c7d8a3e5f0b1c6d9e27", "type": "lead.replied", "api_version": "2026-10-18", "timestamp": "2026-10-26T09:12:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "replied", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": null, "reply": { "id": "e4b7a2d9-6c1f-4e38-9a5b-0d8c3f7e1a62", "replied_at": "2026-10-26T09:12:00.000Z", "automation_stopped": null } } } ], "has_more": false, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU", "meta": { "available_from": "2026-10-20T08:00:00.000Z", "retention_days": 30, "collection": { "state": "running", "enabled_at": "2026-10-20T08:00:00.000Z", "pause_pending": false }, "coverage": { "status": "unknown", "gaps": [], "gaps_truncated": false, "incomplete_from": null }, "observed_at": "2026-10-27T09:15:42.000Z" } } ``` #### Пример ответа: С include_text=true ```json { "object": "list", "data": [ { "object": "event", "id": "evt_3b9e2c7a1f4d4e8a9b6c0d2e5f7a8c91", "type": "lead.first_message_sent", "api_version": "2026-10-18", "timestamp": "2026-10-26T07:30:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "contacted", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": "2026-10-26T07:30:00.000Z", "first_message": { "text": "Добрый день, Анна! Пишу из Примера: подскажите, кто у вас отвечает за аутрич?", "text_truncated": false, "text_unavailable": false, "text_unavailable_reason": null } } }, { "object": "event", "id": "evt_8d4f1a6c2e9b4c7d8a3e5f0b1c6d9e27", "type": "lead.replied", "api_version": "2026-10-18", "timestamp": "2026-10-26T09:12:00.000Z", "test": false, "data": { "organization_id": "2c8f4a61-7d3e-4b9a-8f21-6a0d5e9c3b17", "workspace": { "id": "3f6a9d21-8b4c-4e7f-a102-5c9e8d7b6a43", "name": "Основное пространство" }, "campaign": { "id": "4d8b2f73-6e1a-4c95-b7d0-8a3f5e2c9b14", "name": "Пример кампании" }, "lead": { "id": "5a7c3e92-1f4b-4d68-9c2e-7b0a6d4f8e31", "username": "example_user", "status": "replied", "fields": { "record_id": null, "first_name": "Анна", "company": "ООО Пример" }, "fields_truncated": false, "record_id_status": "unknown" }, "account": { "id": "1d9f6b28-4c3e-4a75-8b19-5e7a2c0f6d83", "username": "example_sender", "name": "Мария из Примера" }, "first_message_sent_at": null, "reply": { "id": "e4b7a2d9-6c1f-4e38-9a5b-0d8c3f7e1a62", "replied_at": "2026-10-26T09:12:00.000Z", "automation_stopped": null, "text": "Здравствуйте! Это я, давайте созвонимся завтра.", "text_truncated": null, "text_unavailable": false, "text_unavailable_reason": null } } } ], "has_more": false, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU", "meta": { "available_from": "2026-10-20T08:00:00.000Z", "retention_days": 30, "collection": { "state": "running", "enabled_at": "2026-10-20T08:00:00.000Z", "pause_pending": false }, "coverage": { "status": "unknown", "gaps": [], "gaps_truncated": false, "incomplete_from": null }, "observed_at": "2026-10-27T09:15:42.000Z" } } ``` #### Пример ответа: С start=now ```json { "object": "list", "data": [], "has_more": false, "next_cursor": "v1.k1.Qm9hcmRfMWZ4.c3RyaWN0bHlfb3BhcXVl.dGFnX2V4YW1wbGU.bWFjX2V4YW1wbGVfdmFsdWU", "meta": { "available_from": "2026-10-20T08:00:00.000Z", "retention_days": 30, "collection": { "state": "running", "enabled_at": "2026-10-20T08:00:00.000Z", "pause_pending": false }, "coverage": { "status": "unknown", "gaps": [], "gaps_truncated": false, "incomplete_from": null }, "observed_at": "2026-10-27T09:15:42.000Z" } } ```