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

Контакты

Контакт — карточка человека или компании, с которой ведётся общение: телефоны, email, привязанные аккаунты каналов (contactAccounts — например конкретный WhatsApp-номер или Instagram-username), метки и статус (lead/active/inactive). Один контакт может иметь несколько привязанных аккаунтов и диалогов. API покрывает CRUD, поиск/резолв по идентификаторам, объединение дублей, управление подпиской на рассылки (opt-in/opt-out), блокировку контакта (block/unblock) и скрытие его из общего рабочего inbox (hide-from-inbox/unhide-from-inbox).

Список: фильтры, сортировка, пагинация

Список поддерживает пагинацию (page; perPage — от 5 до 100, по умолчанию 10), полнотекстовый поиск (search), фильтр statuses[] (lead, active, inactive, blocked; можно передать массивом или строкой через запятую) и сортировку (sortBy: id|display_name|company|status|created_at|last_activity_at; sortDir: asc|desc, по умолчанию desc).

Сортировка по умолчанию изменилась (2026-09-08): вместо created_at теперь last_activity_at — сначала контакты с самой свежей активностью, контакты без активности в конце, вторичный ключ id по убыванию. При sortDir=asc контакты без активности идут, наоборот, первыми (это осознанный компромисс ради индекса, а не ошибка). Клиентам, которым нужен прежний порядок, нужно явно передавать sortBy=created_at.

Фильтры панели контактов (с 2026-09-08, комбинируются по И): channel_type — ключ типа канала (значение value из GET /contacts/channel-options, а не отображаемое имя): отбирает контакты, у которых есть привязанный аккаунт этого типа; whatsapp_business при этом канонизируется в whatsapp, потому что номер Cloud API и «серый» WhatsApp делят один и тот же аккаунт контакта. tag_id — контакты с указанной меткой. responsible_user_id — контакты указанного ответственного менеджера. active_since (дата YYYY-MM-DD) — только контакты с last_activity_at не раньше указанной даты; контакты, у которых активности не было вовсе, из выборки исключаются.

Параметр created_since (дата) оставляет только контакты, созданные не раньше указанного момента; unassigned=1 — только контакты без ответственного (взаимоисключим с responsible_user_id). GET /contacts/counters отдаёт счётчики вкладок списка: {"data": {"all", "mine", "new_week", "blocked", "hidden"}} — all (нескрытые), mine (нескрытые с вами в качестве ответственного), new_week (созданные за последние 7 суток), blocked (заблокированные нескрытые), hidden (скрытые из inbox). Поиск и чипы панели на счётчики не влияют. Право — contact.view, embed-токен получает 403, лимит 60 запросов в минуту на пользователя, значения кэшируются на 60 секунд.

Фильтр hidden (0/1) относится к скрытию из рабочего inbox (см. ниже): hidden=1 возвращает ТОЛЬКО скрытые контакты, отсутствие параметра или 0 — только нескрытые. Значения «показать и те и другие» нет.

Каждый элемент списка дополнительно несёт last_activity_at (последняя активность контакта — сообщение или звонок, в любом из его диалогов), open_deals_count (число открытых сделок) и responsible_user ({id, name} или null). Два последних поля отдаются только эндпоинтами группы «Контакты» (index/show/store/update/merge/hide/unhide) — в контакте, вложенном в ресурсы диалога и треда, их нет, и отсутствие ключа нужно трактовать как «не запрашивалось», а не как ноль или null.

Флаг exclude_lid_only (0/1, по умолчанию 1) по умолчанию скрывает контакты, у которых единственные привязанные аккаунты — приватные WhatsApp-идентификаторы @lid без распознанного номера; передайте 0, чтобы их показать.

Быстрый поиск

Быстрый поиск /lookup ищет по телефонам и привязанным аккаунтам (external_id, username) и возвращает до 20 совпадений.

Доступ и фото

Контакт другой компании при чтении, обновлении, удалении и по вложенным ресурсам возвращает 404 «Contact not found.». Эндпоинты фото отдают изображение потоком с откатом на profile_picture привязанного аккаунта.

Создание и обновление связей

При создании и обновлении phones и emailes сохраняются целиком: PATCH полностью пересобирает эти наборы из тела запроса.

Для contactAccounts правила с 2026-09-08 другие (раньше обычное сохранение карточки могло удалить живой WhatsApp/Instagram-аккаунт контакта вместе с привязкой тредов). Элемент списка может нести id уже существующего аккаунта — тот самый, что отдаёт ответ, — и тогда совпадение ищется по нему; иначе по паре (account_type, external_id); иначе, если external_id не передан, — по единственному аккаунту этого типа у контакта. Аккаунт с непустым external_id этим эндпоинтом никогда не удаляется, и его external_id через него же нельзя переназначить: попытка подставить другое значение молча игнорируется (переносить провайдерскую идентичность нужно объединением контактов). Удаляются только аккаунты без external_id, которых нет во входящем списке. Пустой массив contactAccounts не удаляет ничего, а отсутствие ключа в теле запроса оставляет аккаунты нетронутыми. Логин аккаунта принимается и как login, и как username.

status при создании и обновлении принимает lead, active и inactive (lead — значение по умолчанию у нового контакта; до 2026-09-08 передать его явно было нельзя, эндпоинт отвечал 422). responsibleUserId в PATCH проверяется в пределах текущей компании: id пользователя другой компании даёт 422, а не «тихое» назначение.

