Контакты
Контакт — карточка человека или компании, с которой ведётся общение: телефоны, email, привязанные аккаунты каналов (contactAccounts — например конкретный WhatsApp-номер или Instagram-username), метки и статус (lead/active/inactive). Один контакт может иметь несколько привязанных аккаунтов и диалогов. API покрывает CRUD, поиск/резолв по идентификаторам, объединение дублей, управление подпиской на рассылки (opt-in/opt-out) и блокировку контакта (block/unblock).
Список: фильтры, сортировка, пагинация
Список поддерживает пагинацию (page; perPage — от 5 до 100, по умолчанию 10), полнотекстовый поиск (search), фильтр statuses[] (допустимы только active и inactive — статус lead фильтром не выбирается, попытка отфильтровать по нему даёт 422) и сортировку (sortBy: id|display_name|company|status|created_at, по умолчанию created_at; sortDir: asc|desc, по умолчанию desc).
Флаг exclude_lid_only (0/1, по умолчанию 1) по умолчанию скрывает контакты, у которых единственные привязанные аккаунты — приватные WhatsApp-идентификаторы @lid без распознанного номера; передайте 0, чтобы их показать.
Быстрый поиск
Быстрый поиск /lookup ищет по телефонам и привязанным аккаунтам (external_id, username) и возвращает до 20 совпадений.
Доступ и фото
Контакт другой компании при чтении, обновлении, удалении и по вложенным ресурсам возвращает 404 «Contact not found.». Эндпоинты фото отдают изображение потоком с откатом на profile_picture привязанного аккаунта.
Создание и обновление связей
При создании и обновлении связи (phones, emailes, contactAccounts) сохраняются целиком: PATCH полностью пересобирает набор связанных записей из тела запроса.
Звонки контакту
Телефонная пара эндпоинтов (доступна только при подключённой телефонии) отвечает на вопрос «можно ли позвонить контакту прямо сейчас»: 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).
Фото контакта и пользователя
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.
Эндпоинты
| Метод | Путь | Описание |
|---|---|---|
| GET | /v1/contactsСписок контактов компании (query: search, statuses[]=active|inactive, sortBy, sortDir, exclude_lid_only, page, perPage). | Список контактов компании (query: search, statuses[]=active|inactive, sortBy, sortDir, exclude_lid_only, page, perPage). |
| GET | /v1/contacts/lookupБыстрый поиск контактов по телефону, external_id или username канала. | Быстрый поиск контактов по телефону, external_id или username канала. |
| GET | /v1/contacts/channel-optionsСписок типов каналов, доступных для привязки аккаунта к контакту. | Список типов каналов, доступных для привязки аккаунта к контакту. |
| GET | /v1/contacts/{contact}Карточка одного контакта (phones, emailes, contactAccounts, tags). | Карточка одного контакта (phones, emailes, contactAccounts, tags). |
| POST | /v1/contactsСоздать контакт (с телефонами, email и привязанными аккаунтами каналов). | Создать контакт (с телефонами, email и привязанными аккаунтами каналов). |
| POST | /v1/contacts/resolve-identifiersРазрешить пакет идентификаторов (телефон/username/external_id) в id существующих контактов. | Разрешить пакет идентификаторов (телефон/username/external_id) в id существующих контактов. |
| PATCH | /v1/contacts/{contact}Обновить контакт (полностью пересохраняет связанные телефоны/email/аккаунты). | Обновить контакт (полностью пересохраняет связанные телефоны/email/аккаунты). |
| 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). | Вернуть контакт в рассылки (снять opt-out). |
| GET | /v1/contacts/{contact}/block-previewЧто затронет блокировка до её применения: каналы, куда уйдёт блок, и открыто ли на них 24-часовое окно WhatsApp. | Что затронет блокировка до её применения: каналы, куда уйдёт блок, и открыто ли на них 24-часовое окно WhatsApp. |
| POST | /v1/contacts/{contact}/blockЗаблокировать контакт (body: reason?) — ставит blocked_at и best-effort блокирует его на каждом WhatsApp-канале, с которым у него есть переписка. | Заблокировать контакт (body: reason?) — ставит blocked_at и best-effort блокирует его на каждом WhatsApp-канале, с которым у него есть переписка. |
| POST | /v1/contacts/{contact}/unblockСнять блокировку контакта — очищает blocked_at и best-effort разблокирует его на затронутых WhatsApp-каналах. | Снять блокировку контакта — очищает blocked_at и best-effort разблокирует его на затронутых WhatsApp-каналах. |
| GET | /v1/contacts/{contact}/dealsСписок сделок контакта (требует включённый функционал сделок). | Список сделок контакта (требует включённый функционал сделок). |
| GET | /v1/contacts/{contact}/outbound-eligibilityПроверить право на исходящий звонок контакту по каналу (query: channel_id) — окно сообщений и разрешение на звонок. | Проверить право на исходящий звонок контакту по каналу (query: channel_id) — окно сообщений и разрешение на звонок. |
| POST | /v1/contacts/{contact}/call-permission/requestЗапросить у контакта разрешение на звонок (WhatsApp Calling; body: channel_id). | Запросить у контакта разрешение на звонок (WhatsApp Calling; body: channel_id). |
| GET | /v1/contacts/{contact}/photoФото контакта (fallback на profile_picture привязанного аккаунта) — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком. | Фото контакта (fallback на profile_picture привязанного аккаунта) — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком. |
| GET | /v1/users/{user}/photoФото пользователя компании — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком. | Фото пользователя компании — webp-превью до 256px, если исходник декодируется и шире; иначе оригинал потоком. |
Примеры
Создание контакта
Запрос
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": "+77001234567", "type": "mobile" }],
"emailes": [{ "email": "john@example.com", "type": "work" }]
}'Ответ
{
"data": {
"id": 45,
"company_id": 1,
"firstName": "John",
"lastName": "Doe",
"displayName": "John Doe",
"status": "lead",
"phones": [{ "phone": "+77001234567", "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
}
}Поиск контакта по телефону
Запрос
curl -X GET "https://api.aisar.app/v1/contacts/lookup?q=%2B77001234567" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": [
{
"id": 45,
"displayName": "John Doe",
"status": "lead",
"phones": [{ "phone": "+77001234567", "type": "mobile" }]
}
]
}Список контактов с фильтром по статусу
Запрос
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"Ответ
{
"data": {
"items": [
{
"id": 45,
"displayName": "John Doe",
"status": "active",
"phones": [{ "phone": "+77001234567", "type": "mobile" }],
"tags": []
}
],
"pagination": { "page": 1, "perPage": 10, "total": 120, "lastPage": 12 }
}
}Карточка контакта
Запрос
curl -X GET "https://api.aisar.app/v1/contacts/45" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Ответ
{
"data": {
"id": 45,
"company_id": 10,
"firstName": "John",
"lastName": "Doe",
"displayName": "John Doe",
"status": "lead",
"phones": [{ "phone": "+77001234567", "type": "mobile" }],
"emailes": [{ "email": "john@example.com", "type": "work" }],
"contactAccounts": [
{
"id": 88,
"channel_type": "whatsapp",
"external_id": "77001234567",
"username": null,
"profile_picture": null
}
],
"tags": [{ "id": 3, "name": "VIP" }],
"merged_into_contact_id": null,
"blocked_at": null,
"blocked_reason": null,
"blocked_by_user_id": null
}
}Синхронизация меток контакта
Запрос
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]
}'Ответ
{
"data": {
"tags": [
{ "id": 1, "name": "Lead" },
{ "id": 2, "name": "Newsletter" },
{ "id": 3, "name": "VIP" }
]
}
}Сделки контакта
Запрос
curl -X GET "https://api.aisar.app/v1/contacts/45/deals?perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/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)
Запрос
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"Ответ
{
"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
}
}
}
}Запрос разрешения на звонок
Запрос
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 }'Ответ
{
"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)
Запрос
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 }'Ответ
{
"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
}
}
}
}Блокировка контакта
Запрос
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reason": "Оскорбления в переписке" }'Ответ
{
"data": {
"message": "Contact blocked.",
"contact_id": 45,
"blocked_at": "2026-09-04T10:15:00Z",
"blocked_reason": "Оскорбления в переписке",
"provider_failures": []
}
}Блокировка контакта — провайдер отклонил один из каналов
Запрос
curl -X POST "https://api.aisar.app/v1/contacts/45/block" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'Ответ
{
"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." }
]
}
}Снятие блокировки
Запрос
curl -X POST "https://api.aisar.app/v1/contacts/45/unblock" \
-H "Authorization: Bearer YOUR_API_TOKEN"Ответ
{
"data": {
"message": "Contact unblocked.",
"contact_id": 45,
"blocked_at": null,
"blocked_reason": null,
"provider_failures": []
}
}