Conversations
A conversation is the umbrella record for all communication with a single contact, grouping one or more channel-specific threads (WhatsApp, Telegram, Instagram, etc). These endpoints manage a conversation's lifecycle — opening and closing it, assigning participants (operators) via the participants sub-resource, keeping internal notes and tags, and reading its unified event timeline and thread structure. Most mutating actions append to the conversation's event log, readable via GET /conversations/{id}/events.
Creating a dialog
A dialog can be created two ways: POST /conversations for an already-known contact, or POST /dialogs — the integrator entry point that, given an account identifier (a phone or username), creates or reuses the whole contact → contact_account → conversation → thread chain in a single call; for phone-based channels (sms, whatsapp) the account is normalized as a phone number. GET /dialogs/lookup finds an existing active dialog, taking related channel types into account (whatsapp/whatsapp_business/sms, telegram/telegram_bot, instagram/instagram_business). Both routes — POST /dialogs and GET /dialogs/lookup — are business-companies only: for a family (kids) company both return 403 {"message": "...", "error": "company_type_forbidden"} — these integrator entry points could otherwise create or locate a conversation lying entirely on a channel the "18+" gate closes to the family, bypassing the reachability checks described above.
A channel with can_start_conversation: false (currently only Telegram Business, see the "Channels" group) does not open a conversation with a contact this channel has no thread with yet. POST /dialogs through such a channel answers 422 {"data": {"message": "…", "error_code": "channel_cannot_start_conversation"}}, and the contact and account created for the call are not kept. POST /conversations/{conversation}/threads gets the same answer, also wrapped in data, when the new thread would sit on such a channel. If the channel already has a thread with that contact account in any conversation of the company, the call goes through as usual.
Listing and filters
The GET /conversations list uses page-based pagination (page/perPage; the response carries a pagination block {page, perPage, total, lastPage, has_more}) and supports the thread_filter (all|awaiting|unread|assigned|unassigned|my_team|archived|groups), channel_id, funnel_id, funnel_stage_ids (comma-separated funnel-stage ids), contact_id, lifecycle_status (active|closed), is_archived, unread_only and search filters, sortBy (id|last_message_at|created_at|updated_at) / sortDir (asc|desc) sorting, and include_counters=1 (adds a counters block to the response).
search semantics (updated 2026-09-28 — the conversation/thread search filter no longer reads contacts/contact_accounts with a full scan): the exact same rule governs search on GET /conversations and GET /threads; since 2026-09-29, a separate change, GET /conversations/counters (below) follows the same rule too — before that date the search parameter there was not declared in the request's validation rules and was silently dropped, never narrowing the counters. The term is matched against the conversation's contact (display_name, first_name, last_name, company) OR against the contact account of ANY of the conversation's threads (profile_display_name, address_book_name, external_id, username) — for GET /threads the same fields are read from the thread's own conversation and from the thread's own contact account; a match on any field in either group is enough, and the search is always scoped to the current company. The ILIKE wildcard characters %, _ and their escape character \ are now matched LITERALLY instead of acting as an SQL wildcard: previously search=% effectively meant "no filter" (it matched everything) — that gap is closed.
The term is trimmed before matching: e.g. search=%20 (a single space) is equivalent to no filter at all — every record is returned, as if search had not been passed. This is separate from the case where the parameter itself is passed literally empty (search=, zero characters) — that request returns 422, exactly like any other string query parameter with no value; trimming plays no part in that, it happens earlier, at request validation. The shape of the GET /conversations/counters response is unchanged — search only narrows the set the same counter keys are computed over.
with_total (optional boolean, defaults to true, since 2026-09-06) switches the conversation list to a lighter pagination mode without an exact count. By default (or with with_total=1) nothing changes: total and lastPage are exact values from COUNT(*). With with_total=0 that COUNT(*) — the dominant cost behind a reported p95 ~2s and peaks up to 15s on this list — is skipped entirely, and total/lastPage come back null. has_more (boolean) is always present inside pagination, in both modes — it is the only reliable "is there a next page" signal when with_total=0; do not rely on the "the page came back full" heuristic — it false-positives whenever the number of conversations is an exact multiple of perPage. If your UI does not need page numbers (infinite scroll only), pass with_total=0 and paginate by has_more.
Since 2026-09-08 the conversations of contacts hidden from the working inbox (POST /v1/contacts/{contact}/hide-from-inbox, see the Contacts group) appear neither in GET /conversations nor in GET /threads nor in any counter of GET /conversations/counters. There is no "include hidden" parameter here — hidden contacts are only visible through GET /contacts?hidden=1. The conversation itself stays readable by id (GET /conversations/{id}, GET /messages), and outgoing webhooks for it keep being delivered.
The `aisar_notifications` counter key
The counters block (in the conversation list with include_counters=1, and in GET /conversations/counters) carries an aisar_notifications key — always present, including the empty-response case for a company with no active subscription. Semantics change (2026-08-24, shipped without a new version prefix): before 08-24 this key counted AISAR notification conversations among the conversations themselves (see the history below); platform notifications are now a separate resource, GET /platform-notifications, and counters.aisar_notifications is the count of UNSEEN platform_notifications rows for the company: rows from the last 90 days with created_at later than the current user's ui_preferences.aisar_notifications.seen_until cursor (a null cursor counts every row in the window). The value's shape (a plain integer) is unchanged — only the data source changed, and it is now computed per-user rather than per-company.
Awaiting a reply (`awaiting_since`) and "dismiss awaiting"
ConversationResource (in both the GET /conversations list and GET /conversations/{id}) carries a computed awaiting_since field (ISO-8601 string or null) — the moment of the first unanswered inbound message; null if the conversation is not currently awaiting a reply. The semantics match thread_filter=awaiting. The value is computed by scanning the conversation's history backward (at most the last 100 messages); for a conversation with an exceptionally long unanswered tail this is an approximation — the oldest inbound message found within that window, not necessarily the first ever.
POST /conversations/{conversation}/dismiss-awaiting (2026-08-24, requires conversation.update) lets an operator mark a conversation as "no reply needed": the server sets the internal awaiting_dismissed_at field, and from that point awaiting_since on the conversation stays null until the next inbound message arrives (which re-arms awaiting_since as usual). Response: { "data": { "id": <conversation_id>, "awaiting_since": null } }.
Threads as a standalone entity
A thread can also be listed as a standalone entity: GET /threads returns a cross-conversation thread list (the "by channel" display mode) with the same filters and pagination, and POST /threads/{id}/close|open|archive|unarchive changes a single thread's state independently of its conversation (reopening returns 422 if another active conversation already exists for the contact). The counters block of the GET /threads response carries exactly eight keys — all, awaiting, unread, assigned, unassigned, my_team, archived, groups (all integers, computed over the same thread set as the list but ignoring the applied thread_filter); there is no aisar_notifications key here — that one exists only on the conversation counters (GET /conversations/counters).
Pinning is personal: only the user who set a pin sees it. PUT /conversations/{conversation}/pin pins a conversation, PUT /threads/{thread}/pin (the "by channel" mode) pins a thread; DELETE on the same paths removes your own pin. A 200 response is {"data": {"conversation_id", "thread_id" | null, "pinned_at" | null}}. Limit: 5 pinned conversations and, separately, 5 pinned threads per user; past the limit PUT answers 422 {"data": {"message": "…", "code": "pin_limit_reached"}}, an archived conversation or thread cannot be pinned (422), and archiving drops the pin. DELETE without an own pin answers 404. Rate limit: 60 requests per minute. The /pin endpoints are also available to embed tokens. Items of GET /conversations and GET /threads, and the responses of GET /conversations/{id} and GET /conversations/{id}/threads/{id}, carry a pinned_at field (ISO 8601 or null, for the current user). Pinned items come first in the GET /conversations and GET /threads lists (except requests with search, the thread_filter=archived tab and sorting other than last_message_at); filters still apply as usual, so the first page may hold more than perPage items by the number of shown pins, and total counts them.
Since 2026-09-28, reaching a thread BY ID — GET/PATCH /conversations/{conversation}/threads/{thread}, GET/POST .../threads/{thread}/events, POST /threads/{id}/close|open|archive|unarchive — additionally checks whether the user can see that specific thread: the same channel-access (channel_access) rule the lists already use answers 404 "Thread not found." for a hidden channel (a company admin sees any thread of their own company). Separately, if the thread's channel is blocked by the Kids "18+" correspondence gate, the response is 403 adult_child_correspondence — and this rule has no role exemption, blocking read and write even for a company admin (unlike the channel_access check above, where an admin sees everything). POST /conversations/{conversation}/threads (creating a new thread) returns 404 "Conversation not found." for a hidden conversation, and 422 "Invalid channel for current company." for a channel the user cannot access, an archived one, or one blocked by the 18+ gate. The conversation's thread list — GET /conversations/{conversation}/threads — since 2026-09-29 also returns 404 "Conversation not found." for a hidden conversation (it used to answer 200 with an empty list, which told a hidden conversation apart from a non-existent one — the differing response was an oracle for whether another company's conversation id existed).
PATCH .../threads/{thread} no longer allows changing an existing thread's channel_id or contact_account_id: a value different from the current one returns 422 ("Cannot change thread channel." or "Cannot change thread contact account." respectively), while the same value sent back (e.g. a PATCH that round-trips the whole object) is a no-op, not an error. The same is true for is_group and external_thread_id: a different value returns 422 ("Cannot change thread is_group." / "Cannot change thread external_thread_id."), while the same value (including null = null) is a no-op; the one exception is a widget (live-chat) channel's thread, where external_thread_id holds the chat visitor's token — attempting to change it there returns 422 "Cannot change live chat thread external id.", under the same "same value is a no-op" principle.
Read state
Read state is exposed by GET /conversations/{id}/reads — which operator has read the conversation up to which message (per-user last_read_message_id/last_read_at) — while POST /conversations/{id}/reads sets the current user's read position. On POST /conversations/mark-read the conversation_ids field is optional (omit it to mark every company conversation), and a conversation's read state is also updated automatically when an operator sends an outbound message. Because a bulk mark-read can take tens of seconds for a large company, POST /conversations/mark-read queues the upsert and the CRM badge fan-out and responds 202 Accepted with no updated_count in the body — the real count of updated conversations only arrives asynchronously, via the message.read WebSocket event with bulk: true and updated_count. A call with an empty conversation_ids: [] array is not queued and still returns a plain 200.
POST /conversations/{conversation}/mark-unread is the reverse operation: it rewinds the read cursor to one second before the conversation's (or its thread's, with the optional thread_id) latest inbound message, so that message becomes unread again. Scope follows the company's shared_read_receipts setting: when receipts are shared, EVERY operator's cursor that was past the new mark gets rewound; when private, only the current user's cursor does. If the conversation (or thread) has no inbound messages at all — 422 { "data": { "message": "Conversation has no inbound messages to mark as unread." } }. A successful response is { "data": { message, item, unread_count, shared_read_receipts } } — item has the same shape as in reads (can be null), unread_count is how many inbound messages are now unread for the current user, and shared_read_receipts echoes the company setting.
AI copilot and AI-agent actions
AI copilot: POST /conversations/{id}/ai-suggestions/generate produces a draft reply for the conversation, and .../ai-suggestions/{id}/accept (with an optional edited final_text) and .../reject record the operator's decision on a suggestion.
.../ai-suggestions/generate is not gated by the ai_agents feature at the route level — that check happens inside generation itself, together with the plan's AI-reply allowance, and distinguishes several failure modes: no agent assigned to the conversation — 404 (no code in the body); the agent is inactive — 422 { "code": "agent_inactive" }; the agent's own configurable $ budget is exhausted — 422 { "code": "budget_exceeded" }; the company plan's AI-reply allowance for the billing period is used up (ai_replies_included) — including when the company's current plan no longer carries ai_agents at all (e.g. after a downgrade) — 422 { "code": "plan_limit_exceeded" }; usage has reached the technical anti-abuse threshold (a multiple of the plan allowance) — 422 { "code": "usage_abuse_stopped" }; too many calls in a short window — 429 (no code in the body).
An AI agent can also propose actions that require operator approval (for example closing a conversation, blocking a contact, or deleting messages) — the queue of such proposals for a specific conversation is returned by GET /conversations/{id}/ai-pending-actions: each item carries a tool_key, a human-readable comment, a free-form payload, and an approval_status (pending|approved|rejected); the decision itself is recorded via the company-scoped approve/reject routes.
POST /conversations/{id}/ai-tools/summary returns a short AI summary of the conversation for a shift handoff (summary_text, message_count, tokens_used); it requires the ai_agents feature at the route level (without it — 403 feature_not_available), accepts an optional locale (otherwise the app locale is used), is subject to a 60-requests-per-hour AI-assist limit per company (429 ai_assist_rate_limited), returns 422 empty_conversation for a conversation with no messages, and 502 ai_provider_error on a provider failure.
GET /conversations/{id}/ai-tools/summary returns the STORED summary of the conversation without a new AI call: { "data": { "summary": null | { "summary_text", "message_count", "locale", "generated_at", "generated_by": { "id", "name" } | null } } }. summary: null means no summary has been made yet, or the conversation is mixed and only part of its channels is visible to the user, or the contact is hidden from the inbox. A conversation of another company or one the user cannot access is 404. POST on the same path now also returns generated_at (ISO-8601) and saved (boolean): true when the fresh summary was stored (overwriting the previous one), false when storing was skipped for the same reasons.
GET /conversations/{id}/notes takes an optional order (asc by default, desc) — the order by note creation time; any other value is 422. The other filters (thread_id, is_private) are unchanged.
Since 2026-09-29 the auto-reply AI agent and the copilot draft (.../ai-suggestions/generate) get only the correspondence with the customer — inbound and outbound messages — as context. Internal notes (the AI call summary included) and system cards in the feed never reach the model, so the agent cannot repeat to the customer what was meant for the team only. The shift-handoff AI summary (POST /conversations/{id}/ai-tools/summary) still takes notes into account, but as internal records the customer never saw.
Channel access and ownership checks (since 2026-09-26)
Channel access rules (team channel_access) now apply not just to the list but to every by-id conversation path: GET/PATCH/DELETE /conversations/{id}, close/open, archive, dismiss-awaiting, events, notes, tags, reads, participants, threads and the AI tools. A conversation that has threads but none on a channel accessible to the user answers 404 — the same as a conversation of another company (a conversation with no threads stays reachable). A company admin sees everything. If the conversation's latest message arrived on a channel the user cannot access, last_message/last_message_id come from an accessible thread instead, or are null when there is none. The GET /conversations list's ?unread_only=1 filter also now only considers messages on channels accessible to the user — it used to read inbound messages of ANY channel, so a conversation visible through only one thread could land in the unread set because of an unread message on a different channel hidden from the user, even though its own unread_count_for_current_user was 0; behavior for an admin/platform staff member (unrestricted by channel_access) is unchanged.
Conversation AI tools: POST /conversations/{id}/ai-tools/summary needs the conversation.view permission, while generating, accepting and rejecting a copilot suggestion (.../ai-suggestions/generate|{id}/accept|{id}/reject) needs conversation.update (without the permission — 403, a conversation hidden by channel access — 404). The AI-agent action lists (GET /ai-pending-actions, GET /conversations/{id}/ai-pending-actions, GET /ai-agents/instances/{id}/pending-actions and .../stats) need conversation.view and only show actions on conversations the user can reach (an empty list for a hidden conversation); POST /ai-pending-actions/{event}/approve|reject needs conversation.update and answers 404 for a hidden conversation.
Since 2026-09-28, a "mixed" conversation (threads on several channels) narrows its CROSS-THREAD content to the threads/channels visible to the user, instead of 404-ing the whole conversation: GET /conversations/{id}/events, the notes list (GET .../notes), POST /conversations/{id}/ai-tools/summary and generating a copilot suggestion (POST .../ai-suggestions/generate) all return only what belongs to accessible threads/channels (for the AI summary and copilot this means only messages from accessible threads reach the model's context); a row on a hidden thread is simply absent from the result rather than turning the whole response into a 404. At this level, platform staff and company admins see everything unnarrowed (the same exemption the conversation card itself gets) — this is what sets this group of endpoints apart from the direct by-id thread paths above, where the 18+ gate has no role exemption. For notes (ConversationNoteController) this means: an existing note on a hidden thread, fetched by its id, returns 404 "Conversation note not found."; creating a note with a hidden thread_id returns 422 "Invalid thread for current conversation."; and GET .../notes?thread_id=<hidden> is not an error, just an empty page (the thread exists and belongs to the conversation but is not visible to the user; a thread_id that does not exist at all still returns 422 via exists:threads,id).
Since 2026-09-28, POST /ai-pending-actions/{event}/approve and .../reject additionally verify that {event} is a genuine, platform-created AI proposal rather than a forgery: an event whose type is not ai_action_pending_approval, or that carries a non-null author_user_id (a human-authored event can never be a system proposal), returns 404 { "data": { "message": "Not found." } } — the same code and body as an event of another company or a hidden conversation, with no separate "this was forged" signal. An already-decided proposal (approval_status is approved or rejected) returns 409 { "data": { "code": "already_resolved" } } on a repeat call. For a proposal with payload.tool_key: "block_contact", approve additionally requires the contact.update permission (matching the real POST /contacts/{contact}/block it executes) and always refuses an embed token (403); rejecting the same proposal — i.e. simply declining what the AI proposed, without executing it — needs only conversation.update, but still refuses an embed token with 403. Any other tool_key (e.g. close_conversation) needs nothing beyond the conversation.update the route itself already authorizes, for either decision.
Since 2026-09-30, for family (kids) companies a conversation whose threads are ALL on channels the "18+" gate closes to the family (an adult child's correspondence — the same gate the direct by-id thread paths above use) is fully excluded at this level: it is absent from GET /conversations, its total, GET /conversations/counters, the counters block under include_counters=1, and the realtime counters; by id, every path (the card, updates, close/open, archive, notes, tags, events, participants, reads, AI tools, the thread list) answers 404 "Conversation not found." rather than 403 — the conversation behaves as if it does not exist, not as forbidden. The conversation's realtime channel (conversation.{id}) is not authorized either — a subscription attempt is refused the same way as for another company's conversation. A "mixed" conversation (one allowed thread plus one closed by the 18+ gate) stays visible WITHOUT the closed thread — the same as under ordinary channel_access in the section above; its unread count, awaiting_since, and list order (sortBy=last_message_at) are then computed only over the allowed threads — the closed thread never contributes to those numbers. Platform staff and company admins see everything unnarrowed, matching every other channel-access check in this section. That same conversation's thread, reached BY ITS OWN id (GET/PATCH /conversations/{conversation}/threads/{thread} etc.), still answers 403 adult_child_correspondence with no role exemption (see "Threads as a standalone entity" above), not 404 — the 404 here is for the conversation as a whole only.
Cross-tenant checks: contact_id when creating a conversation and deal_id/team_id when creating or updating one must be (non-deleted) records of your own company, otherwise 422. In a thread PATCH, last_message_id may only point at a message of that same thread of your company (otherwise 422), and on thread creation the field is no longer accepted — a new thread always has it null. The same holds for last_message_at: it is not accepted on thread creation (a new thread always has it null), and on PATCH only a moment at or before the current time is allowed (with a small tolerance for client clock drift) — a later value returns 422.
Group chats
Group chats (WhatsApp groups, etc.) are served by their own routes: GET /group-chats/{id}/participants returns the group card and a paginated (page/per_page, default 50) list of participants with roles (superadmin|admin|member, sorted by seniority), while POST /group-chats/{id}/refresh force-refetches the group metadata from the messaging runtime (503 if the channel has no runtime instance or the runtime rejected the request) and returns the refreshed roster.
Enum values
Enum values used in request bodies:
- Participants are added with a
member_type(user|team|bot). - Events carry a
type(created|comment|status_changed|archived|unarchived|assigned|unassigned and other platform-internal values). As of 2026-09-28 thePOST .../eventsbody (conversation- or thread-scoped) only acceptstype: "comment"— any other value returns422; the remaining values only ever appear in the timeline when the platform itself writes them (status changes, archiving, assignment, etc) — a client cannot mint them through these routes. - Tags with a
source(manual|automation|integration).
On POST /conversations/{conversation}/events, the optional thread_id field attaches the comment to a specific thread of the conversation rather than to the conversation as a whole. Since 2026-09-28 that thread must not only belong to this conversation but also be visible to the user (channel-access and 18+ rules): a thread from another conversation, a thread that does not exist, and a thread of this same conversation that is hidden from the user all answer the same way — 422 "Invalid thread for current conversation.", with no separate code per reason.
Files and media
GET /conversations/{conversation}/attachments returns a paginated attachment gallery for the context panel (across all of the conversation's threads, scoped to the channels the user can access); the optional kind filters by image|video|document|audio. Since 2026-09-30, for a conversation the user cannot reach (hidden by channel-access rules, or lying entirely on a channel closed to an 18+ family) the route answers 404 "Conversation not found." — the same code as every other by-id conversation path — instead of 200 with an empty gallery as before (an empty response used to disguise an unreachable conversation as one with no attachments — the differing response was an oracle for whether another company's conversation existed). Each row is {attachment_id, message_id, kind, mime_type, size, name, thumb_url, created_at}; thumb_url is only minted for image/video (null for other kind values) as a short-lived (20-minute) signed link to the same .../media route with w=640 (a webp preview) — unlike MessageAttachmentResource.url's permanent signature, the lifetime is deliberately short so the gallery page can't be used to bulk-export forever-valid media links.
Since 2026-09-08 the endpoint has a second, optional filter — family (media | document). It is the same classification the contact gallery spells kind (GET /contacts/{contact}/attachments): media — image, video, ig_story, ig_reel, ig_post, story_mention; document — document, file, contact, audio; everything else (voice, sticker, template, ephemeral, unsupported_type, share, ig_story_reply, location) is dropped. The meaning of kind is unchanged — it is still the RAW message_attachments.type (image|video|document|audio), so kind=document still returns exactly type = "document". The two stack: family=document&kind=audio is the audio files only. With neither given the behaviour is unchanged: everything is returned, voice notes and stickers included.
Dialog in instagram (personal account)
For channels of type instagram (personal account), when this type is available to your company, POST /dialogs accepts in account an @username, a profile link (for example https://instagram.com/username/) or a numeric account id; repeated calls with the username, the link or the id of the same account return the same dialog. A malformed username or a link that is not a profile returns 422. Errors come in data and carry message, error_code (and retry_after for 429): 403 instagram_not_available — the type is not available to your company; 429 resolve_rate_limited with data.retry_after (seconds) and a Retry-After header — too many account lookups; 422 instagram_account_not_found — there is no such Instagram account; 422 instagram_resolve_unavailable — the account could not be checked right now, retry later. A numeric id and an already known account do not spend the lookup budget.
Instagram and Messenger reply window: meta_window_state
GET /conversations/{id} returns the meta_window_state field only when the conversation has a personal (non-group) Instagram (Meta API) or Messenger thread. Object: { "is_open": true, "expires_at": 1790000000, "human_agent_open": true, "human_agent_expires_at": 1790400000 }. is_open and expires_at (unix time or null) are the standard 24-hour window shared by all senders, including AI and automations. human_agent_open and human_agent_expires_at (unix time; null while the feature is off) are the live-operator reply window of up to 7 days (see the human_agent field in the "Messages" group). Like wa_window_state, this is a hint for the client on whether a message can be sent right now; the server makes the final decision on send. Each account has its own window: if the client writes to two Instagram accounts of the company, their windows differ. The response therefore also carries the meta_window_states array, one entry per such thread, with the same fields plus channel_id and thread_id; an empty array means the conversation has no Meta threads. meta_window_state equals the first entry and is kept for compatibility. No entry for a channel, or expires_at set to null, means the client has not written to that account yet and it cannot reply from it.
First letter to a contact: contact_email_id
POST /conversations/{conversation}/threads accepts contact_email_id instead of contact_account_id: the id of an address from the emailes of this conversation's contact (the id field is present in emailes of the contact response). Exactly one of the two fields is required and they are mutually exclusive (422). The server itself creates (or finds) a contact account of type email for that address and opens a thread on the given channel. Conditions: channel_id is a channel of type email (otherwise 422), the address belongs to the contact of this very conversation and to your company (a foreign or non-existent id returns the same 422), the address is valid, and it does not already belong to another contact. If a thread with this address on this channel already exists, the response contains it with message: "Thread already exists.". The first letter into the created thread is sent with POST /messages/send and a mandatory metadata.email.subject (see the "Messages" group).
Endpoints
| Method | Path | Summary |
|---|---|---|
| GET | /v1/conversations/countersConversation counters by status (all/awaiting/unread/assigned/unassigned/my_team/archived/groups/aisar_notifications); aisar_notifications = count of the company's unseen platform_notifications rows. Filters: search (honoured since 2026-09-29, as in the list), channel_id, funnel_id, funnel_stage_ids, team_id, unread_only. | Conversation counters by status (all/awaiting/unread/assigned/unassigned/my_team/archived/groups/aisar_notifications); aisar_notifications = count of the company's unseen platform_notifications rows. Filters: search (honoured since 2026-09-29, as in the list), channel_id, funnel_id, funnel_stage_ids, team_id, unread_only. |
| GET | /v1/conversationsList the company's conversations with pagination and filters. | List the company's conversations with pagination and filters. |
| POST | /v1/conversationsCreate a new conversation for a contact. | Create a new conversation for a contact. |
| GET | /v1/conversations/{conversation}Get a conversation by id (with contact, threads, participants). | Get a conversation by id (with contact, threads, participants). |
| PATCH | /v1/conversations/{conversation}Update conversation fields (subject, lifecycle status, archival). | Update conversation fields (subject, lifecycle status, archival). |
| DELETE | /v1/conversations/{conversation}Delete a conversation (soft delete). | Delete a conversation (soft delete). |
| POST | /v1/conversations/{conversation}/closeClose a conversation with a required comment. | Close a conversation with a required comment. |
| POST | /v1/conversations/{conversation}/openOpen (reopen) a closed conversation. | Open (reopen) a closed conversation. |
| POST | /v1/conversations/{conversation}/archiveArchive a conversation. | Archive a conversation. |
| POST | /v1/conversations/{conversation}/unarchiveUnarchive a conversation. | Unarchive a conversation. |
| PUT | /v1/conversations/{conversation}/pinPin a conversation for the current user (personal pin, at most 5 conversations). 422 `code: pin_limit_reached` when the limit is exceeded and 422 for an archived conversation. | Pin a conversation for the current user (personal pin, at most 5 conversations). 422 `code: pin_limit_reached` when the limit is exceeded and 422 for an archived conversation. |
| DELETE | /v1/conversations/{conversation}/pinRemove your own pin from a conversation; 404 if the current user has no pin on it. | Remove your own pin from a conversation; 404 if the current user has no pin on it. |
| POST | /v1/conversations/{conversation}/dismiss-awaitingMark a conversation as "no reply needed" — clears awaiting_since until the next inbound message. | Mark a conversation as "no reply needed" — clears awaiting_since until the next inbound message. |
| GET | /v1/conversations/{conversation}/participantsList conversation participants (operators, teams, bots). | List conversation participants (operators, teams, bots). |
| POST | /v1/conversations/{conversation}/participantsAdd a participant — e.g. assign an operator as the primary owner (is_primary). | Add a participant — e.g. assign an operator as the primary owner (is_primary). |
| DELETE | /v1/conversations/{conversation}/participants/{participant}Remove a participant from the conversation (unassign). | Remove a participant from the conversation (unassign). |
| GET | /v1/conversations/{conversation}/tagsList conversation tags. | List conversation tags. |
| POST | /v1/conversations/{conversation}/tagsAdd a tag to the conversation. | Add a tag to the conversation. |
| DELETE | /v1/conversations/{conversation}/tags/{tag}Remove a tag from the conversation. | Remove a tag from the conversation. |
| GET | /v1/conversations/{conversation}/notesList internal notes on the conversation. | List internal notes on the conversation. |
| POST | /v1/conversations/{conversation}/notesAdd an internal note (not visible to the contact). | Add an internal note (not visible to the contact). |
| PATCH | /v1/conversations/{conversation}/notes/{note}Edit a note's body. | Edit a note's body. |
| DELETE | /v1/conversations/{conversation}/notes/{note}Delete a note. | Delete a note. |
| GET | /v1/conversations/{conversation}/eventsConversation event timeline (created, comments, status changes, assignments). | Conversation event timeline (created, comments, status changes, assignments). |
| POST | /v1/conversations/{conversation}/eventsLog an event on the conversation timeline (e.g. an operator comment). | Log an event on the conversation timeline (e.g. an operator comment). |
| GET | /v1/conversations/{conversation}/attachmentsPaginated attachment gallery for the conversation (all threads; a kind=image|video|document|audio filter on the raw type plus a family=media|document classification filter, which stack) for the "Files & media" context panel. | Paginated attachment gallery for the conversation (all threads; a kind=image|video|document|audio filter on the raw type plus a family=media|document classification filter, which stack) for the "Files & media" context panel. |
| GET | /v1/conversations/{conversation}/threadsList threads (per-channel conversations) inside the conversation. | List threads (per-channel conversations) inside the conversation. |
| POST | /v1/conversations/{conversation}/threadsCreate a new thread inside an existing conversation for an additional channel. | Create a new thread inside an existing conversation for an additional channel. |
| GET | /v1/conversations/{conversation}/threads/{thread}Get a thread by id. | Get a thread by id. |
| PATCH | /v1/conversations/{conversation}/threads/{thread}Update a thread (last_message_at, last_message_id, an event comment); channel_id, contact_account_id, is_group and external_thread_id cannot be changed. | Update a thread (last_message_at, last_message_id, an event comment); channel_id, contact_account_id, is_group and external_thread_id cannot be changed. |
| GET | /v1/conversations/{conversation}/threads/{thread}/eventsEvent timeline of a specific thread. | Event timeline of a specific thread. |
| POST | /v1/conversations/{conversation}/threads/{thread}/eventsLog an event on a specific thread timeline. | Log an event on a specific thread timeline. |
| GET | /v1/threadsCross-conversation thread list (the "by channel" display mode) with filters, pagination and counters. | Cross-conversation thread list (the "by channel" display mode) with filters, pagination and counters. |
| POST | /v1/threads/{thread}/closeClose a single thread (independently of its conversation). | Close a single thread (independently of its conversation). |
| POST | /v1/threads/{thread}/openOpen (reopen) a single thread; 422 if the contact already has an active conversation. | Open (reopen) a single thread; 422 if the contact already has an active conversation. |
| POST | /v1/threads/{thread}/archiveArchive a single thread. | Archive a single thread. |
| POST | /v1/threads/{thread}/unarchiveUnarchive a single thread. | Unarchive a single thread. |
| PUT | /v1/threads/{thread}/pinPin a thread for the current user (personal pin, at most 5 threads). 422 `code: pin_limit_reached` when the limit is exceeded and 422 for an archived thread. | Pin a thread for the current user (personal pin, at most 5 threads). 422 `code: pin_limit_reached` when the limit is exceeded and 422 for an archived thread. |
| DELETE | /v1/threads/{thread}/pinRemove your own pin from a thread; 404 if the current user has no pin on it. | Remove your own pin from a thread; 404 if the current user has no pin on it. |
| GET | /v1/conversations/{conversation}/readsConversation read statuses (who has read up to which point); thread_id/user_id filters. | Conversation read statuses (who has read up to which point); thread_id/user_id filters. |
| POST | /v1/conversations/{conversation}/readsSet the current user's read position (last_read_message_id / last_read_at). | Set the current user's read position (last_read_message_id / last_read_at). |
| POST | /v1/conversations/mark-readMark all (or the given) company conversations as read. | Mark all (or the given) company conversations as read. |
| POST | /v1/conversations/{conversation}/mark-readMark a specific conversation (or one of its threads) as read. | Mark a specific conversation (or one of its threads) as read. |
| POST | /v1/conversations/{conversation}/mark-unreadMark a conversation (or one of its threads) as unread — rewinds the cursor to the latest inbound message. | Mark a conversation (or one of its threads) as unread — rewinds the cursor to the latest inbound message. |
| POST | /v1/conversations/{conversation}/ai-suggestions/generateGenerate an AI copilot draft reply for the conversation. | Generate an AI copilot draft reply for the conversation. |
| POST | /v1/conversations/{conversation}/ai-suggestions/{suggestion}/acceptAccept a suggestion (optionally with an edited final_text). | Accept a suggestion (optionally with an edited final_text). |
| POST | /v1/conversations/{conversation}/ai-suggestions/{suggestion}/rejectReject a copilot suggestion. | Reject a copilot suggestion. |
| GET | /v1/conversations/{conversation}/ai-pending-actionsQueue of AI-agent actions on the conversation awaiting operator approval. | Queue of AI-agent actions on the conversation awaiting operator approval. |
| POST | /v1/conversations/{conversation}/ai-tools/summaryGenerate a short AI summary of the conversation (for a shift handoff). | Generate a short AI summary of the conversation (for a shift handoff). |
| GET | /v1/conversations/{conversation}/ai-tools/summaryThe stored AI summary of the conversation (`summary: null` when there is none). | The stored AI summary of the conversation (`summary: null` when there is none). |
| GET | /v1/group-chats/{groupChat}/participantsGroup-chat card and a paginated list of participants with roles. | Group-chat card and a paginated list of participants with roles. |
| POST | /v1/group-chats/{groupChat}/refreshRe-fetch group-chat metadata and roster from the messaging runtime. | Re-fetch group-chat metadata and roster from the messaging runtime. |
| POST | /v1/dialogsCreate a dialog from an account identifier: contact → conversation → thread in one call. | Create a dialog from an account identifier: contact → conversation → thread in one call. |
| GET | /v1/dialogs/lookupLook up an existing active dialog by channel and account (channel_id, account). | Look up an existing active dialog by channel and account (channel_id, account). |
Examples
List unassigned conversations
Request
curl -X GET "https://api.aisar.app/v1/conversations?thread_filter=unassigned&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 15,
"company_id": 1,
"contact_id": 45,
"subject": null,
"lifecycle_status": "active",
"is_closed": false,
"is_archived": false,
"last_message_id": 1001,
"last_message_at": "2026-02-09T18:00:00Z",
"last_message": { "id": 1001, "message_content": { "text": "Здравствуйте, подскажите по заказу" }, "direction": "inbound" },
"unread_count_for_current_user": 1,
"awaiting_since": "2026-02-09T18:00:00Z",
"closed_at": null,
"archived_at": null,
"created_at": "2026-02-09T17:00:00Z",
"updated_at": "2026-02-09T18:00:00Z",
"contact": { "id": 45, "displayName": "John Doe", "phones": [{ "phone": "+12025550187", "type": "mobile" }] },
"threads": [{ "id": 33, "channel_id": 3, "is_group": false }]
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "has_more": false },
"counters": { "all": 120, "awaiting": 21, "unread": 14, "assigned": 47, "unassigned": 73, "my_team": 32, "archived": 9, "groups": 6, "aisar_notifications": 3 }
}
}Filter and sort the list
Request
curl -X GET "https://api.aisar.app/v1/conversations?funnel_id=1&funnel_stage_ids=3,4&unread_only=1&sortBy=last_message_at&sortDir=desc&perPage=20&include_counters=1" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 22,
"company_id": 1,
"contact_id": 51,
"lifecycle_status": "active",
"is_archived": false,
"last_message_id": 1104,
"last_message_at": "2026-02-09T19:40:00Z",
"unread_count_for_current_user": 2,
"awaiting_since": null,
"contact": { "id": 51, "displayName": "Jane Roe", "phones": [{ "phone": "+12025550168", "type": "mobile" }] },
"threads": [{ "id": 40, "channel_id": 3, "is_group": false }]
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "has_more": false },
"counters": { "all": 120, "awaiting": 21, "unread": 14, "assigned": 47, "unassigned": 73, "my_team": 32, "archived": 9, "groups": 6, "aisar_notifications": 3 }
}
}List conversations without an exact total (`with_total=0`)
Request
curl -X GET "https://api.aisar.app/v1/conversations?perPage=20&with_total=0" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 22,
"company_id": 1,
"contact_id": 51,
"lifecycle_status": "active",
"is_archived": false,
"last_message_id": 1104,
"last_message_at": "2026-02-09T19:40:00Z",
"unread_count_for_current_user": 2,
"awaiting_since": null,
"contact": { "id": 51, "displayName": "Jane Roe", "phones": [{ "phone": "+12025550168", "type": "mobile" }] },
"threads": [{ "id": 40, "channel_id": 3, "is_group": false }]
}
],
"pagination": { "page": 1, "perPage": 20, "total": null, "lastPage": null, "has_more": true }
}
}Cross-conversation thread list
Request
curl -X GET "https://api.aisar.app/v1/threads?thread_filter=unread&sortBy=last_message_at&sortDir=desc&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 33,
"conversation_id": 15,
"company_id": 1,
"channel_id": 3,
"contact_account_id": 78,
"is_group": false,
"external_thread_id": null,
"lifecycle_status": "active",
"is_closed": false,
"is_archived": false,
"closed_at": null,
"archived_at": null,
"last_message_id": 1001,
"last_message_at": "2026-02-09T18:00:00Z",
"unread_count_for_current_user": 1,
"contact_id": 45,
"created_at": "2026-02-09T17:00:00Z",
"updated_at": "2026-02-09T18:00:00Z"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1 },
"counters": { "all": 84, "awaiting": 12, "unread": 6, "assigned": 30, "unassigned": 54, "my_team": 18, "archived": 4, "groups": 5 }
}
}Close a single thread
Request
curl -X POST "https://api.aisar.app/v1/threads/33/close" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comment": "Вопрос по этому каналу закрыт."
}'Response
{
"data": {
"message": "Thread closed.",
"thread": {
"id": 33,
"conversation_id": 15,
"channel_id": 3,
"lifecycle_status": "closed",
"is_closed": true,
"is_archived": false,
"closed_at": "2026-02-09T19:00:00Z"
}
}
}Conversation read statuses
Request
curl -X GET "https://api.aisar.app/v1/conversations/15/reads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 8,
"company_id": 1,
"conversation_id": 15,
"thread_id": 33,
"user_id": 5,
"last_read_message_id": 1001,
"first_read_at": "2026-02-09T18:02:00Z",
"last_read_at": "2026-02-09T18:20:00Z",
"created_at": "2026-02-09T18:02:00Z",
"updated_at": "2026-02-09T18:20:00Z",
"user": { "id": 5, "name": "Alex", "lastname": "Ivanov", "email": "alex@example.com" }
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1 }
}
}Set the read position
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/reads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"thread_id": 33,
"last_read_message_id": 1001
}'Response
{
"data": {
"message": "Conversation read state updated.",
"item": {
"id": 8,
"company_id": 1,
"conversation_id": 15,
"thread_id": 33,
"user_id": 5,
"last_read_message_id": 1001,
"first_read_at": "2026-02-09T18:02:00Z",
"last_read_at": "2026-02-09T18:00:00Z",
"created_at": "2026-02-09T18:02:00Z",
"updated_at": "2026-02-09T18:25:00Z"
}
}
}Mark a conversation as unread
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/mark-unread" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"thread_id": 33
}'Response
{
"data": {
"message": "Conversation marked as unread.",
"item": {
"id": 8,
"company_id": 1,
"conversation_id": 15,
"thread_id": 33,
"user_id": 5,
"last_read_message_id": 999,
"first_read_at": "2026-02-09T18:02:00Z",
"last_read_at": "2026-02-09T17:59:59Z",
"created_at": "2026-02-09T18:02:00Z",
"updated_at": "2026-02-09T18:35:00Z"
},
"unread_count": 1,
"shared_read_receipts": false
}
}AI copilot draft reply
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/ai-suggestions/generate" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"id": 5821,
"instance_id": 12,
"conversation_id": 15,
"message_id": null,
"suggested_text": "Здравствуйте! Доставка по городу занимает 1-2 дня.",
"status": "pending",
"accepted_by": null,
"final_text": null,
"tokens_used": 184,
"created_at": "2026-02-09T18:30:00Z",
"updated_at": "2026-02-09T18:30:00Z"
}
}Accept a copilot suggestion
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/ai-suggestions/5821/accept" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"final_text": "Здравствуйте! Доставка по городу занимает 1-2 дня, курьер согласует время."
}'Response
{
"data": {
"id": 5821,
"instance_id": 12,
"conversation_id": 15,
"message_id": null,
"suggested_text": "Здравствуйте! Доставка по городу занимает 1-2 дня.",
"status": "accepted",
"accepted_by": 5,
"final_text": "Здравствуйте! Доставка по городу занимает 1-2 дня, курьер согласует время.",
"tokens_used": 184,
"created_at": "2026-02-09T18:30:00Z",
"updated_at": "2026-02-09T18:31:00Z"
}
}Close a conversation
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/close" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"comment": "Вопрос клиента решён, заказ подтверждён."
}'Response
{
"data": {
"message": "Conversation closed.",
"conversation": {
"id": 15,
"lifecycle_status": "closed",
"is_closed": true,
"closed_at": "2026-02-09T18:20:00Z"
}
}
}Create a dialog from a phone number
Request
curl -X POST "https://api.aisar.app/v1/dialogs" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"account": "+12025550166",
"is_group": false,
"comment": "Первое касание по заявке с сайта"
}'Response
{
"data": {
"message": "Dialog created.",
"thread": {
"id": 33,
"conversation_id": 15,
"channel_id": 3,
"contact_account_id": 78,
"is_group": false,
"created_at": "2026-02-09T17:00:00Z"
}
}
}Look up an existing dialog
Request
curl -X GET "https://api.aisar.app/v1/dialogs/lookup?channel_id=3&account=%2B12025550166" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"thread_id": 42,
"conversation_id": 15,
"contact_account_id": 78
}
}Add a thread to a conversation (second channel)
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/threads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 7,
"contact_account_id": 78,
"funnel_id": 1,
"funnel_stage_id": 3,
"is_group": false,
"external_thread_id": null
}'Response
{
"data": {
"id": 34,
"conversation_id": 15,
"channel_id": 7,
"contact_account_id": 78,
"funnel_id": 1,
"funnel_stage_id": 3,
"is_group": false,
"external_thread_id": null,
"created_at": "2026-02-09T18:30:00Z"
}
}Assign an operator as a participant
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/participants" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"member_type": "user",
"member_id": 5,
"role": "assignee",
"is_primary": true
}'Response
{
"data": {
"id": 61,
"conversation_id": 15,
"member_type": "user",
"member_id": 5,
"role": "assignee",
"is_primary": true,
"created_at": "2026-02-09T18:05:00Z"
}
}Log an event on the conversation timeline
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/events" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "comment",
"comment": "Клиент просит перезвонить после 18:00",
"thread_id": 33
}'Response
{
"data": {
"id": 900,
"conversation_id": 15,
"thread_id": 33,
"type": "comment",
"comment": "Клиент просит перезвонить после 18:00",
"created_at": "2026-02-09T18:10:00Z"
}
}Add an internal note
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/notes" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"body": "Постоянный клиент, дать скидку 10%",
"is_private": true,
"thread_id": null
}'Response
{
"data": {
"id": 77,
"conversation_id": 15,
"thread_id": null,
"body": "Постоянный клиент, дать скидку 10%",
"is_private": true,
"pinned_at": null,
"created_at": "2026-02-09T18:12:00Z"
}
}Add a tag to a conversation
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/tags" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "vip",
"color": "#e11d48",
"source": "manual"
}'Response
{
"data": {
"id": 12,
"conversation_id": 15,
"name": "vip",
"color": "#e11d48",
"source": "manual",
"created_at": "2026-02-09T18:15:00Z"
}
}Bulk-mark conversations as read (202 — queued)
Request
curl -X POST "https://api.aisar.app/v1/conversations/mark-read" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_ids": [15, 16, 22]
}'Response
{
"data": {
"message": "Conversations marked as read."
}
}AI-agent actions awaiting approval
Request
curl -X GET "https://api.aisar.app/v1/conversations/15/ai-pending-actions" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 4120,
"conversation_id": 15,
"tool_key": "close_conversation",
"comment": "AI agent proposed to close the conversation (reason: customer confirmed the order)",
"payload": {
"tool_key": "close_conversation",
"ai_agent_instance_id": 12,
"reason": "customer confirmed the order"
},
"approval_status": "pending",
"resolved_at": null,
"approved_by_user_id": null,
"rejected_by_user_id": null,
"rejected_reason": null,
"execution_result": null,
"created_at": "2026-02-09T18:40:00.000000Z",
"conversation": null
}
]
}
}AI conversation summary for a handoff
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/ai-tools/summary" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"locale": "ru"
}'Response
{
"data": {
"summary_text": "- Клиент спрашивал про сроки доставки по городу.\n- Оператор ответил, что доставка занимает 1–2 дня, курьер согласует время.\n- Статус: заказ подтверждён, клиент ожидает звонка курьера.",
"message_count": 14,
"tokens_used": 512,
"generated_at": "2026-10-07T09:30:00+00:00",
"saved": true
}
}Stored conversation summary
Request
curl -X GET "https://api.aisar.app/v1/conversations/15/ai-tools/summary" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"summary": {
"summary_text": "- Клиент спрашивал про сроки доставки по городу.\n- Статус: заказ подтверждён.",
"message_count": 14,
"locale": "ru",
"generated_at": "2026-10-07T09:30:00+00:00",
"generated_by": { "id": 7, "name": "Айгерим К." }
}
}
}Empty conversation — summary not possible (422)
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/ai-tools/summary" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{}'Response
{
"data": {
"message": "Conversation has no messages to summarise",
"code": "empty_conversation"
}
}Group-chat participants
Request
curl -X GET "https://api.aisar.app/v1/group-chats/8/participants?page=1&per_page=50" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"group_chat": {
"id": 8,
"external_group_id": "120363000000000001@g.us",
"subject": "Отдел продаж — клиенты",
"description": "Рабочая группа по заявкам",
"photo": null,
"announce": false,
"restrict": false,
"size": 3,
"updated_at": "2026-02-09T18:00:00.000000Z"
},
"participants": [
{
"id": 51,
"role": "superadmin",
"display_name": "Alex Ivanov",
"photo": null,
"phone": "12025550166",
"contact_account_id": 78,
"external_id": "12025550166"
},
{
"id": 52,
"role": "member",
"display_name": "Jane Roe",
"photo": null,
"phone": "12025550168",
"contact_account_id": 79,
"external_id": "12025550168"
}
],
"pagination": { "page": 1, "per_page": 50, "total": 3, "last_page": 1 }
}
}Refresh group-chat roster from the runtime
Request
curl -X POST "https://api.aisar.app/v1/group-chats/8/refresh" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"group_chat": {
"id": 8,
"external_group_id": "120363000000000001@g.us",
"subject": "Отдел продаж — клиенты",
"description": "Рабочая группа по заявкам",
"photo": null,
"announce": false,
"restrict": false,
"size": 4,
"updated_at": "2026-02-09T18:45:00.000000Z"
},
"participants": [],
"pagination": { "page": 1, "per_page": 50, "total": 4, "last_page": 1 }
}
}Refresh not possible — channel has no runtime instance (503)
Request
curl -X POST "https://api.aisar.app/v1/group-chats/8/refresh" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"message": "Channel has no runtime instance"
}Mark a conversation as "no reply needed"
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/dismiss-awaiting" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"id": 15,
"awaiting_since": null
}
}/v1/dialogs is not available to a family company (403)
Request
curl -X POST "https://api.aisar.app/v1/dialogs" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "channel_id": 3, "account": "+12025550187" }'Response
{
"data": {
"message": "This area is not available for your account type.",
"error": "company_type_forbidden"
}
}Email: a thread for the first letter (contact_email_id)
Request
curl -X POST "https://api.aisar.app/v1/conversations/15/threads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "channel_id": 41, "contact_email_id": 12 }'Response
{
"data": {
"id": 77,
"conversation_id": 15,
"channel_id": 41,
"contact_account_id": 95,
"is_group": false
}
}Pin a conversation
Request
curl -X PUT "https://api.aisar.app/v1/conversations/15/pin" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"conversation_id": 15,
"thread_id": null,
"pinned_at": "2026-10-24T09:30:00.000000Z"
}
}Pin a thread, the limit and unpinning
Request
# В режиме «по каналам» закрепляется тред: PUT /v1/threads/{thread}/pin
# Шестой пин подряд (лимит 5 на пользователя и вид списка):
curl -X PUT "https://api.aisar.app/v1/threads/77/pin" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
// 422: лимит исчерпан
{
"data": {
"message": "Pin limit reached: at most 5 pinned threads.",
"code": "pin_limit_reached"
}
}
// DELETE /v1/threads/77/pin — 200, пина больше нет
{
"data": {
"conversation_id": 15,
"thread_id": 77,
"pinned_at": null
}
}