С 2026-09-23 photo и contactAccounts.*.profilePicture больше не свободная строка: значение должно быть либо абсолютной http(s)-ссылкой, либо путём внутри каталога хранилища текущей компании — любое другое значение (например, путь другой компании или произвольная строка) отклоняется с 422. Форма отдаёт назад подписанную ссылку на фото, которую сама же и выдала в ответе (GET/POST/PATCH) — если её прислать обратно без изменений, это читается как «фото не менялось»: photo тогда просто не сохраняется заново (поле опциональное, старое фото остаётся), а такой же profilePicture внутри contactAccounts превращается в null (контроллер сохраняет фото уже существующего аккаунта, если пришёл null).

Звонки контакту

Телефонная пара эндпоинтов (доступна только при подключённой телефонии) отвечает на вопрос «можно ли позвонить контакту прямо сейчас»: GET /outbound-eligibility возвращает по указанному channel_id DTO из двух блоков — message (состояние 24-часового окна сообщений WhatsApp) и call (можно ли звонить и нужно ли/можно ли запросить разрешение), а POST /call-permission/request отправляет контакту запрос на разрешение звонка (WhatsApp Calling).

Для SIP-каналов звонок разрешён всегда (permission_status=not_required), разрешение не запрашивается. Лимиты Meta на запрос разрешения: не чаще 1 раза в 24 часа и не более 2 раз за скользящие 7 дней — при исчерпании POST отвечает 429 с полем eligibility, в котором заполнен request_cooldown_until (Unix-время снятия ограничения).

Блокировка контакта

POST /contacts/{contact}/block и /unblock (с 2026-09-04) — настоящая блокировка: blocked_at на контакте выставляется первым и остаётся источником истины, даже если провайдерские вызовы ниже отклонены целиком. После этого сервис best-effort блокирует контакт на каждом WhatsApp-канале компании (whatsapp и whatsapp_business), с которым у контакта есть хотя бы один тред, — канал, которому контакт никогда не писал, не трогается. Результат по каждому такому каналу попадает в provider_failures[] ответа ({channel_id, error_code, message}); пустой массив — полный успех (или провайдерских каналов не было вовсе), непустой — не отменяет локальную блокировку: blocked_at в том же ответе уже заполнен. Повторный block на уже заблокированном контакте — no-op (message: "Contact was already blocked.", provider_failures: []), без обращений к провайдерам; аналогично unblock на уже разблокированном.

GET /contacts/{contact}/block-preview отвечает на вопрос «что произойдёт, если заблокировать» ДО самой блокировки: channels[] перечисляет каналы, на которые уйдёт провайдерский блок (channel_id, channel_name, channel_type, provider_block_supported), и для каждого — window_open. Поле window_open осмысленно только для whatsapp_business: false означает, что контакт не писал последние 24 часа и Cloud API блокировку сейчас не примет (та же причина, что приходит потом как not_reachable_24h в provider_failures[]); у «серых» каналов такого ограничения нет и там всегда true. Пустой channels[] — блокировка будет только локальной. Право то же (contact:update), лимит — throttle:api-token-heavy.

Оба эндпоинта требуют право contact:update (как opt-out/opt-in) и ограничены отдельным лимитом throttle:contact-block: 6 изменений в час на контакт, 60 в час и 300 в сутки на компанию. Чтобы прочитать список номеров, заблокированных самим провайдером на конкретном канале (а не то, что заблокировано внутри AISAR), используйте GET /channels/{channel}/whatsapp/blocked-users из группы «Каналы» — авторизация там другая (channel:view). POST /contacts/{contact}/block, POST /contacts/{contact}/unblock и GET /contacts/{contact}/block-preview доступны только business-компаниям: для семейной (kids) компании все три отвечают 403.

Активность контакта и CSAT

GET /contacts/{contact}/activity возвращает объединённую ленту последних событий по контакту из четырёх источников: звонки (type: "call"), смены стадии сделки и её выигрыш/проигрыш (deal_stage), ответы на CSAT-опросы (csat) и участие в рассылках (broadcast). Каждый источник сначала ограничивается параметром limit по отдельности, затем всё сливается и сортируется по occurred_at (по убыванию) и снова обрезается до limit — так шумный источник не вытесняет остальные. limit — целое от 1 до 50, по умолчанию 10. Ответ: {"data": {"items": [...]}}, пагинации нет. Элемент — {type, occurred_at, title, meta, link}, где title — короткая метка данных (номер для звонка, название сделки/рассылки, sentiment для CSAT), а не готовая фраза; meta у каждого типа свой (звонок: direction, status, duration_sec, recording_status, has_transcript; сделка: deal_id, activity_type, to_stage_id, to_stage_name; CSAT: sentiment; рассылка: broadcast_id, status); link — либо null, либо {kind, id} с kind из call_transcript|deal|conversation|broadcast. Право — contact.view, лимит — 60 запросов/мин на пользователя (throttle:contact-activity-index).

Звонки в этой ленте отдаются по тому же правилу видимости, что и журнал звонков: рядовой оператор видит только звонки, в которых участвовал или на которые был назначен, а полную картину по компании — только обладатель права company_telephony.manage. Права contact.view самого по себе для чужих звонков недостаточно. Внутренние поля наружу не отдаются: metadata звонка, variables и error_message получателя рассылки в meta не попадают никогда.

GET /contacts/{contact}/csat отдаёт сводку удовлетворённости по контакту: {"data": {"positive_count", "negative_count", "last": {"sentiment", "responded_at"} | null}}. Числовой оценки нет вовсе — в системе хранится только двузначный sentiment (positive/negative), поэтому и в ответе его нет. Считаются только опросы, на которые контакт ОТВЕТИЛ (у отправленного, просроченного или пропущенного опроса сигнала удовлетворённости ещё нет). Свободный текст ответа клиента (raw_response) в этот ответ намеренно не включается. Право — contact.view, лимит — 120 запросов/мин на пользователя (throttle:contact-csat-show). Оба эндпоинта отвечают 404 «Contact not found.» на контакт чужой компании.

