Перейти к содержимому
AISARAISAR
REST API

Сообщения

Сообщения — основная единица переписки внутри треда. 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_urlURL объявления или поста; только WhatsApp
referral.source_typead или post
referral.source_idID объявления/поста (для Instagram — ad_id)
referral.headlineЗаголовок объявления
referral.bodyТекст объявления; только WhatsApp
referral.ctwa_clidКлик-ID для точной атрибуции; только WhatsApp
referral.media_typeimage или video; только WhatsApp
referral.image_urlURL изображения объявления (если есть)
referral.video_urlURL видео объявления (если есть)
referral.post_idID поста, из которого крутилось объявление; только 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).

GET/v1/messages

Список сообщений с фильтрами (диалог/тред/канал/направление) и постраничной либо курсорной (cursor из next_cursor, before_message_id/after_message_id) пагинацией.

GET/v1/messages/search

Полнотекстовый поиск сообщений (`conversation_id` опционален — без него поиск идёт по всей компании).

POST/v1/messages

Создать сообщение напрямую, в т.ч. цитируемый ответ через quoted_message_id (обычно вместо этого используется /messages/send).

GET/v1/messages/{message}

Получить сообщение по id.

PATCH/v1/messages/{message}

Отредактировать текст своего исходящего сообщения (в пределах окна редактирования канала).

DELETE/v1/messages/{message}

Удалить сообщение (мягкое удаление; mode=revoke в JSON-теле — отозвать у получателя там, где канал поддерживает, кроме whatsapp_business).

GET/v1/messages/{message}/around

Окно сообщений вокруг конкретного сообщения (для прыжка к контексту); с 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).

POST/v1/messages/{message}/statuses

Записать статус доставки сообщения.

GET/v1/messages/{message}/reactions

Список реакций (эмодзи) на сообщение.

POST/v1/messages/{message}/reactions

Поставить реакцию {reaction:"<эмодзи>"} на сообщение от имени компании (одна исходящая реакция на сообщение — новая заменяет прежнюю).

DELETE/v1/messages/{message}/reactions

Снять исходящую реакцию компании с сообщения (идемпотентно; id реакции не нужен — она одна на сообщение).

POST/v1/messages/{message}/like

Лайкнуть комментарий Instagram (только комментарийные треды Instagram Business; у сообщения должен быть внешний id).

POST/v1/messages/{message}/forward

Переслать сообщение (copy-send) в существующий тред или кросс-канально по контакту.

POST/v1/messages/{message}/attachments/{attachment}/fetch

Догрузить медиа вложения из рантайма/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-превью изображения вместо оригинала.

POST/v1/uploads

Загрузить файл (до 64 МБ) и получить ссылку на медиа для прикрепления к сообщению.

POST/v1/uploads/voice

Загрузить голосовую заметку: транскодирует аудио/видео в voice-OGG (Opus) и возвращает media_id для прикрепления как голосовое сообщение.

GET/v1/messages/{message}/email-body

Email: письмо целиком — очищенный html, text, has_remote_images, has_quoted_text, html_too_complex и cid_map; внешние картинки отключены и идут через подписанную ссылку прокси.

GET/v1/email/remote-image

Email: картинка из письма через прокси AISAR по подписанному токену t из email-body (10 минут, привязан к пользователю и сообщению).

Примеры

Отправка текстового сообщения

Запрос

bash
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 передан в доставку."
  }'

Ответ

json
{
  "success": true
}

Отправка стикера из библиотеки

Запрос

bash
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 }
    ]
  }'

Ответ

json
{
  "success": true
}

Цитируемый ответ (reply)

Запрос

bash
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
  }'

Ответ

json
{
  "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"
    }
  }
}

Список сообщений диалога

Запрос

bash
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&perPage=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

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)

Запрос

bash
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"

Ответ

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

Запрос

bash
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"

Ответ

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" }
  }
}

Поиск сообщений в диалоге

Запрос

bash
curl -X GET "https://api.aisar.app/v1/messages/search?conversation_id=15&q=заказ" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

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)

Запрос

bash
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

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`)

Запрос

bash
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"

Ответ

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
  }
}

Окно сообщений вокруг конкретного

Запрос

bash
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"

Ответ

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
  }
}

Загрузка файла для вложения

Запрос

bash
curl -X POST "https://api.aisar.app/v1/uploads" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@invoice-1045.pdf"

Ответ

json
{
  "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"
}

Прикрепить загруженный файл к сообщению

Запрос

bash
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)

Запрос

bash
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments/55/fetch" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

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
        }
      ]
    }
  }
}

Редактирование исходящего сообщения

Запрос

bash
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 уже в пути." }'

Поставить реакцию на сообщение

Запрос

bash
curl -X POST "https://api.aisar.app/v1/messages/1001/reactions" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reaction": "👍" }'

Ответ

json
{
  "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"
    }
  }
}

Снять реакцию с сообщения

Запрос

bash
curl -X DELETE "https://api.aisar.app/v1/messages/1001/reactions" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "message": "Message reaction removed."
  }
}

Удаление с отзывом у получателя (mode в теле)

Запрос

bash
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": "Отправлено по ошибке" }'

Ответ

json
{
  "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)

Запрос

bash
curl -X DELETE "https://api.aisar.app/v1/messages/1001" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "revoke" }'

Ответ

json
{
  "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."
}

Форвард в существующий тред

Запрос

bash
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 }'

Ответ

json
{
  "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"
    }
  }
}

Кросс-канальный форвард по контакту

Запрос

bash
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
  }'

Ответ

json
{
  "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)

Запрос

bash
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 }'

Ответ

json
{
  "message": "Несколько подключённых каналов подходят этому контакту — укажите target_channel_id.",
  "error_code": "channel_required"
}

Загрузка голосовой заметки

Запрос

bash
curl -X POST "https://api.aisar.app/v1/uploads/voice" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@voice-note.webm"

Ответ

json
{
  "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

Запрос

bash
curl -X POST "https://api.aisar.app/v1/messages/1001/like" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "message": "Comment liked."
}

Отправка с заголовком Idempotency-Key (безопасный повтор при сетевом сбое)

Запрос

bash
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 передан в доставку."
  }'

Ответ

json
{
  "success": true
}

Сообщение, начатое из рекламы (referral)

Запрос

bash
curl -X GET "https://api.aisar.app/v1/messages/1006" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

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)

Запрос

bash
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": "Здравствуйте!"
  }'

Ответ

json
{
  "message": "This contact is blocked; the message was not sent.",
  "error_code": "contact_blocked"
}

Отметить входящее сообщение прочитанным

Запрос

bash
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" }'

Ответ

json
{
  "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)

Запрос

bash
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"
  }'

Ответ

json
{
  "message": "This idempotency key has already been used for a different message.",
  "error_code": "idempotency_key_conflict"
}

Email: письмо целиком (email-body)

Запрос

bash
curl -X GET "https://api.aisar.app/v1/messages/9001/email-body" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответ

json
{
  "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 в сообщении и первое письмо с темой

Запрос

bash
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": "Коммерческое предложение" } }
  }'

Ответ

json
{
  "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
    }
  }
}