Сообщения
Сообщения — основная единица переписки внутри треда. API позволяет читать историю сообщений, искать по тексту, отправлять новые исходящие сообщения (текст, вложения, голосовые заметки), редактировать и удалять недавние сообщения оператора, ставить реакции и отслеживать статусы доставки.
Точки входа и идемпотентность
POST /messages/send и POST /messages/send-template — самые частые точки входа: они сами находят или создают нужный тред по паре канал + получатель и сами генерируют ключ идемпотентности (клиентский body-параметр idempotency_key они не принимают). Идемпотентность по вашему ключу доступна на прямом POST /messages и на POST /messages/{message}/forward: передайте idempotency_key (до 100 символов). Ключ не может начинаться с префикса, зарезервированного за серверными интеграциями (b24-ol:, b24robot:, amo-chat:) — такое значение отвечает 422. Повторный запрос с тем же ключом возвращает то же сообщение только тогда, когда у повтора совпадают тред и автор запроса И это сообщение видно вызывающему (по тем же правилам, что GET /messages/{id}); в остальных случаях — например ключ уже занят сообщением другого треда, другого автора или скрытым от вас правилами доступа к каналам — ответ 409 {"message": "...", "error_code": "idempotency_key_conflict"}, без каких-либо данных существующего сообщения. Ключ необязателен: без него сервер сам генерирует случайный при каждом вызове — тогда повторы НЕ схлопываются, каждый создаёт и доставляет отдельное сообщение, поэтому клиентам с сетевыми автоповторами лучше передавать ключ явно. Строка "0" — обычное валидное значение ключа, а не пустое.
Отдельно от этого, POST /messages/send и POST /messages/send-template принимают необязательный HTTP-заголовок Idempotency-Key (до 128 символов) — механизм на уровне мидлвара, не связанный с body-параметром idempotency_key выше. Повторный запрос с тем же значением заголовка (скоуп — ключ + эндпоинт + пользователь) в течение 24 часов не выполняет отправку заново, а возвращает закэшированный ответ первого запроса; тот же ключ с другим телом запроса вернёт 422. Заголовок опционален — без него поведение эндпоинтов не меняется; это защита для клиентов с сетевыми автоповторами (например, мобильного приложения при потере соединения).
Треды удалённых каналов (channel_archived)
Без channel_id прямой POST /messages отправляет через канал треда. Если этот канал удалён (в том числе удалён с сохранением истории: переписка остаётся видна, но отправлять через него нельзя), с 2026-10-12 запрос сразу отвечает 422 {"message": "...", "error_code": "channel_archived"}, и сообщение не создаётся. Раньше такое сообщение сохранялось и затем получало статус ошибки с причиной no_runtime_instance. Чтобы ответить такому контакту, передайте в том же запросе channel_id действующего канала совместимого типа (например, другой номер WhatsApp): сообщение уйдёт через него в тред этого канала с тем же контактом, тред создастся при необходимости. Тот же код channel_archived возвращает POST /messages/{message}/forward, если целевой тред принадлежит удалённому каналу. Список действующих каналов: GET /channels.
Допустимые типы сообщений (message_type)
Прямой POST /messages принимает message_type только из закрытого списка: text, image, video, audio, voice, document, file, sticker, media, location, contact — любое другое значение (включая template) отвечает 422 в data.errors.message_type. Во входящих сообщениях template при этом встречается: так приходят бизнес-шаблоны, присланные клиенту на личный WhatsApp-канал. Шаблоны отправляются исключительно через POST /messages/send-template — прямой POST /messages их не принимает: строковое message_content не может нести имя шаблона и его параметры. Служебные типы (заметка, карточка звонка, запрос разрешения на звонок, системные и интерактивные сообщения) создаёт только сама платформа, через свои внутренние обработчики, а не через этот эндпоинт. Если message_type не передан или равен text, а в запросе есть attachments, тип сообщения берётся из attachments[0].type — так вложение автоматически определяет, что это на самом деле image/document/sticker/и т.д., а не текст.
С 2026-10-07 каждый элемент items[] в GET /messages/search несёт ещё три поля: contact_id (id контакта треда либо null), contact_photo (URL фото контакта либо null) и is_group (boolean: тред или аккаунт контакта — групповой чат). По ним результат отрисовывается строкой с аватаром без запроса за контактом.
Объект сообщения (MessageResource) с 2026-10-07 содержит deleted_via и deleted_at. deleted_via — кто или что удалило сообщение: contact (контакт удалил у себя), revoke (оператор отозвал у всех), local (скрыто только в AISAR), system, ai_agent, safety либо null, если сообщение не удалено; у старых удалённых входящих без записи о способе — contact. deleted_at — ISO-8601 время удаления либо null.
is_deleted при создании сообщения принимает только false (в т.ч. неявно, если поле не передано) — true отвечает 422: сообщение не может родиться уже удалённым, для удаления существует DELETE /messages/{message}.
Доступ к каналам (с 2026-09-26)
Сообщение по id (GET /messages/{id}, правка, удаление, статусы, вложения, реакции, лайк комментария) в канале, закрытом для пользователя правилами channel_access, отвечает 404, как сообщение другой компании. Отправка через такой канал отклоняется так же, как через канал чужой компании: POST /messages/send и POST /messages/send-template — 422 «Invalid channel for current company.», прямой POST /messages — 422 «Invalid thread for current company.» / «Invalid channel for current company.», а цитата недоступного сообщения — «Invalid quoted message for current company.». У звонков тот же отказ выглядит как 404 (см. группу «Звонки»).
Адресат исходящего сообщения
Прямой POST /messages создаёт только исходящие сообщения: direction — обязательное поле и принимает только outbound, любое другое значение отвечает 422. Адресатом исходящего всегда считается контакт-аккаунт треда, в который в итоге попадает сообщение. Если recipient_contact_account_id указывает на ДРУГОЙ аккаунт ТОГО ЖЕ контакта диалога (например второй номер того же клиента), сообщение перекладывается в тред этого аккаунта — существующий либо созданный на лету для той же пары диалог+канал; аккаунт ЧУЖОГО контакта по-прежнему отвечает 422 «Invalid contact account for current company.». Поле external_thread_id в теле запроса игнорируется — итоговое значение всегда берётся из треда, в который в итоге попало сообщение, а не из тела запроса.
external_message_id — идентификатор провайдера, который сервер сам проставляет ПОСЛЕ успешной отправки; клиент его не задаёт. На прямом POST /messages любое непустое значение этого поля отвечает 422 (пустое или отсутствующее — не ошибка); на POST /messages/{message}/forward поле вообще не читается из тела запроса, поэтому переданное значение просто не имеет эффекта. Любое исходящее, созданное через API этими двумя маршрутами, всегда получает автора (author_user_id — вызывающий пользователь) и уходит в очередь на доставку — создать через них «эхо без автора» нельзя.
Блокировка контакта
Если получатель — заблокированный контакт (blocked_at заполнен; блокируется через POST /contacts/{contact}/block, см. группу «Контакты»), исходящее сообщение отклоняется до создания: POST /messages/send, прямой POST /messages и POST /messages/{message}/forward возвращают 422 {"message": "This contact is blocked; the message was not sent.", "error_code": "contact_blocked"}. Проверяются ОБА контакта: контакт целевого треда (conversation_id; для /messages/send и /forward резолвится автоматически) И контакт фактического адресата, когда он отличается (см. «Адресат исходящего сообщения» выше, recipient_contact_account_id), — блокировки любого из них достаточно для отказа.
Каналы, которые не пишут первыми
Канал с can_start_conversation: false (сейчас это только Telegram Business, см. группу «Каналы») отвечает лишь тем, кто написал сам, и не открывает переписку с контактом, с которым у этого канала ещё нет треда. Такой запрос отклоняется до создания сообщения с 422 и error_code: channel_cannot_start_conversation. У POST /messages/send тело обёрнуто: {"data": {"message": "…", "error_code": "channel_cannot_start_conversation"}}, а контакт, созданный под отклонённый вызов, не сохраняется. Кросс-канальная пересылка POST /messages/{message}/forward отвечает плоским {"message": "…", "error_code": "channel_cannot_start_conversation"}, как прочие отказы пересылки. Прямой POST /messages отказывает тем же плоским телом, когда channel_id или recipient_contact_account_id уводят сообщение в новый тред такого канала. Тот же отказ дают POST /dialogs и создание треда POST /conversations/{conversation}/threads (см. группу «Диалоги»). Тред считается существующим, если у канала есть тред с этим же аккаунтом контакта в любом диалоге компании, поэтому ответ после закрытия прежнего диалога проходит.
Флаги происхождения сообщений Telegram Business
Объект metadata сообщения в ответах REST содержит три булевых флага происхождения. Значение true они принимают только у сообщений канала Telegram Business, у остальных каналов всегда false. sent_from_device — исходящее, которое владелец аккаунта написал в самом приложении Telegram, а не из AISAR. sent_by_business_bot — исходящее, которое от имени аккаунта отправил бот канала. is_from_offline — автоматическое сообщение самого Telegram: приветствие или «нет на месте», настроенные в Telegram для бизнеса. По этим флагам ответы с телефона и автоответы Telegram отличают от ответов операторов из AISAR. В вебхук-событиях message.* этих флагов нет. Ключи зарезервированы: переданные клиентом в metadata прямого POST /messages, они молча вырезаются (см. «Зарезервированные ключи metadata» ниже), поэтому подделать пометку нельзя.
Зарезервированные ключи metadata (с 2026-09-27)
Прямой POST /messages и POST /conversations/{conversation}/notes (см. группу «Диалоги») принимают произвольный объект metadata — собственные ключи интегратора (order_id, ссылка на карточку CRM и т.п.) сохраняются как есть. Часть имён ключей сервер использует как внутренний канал (например raw_payload, pending_echo, echo_external_ids, is_history_sync, bitrix24, amocrm, is_ai_response, ai_agent_instance_id, voice_call_id, mentions и другие служебные маркеры дедупликации/атрибуции) — с 25–27.09.2026 (ИБ-ревью против SSRF через metadata.raw_payload) такие ключи молча вырезаются из тела запроса до валидации и не попадают в сохранённое сообщение или заметку; сама отправка при этом не отклоняется, ответ как при обычном успехе. Список зарезервированных имён — денилист, а не вайтлист, он не публикуется полностью и может расшириться.
Поле my_ai_rating (good, bad или null) есть у сообщений в списках и карточке диалога: личная оценка текущего пользователя ответу ИИ-агента; ставится и снимается через POST/DELETE /conversations/{conversation}/messages/{message}/ai-rating (см. раздел «ИИ-агенты и голос» гайда по MCP). В WebSocket-трансляции сообщения поля нет: канал общий для всей компании, а оценка личная.
Список и пагинация
GET /messages фильтруется по conversation_id/thread_id/channel_id/direction/message_type (канонический список значений message_type — см. поле message.type события message.created в справочнике событий вебхуков) и пагинируется либо постранично (page/perPage), либо курсором — передайте before_message_id, чтобы идти к более старым сообщениям (направление прокрутки истории вверх), или after_message_id для более новых; приняты и легаси-псевдонимы before_id/after_id, limit (=perPage) и order (=sortDir), sortBy — одно из id|timestamp|created_at|updated_at (по умолчанию timestamp), а в курсорном режиме ответ сохраняет форму {items, pagination}, но total/lastPage возвращаются как null (тяжёлый COUNT пропускается).
cursor (с 2026-09-23) — непрозрачный курсор истории, рекомендуемый способ листать длинную историю. Каждый ответ GET /messages несёт data.pagination.next_cursor (то же значение продублировано в data.meta.next_cursor): строку для следующей страницы либо null, когда дальше сообщений нет. Передайте её как cursor в следующий запрос с теми же фильтрами и тем же sortDir, без page. Курсор привязан к тому же порядку, по которому отсортирован список, COALESCE(timestamp, created_at) плюс id, поэтому, в отличие от before_message_id/after_message_id, не обрывает историю, где id не совпадает с порядком по времени (например, импортированная переписка: большие id у старых сообщений). Курсор выдаётся и принимается только при sortBy=timestamp (по умолчанию); при другом sortBy поле next_cursor равно null, а переданный cursor даёт 422. cursor нельзя сочетать с page, before_message_id и after_message_id (422); строка, которую API не выдавал, тоже даёт 422. Разбирать курсор не нужно, его формат может измениться. Со страницей по курсору ответ идёт в курсорном режиме: total/lastPage равны null. Прежние параметры page, before_message_id и after_message_id работают как раньше.
GET /messages также поддерживает текстовый фильтр search (тот же ILIKE по message_content/external_message_id/external_thread_id) и его официальный алиас q (с 2026-08-20): если передан только q, его значение подставляется в search; если переданы оба — побеждает явный search. До этой даты нераспознанный q тут молча отбрасывался — запрос выглядел рабочим, но не находил ничего; для новых интеграций предпочитайте явный search.
Поиск (`GET /messages/search`)
GET /messages/search — обязательный q (строка, от 2 до 255 символов) ищет по тексту сообщения (ILIKE по message_content). conversation_id необязателен (с 2026-08-20): передан — поиск идёт внутри конкретного диалога (диалог другой компании, несуществующий, недоступный по правилам доступа к каналам либо целиком на канале, закрытом для семьи с 18-летним ребёнком, — 404); не передан — поиск идёт по всей компании, в границах доступных пользователю каналов, и тогда каждый элемент результата несёт собственный conversation_id — так можно найти диалог, которого иначе не видно в списке. Результаты отсортированы от новых к старым (created_at desc). Пагинация — page/perPage (5–100, по умолчанию 20). Ответ: { "data": { items[], pagination{page,perPage,total,lastPage,has_more}, total_matches } }, где каждый items[] — {message_id, conversation_id, thread_id, created_at, snippet, query, contact_name, channel_id, channel_type}; последние три поля (с 2026-08-20) — contact_name (readable-имя контакта треда, строка либо null), channel_id и channel_type (ключ типа канала, например whatsapp/telegram, либо null) — добавлены, чтобы результат из company-wide поиска можно было отрисовать отдельной строкой без дополнительного запроса за контактом и каналом. snippet (с 2026-08-22) — это окно текста вокруг НАЙДЕННОГО СОВПАДЕНИЯ (регистронезависимо, до ~240 символов, с ... по краям там, где текст обрезан), а не первые символы сообщения — до этой даты снippet всегда начинался с начала сообщения, и совпадение в длинном сообщении могло оказаться невидимым; при отсутствии текстового совпадения (например, попадание пришло по другому полю или сообщение — медиа без подписи) используется прежнее поведение (первые ~240 символов). Формат поля не менялся, изменилось только содержимое. Роут — под усиленным лимитом (throttle:api-token-heavy, 120 запросов/мин и для программных токенов, и — с 2026-08-22 — для обычных сессионных пользователей веб-клиента; раньше для сессионных клиентов лимита не было вовсе).
with_total (необязательный boolean, по умолчанию true, с 2026-09-05) — управляет тем, считает ли пагинация точный COUNT(*). По умолчанию (или при явном with_total=1) поведение не меняется: total и lastPage в ответе точные. При with_total=0 запрос переключается на постраничный обход без подсчёта общего количества — total и lastPage возвращаются null, а сам COUNT(*) (на крупной компании — до ~3.4 из ~3.7 с времени ответа) не выполняется вовсе. Поле has_more (boolean) присутствует всегда, независимо от with_total, и это единственный надёжный признак «есть ли следующая страница» — не полагайтесь на эвристику «страница пришла полной»: она даёт ложное срабатывание, когда количество найденных сообщений кратно perPage. Если интерфейсу не нужна постраничная нумерация (только бесконечная подгрузка) — передавайте with_total=0 и листайте по has_more.
С 2026-09-08 поиск не возвращает сообщения контактов, скрытых из рабочего inbox (POST /v1/contacts/{contact}/hide-from-inbox) — ни в company-wide режиме, ни при явно переданном conversation_id. Читать такую переписку по-прежнему можно напрямую: GET /messages?conversation_id=....
Цитируемые ответы
Чтобы отправить цитируемый ответ, используйте прямой POST /messages с quoted_message_id (внутренний id цитируемого сообщения) или quoted_external_message_id — /messages/send этот параметр не принимает.
Атрибуция рекламы (referral)
Объект сообщения (GET /messages, /messages/{message}, /messages/{message}/around) несёт поле referral — те же данные о переходе из рекламы, что и в вебхуке message.created (объект data.referral), просто без обёртки события. null для обычных сообщений; заполняется на первом сообщении диалога, начатого из объявления, только для каналов whatsapp/whatsapp_business (Click-to-WhatsApp) и instagram_business (реклама с переходом в Direct). Набор полей зависит от канала: WhatsApp отдаёт source_url, ctwa_clid, body, media_type; Instagram — post_id и ref. Общие для обоих: source_type, source_id, headline, image_url, video_url. Полное описание каждого поля — в справочнике событий вебхуков, событие message.created.
| Поле | Описание |
|---|---|
referral.source_url | URL объявления или поста; только WhatsApp |
referral.source_type | ad или post |
referral.source_id | ID объявления/поста (для Instagram — ad_id) |
referral.headline | Заголовок объявления |
referral.body | Текст объявления; только WhatsApp |
referral.ctwa_clid | Клик-ID для точной атрибуции; только WhatsApp |
referral.media_type | image или video; только WhatsApp |
referral.image_url | URL изображения объявления (если есть) |
referral.video_url | URL видео объявления (если есть) |
referral.post_id | ID поста, из которого крутилось объявление; только Instagram |
referral.ref | Метка из диплинка (ig.me/...?ref=), если задана; только Instagram |
Вложения
Вложения передаются в два шага: сначала загрузите файл через POST /uploads (до 64 МБ — image, video, audio, document), затем сошлитесь на возвращённые media_id/storage_path при создании сообщения или в POST /messages/{message}/attachments; источник проверяется и должен принадлежать вашей компании, иначе возвращается плоский 422 {error_code: attachment_source_invalid} (перезалейте файл).
Входящие вложения сначала несут рантайм/CDN-ссылку провайдера url, которая может протухнуть за часы (например, у Instagram) — вызовите POST /messages/{message}/attachments/{attachment}/fetch, чтобы догрузить байты в локальное хранилище, после чего у вложения появляется стабильный подписанный url, а GET .../attachments/{attachment}/media отдаёт эти байты потоком.
fetch работает и для вложений Instagram-каналов (meta_instagram) — ig_post/ig_reel/сторис резолвятся через Graph API/CDN так же, как это делает вебхук-обработчик при получении сообщения. Для вложений, у которых в принципе нет скачиваемого медиа (type = template, ephemeral или unsupported_type — карточка generic-шаблона, самоуничтожающееся или нераспознанное вложение), fetch сразу отвечает 422 {message: "...", error_code: "attachment_not_fetchable"}, а не пытается округлить попытку в 502 — вызывать fetch для таких типов бессмысленно, у них никогда не появится storage_path.
thumbnail_url (с 2026-08-24) заполняется автоматически для локально сохранённых image-вложений, кроме image/svg+xml (не растрируется и не отдаётся инлайном по соображениям ИБ) — это тот же подписанный роут .../media, но с параметром w=640, отдающий webp-превью вместо оригинала. Ранее поле было почти всегда null; провайдерский thumbnail_url (если он уже пришёл, например от Instagram lookaside CDN) имеет приоритет и не переопределяется. На GET .../attachments/{attachment}/media параметр w принимает только 256 или 640 (закрытый белый список) — другое значение или отсутствие w отдаёт оригинал как раньше; для не-изображений w игнорируется. Начиная с этой же даты «небезопасные для инлайна» MIME (svg, html, xml, txt и т.п.) отдаются с Content-Type: application/octet-stream и Content-Disposition: attachment вместо инлайн-рендера — как защита от XSS через сообщение с произвольным заявленным mime_type на origin с сессионной кукой. Роут — под лимитом throttle:signed-media (2000 запросов/мин на IP).
Редактирование и удаление
Редактирование (PATCH) и удаление (DELETE) ограничены окнами канала: гейтите контролы «редактировать» и «отозвать у всех» по предвычисленным per-message полям can_edit_until / can_revoke_until, а не по собственной per-channel логике — каждое поле это ISO-8601-дедлайн, пока действие доступно, либо null, когда недоступно (входящее сообщение, уже удалённое, не-текст для редактирования, канал не поддерживает действие — как whatsapp_business для отзыва — либо истёкшее окно).
DELETE по умолчанию мягко удаляет (обнуляет текст); mode=revoke дополнительно отзывает сообщение у получателя там, где канал поддерживает provider-side удаление (delete_scope='provider': QR-WhatsApp, Telegram, Instagram) — WhatsApp Cloud (whatsapp_business) отзывать не умеет и вернёт 422. Передавайте mode в JSON-теле, а не в query-строке: часть прокси срезает query-параметры на DELETE и тихо понизит revoke до локального удаления.
Отметка входящего сообщения прочитанным
POST /messages/{message}/statuses записывает статус доставки сообщения; для клиентского токена (сессия, PAT, embed) он принимает только status_name: "read" — любое другое значение отвечает 422 с пояснением, что делать статусы доставки (queued/sent/delivered/failed) может только сам коннектор канала. Статус можно поставить только ВХОДЯЩЕМУ сообщению (direction: "inbound") — попытка отметить своё же исходящее как прочитанное отвечает 422 тем же сообщением; сообщение, недоступное вызывающему (чужая компания, закрытый правилами доступа канал, диалог целиком на канале, скрытом от семьи 18+), отвечает 404 — раньше проверки статуса. Необязательный status_timestamp не может указывать на момент в будущем (допуск — 5 минут скоса часов, тот же, что и у last_message_at треда) и сохраняется в UTC. Поля external_message_id и status_details — собственность коннектора провайдера, клиенту их передавать нельзя (422, prohibited). Необязательный payload — плоский объект: не более 20 ключей, имя ключа до 100 символов, значение — строка до 1000 символов, число, bool или null; он только сохраняется (ни вебхук, ни ответ его не перечитывают) — ни во что не разворачивается и никуда, кроме БД, не идёт. Маршрут ограничен именованным лимитером в 60 запросов в минуту на пользователя (429 при превышении).
Реакции
POST /messages/{message}/reactions принимает {reaction: "<эмодзи>"} (обязательное, до 32 символов) и ставит реакцию от имени компании (по умолчанию автор — аутентифицированный пользователь; явные author_user_id/author_contact_account_id имеют приоритет и валидируются по компании) — канал допускает одну исходящую реакцию на сообщение, поэтому новый вызов заменяет предыдущую. DELETE /messages/{message}/reactions снимает исходящую реакцию компании с сообщения — id реакции не нужен (реакция у канала одна на сообщение), запрос идемпотентен (200 даже если снимать нечего). Оба действия рассылают вебхук message.reaction.updated (action: "added" / "removed").
Пересылка
POST /messages/{message}/forward выполняет copy-send: создаёт новое исходящее сообщение с тем же контентом и вложениями либо в существующий тред (target_thread_id), либо кросс-канально по контакту (target_contact_account_id, при необходимости с явным target_channel_id); пересылается только сообщение с содержимым для копирования — message_type из text, image, video, audio, voice, document, file, sticker, location, contact. Служебное сообщение (заметка, карточка звонка, запрос разрешения на звонок, системное, интерактивное) или сообщение типа media пересылать нельзя — ответ 422 message_not_forwardable. Необязательный idempotency_key подчиняется тем же правилам, что и на прямом POST /messages (см. «Точки входа и идемпотентность» выше): зарезервированный серверный префикс — 422, повтор с тем же ключом того же треда/автора при видимом сообщении — та же пересылка повторно, иначе — 409 idempotency_key_conflict. При отказе возвращается 422 с error_code из набора message_deleted, message_not_forwardable, channel_not_textable, content_type_unsupported, wa_window_closed, channel_incompatible, channel_not_connected, no_compatible_channel, channel_required, channel_cannot_start_conversation (канал не пишет первым — см. «Каналы, которые не пишут первыми» выше), contact_blocked (получатель заблокирован — см. «Блокировка контакта» выше), attachment_not_available (с 2026-09-27: вложение исходного сообщения ещё не было скачано в локальное хранилище, а догрузить его на лету перед пересылкой не удалось — сообщение при этом не создаётся, в отличие от прежнего поведения молчаливой пересылки без медиа). В отличие от прямого POST /messages, поле external_message_id этим маршрутом вообще не читается из тела запроса — переданное значение просто не имеет эффекта (не ошибка, но и не сохраняется).
Голосовые заметки
Голосовые заметки загружаются отдельным путём POST /uploads/voice: он принимает записанный аудио- или видеофайл (multipart-поле file, до 32 МБ, mimetypes webm/ogg/mp4/mpeg/wav/aac/m4a) и транскодирует его в voice-OGG (Opus), возвращая плоский объект с media_id/storage_path той же формы, что и POST /uploads, — сошлитесь на этот media_id при отправке, и получатель увидит именно голосовую заметку (ptt), а не обычный аудиофайл; при сбое транскодирования возвращается 422.
Отправка стикера
Стикер из библиотеки компании (см. группу «Стикеры») отправляется как обычное вложение, но по attachments[].sticker_id вместо attachments[].media_id — сошлитесь на id стикера из GET /v1/sticker-packs. Каждая строка attachments[] должна нести ровно одно из двух полей: и то, и другое сразу, и ни одного из двух — ошибка 422. Указывать attachments[].type для стикера не нужно — он выставляется автоматически (sticker); POST /v1/messages/send и прямой POST /v1/messages принимают sticker_id одинаково.
Лайк комментария Instagram
POST /messages/{message}/like ставит лайк на комментарий: он работает только для комментарийных тредов Instagram Business (thread.is_group + канал типа instagram_business) и требует, чтобы у сообщения был внешний id, иначе 422; при успехе возвращается плоский { "message": "Comment liked." }.
Временное ограничение instagram-аккаунта
Для канала типа instagram (личный аккаунт) POST /messages может ответить 409 с плоским телом (без обёртки data): { "message": "Instagram has temporarily restricted this account.", "error_code": "ig_frozen", "frozen_until": "2026-10-06T10:00:00Z" } и заголовком Retry-After (секунды до конца ограничения). Instagram временно ограничил аккаунт: повторите отправку после frozen_until. То же окно видно в состоянии канала.
Ответ оператора в Instagram и Messenger: human_agent
Для Instagram (Meta API) и Facebook Messenger обычное окно ответа — 24 часа после последнего сообщения клиента. Ответ живого оператора можно отправить в течение 7 дней (тег Meta HUMAN_AGENT). Необязательное булево поле human_agent на POST /messages и POST /messages/{message}/forward сообщает, что сообщение набрал человек. Пользователи кабинета, мобильного приложения и встроенного чата считаются людьми автоматически и поле не передают. Интеграции по API-токену (и серверный embed-токен) должны передать human_agent: true явно, и только если сообщение действительно написал человек; для ботов и автоматики передавать его запрещено (политика Meta, злоупотребление может привести к отключению функции). Ключи human_agent и human_agent_source в metadata зарезервированы сервером: переданные клиентом значения отбрасываются. При пересылке отметка ставится, только если исходное сообщение — ручной ответ оператора. Ответы операторов из открытых линий Битрикс24 и чатов amoCRM получают отметку, только если пользователь CRM сопоставлен с пользователем AISAR в настройках интеграции: сопоставляя пользователя, компания подтверждает, что от его имени пишет человек, а не робот или бизнес-процесс. Функция включается флагом на стороне платформы и может быть ещё выключена: тогда действует обычное 24-часовое окно. Состояние окон диалога см. в meta_window_state группы «Диалоги».
Email-сообщения
В каждом объекте сообщения есть поле email: для писем email-канала это блок с конвертом письма, для всех остальных сообщений null. Поля блока: subject (тема, до 998 символов, null если темы нет), from ({ address, name }), to, cc, reply_to (списки { address, name }; в realtime-представлении сообщения не более 5 адресов в списке, в обычном до 20), sender_auth (pass, fail или unknown: подтвердил ли почтовый сервер отправителя по DMARC), is_automated (автоматическое письмо), auto_class (класс письма: bounce, auto_reply, bulk, self, spam или human), bounce ({ status, diagnostic_code } для отчёта о недоставке; diagnostic_code отдаётся, только если не длиннее 200 символов), stub (too_large для письма больше 25 МБ и unprocessable для письма, которое не удалось разобрать: тела у таких сообщений нет, в чате показывается заглушка), has_html, has_quoted, attachments_skipped (сколько вложений не загружено сверх лимита 50 на письмо). Тело письма и заголовки Message-ID, References и Authentication-Results в блок не входят. На письма с auto_class, отличным от human, и с sender_auth: "fail" ИИ-агент, автоматизации и автоответы не срабатывают.
Отправка письма: обычный POST /messages/send или POST /messages в тред email-канала. Из metadata.email клиент может задать только subject (одна строка, до 255 символов); остальное (message_id, in_reply_to, references, bounce, class и т. д.) сервер пишет сам, переданное молча отбрасывается, чтобы нельзя было подделать цепочку писем. Для ответа тему передавать не нужно: сервер выставит «Re: …» от темы исходного письма, ответ уходит в ту же цепочку и только отправителю (адреса из копии в ответ не добавляются). Для первого письма контакту (в треде ещё нет ни одного письма) metadata.email.subject обязательно: иначе 422 на поле metadata.email.subject. К исходящему письму добавляется подпись ящика. Создать такой тред можно через POST /conversations/{conversation}/threads с contact_email_id (группа «Диалоги»). Ошибки доставки приходят статусом сообщения с кодом: email_auth_failed (сервер не принял пароль, канал переходит в expired), email_host_blocked, email_message_too_large, email_attachment_too_large, email_recipient_invalid, email_header_invalid, email_subject_required, email_bounced (сервер получателя вернул письмо), email_auto_rate_capped (превышен лимит автоматических писем с ящика; ответы сотрудников не ограничиваются).
GET /messages/{message}/email-body возвращает письмо целиком: data = { html, text, has_remote_images, has_quoted_text, html_too_complex, cid_map }. html это очищенный HTML (скрипты, формы и активное содержимое удалены) или null, text текстовая версия. Картинки из интернета отключены: в HTML они остаются как data-remote-src со ссылкой на прокси GET /email/remote-image?t=… (подписанная ссылка, привязана к пользователю и сообщению, действует 10 минут). Встроенные cid:-картинки это data-cid, а cid_map связывает content id с краткоживущей подписанной ссылкой на вложение. html_too_complex: true означает, что письмо слишком сложное для показа с оформлением и стоит показывать text. Доступ такой же, как к чтению сообщения по id (право conversation.view, видимость канала и треда); недоступное сообщение и сообщение без сохранённого письма отвечают одинаково: 404.
GET /email/remote-image?t=… отдаёт байты картинки из письма через прокси AISAR, чтобы отправитель не видел адрес читателя. Токен t берётся только из email-body; при каждом вызове заново проверяются подпись и срок токена, что он выдан именно этому пользователю, что сообщение ему видно и что адрес картинки есть в сохранённом письме (иначе 403 или 404). Не картинка, слишком большой файл или недоступный адрес дают ошибку с data.code: email_image_not_an_image, email_image_too_large, email_image_blocked, email_image_unreachable, email_image_upstream_error. Одновременно у одного пользователя идёт не более 4 загрузок (429 email_image_busy с Retry-After), кроме того действует лимит 120 запросов в минуту на пользователя и 600 на компанию. Ответ приходит с заголовками Content-Security-Policy: default-src 'none'; sandbox, X-Content-Type-Options: nosniff и Cache-Control: private, max-age=600.
Эндпоинты
| Метод | Путь | Описание |
|---|---|---|
| POST | /v1/messages/sendОтправить сообщение по каналу и получателю (создаёт тред при необходимости). | Отправить сообщение по каналу и получателю (создаёт тред при необходимости). |
| POST | /v1/messages/send-templateОтправить одобренный WhatsApp-шаблон по каналу и получателю (или thread_id). | Отправить одобренный WhatsApp-шаблон по каналу и получателю (или thread_id). |
| GET | /v1/messagesСписок сообщений с фильтрами (диалог/тред/канал/направление) и постраничной либо курсорной (cursor из next_cursor, before_message_id/after_message_id) пагинацией. | Список сообщений с фильтрами (диалог/тред/канал/направление) и постраничной либо курсорной (cursor из next_cursor, before_message_id/after_message_id) пагинацией. |
| GET | /v1/messages/searchПолнотекстовый поиск сообщений (`conversation_id` опционален — без него поиск идёт по всей компании). | Полнотекстовый поиск сообщений (`conversation_id` опционален — без него поиск идёт по всей компании). |
| POST | /v1/messagesСоздать сообщение напрямую, в т.ч. цитируемый ответ через quoted_message_id (обычно вместо этого используется /messages/send). | Создать сообщение напрямую, в т.ч. цитируемый ответ через quoted_message_id (обычно вместо этого используется /messages/send). |
| GET | /v1/messages/{message}Получить сообщение по id. | Получить сообщение по id. |
| PATCH | /v1/messages/{message}Отредактировать текст своего исходящего сообщения (в пределах окна редактирования канала). | Отредактировать текст своего исходящего сообщения (в пределах окна редактирования канала). |
| DELETE | /v1/messages/{message}Удалить сообщение (мягкое удаление; mode=revoke в JSON-теле — отозвать у получателя там, где канал поддерживает, кроме whatsapp_business). | Удалить сообщение (мягкое удаление; mode=revoke в JSON-теле — отозвать у получателя там, где канал поддерживает, кроме whatsapp_business). |
| GET | /v1/messages/{message}/aroundОкно сообщений вокруг конкретного сообщения (для прыжка к контексту); с 2026-08-22 под тем же усиленным лимитом, что и /messages/search (throttle:api-token-heavy). | Окно сообщений вокруг конкретного сообщения (для прыжка к контексту); с 2026-08-22 под тем же усиленным лимитом, что и /messages/search (throttle:api-token-heavy). |
| GET | /v1/messages/{message}/attachmentsСписок вложений сообщения. | Список вложений сообщения. |
| POST | /v1/messages/{message}/attachmentsПрикрепить файл к уже созданному сообщению. | Прикрепить файл к уже созданному сообщению. |
| GET | /v1/messages/{message}/statusesИстория статусов доставки (sent/delivered/read/failed). | История статусов доставки (sent/delivered/read/failed). |
| POST | /v1/messages/{message}/statusesЗаписать статус доставки сообщения. | Записать статус доставки сообщения. |
| GET | /v1/messages/{message}/reactionsСписок реакций (эмодзи) на сообщение. | Список реакций (эмодзи) на сообщение. |
| POST | /v1/messages/{message}/reactionsПоставить реакцию {reaction:"<эмодзи>"} на сообщение от имени компании (одна исходящая реакция на сообщение — новая заменяет прежнюю). | Поставить реакцию {reaction:"<эмодзи>"} на сообщение от имени компании (одна исходящая реакция на сообщение — новая заменяет прежнюю). |
| DELETE | /v1/messages/{message}/reactionsСнять исходящую реакцию компании с сообщения (идемпотентно; id реакции не нужен — она одна на сообщение). | Снять исходящую реакцию компании с сообщения (идемпотентно; id реакции не нужен — она одна на сообщение). |
| POST | /v1/messages/{message}/likeЛайкнуть комментарий Instagram (только комментарийные треды Instagram Business; у сообщения должен быть внешний id). | Лайкнуть комментарий Instagram (только комментарийные треды Instagram Business; у сообщения должен быть внешний id). |
| POST | /v1/messages/{message}/forwardПереслать сообщение (copy-send) в существующий тред или кросс-канально по контакту. | Переслать сообщение (copy-send) в существующий тред или кросс-канально по контакту. |
| POST | /v1/messages/{message}/attachments/{attachment}/fetchДогрузить медиа вложения из рантайма/Instagram Graph API в локальное хранилище (lazy-load); для нескачиваемых типов (template/ephemeral/unsupported_type) — 422 attachment_not_fetchable. | Догрузить медиа вложения из рантайма/Instagram Graph API в локальное хранилище (lazy-load); для нескачиваемых типов (template/ephemeral/unsupported_type) — 422 attachment_not_fetchable. |
| GET | /v1/messages/{message}/attachments/{attachment}/mediaОтдать (streaming) медиа вложения из локального хранилища; необязательный подписанный w=256|640 отдаёт webp-превью изображения вместо оригинала. | Отдать (streaming) медиа вложения из локального хранилища; необязательный подписанный w=256|640 отдаёт webp-превью изображения вместо оригинала. |
| POST | /v1/uploadsЗагрузить файл (до 64 МБ) и получить ссылку на медиа для прикрепления к сообщению. | Загрузить файл (до 64 МБ) и получить ссылку на медиа для прикрепления к сообщению. |
| POST | /v1/uploads/voiceЗагрузить голосовую заметку: транскодирует аудио/видео в voice-OGG (Opus) и возвращает media_id для прикрепления как голосовое сообщение. | Загрузить голосовую заметку: транскодирует аудио/видео в voice-OGG (Opus) и возвращает media_id для прикрепления как голосовое сообщение. |
| GET | /v1/messages/{message}/email-bodyEmail: письмо целиком — очищенный html, text, has_remote_images, has_quoted_text, html_too_complex и cid_map; внешние картинки отключены и идут через подписанную ссылку прокси. | Email: письмо целиком — очищенный html, text, has_remote_images, has_quoted_text, html_too_complex и cid_map; внешние картинки отключены и идут через подписанную ссылку прокси. |
| GET | /v1/email/remote-imageEmail: картинка из письма через прокси AISAR по подписанному токену t из email-body (10 минут, привязан к пользователю и сообщению). | Email: картинка из письма через прокси AISAR по подписанному токену t из email-body (10 минут, привязан к пользователю и сообщению). |
Примеры
Отправка текстового сообщения
Запрос
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте! Ваш заказ №1045 передан в доставку."
}'Ответ
{
"success": true
}Отправка стикера из библиотеки
Запрос
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"attachments": [
{ "sticker_id": 87 }
]
}'Ответ
{
"success": true
}Цитируемый ответ (reply)
Запрос
curl -X POST "https://api.aisar.app/v1/messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_type": "text",
"message_content": "Да, доставим сегодня до 18:00.",
"quoted_message_id": 998
}'Ответ
{
"data": {
"message": "Message created.",
"item": {
"id": 1005,
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Да, доставим сегодня до 18:00." },
"quoted_message_id": 998,
"is_deleted": false,
"created_at": "2026-02-09T18:10:00Z"
}
}
}Список сообщений диалога
Запрос
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"id": 1001,
"conversation_id": 15,
"thread_id": 33,
"channel_id": 3,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"display_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_deleted": false,
"timestamp": "2026-02-09T18:00:00Z",
"created_at": "2026-02-09T18:00:00Z",
"updated_at": "2026-02-09T18:00:00Z",
"can_revoke_until": "2026-02-09T18:15:00Z",
"can_edit_until": "2026-02-09T18:15:00Z"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "next_cursor": null },
"meta": { "next_cursor": null }
}
}Прокрутка истории вверх (курсор before_message_id)
Запрос
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&before_message_id=998&perPage=20&sortDir=desc" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{ "id": 997, "direction": "inbound", "message_type": "text", "message_content": { "text": "Здравствуйте, подскажите статус заказа?" } },
{ "id": 996, "direction": "outbound", "message_type": "text", "message_content": { "text": "Добрый день! Секунду, проверяю." } }
],
"pagination": { "page": 1, "perPage": 20, "total": null, "lastPage": null, "next_cursor": null },
"meta": { "next_cursor": null }
}
}Листание истории по cursor
Запрос
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&perPage=5&cursor=MjAyNi0wMi0wOVQxNzo1ODowMC4wMDAwMDBafDk5Ng" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{ "id": 995, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:55:00Z", "message_content": { "text": "Добрый день, заказ ещё в пути?" } },
{ "id": 994, "direction": "outbound", "message_type": "text", "timestamp": "2026-02-09T17:50:30Z", "message_content": { "text": "Уточняю у склада." } },
{ "id": 12870, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:45:00Z", "message_content": { "text": "Сообщение из импортированной истории" } },
{ "id": 952, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:42:00Z", "message_content": { "text": "Спасибо." } },
{ "id": 951, "direction": "outbound", "message_type": "text", "timestamp": "2026-02-09T17:40:12Z", "message_content": { "text": "Заказ передан курьеру." } }
],
"pagination": { "page": 1, "perPage": 5, "total": null, "lastPage": null, "next_cursor": "MjAyNi0wMi0wOVQxNzo0MDoxMi4wMDAwMDBafDk1MQ" },
"meta": { "next_cursor": "MjAyNi0wMi0wOVQxNzo0MDoxMi4wMDAwMDBafDk1MQ" }
}
}Поиск сообщений в диалоге
Запрос
curl -X GET "https://api.aisar.app/v1/messages/search?conversation_id=15&q=заказ" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"message_id": 1001,
"conversation_id": 15,
"thread_id": 33,
"created_at": "2026-02-09T18:00:00Z",
"snippet": "...Ваш заказ №1045 передан в доставку...",
"query": "заказ",
"contact_name": "Aigerim K.",
"contact_id": 88,
"contact_photo": null,
"is_group": false,
"channel_id": 3,
"channel_type": "whatsapp"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "has_more": false },
"total_matches": 1
}
}Поиск по всей компании (без conversation_id)
Запрос
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"message_id": 1001,
"conversation_id": 15,
"thread_id": 33,
"created_at": "2026-02-09T18:00:00Z",
"snippet": "...Ваш заказ №1045 передан в доставку...",
"query": "заказ",
"contact_name": "Aigerim K.",
"contact_id": 88,
"contact_photo": null,
"is_group": false,
"channel_id": 3,
"channel_type": "whatsapp"
},
{
"message_id": 2044,
"conversation_id": 27,
"thread_id": 61,
"created_at": "2026-02-08T11:20:00Z",
"snippet": "...уточните, пожалуйста, номер заказа...",
"query": "заказ",
"contact_name": "Jane Roe",
"channel_id": 9,
"channel_type": "telegram"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 2, "lastPage": 1, "has_more": false },
"total_matches": 2
}
}Поиск без точного total (`with_total=0`)
Запрос
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ&with_total=0&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{
"message_id": 2044,
"conversation_id": 27,
"thread_id": 61,
"created_at": "2026-02-08T11:20:00Z",
"snippet": "...уточните, пожалуйста, номер заказа...",
"query": "заказ",
"contact_name": "Jane Roe",
"channel_id": 9,
"channel_type": "telegram"
}
],
"pagination": { "page": 1, "perPage": 20, "total": null, "lastPage": null, "has_more": true },
"total_matches": null
}
}Окно сообщений вокруг конкретного
Запрос
curl -X GET "https://api.aisar.app/v1/messages/1001/around?before=20&after=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"items": [
{ "id": 998, "direction": "inbound", "message_type": "text", "message_content": { "text": "Добрый день! Где мой заказ №1045?" } },
{ "id": 1001, "direction": "outbound", "message_type": "text", "message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." } }
],
"anchor_message_id": 1001,
"has_older": true,
"has_newer": false
}
}Загрузка файла для вложения
Запрос
curl -X POST "https://api.aisar.app/v1/uploads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@invoice-1045.pdf"Ответ
{
"media_id": "med_9f3a2b7c",
"type": "document",
"storage_disk": "local",
"storage_path": "10/attachments/8b1e2c4f-0a3d-4e57-9c21-1f2a3b4c5d6e.pdf",
"filename": "invoice-1045.pdf",
"mime_type": "application/pdf",
"size": 245113,
"width": null,
"height": null,
"duration": null,
"expires_at": "2026-03-11T18:00:00Z"
}Прикрепить загруженный файл к сообщению
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "document",
"media_id": "med_9f3a2b7c",
"filename": "invoice-1045.pdf",
"mime_type": "application/pdf",
"size": 245113
}'Догрузить медиа входящего вложения (lazy-load)
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments/55/fetch" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"item": {
"id": 1001,
"direction": "inbound",
"message_type": "image",
"attachments": [
{
"id": 55,
"type": "image",
"url": "https://api.aisar.app/v1/messages/1001/attachments/55/media?company=3&signature=PLACEHOLDER",
"thumbnail_url": "https://api.aisar.app/v1/messages/1001/attachments/55/media?company=3&w=640&signature=PLACEHOLDER",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"width": 1280,
"height": 960
}
]
}
}
}Редактирование исходящего сообщения
Запрос
curl -X PATCH "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message_content": "Здравствуйте! Ваш заказ №1045 уже в пути." }'Поставить реакцию на сообщение
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/reactions" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reaction": "👍" }'Ответ
{
"data": {
"message": "Message reaction created.",
"item": {
"id": 77,
"message_id": 1001,
"author_user_id": 8,
"author_name": "Азиз Каримов",
"reaction": "👍",
"created_at": "2026-02-09T18:12:00Z"
}
}
}Снять реакцию с сообщения
Запрос
curl -X DELETE "https://api.aisar.app/v1/messages/1001/reactions" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"message": "Message reaction removed."
}
}Удаление с отзывом у получателя (mode в теле)
Запрос
curl -X DELETE "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "revoke", "reason": "Отправлено по ошибке" }'Ответ
{
"data": {
"message": "Message deleted.",
"item": {
"id": 1001,
"direction": "outbound",
"is_deleted": true,
"deleted_via": "revoke",
"deleted_at": "2026-10-07T09:12:00+00:00",
"can_revoke_until": null,
"can_edit_until": null
}
}
}Отзыв не поддержан каналом whatsapp_business (422)
Запрос
curl -X DELETE "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "revoke" }'Ответ
{
"message": "This channel does not support revoking a sent message for the recipient. Provider-side revoke (\"delete for everyone\") is not available on API-based channels such as WhatsApp Cloud (whatsapp_business). Omit \"mode\" for a local delete."
}Форвард в существующий тред
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "target_thread_id": 45 }'Ответ
{
"data": {
"message": "Message forwarded.",
"item": {
"id": 1002,
"thread_id": 45,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_forwarded": true,
"created_at": "2026-02-09T18:05:00Z"
}
}
}Кросс-канальный форвард по контакту
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_contact_account_id": 42,
"target_channel_id": 5
}'Ответ
{
"data": {
"message": "Message forwarded.",
"item": {
"id": 1003,
"thread_id": 51,
"channel_id": 5,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_forwarded": true,
"created_at": "2026-02-09T18:06:00Z"
}
}
}Форвард отклонён: нужен явный канал (422)
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "target_contact_account_id": 42 }'Ответ
{
"message": "Несколько подключённых каналов подходят этому контакту — укажите target_channel_id.",
"error_code": "channel_required"
}Загрузка голосовой заметки
Запрос
curl -X POST "https://api.aisar.app/v1/uploads/voice" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@voice-note.webm"Ответ
{
"media_id": "med_01JBQZ8VN2K3M4N5P6Q7R8S9T0",
"type": "audio",
"storage_disk": "local",
"storage_path": "10/attachments/7c1e2a4f-0b3d-4e57-9c21-2f3a4b5c6d7e.ogg",
"filename": "voice.ogg",
"mime_type": "audio/ogg",
"size": 18342,
"duration": 7,
"expires_at": "2026-03-11T18:00:00Z"
}Лайк комментария Instagram
Запрос
curl -X POST "https://api.aisar.app/v1/messages/1001/like" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"message": "Comment liked."
}Отправка с заголовком Idempotency-Key (безопасный повтор при сетевом сбое)
Запрос
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9c2b7e-8b7a-4e2d-9c3f-7a1b2c3d4e5f" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте! Ваш заказ №1045 передан в доставку."
}'Ответ
{
"success": true
}Сообщение, начатое из рекламы (referral)
Запрос
curl -X GET "https://api.aisar.app/v1/messages/1006" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"item": {
"id": 1006,
"conversation_id": 22,
"thread_id": 48,
"channel_id": 9,
"direction": "inbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте, интересует товар из рекламы" },
"referral": {
"source_type": "ad",
"source_id": "120210000000000",
"headline": "Летняя коллекция — скидка 20%",
"image_url": "https://scontent.xx.fbcdn.net/example.jpg",
"post_id": "17900000000000000",
"ref": "summer_sale"
},
"created_at": "2026-02-09T18:00:00Z"
}
}
}Отправка заблокированному контакту отклонена (422)
Запрос
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте!"
}'Ответ
{
"message": "This contact is blocked; the message was not sent.",
"error_code": "contact_blocked"
}Отметить входящее сообщение прочитанным
Запрос
curl -X POST "https://api.aisar.app/v1/messages/998/statuses" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status_name": "read" }'Ответ
{
"data": {
"message": "Message status recorded.",
"item": {
"id": 214,
"message_id": 998,
"status_name": "read",
"status_timestamp": "2026-02-09T18:11:00Z",
"created_at": "2026-02-09T18:11:00Z"
}
}
}Отклонённый повтор idempotency_key (409)
Запрос
curl -X POST "https://api.aisar.app/v1/messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_content": "Здравствуйте!",
"idempotency_key": "order-1045-followup"
}'Ответ
{
"message": "This idempotency key has already been used for a different message.",
"error_code": "idempotency_key_conflict"
}Email: письмо целиком (email-body)
Запрос
curl -X GET "https://api.aisar.app/v1/messages/9001/email-body" \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"html": "<div>Здравствуйте! Нужен счёт на 20 мешков.</div><img data-remote-src=\"https://api.aisar.app/v1/email/remote-image?t=eyJ...\">",
"text": "Здравствуйте! Нужен счёт на 20 мешков.",
"has_remote_images": true,
"has_quoted_text": false,
"html_too_complex": false,
"cid_map": {}
}
}Email: блок email в сообщении и первое письмо с темой
Запрос
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"thread_id": 77,
"message_type": "text",
"message_content": { "text": "Добрый день! Высылаем коммерческое предложение." },
"metadata": { "email": { "subject": "Коммерческое предложение" } }
}'Ответ
{
"data": {
"id": 9002,
"thread_id": 77,
"direction": "outbound",
"message_type": "text",
"email": {
"subject": "Коммерческое предложение",
"from": { "address": "zakaz@example.com", "name": "Example, отдел заказов" },
"to": [{ "address": "client@example.org", "name": null }],
"cc": [],
"reply_to": [],
"sender_auth": null,
"is_automated": false,
"auto_class": null,
"bounce": null,
"stub": null,
"has_html": false,
"has_quoted": false,
"attachments_skipped": 0
}
}
}