Файлы и медиа контакта

GET /contacts/{contact}/attachments (с 2026-09-08) отдаёт постраничную галерею файлов контакта сразу по ВСЕМ его диалогам — в отличие от GET /conversations/{conversation}/attachments, ограниченного одним диалогом. Право — conversation.view (это содержимое переписки, а не карточка контакта), нужна активная подписка, лимит — 60 запросов/мин на пользователя (throttle:contact-attachments-index). Контакт чужой компании — 404 «Contact not found.». Дополнительно применяется тот же фильтр доступных каналов, что и в списке диалогов: вложения из переписки на канале, который оператору не виден, в галерею не попадают. Скрытие контакта из inbox (hidden_from_inbox_at) на этот эндпоинт почти не влияет — карточка скрытого контакта по-прежнему читается по id, и её вкладка «Файлы» не пустеет; исключение с 2026-09-29 — записи звонков такого контакта в галерею не попадают (см. ниже).

Параметры: kind (необязательный, media | document; без него отдаются обе категории), page, perPage (по умолчанию 24, максимум 60). Ответ — {"data": {"items": [...], "pagination": {"page", "perPage", "total", "lastPage"}, "counts": {"media", "document"}}}. counts считаются по всей галерее и НЕ зависят от kind (чипы «Медиа N» / «Документы M» не меняются при переключении вкладки), а pagination.total, наоборот, учитывает активный kind. Сортировка — по времени отправки сообщения (провайдерский timestamp, при его отсутствии — время создания записи) по убыванию, при равенстве — по id вложения, поэтому альбом из нескольких фото сохраняет свой порядок.

Классификация: media — image, video, ig_story, ig_reel, ig_post, story_mention; document — document, file, contact (vCard) и audio (аудиофайл — его скачивают или проигрывают из списка, а не разглядывают в сетке). Всё остальное в галерею не попадает вовсе и не учитывается в counts: voice, sticker, template, ephemeral, unsupported_type, share, ig_story_reply, location. Голосовые исключены намеренно — это реплика в диалоге, а не файл. Та же классификация доступна в галерее диалога, но там она задаётся параметром family: у GET /conversations/{conversation}/attachments имя kind уже занято сырыми типами вложений.

Строка галереи — поля вложения (id, type, url, thumbnail_url, playback_url, filename, mime_type, size, width, height, duration; metadata в галерее намеренно НЕ отдаётся) плюс kind, family, extension, source, message_id, conversation_id, channel_id, channel_type, direction, sent_at, sender. family — одно из pdf|word|excel|powerpoint|archive|audio|contact|certificate|text|image|video|other (выводится из типа вложения, расширения имени файла и MIME); extension — заглавный штамп расширения (PDF, DOCX) или null, если ни имя, ни MIME его не дают; source заполняется только у Instagram-медиа (story|reel|post|story_mention), у остальных null; direction — inbound|outbound|system; sent_at — провайдерское время отправки сообщения; sender — {type: contact|user|system, name}. Для vCard (type: "contact") добавляются contact_name и phone — краткая выжимка карточки; полное тело vCard наружу не отдаётся.

Ссылки url, thumbnail_url и playback_url в этой галерее — короткоживущие подписанные ссылки со сроком жизни 20 минут (та же политика, что и у галереи диалога), в отличие от постоянной подписи в GET /messages/{message}/attachments. Не сохраняйте их: страница галереи, пролежавшая дольше 20 минут, начнёт отдавать 403 по этим ссылкам — правильная реакция клиента — перезапросить страницу и получить свежие ссылки.

Скрытие контакта из рабочего inbox

POST /contacts/{contact}/hide-from-inbox и /unhide-from-inbox (с 2026-09-08) убирают контакт из общего рабочего inbox компании и возвращают его обратно. Это не блокировка и не исключение чата на канале: всё, что пишет контакт, по-прежнему принимается и сохраняется — закрываются только поверхности, которые показывают его команде. Право то же, что у блокировки (contact:update), тот же лимит throttle:contact-block и тот же отказ для embed-токена (403). Оба эндпоинта идемпотентны: повторный вызов на уже скрытом (или уже видимом) контакте отвечает 200 и не порождает события. Ответ — карточка контакта: {"data": {...}} с полями hidden_from_inbox_at и hidden_from_inbox_by_user_id.

Пока контакт скрыт, его диалоги не попадают в GET /conversations, GET /conversations/counters (все счётчики) и GET /threads, не находятся в поиске по сообщениям GET /messages/search, не показываются в журнале звонков (включая супервизоров телефонии), не попадают в аудитории рассылок, не запускают автоматизации, автоответы и ИИ-агента, не порождают автоматических исходящих (CSAT-опрос фиксирует пропуск), не шлют пуш о новом сообщении и уведомление о пропущенном звонке и не зеркалируются в Bitrix24/amoCRM.

Не меняется при этом следующее: диалог и его сообщения по-прежнему читаются по id (GET /conversations/{id}, GET /messages), оператор-человек может ответить вручную, исходящие вебхуки (в том числе message.created) продолжают уходить в ваши интеграции, а экспорты диалогов включают скрытые контакты. Найти скрытые контакты можно только через GET /contacts?hidden=1 — в списке диалогов «показать скрытые» нет. Отдельного типа события для скрытия не вводилось: и скрытие, и возврат отправляют contact.updated. Объединение контактов работает fail-closed: если скрыт был хотя бы один из двух, победивший контакт остаётся скрытым. Исключение — содержимое звонков (с 2026-09-29): пока контакт скрыт, запись звонка не проигрывается и не показывается в галереях вложений, GET /calls/{call}/recording-url и GET /calls/{call}/transcript отвечают 404, а ИИ-сводка звонка не отдаётся ни в ленте сообщений, ни в списке заметок, ни в экспорте. Ничего не удаляется — после возврата контакта в inbox всё снова доступно; уже доставленные вебхуки не отзываются.

Исключение из авто-лидов

С 2026-09-29 PATCH /contacts/{contact} принимает auto_lead_excluded (boolean) — «Не создавать лиды по этому контакту»: true ставит исключение (повторная отправка true сохраняет исходный момент и автора), false снимает его, в том числе поставленное при отклонении лида (POST /deals/{deal}/decline с exclude_contact: true). Право то же, что у остальных полей, — contact.update. Исключение действует только на автоматические лиды: сообщения и звонки контакта приходят как обычно, сделку можно создать вручную, автоматизацией или ИИ-агентом.

Карточка контакта отдаёт состояние исключения полями auto_lead_excluded_at (ISO-время или null — не null ⇔ авто-лиды по контакту не создаются), auto_lead_excluded_via (manual — из карточки контакта, decline — при отклонении лида, или null) и auto_lead_excluded_by_user_id — они есть в любом объекте контакта. Имя того, кто исключил, — auto_lead_excluded_by (строка или null) — только в ответах GET /contacts/{contact} и PATCH /contacts/{contact}; в списках и во вложенных контактах ключа нет.

Фото контакта и пользователя

GET /contacts/{contact}/photo и GET /users/{user}/photo (с 2026-08-24) всегда отдают webp-превью шириной до 256px, если исходник — декодируемое изображение шире 256px; в остальных случаях (уже ≤256px, либо GD не смог декодировать формат) отдаётся оригинал потоком, как раньше. Оригинал этих двух роутов больше не запросить явно — если он понадобится, потребуется отдельный параметр. Оба роута — signed и под лимитом throttle:signed-media (2000 запросов/мин на IP), общим с GET .../attachments/{attachment}/media.

С 2026-09-23 поле photo в карточке контакта — это подписанная ссылка вида /v1/contacts/{contact}/photo/{version}, где version — 16-символьный hex, производный от пути хранимого файла: смена фото даёт новый путь и, значит, новую версию и новую ссылку, поэтому её можно кэшировать в браузере как immutable, без отдельной инвалидации. Старая ссылка без версии (GET /contacts/{contact}/photo) по-прежнему работает — она осталась как основной, невезионированный роут и продолжает отдаваться для уже выданных ссылок. Подпись обеих ссылок не имеет срока действия.

Импорт контактов из CSV

Импорт идёт в несколько шагов: загрузка файла (POST /contact-imports) → при необходимости пересчитать кодировку/разделитель (/reparse) и получить подсказку сопоставления колонок от ИИ (/ai-mapping) → проверка без записи (/validate, асинхронно) → запуск (/start) → итоговый отчёт (/report). Результат всех шагов — один объект импорта data.import. Поддерживаются файлы .csv/.txt до 10 МБ, до 50 000 строк, до 200 колонок; кодировка auto|utf-8|windows-1251, разделитель auto, ;, ,, табуляция или | (по умолчанию определяются автоматически, фактически выбранные значения — в encoding/delimiter, определённые — в detected_encoding/detected_delimiter). GET /contact-imports/template?locale=ru|en|kk отдаёт CSV-шаблон (UTF-8 с BOM, разделитель ;).

Импорт намеренно НЕ шлёт вебхук contact.created, не запускает автоматизации («новый контакт» → приветствие) и не синхронизирует контакты с Bitrix24/amoCRM: файл часто сам выгружен из CRM, а массовая рассылка приветствий по файлу закончилась бы блокировкой канала. О результате узнавайте из realtime-событий contact_import.updated / completed / failed (канал private-user.{userId}, только автору) или опросом GET /contact-imports/{import}.

Права: contact.import вместе с contact.create для любого эндпоинта группы; режимы fill_empty и overwrite дополнительно требуют contact.update (без него 422 по options.duplicate_mode с кодом duplicate_mode_forbidden), создание новой метки — tag.create (без права метка не создаётся, в статистике предупреждение tag_not_created). Embed-токен получает 403 на всю группу, включая чтение. Импорт видит его автор; роли с обзором всей компании видят все импорты компании; чужой или несуществующий импорт — 404. Записи и файлы хранятся 7 суток (expires_at), в списке — только они.

Лимиты: загрузка и /reparse вместе — 10 в час на пользователя (429); не более 20 загрузок в сутки на компанию (422, daily_limit; удаление импорта слот не возвращает); /ai-mapping — 10 в час на пользователя и 60 в час на компанию (при исчерпании ответ 200 с ai_status: "unavailable"); /start — 10 в минуту; /validate, скачивание CSV и удаление — 30 в минуту; чтение списка — 60, карточки — 120 в минуту. В компании одновременно выполняется не более одного импорта (валидация или запись): иначе 409 import_in_progress.

Статусы (status): uploaded → validating → validated → importing → completed; также failed и cancelled. Сопоставление и параметры меняются только в uploaded/validated (иначе 409 import_not_editable). mapping — массив {column_index, field, transform?, date_format?, source?}; field — first_name, last_name, full_name, phone, email, company, title, dob, tags, responsible, comments или null (пропустить колонку); несколько колонок можно отдать только в phone, email, tags, comments; transform — split_full_name, normalize_phone, match_user, date_format (только к своему полю); нужно хотя бы имя или телефон. options: duplicate_mode — skip | fill_empty (по умолчанию: заполнить только пустые поля существующего контакта) | overwrite; tag_name (до 50 символов; метка, которую получат все импортированные контакты); default_responsible_user_id (участник компании); create_whatsapp_accounts (по умолчанию true). Дубликаты ищутся по телефону внутри компании; на контакт принимается не более 10 телефонов и 10 e-mail — лишние отбрасываются с предупреждением.

Объект импорта: id, status, original_filename, file_size, total_rows, encoding, delimiter, detected_encoding, detected_delimiter, headers, column_samples, suggested_mapping (подсказка по словарю заголовков), mapping, mapping_hash, options, stats (после проверки: new, update, skip, file_duplicates, errors, error_breakdown, warning_breakdown), preview (первые 5 строк к созданию/обновлению), счётчики processed_rows, created_count, updated_count, skipped_count, failed_count, tag ({id, name} или null), error_code (причина failed), has_errors_file, has_report, ai_mapping_used, ai_mapping_available, is_mine (загрузил ли файл текущий пользователь), метки времени created_at, validated_at, started_at, finished_at, expires_at. POST /validate и POST /start отвечают 202 и продолжают работу в фоне. mapping_hash фиксирует сопоставление и параметры, проверенные при validate; POST /start обязан передать его в теле — иначе 409 stale_validation (нужна новая проверка).

Ошибки бизнес-правил приходят как {"data": {"message", "code", "errors": {поле: [код]}}}. 422: not_csv, empty_file, no_header, too_many_rows, too_many_columns, cell_too_long, invalid_encoding, daily_limit, mapping_invalid_column, mapping_duplicate_column, mapping_duplicate_field, mapping_invalid_transform, mapping_name_conflict, mapping_requires_phone_or_name, duplicate_mode_forbidden. 409: import_not_editable, import_in_progress (в том числе при удалении импорта, который сейчас выполняется), stale_validation. 410 file_missing — исходный файл уже удалён. 404 с code: "feature_disabled" — ИИ-подсказка сопоставления выключена (см. ai_mapping_available). GET /errors (CSV строк с ошибками, доступен после проверки) и GET /report (CSV построчного результата, после завершения) отдают файл потоком; если файла нет — 404.

Каждый элемент emailes в ответе содержит id (id адреса), email и type. Этот id передаётся как contact_email_id в POST /conversations/{conversation}/threads, чтобы написать контакту первое письмо через email-канал. В теле создания и обновления контакта id не нужен: набор адресов пересобирается из email и type, поэтому id адреса может смениться после PATCH. Берите свежий id из актуального ответа.

Эндпоинты

МетодПуть
GET/v1/contacts

Список контактов компании (query: search, statuses[]=lead|active|inactive|blocked, channel_type, tag_id, responsible_user_id, unassigned, active_since, created_since, hidden, sortBy, sortDir, exclude_lid_only, page, perPage; по умолчанию сортировка по last_activity_at desc).

GET/v1/contacts/counters

Счётчики вкладок списка контактов: all, mine, new_week, blocked, hidden (поиск и чипы не учитываются).

GET/v1/contacts/lookup

Быстрый поиск контактов по телефону, external_id или username канала.

GET/v1/contacts/channel-options

Список типов каналов, доступных для привязки аккаунта к контакту.

GET/v1/contacts/{contact}

Карточка одного контакта (phones, emailes, contactAccounts, tags).

POST/v1/contacts

Создать контакт (с телефонами, email и привязанными аккаунтами каналов).

POST/v1/contacts/resolve-identifiers

Разрешить пакет идентификаторов (телефон/username/external_id) в id существующих контактов.

PATCH/v1/contacts/{contact}

Обновить контакт (полностью пересохраняет связанные телефоны/email/аккаунты; auto_lead_excluded — исключить контакт из авто-лидов или вернуть).

DELETE/v1/contacts/{contact}

Удалить контакт (мягкое удаление).

POST/v1/contacts/{contact}/merge

Объединить контакт с другим (все диалоги и аккаунты переносятся в целевой).

POST/v1/contacts/{contact}/tags

Синхронизировать метки контакта (полная замена набора).

POST/v1/contacts/{contact}/opt-out

Отписать контакт от рассылок.

POST/v1/contacts/{contact}/opt-in

Вернуть контакт в рассылки (снять opt-out).

GET/v1/contacts/{contact}/block-preview

Что затронет блокировка до её применения: каналы, куда уйдёт блок, и открыто ли на них 24-часовое окно WhatsApp.

POST/v1/contacts/{contact}/block

Заблокировать контакт (body: reason?) — ставит blocked_at и best-effort блокирует его на каждом WhatsApp-канале, с которым у него есть переписка.

POST/v1/contacts/{contact}/unblock

Снять блокировку контакта — очищает blocked_at и best-effort разблокирует его на затронутых WhatsApp-каналах.

GET/v1/contacts/{contact}/activity

Объединённая лента активности контакта: звонки, смены стадий сделок, ответы CSAT и рассылки (query: limit, 1–50, по умолчанию 10).

GET/v1/contacts/{contact}/csat

Сводка CSAT по контакту: positive_count, negative_count и последний ответ (sentiment, responded_at).

GET/v1/contacts/{contact}/attachments

Постраничная галерея файлов контакта по всем его диалогам (query: kind=media|document, page, perPage — по умолчанию 24, максимум 60) со счётчиками counts по обеим категориям; ссылки на медиа живут 20 минут.

POST/v1/contacts/{contact}/hide-from-inbox

Скрыть контакт из общего рабочего inbox — его диалоги уходят из списков, счётчиков, поиска, автоматизаций и рассылок, но продолжают приниматься.

POST/v1/contacts/{contact}/unhide-from-inbox

Вернуть скрытый контакт в общий рабочий inbox (очищает hidden_from_inbox_at).

GET/v1/contacts/{contact}/deals

Список сделок контакта (требует включённый функционал сделок).

GET/v1/contacts/{contact}/outbound-eligibility

Проверить право на исходящий звонок контакту по каналу (query: channel_id) — окно сообщений и разрешение на звонок.

POST/v1/contacts/{contact}/call-permission/request

Запросить у контакта разрешение на звонок (WhatsApp Calling; body: channel_id).

GET/v1/contacts/{contact}/photo

Фото контакта (fallback на profile_picture привязанного аккаунта) — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком. Невезионированная ссылка, продолжает работать для уже выданных ссылок.

GET/v1/contacts/{contact}/photo/{version}

Фото контакта с версией пути в ссылке (с 2026-09-23) — та же отдача, что у невезионированного роута, но ссылку можно кэшировать в браузере как immutable.

GET/v1/contact-imports/template

CSV-шаблон импорта контактов (query: locale = ru|en|kk, по умолчанию ru).

GET/v1/contact-imports

Список импортов контактов за последние 7 суток (query: page, perPage 1–20); автор видит свои, роли с обзором компании — все.

POST/v1/contact-imports

Загрузить CSV (multipart: file до 10 МБ, encoding?, delimiter?) — 201 с объектом импорта и подсказкой сопоставления колонок.

GET/v1/contact-imports/{import}

Карточка импорта: статус, сопоставление, статистика проверки, предпросмотр и счётчики.

POST/v1/contact-imports/{import}/reparse

Перечитать файл с другой кодировкой/разделителем (body: encoding?, delimiter?); сбрасывает сопоставление и проверку.

POST/v1/contact-imports/{import}/ai-mapping

ИИ-подсказка сопоставления колонок: ai_status (ok|unavailable), mapping[] и summary {mapped, total, to_check}.

POST/v1/contact-imports/{import}/validate

Сохранить сопоставление (mapping) и параметры (options) и запустить проверку без записи — 202.

GET/v1/contact-imports/{import}/errors

CSV со строками, не прошедшими проверку (после validate).

POST/v1/contact-imports/{import}/start

Запустить импорт проверенного файла (body: mapping_hash) — 202; контакты создаются без вебхука contact.created, автоматизаций и CRM-синхронизации.

GET/v1/contact-imports/{import}/report

CSV-отчёт по итогам импорта: результат по каждой строке.

DELETE/v1/contact-imports/{import}

Удалить запись импорта и его файлы (409, пока идёт проверка или запись).

GET/v1/users/{user}/photo

Фото пользователя компании — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком.

Примеры

Создание контакта

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "John",
    "lastName": "Doe",
    "phones": [{ "phone": "+12025550187", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }]
  }'

Ответ

json
{
  "data": {
    "id": 45,
    "company_id": 1,
    "firstName": "John",
    "lastName": "Doe",
    "displayName": "John Doe",
    "status": "lead",
    "phones": [{ "phone": "+12025550187", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }],
    "contactAccounts": [],
    "merged_into_contact_id": null,
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null
  }
}

Поиск контакта по телефону

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts/lookup?q=%2B12025550187" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": [
    {
      "id": 45,
      "displayName": "John Doe",
      "status": "lead",
      "phones": [{ "phone": "+12025550187", "type": "mobile" }]
    }
  ]
}

Список контактов с фильтром по статусу

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts?perPage=10&statuses=active,inactive&sortBy=created_at&sortDir=desc" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "items": [
      {
        "id": 45,
        "displayName": "John Doe",
        "status": "active",
        "phones": [{ "phone": "+12025550187", "type": "mobile" }],
        "tags": []
      }
    ],
    "pagination": { "page": 1, "perPage": 10, "total": 120, "lastPage": 12 }
  }
}

Карточка контакта

Запрос

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

Ответ

json
{
  "data": {
    "id": 45,
    "company_id": 10,
    "firstName": "John",
    "lastName": "Doe",
    "displayName": "John Doe",
    "status": "lead",
    "phones": [{ "phone": "+12025550187", "type": "mobile" }],
    "emailes": [{ "email": "john@example.com", "type": "work" }],
    "contactAccounts": [
      {
        "id": 88,
        "contact_id": 45,
        "account_type": "whatsapp",
        "external_id": "+12025550187",
        "username": null,
        "name": "John",
        "profileDisplayName": "John",
        "addressBookName": null,
        "profilePicture": null,
        "is_group": false,
        "status": "active",
        "presence": null
      }
    ],
    "tags": [{ "id": 3, "name": "VIP" }],
    "merged_into_contact_id": null,
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null
  }
}

Синхронизация меток контакта

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/tags" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "tag_ids": [1, 2, 3]
  }'

Ответ

json
{
  "data": {
    "tags": [
      { "id": 1, "name": "Lead" },
      { "id": 2, "name": "Newsletter" },
      { "id": 3, "name": "VIP" }
    ]
  }
}

Сделки контакта

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/deals?perPage=20" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "items": [
      {
        "id": 7,
        "title": "Website enquiry",
        "status": "open",
        "amount": 150000,
        "funnel_id": 2,
        "stage_id": 5
      }
    ],
    "pagination": { "page": 1, "perPage": 20, "total": 3, "lastPage": 1 }
  }
}

Право на исходящий звонок (WhatsApp)

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/outbound-eligibility?channel_id=7" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "none",
        "permission_expires_at": null,
        "can_request_permission": true,
        "request_cooldown_until": null,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Запрос разрешения на звонок

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/call-permission/request" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "channel_id": 7 }'

Ответ

json
{
  "data": {
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "pending",
        "permission_expires_at": null,
        "can_request_permission": false,
        "request_cooldown_until": 1710586400,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Лимит запросов разрешения исчерпан (429)

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/call-permission/request" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "channel_id": 7 }'

Ответ

json
{
  "data": {
    "message": "Call-permission request limit reached.",
    "eligibility": {
      "message": {
        "window_open": true,
        "window_expires_at": 1710500000,
        "requires_template": false
      },
      "call": {
        "channel_kind": "whatsapp",
        "can_call": false,
        "permission_status": "pending",
        "permission_expires_at": null,
        "can_request_permission": false,
        "request_cooldown_until": 1710586400,
        "attempts_remaining": 5,
        "has_whatsapp_account": true
      }
    }
  }
}

Блокировка контакта

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "Оскорбления в переписке" }'

Ответ

json
{
  "data": {
    "message": "Contact blocked.",
    "contact_id": 45,
    "blocked_at": "2026-09-04T10:15:00Z",
    "blocked_reason": "Оскорбления в переписке",
    "provider_failures": []
  }
}

Блокировка контакта — провайдер отклонил один из каналов

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'

Ответ

json
{
  "data": {
    "message": "Contact blocked.",
    "contact_id": 45,
    "blocked_at": "2026-09-04T10:15:00Z",
    "blocked_reason": null,
    "provider_failures": [
      { "channel_id": 9, "error_code": "channel_offline", "message": "The channel is not connected right now — connect it and try again." }
    ]
  }
}

Список контактов: фильтры панели

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts?channel_type=whatsapp_business&tag_id=3&responsible_user_id=12&active_since=2026-08-01&perPage=10" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "items": [
      {
        "id": 45,
        "displayName": "John Doe",
        "status": "active",
        "responsibleUserId": 12,
        "responsible_user": { "id": 12, "name": "Aigerim K." },
        "open_deals_count": 2,
        "last_activity_at": "2026-09-07T18:42:11.000000Z",
        "hidden_from_inbox_at": null,
        "hidden_from_inbox_by_user_id": null,
        "phones": [{ "phone": "+12025550187", "type": "mobile" }],
        "tags": [{ "id": 3, "name": "VIP", "color": "#22c55e" }]
      }
    ],
    "pagination": { "page": 1, "perPage": 10, "total": 4, "lastPage": 1 }
  }
}

Лента активности контакта

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/activity?limit=5" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "items": [
      {
        "type": "call",
        "occurred_at": "2026-09-07T18:42:11.000000Z",
        "title": "+12025550187",
        "meta": {
          "direction": "outbound",
          "status": "completed",
          "duration_sec": 96,
          "recording_status": "ready",
          "has_transcript": true
        },
        "link": { "kind": "call_transcript", "id": 3120 }
      },
      {
        "type": "deal_stage",
        "occurred_at": "2026-09-06T11:05:00.000000Z",
        "title": "Website enquiry",
        "meta": {
          "deal_id": 7,
          "activity_type": "stage_changed",
          "to_stage_id": 5,
          "to_stage_name": "Договор"
        },
        "link": { "kind": "deal", "id": 7 }
      },
      {
        "type": "csat",
        "occurred_at": "2026-09-05T09:20:00.000000Z",
        "title": "positive",
        "meta": { "sentiment": "positive" },
        "link": { "kind": "conversation", "id": 15 }
      },
      {
        "type": "broadcast",
        "occurred_at": "2026-09-01T08:00:00.000000Z",
        "title": "Сентябрьская акция",
        "meta": { "broadcast_id": 12, "status": "delivered" },
        "link": { "kind": "broadcast", "id": 12 }
      }
    ]
  }
}

Сводка CSAT по контакту

Запрос

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

Ответ

json
{
  "data": {
    "positive_count": 3,
    "negative_count": 1,
    "last": {
      "sentiment": "positive",
      "responded_at": "2026-09-05T09:20:00.000000Z"
    }
  }
}

Файлы и медиа контакта

Запрос

bash
curl -X GET "https://api.aisar.app/v1/contacts/45/attachments?kind=document&perPage=2" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "items": [
      {
        "id": 8801,
        "type": "document",
        "url": "https://api.aisar.app/v1/messages/1001/attachments/8801/media?company=3&expires=1757352000&signature=PLACEHOLDER",
        "thumbnail_url": null,
        "playback_url": null,
        "filename": "contract-2026.pdf",
        "mime_type": "application/pdf",
        "size": 481233,
        "width": null,
        "height": null,
        "duration": null,
        "kind": "document",
        "family": "pdf",
        "extension": "PDF",
        "source": null,
        "message_id": 1001,
        "conversation_id": 15,
        "channel_id": 7,
        "channel_type": "whatsapp",
        "direction": "inbound",
        "sent_at": "2026-09-07T18:42:11.000000Z",
        "sender": { "type": "contact", "name": "Айгуль Сатпаева" }
      },
      {
        "id": 8794,
        "type": "contact",
        "url": null,
        "thumbnail_url": null,
        "playback_url": null,
        "filename": null,
        "mime_type": "text/vcard",
        "size": null,
        "width": null,
        "height": null,
        "duration": null,
        "kind": "document",
        "family": "contact",
        "extension": "VCF",
        "source": null,
        "message_id": 994,
        "conversation_id": 15,
        "channel_id": 7,
        "channel_type": "whatsapp",
        "direction": "inbound",
        "sent_at": "2026-09-06T10:11:02.000000Z",
        "sender": { "type": "contact", "name": "Айгуль Сатпаева" },
        "contact_name": "Дамир Ахметов",
        "phone": "+12025550187"
      }
    ],
    "pagination": { "page": 1, "perPage": 2, "total": 5, "lastPage": 3 },
    "counts": { "media": 14, "document": 5 }
  }
}

Скрыть контакт из рабочего inbox

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/hide-from-inbox" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "id": 45,
    "company_id": 10,
    "displayName": "John Doe",
    "status": "active",
    "last_activity_at": "2026-09-07T18:42:11.000000Z",
    "hidden_from_inbox_at": "2026-09-08T09:30:00.000000Z",
    "hidden_from_inbox_by_user_id": 12,
    "open_deals_count": 2,
    "responsible_user": { "id": 12, "name": "Aigerim K." },
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null,
    "phones": [{ "phone": "+12025550187", "type": "mobile" }],
    "emailes": [],
    "contactAccounts": [],
    "tags": []
  }
}

Вернуть скрытый контакт в inbox

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/unhide-from-inbox" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Ответ

json
{
  "data": {
    "id": 45,
    "company_id": 10,
    "displayName": "John Doe",
    "status": "active",
    "last_activity_at": "2026-09-07T18:42:11.000000Z",
    "hidden_from_inbox_at": null,
    "hidden_from_inbox_by_user_id": null,
    "open_deals_count": 2,
    "responsible_user": { "id": 12, "name": "Aigerim K." },
    "blocked_at": null,
    "blocked_reason": null,
    "blocked_by_user_id": null,
    "phones": [{ "phone": "+12025550187", "type": "mobile" }],
    "emailes": [],
    "contactAccounts": [],
    "tags": []
  }
}

Снятие блокировки

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contacts/45/unblock" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответ

json
{
  "data": {
    "message": "Contact unblocked.",
    "contact_id": 45,
    "blocked_at": null,
    "blocked_reason": null,
    "provider_failures": []
  }
}

Импорт контактов из CSV: загрузка файла

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contact-imports" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@contacts.csv"

Ответ

json
{
  "data": {
    "import": {
      "id": 31,
      "status": "uploaded",
      "original_filename": "contacts.csv",
      "file_size": 20480,
      "total_rows": 240,
      "encoding": "utf-8",
      "delimiter": ";",
      "detected_encoding": "utf-8",
      "detected_delimiter": ";",
      "headers": ["ФИО", "Телефон", "Email"],
      "column_samples": [["Иванова Анна"], ["+12025550187"], ["anna@example.com"]],
      "suggested_mapping": [
        { "column_index": 0, "field": "full_name", "transform": "split_full_name" },
        { "column_index": 1, "field": "phone", "transform": "normalize_phone" },
        { "column_index": 2, "field": "email" }
      ],
      "mapping": null,
      "mapping_hash": null,
      "options": null,
      "stats": null,
      "preview": null,
      "processed_rows": 0,
      "created_count": 0,
      "updated_count": 0,
      "skipped_count": 0,
      "failed_count": 0,
      "tag": null,
      "error_code": null,
      "has_errors_file": false,
      "has_report": false,
      "ai_mapping_used": false,
      "ai_mapping_available": true,
      "is_mine": true,
      "created_at": "2026-10-08T09:00:00.000000Z",
      "validated_at": null,
      "started_at": null,
      "finished_at": null,
      "expires_at": "2026-10-15T09:00:00.000000Z"
    }
  }
}

Импорт контактов из CSV: проверка и запуск

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contact-imports/31/validate" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "mapping": [
      { "column_index": 0, "field": "full_name", "transform": "split_full_name" },
      { "column_index": 1, "field": "phone", "transform": "normalize_phone" },
      { "column_index": 2, "field": "email" }
    ],
    "options": { "duplicate_mode": "fill_empty", "tag_name": "Импорт октябрь" }
  }'

# после события contact_import.updated со status "validated" (или опроса GET /v1/contact-imports/31):
curl -X POST "https://api.aisar.app/v1/contact-imports/31/start" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "mapping_hash": "<mapping_hash из объекта импорта, 64 символа>" }'

Ответ

json
{
  "data": {
    "import": {
      "id": 31,
      "status": "importing",
      "total_rows": 240,
      "stats": {
        "new": 212,
        "update": 20,
        "skip": 3,
        "file_duplicates": 2,
        "errors": 3,
        "error_breakdown": { "no_name_no_phone": 3 },
        "warning_breakdown": { "invalid_email": 4 }
      },
      "processed_rows": 0,
      "created_count": 0,
      "updated_count": 0,
      "skipped_count": 0,
      "failed_count": 0,
      "tag": { "id": 77, "name": "Импорт октябрь" },
      "has_errors_file": true,
      "has_report": false
    }
  }
}

Импорт контактов из CSV: устаревшая проверка (409)

Запрос

bash
curl -X POST "https://api.aisar.app/v1/contact-imports/31/start" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "mapping_hash": "0000000000000000000000000000000000000000000000000000000000000000" }'

Ответ

json
{
  "data": {
    "message": "stale_validation",
    "code": "stale_validation",
    "errors": { "mapping_hash": ["stale_validation"] }
  }
}