Skip to content
AISARAISAR
REST API

Channels

A channel is a connected messaging source: WhatsApp, Telegram, Instagram, SIP telephony, etc. Through the public REST API you read the channel list and its connection state, manage the lifecycle of an already-connected channel (pause/resume/reconnect/disconnect), and configure operator access rules for it. Connecting and initial provisioning happen through the wizard in the client dashboard, not through this API.

Connecting a channel

Connecting and provisioning a new channel (QR pairing, Meta OAuth, SIP setup) happens through the onboarding wizard in the client dashboard (my.aisar.app/settings/channels/add) rather than directly through this API — the wizard uses separate, provider-specific setup endpoints. The exception is Telegram Business (telegram_business): connecting it takes a single call with the bot token, described below in the "Telegram Business" section.

Lifecycle

A channel lifecycle is expressed by two fields:

  • setup_state (draft → pairing → ready | failed) — the initial setup stage.
  • status (connected | disconnected | connecting | reconnecting | paused | expired | banned) — the current state of an already-configured channel. banned is not terminal: once the provider lifts the account ban, the channel can be reconnected via the regular reconnect/pairing flow; if the ban still stands, the provider rejects the connection and the channel returns to banned.

What the channel can do right now: calls and messages

Both the list (GET /channels) and the detail view (GET /channels/{channel}) return a pair of booleans that answer "what can this channel be given to do right now" without your having to reason about types and statuses yourself. calling_enabled — the channel can take and place calls: always true for SIP telephony; for personal (QR-connected) WhatsApp only while the channel's calling toggle is on; for WhatsApp Business only once a calling number is configured for it (config.calling_did) — so a WABA without calling enabled does not count as telephony. messaging_enabled is the other half of the same pair: true for a connected (status = connected) messenger, widget or social channel, and false for a SIP trunk (voice only) and for any channel not in the connected status. Both fields are always present, on channels of every type.

A third field on the same pair — voice_calls_supported — describes capability, not the current state: true only for a personal (QR) WhatsApp channel connected to the AISAR runtime that carries voice calls. For every other channel type (WhatsApp Business, SIP telephony, Telegram, etc.) the field always reads false and adds nothing new — their calling capability is already fully described by calling_enabled. Don't confuse the pair: voice_calls_supported answers "can this channel carry calls at all", calling_enabled answers "are calls switched on right now" — the client dashboard only shows the call-receiving toggle once voice_calls_supported is true. voice_calls_supported: false on a regular WhatsApp channel does not mean AISAR cannot do voice calls over regular WhatsApp at all — the platform can, and a share of channels already run on the runtime that supports it. It only means THIS channel is connected to a runtime without call support; enabling calls on it requires reconnecting the channel through AISAR support — there is no self-service migration for this in the dashboard.

One more boolean on both the list and the detail view is can_start_conversation: whether the channel can write to a client first, i.e. open a conversation with a contact this channel has no thread with yet. It is true for every type except Telegram Business (telegram_business): such a channel only replies to people who wrote first, so do not offer it when picking a channel for a new dialog or for a broadcast (details in the "Telegram Business" section below). The GET /channels list also returns status_message and last_error_code — the same fields as the detail view: a short machine-readable code of the channel state and the code of its last error. They tell you why a channel cannot send without a separate request for the detail view.

What the channel does with an incoming WhatsApp call

The behaviour is set by config.call_handling.mode, and three of its values are changed through the ordinary PATCH /v1/channels/{channel}: ignore leaves the call alone (it stays a missed call in the dashboard), reject declines it immediately, reject_with_message declines it and sends the contact the text from config.call_handling.auto_reply_text. The config is merged key by key — you do not have to send the whole object.

Two behaviours are worth knowing in advance. Anything other than ignore declines the call — a typo in mode quietly turns into "decline everything", while the auto-reply text is only sent for the exact value reject_with_message. And the fourth state, answering the call (answer), cannot be set through PATCH: the request returns 422 naming the endpoint to use instead.

Answering is switched on through a dedicated endpoint: POST /v1/channels/{channel}/whatsmeow-calling/enable with the body {"ban_risk_ack": true} — acknowledging the number-ban risk is mandatory, and 422 comes back without it. The endpoint is available only for a channel with voice_calls_supported: true (otherwise 404) and to a token allowed to manage telephony (otherwise 403); when there is no call capacity free right now the answer is 422 with error: calls_instance_unavailable, and the channel stays as it was rather than half-enabled. To switch it off use POST /v1/channels/{channel}/whatsmeow-calling/disable: it returns mode to ignore and reopens the choice between the three values above. While answering is on, PATCH cannot downgrade mode — the answer value is held, so that an ordinary save of the channel settings form cannot switch calls off in passing.

WhatsApp number profile

The avatar and profile texts are read and changed through /v1/channels/{channel}/whatsapp/profile. Which fields are available depends on the connection rather than on the channel type, so the response always carries capabilities: WhatsApp Business exposes about, description, address, email, websites and category, while a QR-connected WhatsApp exposes the photo and about, with a description only when the number runs the WhatsApp Business app. Branch on capabilities and limits from the response: sending an unsupported field rejects the whole request instead of applying it in part.

WhatsApp blocked users

GET /channels/{channel}/whatsapp/blocked-users (since 2026-09-04) reads the list of numbers the PROVIDER itself has blocked on this channel — it is not AISAR's own blocked-contact list (that is managed by POST /contacts/{contact}/block and /unblock, see the "Contacts" group). The shape of numbers[] depends on the connection: WhatsApp Business (Cloud API) returns the provider's number format (e.g. +12025550179), QR-based WhatsApp (whatsmeow) returns a WhatsApp JID (e.g. 12125550175@s.whatsapp.net). Pagination is cursor-based: pass after set to the previous response's next_cursor to get the next page; next_cursor: null means the end of the list. limit ranges 1–1000, default 100.

Available only for WhatsApp channels (whatsapp/whatsapp_business) — a channel of another type returns 422 {error_code: "channel_not_whatsapp"}; a disconnected channel returns 409 {error_code: "channel_offline"}. Authorization is the channel:view permission (not contact:update, unlike block/unblock) — this reads channel state rather than acting on a contact; a foreign channel returns 403.

Temporary SIP-trunk outbound hold (`sip_outbound_gated`)

A sip_telephony channel connected via the operator equipment's IP address (see the channel-connection guide, "SIP telephony (BYO trunk)" section) may have its outbound calls temporarily held if the same address is already in use by another AISAR company with the same operator. This is reflected in the boolean sip_outbound_gated field — present on EVERY channel (not just sip_telephony), false when no hold applies. While sip_outbound_gated: true, the channel still accepts inbound calls, but POST /calls/originate and POST /softphone/external-call behave as if the company had no connected SIP channel at all. The hold lifts automatically, with no endpoint call needed, as soon as the channel's number receives its first confirmed inbound call.

Archived channels

A channel deleted with its history kept (DELETE /channels/{channel} without keep_history=false — the default, see below) does not disappear: it is soft-deleted, stamped with archived_at, and its conversation history stays readable inside AISAR. The regular GET /channels never returns it, and there is no parameter that puts it back in that list — include_archived is not part of GET /channels validation, so any value for it is silently ignored. The only way to see an archived channel is the separate GET /channels/archived list.

GET /channels/archived returns the same ChannelListResource as the regular list, with archived_at and messages_count filled in (the channel's message count, including messages later deleted individually). The moment of archiving is marked by the channel.archived webhook event (see the webhook events reference) — this list is where the channel that event notified you about can be found afterwards. GET /channels/{channel} by an archived channel's id returns 404 — that is expected (route-model binding does not resolve a soft-deleted model), not a sign that the channel was lost.

Permanently and irreversibly deleting an archived channel together with its history is DELETE /channels/archived/{channelId} (channelId is a plain integer: a soft-deleted channel is not resolved via route-model binding, so the path takes the id directly rather than {channel}). A 200 response means the deletion was queued as a background job, not that it has already finished.

Lifecycle request bodies

Pause/resume/disconnect take no request body, whereas reconnect (POST /channels/{channel}/reconnect) accepts a provider-specific body for OAuth/bot channels (Instagram/Threads/Facebook Messenger — { code }, WhatsApp Business — { authorization_code, waba_id, phone_number_id }, Telegram Bot — { bot_token }, Telegram Business — { bot_token } with the token optional), otherwise 422; WhatsApp/Telegram QR and Live Chat take an empty body. The full per-type contract is covered in the partner documentation (channel-connection guide, "Direct API" section).

Since 2026-09-26 reconnecting a Telegram Bot ({ bot_token }) with the token of a bot already bound to another live AISAR channel is refused with 409 and code/error_code bot_already_connected (the owning company is not revealed). In PATCH /channels/{channel} the crm_settings.funnel_id field only accepts a funnel of your own company (otherwise 422), and crm_settings: null is stored as { "funnel_id": null } — "no funnel override".

Telegram Business (`telegram_business`)

A Telegram Business channel runs a personal Telegram account through the company's own bot with Business Mode switched on: this is the official Bot API, and AISAR never stores the account session. Connecting takes two steps. First, POST /v1/channels/telegram-business/complete-signup with the bot token: AISAR checks the token and Business Mode, creates the channel and sets up the bot's webhook. The second step is done by the account owner in the Telegram app (Settings → Telegram Business → Chatbots) and cannot be performed through the API. Until then the channel sits in setup_state: pairing, status: connecting, status_message: awaiting_business_connection and already counts against the plan's channel limit. Once connected it moves to ready/connected. No channel.connected webhook event is sent for this connection, so track the state through GET /channels/{channel}.

The complete-signup body: bot_token (required, format 123456789:AAF…, up to 200 characters), name (up to 255 characters), description, timezone (defaults to UTC). Without a name the channel is called Telegram Business @bot_username, and on the first account connection it takes the owner's name, unless you have renamed it by then. The config, allowed_team_ids, allow_broadcasts, allow_automations, allow_inbound_handling and child_id fields are prohibited: any of them yields 422. The channel.create permission is required. The plan's channel limit applies, and so does a limit of 10 requests per minute per user. A 201 answer returns the channel object. The bot token is never returned and is stored encrypted. A partner connect-session's ephemeral token is not accepted here (403): the iframe channel-connection wizard has no Telegram Business flow yet.

ResponseWhen
422 invalid_tokenTelegram did not recognise the token. A token in the wrong format is rejected earlier, as an ordinary validation error on the bot_token field with no error_code.
422 business_mode_disabledBusiness Mode is off for the bot. It is switched on in @BotFather: /mybots → the bot → Bot Settings → Business Mode → Turn on. Then repeat the request with the same token.
409 bot_already_connectedThe bot is already bound to another live AISAR channel of any company, including as a Telegram Bot. The answer does not reveal which company owns that channel.
502 webhook_setup_failedTelegram refused the webhook. No channel is created, the bot and the plan slot stay free, and the request can be repeated with the same token.
502 telegram_unavailableTelegram did not respond and the token was not checked. No channel is created.
403No channel.create permission, or the request came with a connect-session's ephemeral token. An exhausted plan channel limit is also 403, with the body {"data": {"error": "limit_exceeded", "limit_type": "max_channels", "usage": …, "max": …, "upgrade_required": true, "addon_type": "extra_channel", "remaining": 0, "hint": "…"}}, where hint says what to do next. A SIP telephony line counts as a channel too.
429More than 10 requests per minute from one user.

Refusals with a code come as a flat body without the data envelope: {"message": "…", "code": "…", "error_code": "…"}, where code and error_code are equal.

The status_message values of a Telegram Business channel:

  • awaiting_business_connection — the channel exists, but the owner has not connected the bot in Telegram yet (pairing, connecting).
  • null with connected — everything works.
  • read_only_no_reply_right — the channel is connected, but the owner did not give the bot the right to reply to messages. Incoming messages arrive, and sending from AISAR is refused with no_reply_right until the right is switched on in Telegram.
  • business_connection_disabled — the owner switched the bot off in Telegram settings (disconnected). The history in AISAR is kept, and the channel comes back once the bot is switched on again.
  • owner_connected_as_personal_channel:{id} — the same account already runs the company's personal Telegram channel with id {id}. The connection is stopped (setup_state: failed, status: disconnected), otherwise every message would arrive twice. Only a personal channel that receives messages blocks (connected, connecting, reconnecting, paused); a disconnected, expired or banned one does not. The rule also works the other way round: if a personal channel on the same account connects later, the working Telegram Business channel moves to this same state. To lift the block, disconnect the personal channel, then remove the bot from chatbots in Telegram and add it again, or reconnect the Telegram Business channel.
  • bot_token_revoked — Telegram rejected the bot token, usually after it was reissued in @BotFather (expired). Reconnect the channel with a new token of the same bot.

Once the account is connected, the GET /channels/{channel} detail view returns config.telegram_business — a non-secret projection of the connection: owner_user_id, owner_username, owner_name (the account owner), bot_username, rights with the booleans can_reply, can_read_messages, can_delete_sent_messages, then dangerous_rights and personal_channel_identity_unknown. dangerous_rights lists the rights the owner granted the bot although AISAR never uses them: transferring stars, gifts, editing the profile name, bio, photo and username, stories, deleting any message. An empty list means there are no excess rights. personal_channel_identity_unknown: true means the company has a personal Telegram channel whose account could not be matched yet, so duplicated messages cannot be ruled out. In the GET /channels list a channel of this type carries only a trimmed config.telegram_business with rights and personal_channel_identity_unknown; the list returns no other config. Only the server writes this projection: config.telegram_business in PATCH /channels/{channel} is silently dropped and the stored value stays unchanged. The bot token and the connection id are never returned anywhere.

Such a channel cannot write to a client first. Telegram lets a bot reply on behalf of the account only in private chats where the client wrote within the last 24 hours, and Telegram has no templates that would reopen that window. So the channel has can_start_conversation: false, and it cannot open a conversation with a new contact: such requests are refused with 422 channel_cannot_start_conversation (see the "Messages" and "Conversations" groups). The channel takes no part in broadcasts: picking it explicitly in channels.channelIds or content.channels[].channelId yields 422, the last_interacted mode skips its threads, and allow_broadcasts: true in PATCH /channels/{channel} is refused with 422. The server does not check the 24-hour window when you reply; limits.reply_window_hours: 24 is a hint for the interface. If the client has been silent longer, Telegram refuses delivery and the message goes failed with the outside_window code. Telegram gives the bot no history from before the connection. Only private chats are supported, no Telegram groups or channels. Message buttons and reactions are not supported. The generic POST /v1/channels does not create this channel type (422 on the channel_type field), and an existing channel cannot be switched to this type through PATCH either (422 on channel_type_id).

Reconnecting is POST /channels/{channel}/reconnect with the body { "bot_token": "…" }, where the token is optional. Without it AISAR takes the stored token and sets the webhook up again. A new token is accepted only from the same bot: this is the "reissued the token in @BotFather" case. Every reconnect changes the webhook address and secret. If Telegram does not accept them, the previous ones are restored and the channel keeps working as before the request. When the account is already connected, AISAR re-reads the connection state and rights from Telegram. Refusals: 422 different_bot (the token of another bot, which is connected as a new channel instead), 422 invalid_token, 422 business_mode_disabled, 409 bot_already_connected, 502 webhook_setup_failed and 502 telegram_unavailable with the same flat body as on signup, and 403 connect_session_not_supported: this channel cannot be reconnected with a partner connect-session's ephemeral token.

Send failures through this channel carry their own codes. The code shows up in status.error_details of the message.status.updated event (in parentheses, "text (code)" format) and in the channel log as a message.send.failed.{code} entry. The codes listed below, except telegram_error, are terminal: the message goes failed at once, with no retries. The Bot API gives no delivery or read receipts, so a successfully sent message goes straight to delivered, and read never comes.

  • outside_window — the client has not written for more than 24 hours, and Telegram will not deliver a reply until the client writes again. The same code as for Facebook Messenger.
  • no_reply_right — the bot has no right to reply on behalf of the account.
  • business_not_connected — the owner has not connected the bot to the account yet.
  • business_connection_inactive — the connection was switched off or removed in Telegram, and the channel moves to disconnected.
  • bot_token_revoked — Telegram rejected the bot token, and the channel moves to expired.
  • owner_connected_as_personal_channel — the account is already connected as the company's personal Telegram channel, and sending through the bot is stopped.
  • telegram_error — any other Telegram error. The send is retried on the usual schedule, then the message goes failed. When Telegram asks to wait (HTTP 429), the retry comes after the interval it names, but no later than in 5 minutes, and it also uses up an attempt.

provider_key and channel type

The provider_key field is a public, neutral provider identifier; internal runtime-library names are never exposed. The full list of values and the QR-channel mapping rules live in the «Types: messaging» reference, the Channel type. To distinguish the channel type in code, rely on channel_type.key rather than the display name.

Channel log pagination

GET /channels/{channel}/logs is paginated: page/per_page (the perPage alias is accepted too), capped at 500, defaulting to 200. Until 2026-08-25 the endpoint silently ignored every paging parameter and always returned the newest 200 rows — there was no way to look further back into the history. Sorting is created_at desc, then id desc: the runtime writes bursts of log rows within the same second, and without the id tiebreaker the same row could repeat on one page and vanish from another. The response is wrapped like other paginated lists: items in data, paging info in meta (current_page, last_page, per_page, total); the shape of an individual log entry is unchanged.

Send-failure codes in the channel log

Besides connection codes (connection.ready, connection.lost, etc.), GET /channels/{channel}/logs records entries about the fate of outbound messages and broadcasts. message.send.failed.{error_code} — error level: one entry per send that exhausted all retries (not per attempt, or a broadcast would flood the channel log), {error_code} is the last attempt's error code or unknown; context carries message_id, attempts, error_code and a truncated response (only reason/status/response from the runtime/provider response — the runtime's internal address is stripped from response for security reasons). message.status.failed_suppressed — warning level: the provider sent a failure receipt AFTER the message had already reached delivered/read/played — the message's status is not downgraded (see the message.status.updated event), but the provider-side failure is still recorded here; context carries message_id and reason (a human-readable cause). message.status.orphan_failed (since 2026-09-09) — warning level: the provider sent a failed status for an external message id AISAR has no record of (never persisted at send time, or persisted under a different id) — only failed is logged this way, so the log isn't flooded by sent/delivered/read receipts on personal WhatsApp numbers, where such "orphan" receipts are routine for messages typed directly on the phone; context carries external_message_id, recipient, and the provider's raw errors. broadcast.preflight_blocked (since 2026-09-09) — error level: recorded when a broadcast run refused to start because Meta reports the WABA channel unable to send (health_status.can_send_message = BLOCKED); context carries broadcast_id, error_code, and Meta's possible_solution (when present).

sip_status in the eligibility response

sip_status inside eligibility.calling (in the GET .../whatsapp-calling response, and also POST .../whatsapp-calling/enable|disable) is an AISAR-derived field, not Meta's raw status: ENABLED once calling is turned on for the channel (config.calling_did is set), NOT_SET when calling isn't enabled, UNKNOWN when the Graph API call itself failed (see the top-level unknown_reason/detail). To decide whether calling is actually available right now, rely on active (true only when can_receive_call_sip === "AVAILABLE") and can_receive_call_sip itself — both always reflect Meta's current state, while sip_status is a local mirror of AISAR's last successful enable/disable and can miss a change made directly in WhatsApp Manager.

Family (Kids) companies and general channel mutations

Since 2026-09-30, practically ANY channel change — not just deletion — is unavailable to a family (Kids) company: both POST/PUT/PATCH /channels/{channel}, disconnect, reconnect, pause, resume, every runtime-pairing/* route (passkey steps included), the WhatsApp profile/username/conversational-components routes, templates/sync, POST/PUT .../access, every */complete-signup route (Meta, Instagram Business, Threads, Messenger, Telegram Bot, SIP telephony, Live Chat), meta/discover-from-code, messenger/pages, connect-sessions, and setup-failures all return 403 {"message": "...", "error": "company_type_forbidden"}. On a family company's channel, ownership belongs to the child — writing to it is not parental oversight, it is effectively acting as the minor inside someone else's messenger, so this whole surface is closed to the family. Reads (GET) stay open everywhere — QR pairing and viewing a conversation depend on them. Only these stay available to a family: POST /channels (in a narrowed form — see below), POST /channels/{channel}/connect/start, POST /channels/{channel}/auth-code, and unlinking a channel from a child in the Kids cabinet (POST /v1/kids/children/{child}/channels/{channel}/unlink, outside this group). DELETE /channels/{channel} and DELETE /channels/archived/{channelId} remain separately unavailable to a family company with the same code — the only way to remove a channel for Kids is still that cabinet unlink, not these general routes.

For a family company, POST /channels only accepts channel_type values whatsapp or telegram (any other type returns 422), and only the channel_type, name, account_name, child_id, start_pairing, pairing_mode, pairing_phone_number fields — the presence of any other field (e.g. status, config, allow_inbound_handling) returns 422; child_id is required and must be a non-deleted child of the current company (see below). POST /channels/{channel}/connect/start for a family company additionally checks: trying to (re)connect an already-connected channel (status = connected) returns 409 {"error_code": "channel_already_connected"} instead of an ordinary re-pair — unlink the channel in the Kids cabinet first; the force field is unavailable to a family (422, "Forced re-pairing is not available for your account type."); mode only accepts qr, code, or phone (the other values business channels may use return 422).

Separately from the above, POST /channels with child_id (for any company where the field applies) only accepts an existing, non-deleted child of the current company — a reference to another company's child, a non-existent one, or an already-deleted one returns 422.

Channel access rules (`.../access`)

rules in the POST/PUT /channels/{channel}/access body — at most 500 elements, otherwise 422. Each element's model_id is checked against its sibling model_type: for user it must be a user with a role in the current company, for team a non-deleted team of the current company; otherwise 422. The one exception is a model_type+model_id pair already recorded on THIS channel — resubmitting it is accepted even if that user has since left the company or lost the role, because the cabinet typically resends the channel's whole rule list rather than just the changed row. A channel of another company returns 404. Deleting a team (DELETE /teams/{team}) also removes its access rules on every channel that had them.

Each element's rule is allow or block, but for model_type: "team" the value block is never supported — it always returns 422 on rules.N.rule ("A team can only be allowed on a channel. To keep someone out, block that user personally."). A team can only open a channel to its members; to close a channel to a specific person, block that person individually — model_type: "user", rule: "block".

When a company has team channel-access restrictions turned on (a platform-level setting), changing .../access — and a team's member list or channel set (see the "Teams and users" group) — is limited to the company admin or platform staff: for anyone else (even with the channel:update permission) the request returns 403 {"message": "...", "code": "channel_access_admin_only"} and the channel's rules are left unchanged. While this setting is off (the current default), only the ordinary channel:update permission check applies, as in the examples below. Whether the CURRENT user can edit access right now can be read without a trial write, from the can_edit_channel_access field of the GET /v1/me response (see the "Teams and users" group).

Picking teams in the connect wizard (allowed_team_ids)

The connect wizards — POST /channels/meta/complete-signup, .../instagram-business/complete-signup, .../threads/complete-signup, .../messenger/complete-signup, .../telegram-bot/complete-signup, .../sip-telephony/complete-signup, and POST /channels/{channel}/sip/update — accept an optional allowed_team_ids: an array of the CURRENT company's team ids, at most 100. Since 2026-09-30, every id you pass becomes a team allow channel-access rule — the same thing POST .../access with model_type: "team" would write — and shows up in GET /channels/{channel}/access right after signup. It used to just sit in channel.config.allowed_team_ids without affecting anything — the channel stayed open to everyone regardless. A foreign, deleted, or non-existent team id returns 422; the field is not available at all for a family (Kids) company — that company type does not use teams. When team access restrictions are turned on (see above), picking even one team requires the company admin or platform staff — otherwise 403 channel_access_admin_only, and in that case the channel is not created/updated at all. Changes are add-only: resubmitting the wizard (sip/update) never removes an already-recorded team rule, even with an empty list or the field left out entirely — remove a rule through .../access or the team form instead.

The config.allowed_team_ids key is no longer written anywhere: not by these wizards (replaced by the team access rules above), and not by the general PATCH/POST /channels/{channel} either — sending config.allowed_team_ids in a general channel update silently drops the value from the request body (the rest of config is saved as usual) instead of storing it verbatim. In SIP and Facebook Messenger responses (the only two channel types whose config used to carry this key), a channel created or updated going forward no longer carries config.allowed_team_ids.

Connecting an instagram channel (personal account) with a login and password

Only for channels of type instagram (personal account), when this type is available to your company; otherwise connecting returns 403. POST /channels/{channel}/connect/start takes the body { "username": "...", "password": "...", "restart"?: true }: username is the account username, email or phone (required, up to 255 characters), password is the password (required, up to 255 characters, never stored and never written to logs), restart is an optional flag meaning "start the login over". The optional mode, if sent, may only be credentials (anything else returns 422). Success is 200: data.message, data.channel_id, data.connection (status: "starting", mode: "credentials", the login object, see below) and data.runtime_push ({ "sent": true, "reason": null, "status": null }). Then poll GET /channels/{channel}/connection.

  • 403 — this channel type is not available to your company; 404 — the channel was not found or belongs to another company.
  • 409 with data.error_code: "pairing_in_progress" — a login on this channel is already in progress (a code or approval step); the response also carries data.channel_id and data.connection. To start over, send "restart": true.
  • 429 with data.error_code: "login_rate_limited", data.retry_after (seconds) and a Retry-After header — too many login attempts. The response does not reveal which limit fired; the body is always message, error_code, retry_after. After several wrong passwords in a row the login is paused for that username (the end of the pause is visible in login.paused_until).
  • 503 with data.error_code: "runtime_unavailable" — no Instagram runtime is available right now; 502 with data.runtime_push: { "sent": false, "reason": ... } — the runtime did not accept the command. In both cases the channel state is unchanged.
  • 422 — username/password missing or too long, a wrong mode, or restart is not a boolean.

When the login step needs a code from Instagram, send it through POST /channels/{channel}/auth-code with the body { "code": "123456" }: digits only, 4-12 characters, spaces and dashes are allowed and stripped (anything else returns 422); the phoneCodeHash and password fields are ignored for instagram. Success is 200 with data.message: "Auth code submitted.", data.channel_id, data.runtime_push. If no code is expected right now (login.step is not waiting_code) the answer is 409 with data.error_code: "code_not_expected". At most 5 codes per 10 minutes per channel: after that 429 login_rate_limited with retry_after. If the runtime did not accept the code, 502.

For instagram the connection snapshot (GET /channels/{channel}/connection and GET /channels/{channel}/runtime-pairing) additionally carries data.connection.login with exactly these fields: step, code_kind, masked_destination, attempts_left, expires_at, kind, paused_until, failure. step: logging_in, waiting_code, waiting_approval, bad_password, unsupported_challenge, account_mismatch, ig_wait, or null. code_kind is where the code was sent: sms, email, totp or sms_2fa; masked_destination is the masked recipient of the code; attempts_left is how many code attempts remain; expires_at is when the step expires (ISO 8601); kind is the kind of Instagram check (for example change_password); paused_until is when the login pause ends, otherwise null; failure is the reason a login ended without a step: code_timeout, code_invalid, approval_timeout or null. The values of connection.pairing_state (and of connection.status, which matches it) for the steps are: authorizing, code_required, approval_required, bad_password, action_in_app_required, account_mismatch, ig_wait. Channels of other types have no login object.

For all channels: the responses of connect/start, auth-code, runtime-pairing and of channel creation with pairing start no longer contain the raw runtime response. runtime_push keeps only three fields: sent (boolean), reason (a string or null) and status (the HTTP code of the runtime answer or null).

Email channel: signup and mailbox settings

A channel of type email is connected with two calls. POST /channels/email/test-connection checks the mailbox and stores nothing, POST /channels/email/complete-signup repeats the check and creates the channel (201, a regular channel object with provider_key: "imap_smtp"). Both calls need the channel.create permission, are unavailable to a family (Kids) company (403 company_type_forbidden) and are rate limited: 10 requests per minute per user and 30 per hour per company (429); failed checks of one mailbox are additionally capped per (server, login) pair. complete-signup also enforces the plan's channel limit, like the other connect wizards. The channel type is available to every company since 2026-10-09.

The body of both calls: provider (only imap_smtp for now), email (the mailbox address, up to 320 characters), username (the login, usually equal to the address), password (an app password or the mailbox password, up to 1024 characters; never returned and never logged), imap_host and smtp_host (public server names; internal-network IPs and service addresses return 422), imap_port (993 or 143), smtp_port (465 or 587). The optional imap_security and smtp_security (ssl or starttls) must agree with the port: 993 and 465 are ssl, 143 and 587 are starttls, any other combination returns 422. Encryption is mandatory; plain connections and self-signed certificates are not supported. The child_id field is prohibited (422): an email channel is never a child channel.

test-connection answers 200 with the report data = { imap: { ok, code }, smtp: { ok, code }, sent_folder }: per protocol ok and either code: null or a normalized email_* code. Raw mail-server replies and library messages never reach the response, so the call cannot be used as a port scanner. No test email is sent. Codes: email_auth_failed (the server rejected the login or password; most often it is not an app password or IMAP is off), email_tls_failed (no secure connection or the certificate is issued for another name), email_host_blocked (the server address is not allowed), email_dns_failed (the server name was not found), email_host_unreachable (the server does not answer on the port), email_timeout (the server did not answer in time), email_port_not_allowed, email_protocol_error (an unexpected server reply), email_folder_not_found (the Inbox cannot be opened), email_provider_requires_oauth (a Microsoft 365 or Outlook.com mailbox: passwords are not accepted, Microsoft sign-in comes later). Too many failed checks of one mailbox return 429 with data.code = "email_test_rate_limited" and a Retry-After header.

complete-signup takes the same mailbox fields plus optional ones: name (the channel name), display_name (the sender name in the From header; line breaks are rejected), signature_text (the signature, up to 2000 characters, plain text), save_to_sent (save a copy of outgoing letters to Sent, true by default), initial_sync_days (0, 3 or 7: how many days of letters already in the Inbox to load, 0 by default), poll_interval_seconds (the mailbox check interval, from 60 to 900 seconds; the server default applies when omitted), timezone (a time zone identifier). Loaded history lands in the chat as history: the AI agent, automations and notifications do not fire on it. Responses: 201 the channel is created; 409 with data.code = "mailbox_already_connected" when this address is already connected in your company; 422 for an invalid body; 422 with data = { message, code, imap, smtp, sent_folder } when the mailbox check failed (code is the code of the first failing side, IMAP first, then SMTP; the report has the same shape as in test-connection); 429 email_test_rate_limited. The password is stored encrypted and is never returned in any response.

GET /channels/{channel}/email-settings (the channel.view permission) returns the mailbox settings without secrets: channel_id, provider, email_address, display_name, username, imap_host, imap_port, imap_security, smtp_host, smtp_port, smtp_security, sent_folder, save_to_sent, quote_on_reply, poll_interval_seconds, initial_sync_days, signature_text, filters (skip_bulk, true by default, and skip_auto_replies, false by default), and the check state: last_polled_at, last_success_at, consecutive_failures, last_error_code. PATCH on the same path (the channel.update permission) changes harmless options only: display_name, signature_text, poll_interval_seconds (60-900), save_to_sent, quote_on_reply and filters.skip_bulk / filters.skip_auto_replies (send only the flags you want to change; the others are kept). An empty string in display_name or signature_text resets the value to null. The address, login, password and servers cannot be changed with PATCH: sending them returns 422 with a hint to use reconnect. A channel of another company and a channel that is not of type email answer the same way: 404.

The password, login and servers of an email channel are changed with POST /channels/{channel}/reconnect. password is required; username, imap_host, imap_port, smtp_host, smtp_port are optional (omitted ones are taken from the current settings). The mailbox address cannot be changed. The full mailbox check runs again with the same request budget as test-connection, and the previous secret is replaced only after a successful check. If the check fails, the 422 response carries data.code (the first failing side) and the imap / smtp report, and the previous password stays untouched; frequent failures return 429 email_test_rate_limited. After a successful update the channel resumes checking and letters that arrived during the downtime are fetched. When the server stops accepting the password the channel gets the expired status with last_error_code = "email_auth_failed"; when the server stays unreachable (10 failed checks in a row) the status becomes disconnected with last_error_code = "email_unreachable".

Endpoints

MethodPath
GET/v1/channels

List the company's channels (setup drafts excluded by default).

GET/v1/channels/{channel}

Get a channel by id along with its provider configuration.

POST/v1/channels

Create a channel (optionally starting pairing right away); channels are usually connected through the in-dashboard wizard.

POST/v1/channels/telegram-business/complete-signup

Connect Telegram Business: body `{ bot_token, name?, description?, timezone? }`. Checks the token and the bot's Business Mode, creates the channel in `pairing` and sets up the webhook. The channel becomes `connected` once the owner connects the bot in the Telegram app. Requires `channel.create`, the plan's channel limit applies, 10 requests per minute; answers `201`, refusals `422`/`409`/`502` with `error_code` (see "Telegram Business" above).

PATCH/v1/channels/{channel}

Update channel settings: name, description, account_name and the allow_broadcasts / allow_automations / allow_inbound_handling toggles, and through `config` also `call_handling` for incoming calls (see "What the channel does with an incoming WhatsApp call" above). The optional `owner_user_id` (int or null) names the employee whose number this is (null clears the owner); available on channels of any type; requires channel.update. A wrong employee returns 422 with the text in `errors.owner_user_id[0]`. POST and PUT on this path are accepted too.

GET/v1/channel-types

Reference list of channel types with their capabilities and providers.

POST/v1/channels/{channel}/connect/start

Start connecting a runtime channel (WhatsApp/Telegram via QR): body { "mode": "qr" | "code" | "phone", "phone_number"? }. Then poll GET /channels/{channel}/connection.

GET/v1/channels/{channel}/connection

Connection-state snapshot for polling a custom QR UI: connection.status (draft, qr_required, pairing_required, authorizing, 2fa_required, connected, reconnecting, expired, error, disconnected), a qr object, pairing_code (for code mode), and pairing_state; pairing_state is also pairing between the start of pairing and the first QR, so device_linked=true (only when pairing_state=pairing and the channel status is connecting) separately signals an already-scanned QR/entered code as opposed to still waiting for the scan.

POST/v1/channels/{channel}/auth-code

Submit an SMS code or a 2FA password to the runtime (Telegram Personal login).

POST/v1/channels/{channel}/disconnect

Disconnect a channel (stop sending/receiving messages).

POST/v1/channels/{channel}/reconnect

Reconnect a previously disconnected channel. The request body depends on the channel type: OAuth/bot channels require provider fields (Instagram/Threads/Facebook Messenger — { code }, WhatsApp Business — { authorization_code, waba_id, phone_number_id }, Telegram Bot — { bot_token }, Telegram Business — { bot_token } with the token optional), otherwise 422; WhatsApp/Telegram QR and Live Chat take an empty body. Requires channel:update; a foreign channel returns 404. Full per-type contract — in the partner documentation (channel-connection guide, "Direct API" section).

POST/v1/channels/{channel}/pause

Pause a channel (temporarily, without tearing down the session).

POST/v1/channels/{channel}/resume

Resume a paused channel.

DELETE/v1/channels/{channel}

Delete a channel (soft delete). By default (`keep_history` omitted or `true`) the conversation history is kept and the channel becomes archived — retrievable via GET /channels/archived; `keep_history=false` deletes the channel together with its history for good.

GET/v1/channels/archived

Archived channels — deleted with history kept (see "Archived channels" above). Not returned by the regular list; there is no parameter that adds them back.

DELETE/v1/channels/archived/{channelId}

Permanently and irreversibly delete an archived channel together with its history (runs as a background job).

GET/v1/channels/{channel}/logs

Channel logs (connection events, errors), paginated: `page`/`per_page` (`perPage` accepted too; max 500, default 200), sorted by `created_at` desc, then `id` desc.

GET/v1/channels/{channel}/whatsapp-calling

WhatsApp calling readiness (eligibility): Meta prerequisites and blocking issues. Meta/WABA channels only (otherwise 404); requires telephony-management rights (otherwise 403).

POST/v1/channels/{channel}/whatsapp-calling/enable

Enable WhatsApp calling for the number. Meta/WABA channels, telephony rights. Unmet prerequisites return HTTP 422 with ok:false and error_code.

POST/v1/channels/{channel}/whatsapp-calling/disable

Disable WhatsApp calling for the number. Meta/WABA channels, telephony rights.

POST/v1/channels/{channel}/whatsmeow-calling/enable

Switch on answering incoming calls for a QR-connected WhatsApp channel: `config.call_handling.mode` becomes `answer`. The body `{ "ban_risk_ack": true }` is mandatory (otherwise 422). Channels with `voice_calls_supported: true` only (otherwise 404) and telephony-management rights (otherwise 403); with no call capacity free the answer is 422 with `error: calls_instance_unavailable`.

POST/v1/channels/{channel}/whatsmeow-calling/disable

Switch off answering incoming calls: `mode` returns to `ignore` and the three ordinary `call_handling` values become settable through PATCH again. Same channel-type and rights restrictions.

GET/v1/channels/{channel}/whatsapp/profile

The WhatsApp number's profile: the avatar and the texts a contact sees on the business card. The response carries `capabilities` — the fields this particular channel supports — and `limits` with length ceilings; branch on those, not on the channel type. When the provider is unreachable, the last saved copy is returned with `stale: true`. Requires channel:view.

POST/v1/channels/{channel}/whatsapp/profile

Update the number's profile. Send only what changes; a field the channel does not support rejects the whole request with HTTP 422 and `field_not_supported`. The avatar goes as a multipart `picture` file (JPG/PNG/WebP, up to 5 MB, at least 192 px per side) — we crop it to a square and strip the capture metadata ourselves. `about` cannot be empty. Requires channel:update; the change rate is limited.

DELETE/v1/channels/{channel}/whatsapp/profile/picture

Clear the number's avatar. Not supported by every provider: check `capabilities.picture_remove` in the GET response — a WhatsApp Business photo can only be replaced. Requires channel:update.

GET/v1/channels/{channel}/whatsapp/blocked-users

List of numbers the provider has blocked on this WhatsApp channel (query: after, limit) — not AISAR's own blocked-contact list. WhatsApp channels only; requires channel:view.

GET/v1/channels/{channel}/access

Operator/team access rules for the channel.

POST/v1/channels/{channel}/access

Update channel access rules (allow/block per team or user); rules — up to 500 elements. PUT on this path is accepted too.

GET/v1/channels/{channel}/owner-candidates

Company employees who can be named as the number's owner (`owner_user_id`), each with a `has_access` flag saying whether the employee may use the channel. Requires channel.view; limited to 30 requests per minute.

GET/v1/channels/{channel}/templates/sendable

Templates available for this channel's in-chat picker. For WhatsApp Business — channel-bound templates with `status=published` and `meta_status=approved` (sent via POST /messages/send-template). For every other channel type — the company's published templates of the same channel_type (not bound to a channel, no Meta moderation); the frontend inserts their filled text into the chat input as a plain message rather than through the template-send API.

POST/v1/channels/email/test-connection

Email: check a mailbox over IMAP and SMTP without storing anything; the report { imap, smtp, sent_folder } carries normalized email_* codes.

POST/v1/channels/email/complete-signup

Email: repeat the check and create the channel (201); 409 mailbox_already_connected, 422 with the check report. Options: poll_interval_seconds, initial_sync_days, signature_text, save_to_sent.

GET/v1/channels/{channel}/email-settings

Email: mailbox settings without secrets and the check state (last_polled_at, last_success_at, consecutive_failures, last_error_code).

PATCH/v1/channels/{channel}/email-settings

Email: change the sender name, signature, check interval, saving to Sent, quoting and the bulk and auto-reply filters; the address, login, password and servers change only through reconnect.

Examples

List connected channels

Request

bash
curl -X GET "https://api.aisar.app/v1/channels?status=connected" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 3,
      "company_id": 1,
      "channel_type_id": 1,
      "provider_key": "whatsapp",
      "name": "Основной WhatsApp",
      "account_name": "+12025550187",
      "status": "connected",
      "setup_state": "ready",
      "voice_calls_supported": false,
      "calling_enabled": false,
      "messaging_enabled": true,
      "can_start_conversation": true,
      "status_message": null,
      "last_error_code": null,
      "allow_broadcasts": true,
      "allow_automations": true,
      "allow_inbound_handling": true,
      "connected_at": "2026-01-15T09:00:00Z",
      "last_inbound_at": "2026-02-09T18:00:00Z",
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" }
    }
  ]
}

Pause a channel

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/3/pause" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "message": "Channel status updated.",
    "channel": {
      "id": 3,
      "status": "paused",
      "status_changed_at": "2026-02-09T18:30:00Z"
    }
  }
}

Reconnect a channel (Instagram — with a body)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/9/reconnect" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "code": "AQB..." }'

Response

json
{
  "data": {
    "id": 9,
    "provider_key": "meta_instagram",
    "status": "connected",
    "setup_state": "ready",
    "status_message": null,
    "channel_type": { "id": 4, "key": "instagram_business", "name": "Instagram Business" }
  }
}

Connect Telegram Business

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/telegram-business/complete-signup" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "bot_token": "123456789:EXAMPLE_BOT_TOKEN_REDACTED",
    "name": "Telegram отдела продаж"
  }'

Response

json
{
  "data": {
    "id": 42,
    "company_id": 1,
    "channel_type_id": 12,
    "provider_key": "telegram_business",
    "provider_version": "1",
    "messaging_enabled": false,
    "can_start_conversation": false,
    "name": "Telegram отдела продаж",
    "account_name": "@example_sales_bot",
    "timezone": "UTC",
    "status": "connecting",
    "setup_state": "pairing",
    "status_message": "awaiting_business_connection",
    "limits": { "max_text_length": 4096, "reply_window_hours": 24 },
    "channel_type": { "id": 12, "key": "telegram_business", "name": "Telegram Business" }
  }
}

Refusal: Business Mode is off for the bot

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/telegram-business/complete-signup" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "bot_token": "123456789:EXAMPLE_BOT_TOKEN_REDACTED" }'

Response

json
{
  "message": "Business Mode is disabled for this bot. Turn it on in @BotFather: Bot Settings → Business Mode.",
  "code": "business_mode_disabled",
  "error_code": "business_mode_disabled"
}

Reconnect Telegram Business with the stored token

Request

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

Response

json
{
  "data": {
    "id": 42,
    "provider_key": "telegram_business",
    "name": "Анна Смирнова",
    "account_name": "@example_owner",
    "status": "connected",
    "setup_state": "ready",
    "status_message": null,
    "can_start_conversation": false,
    "config": {
      "telegram_business": {
        "owner_user_id": "100000001",
        "owner_username": "example_owner",
        "owner_name": "Анна Смирнова",
        "bot_username": "example_sales_bot",
        "rights": {
          "can_reply": true,
          "can_read_messages": true,
          "can_delete_sent_messages": false
        },
        "dangerous_rights": [],
        "personal_channel_identity_unknown": false
      }
    },
    "channel_type": { "id": 12, "key": "telegram_business", "name": "Telegram Business" }
  }
}

Rename a channel and toggle its flags

Request

bash
curl -X PATCH "https://api.aisar.app/v1/channels/3" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Отдел продаж — WhatsApp",
    "allow_broadcasts": false,
    "allow_automations": true
  }'

Response

json
{
  "data": {
    "message": "Channel updated.",
    "channel": {
      "id": 3,
      "name": "Отдел продаж — WhatsApp",
      "allow_broadcasts": false,
      "allow_automations": true,
      "allow_inbound_handling": true,
      "status": "connected",
      "setup_state": "ready"
    }
  }
}

Set whose number it is (owner_user_id)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/owner-candidates" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

curl -X PATCH "https://api.aisar.app/v1/channels/3" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "owner_user_id": 12 }'

Response

json
// GET owner-candidates
{
  "data": [
    { "id": 12, "name": "Адина", "lastname": "Калыбекова", "has_access": true }
  ]
}

// PATCH с сотрудником без доступа к каналу: 422
{
  "data": {
    "message": "У сотрудника нет доступа к этому каналу. Выдайте доступ на вкладке «Доступ» или выберите другого сотрудника.",
    "errors": {
      "owner_user_id": ["У сотрудника нет доступа к этому каналу. Выдайте доступ на вкладке «Доступ» или выберите другого сотрудника."]
    }
  }
}

// Чужой, несуществующий или сервисный id: "Сотрудник не найден в компании."
// Текст ошибки — на языке пользователя (ru, en или kk). "owner_user_id": null снимает владельца.

A channel with its provider config

Request

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

Response

json
{
  "data": {
    "channel": {
      "id": 3,
      "company_id": 1,
      "channel_type_id": 1,
      "provider_key": "whatsapp",
      "name": "Основной WhatsApp",
      "account_name": "+12025550100",
      "status": "connected",
      "setup_state": "ready",
      "voice_calls_supported": false,
      "calling_enabled": false,
      "messaging_enabled": true,
      "can_start_conversation": true,
      "owner_user_id": 12,
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" }
    },
    "provider_config": {
      "mark_online_on_connect": {
        "value": true,
        "label": "Отмечать онлайн при подключении",
        "description": "Показывать аккаунт онлайн, пока канал подключён"
      }
    }
  }
}

Create a channel and start pairing

Request

bash
curl -X POST "https://api.aisar.app/v1/channels" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_type_id": 1,
    "provider_key": "whatsapp",
    "name": "Sales WhatsApp",
    "account_name": "+12025550160",
    "start_pairing": true,
    "pairing_mode": "qr"
  }'

Response

json
{
  "data": {
    "message": "Channel created.",
    "channel": {
      "id": 7,
      "provider_key": "whatsapp",
      "status": "connecting",
      "setup_state": "pairing"
    },
    "pairing": {
      "mode": "qr",
      "request_id": "00000000-0000-0000-0000-000000000000"
    }
  }
}

Poll the connection state (QR)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/7/connection" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "channel_id": 7,
    "setup_state": "pairing",
    "connection": {
      "status": "qr_required",
      "mode": "qr",
      "qr": {
        "value": "2@EXAMPLE_QR_PAIRING_PAYLOAD_REDACTED",
        "expires_at": "2026-02-09T18:00:20Z",
        "expires_in_seconds": 18,
        "is_expired": false,
        "attempt": 1
      },
      "pairing_code": null,
      "pairing_code_expires_at": null,
      "pairing_state": "qr",
      "device_linked": false,
      "last_error_code": null,
      "last_error_message": null,
      "retry_count": null,
      "next_retry_at": null,
      "status_message": null,
      "updated_at": "2026-02-09T18:00:05Z"
    },
    "metrics": {}
  }
}

Submit an auth code (Telegram Personal)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/8/auth-code" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "12345",
    "phone_code_hash": "EXAMPLE_PHONE_CODE_HASH"
  }'

Response

json
{
  "data": {
    "message": "Auth code submitted.",
    "channel_id": 8
  }
}

Channel logs

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/logs" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "id": "1042",
      "level": "info",
      "code": "connection.ready",
      "message": "Channel connected",
      "context": {},
      "created_at": "2026-02-09T18:00:00Z"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 200, "total": 1 }
}

Channel logs — paging through the history

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/logs?per_page=50&page=2" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "id": "992",
      "level": "error",
      "code": "connection.lost",
      "message": "Runtime session dropped",
      "context": { "reason": "device_removed" },
      "created_at": "2026-02-08T21:14:07Z"
    }
  ],
  "meta": { "current_page": 2, "last_page": 3, "per_page": 50, "total": 121 }
}

Channel logs — a message send failure

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/logs" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "id": "1110",
      "level": "error",
      "code": "broadcast.preflight_blocked",
      "message": "There has been an error processing your payment method. (131042)",
      "context": {
        "broadcast_id": 90,
        "error_code": 131042,
        "possible_solution": "Please add a new payment method or contact your bank."
      },
      "created_at": "2026-09-09T08:15:44Z"
    },
    {
      "id": "1109",
      "level": "warning",
      "code": "message.status.orphan_failed",
      "message": "There has been an error processing your payment method. (131042)",
      "context": {
        "external_message_id": "wamid.HBgMNDQ3NzAwOTAwMTIzFQIAERgSOUM3MUUzOEZDQTIxOEVGNUJBAA",
        "recipient": "12025550102",
        "errors": [{ "code": 131042, "title": "There has been an error processing your payment method." }]
      },
      "created_at": "2026-09-09T08:15:41Z"
    },
    {
      "id": "1108",
      "level": "error",
      "code": "message.send.failed.rate_limit_hit",
      "message": "Too many messages sent, please wait before sending more",
      "context": {
        "message_id": 1042,
        "attempts": 3,
        "error_code": "rate_limit_hit",
        "response": "{\"status\":429,\"reason\":\"rate_limited\"}"
      },
      "created_at": "2026-08-26T10:02:11Z"
    },
    {
      "id": "1107",
      "level": "warning",
      "code": "message.status.failed_suppressed",
      "message": "Delivery failure receipt arrived after the message was already delivered",
      "context": {
        "message_id": 1039,
        "reason": "Message expired (131053)"
      },
      "created_at": "2026-08-26T09:47:03Z"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 200, "total": 4 }
}

List archived channels

Request

bash
# GET /channels никогда не возвращает архивные каналы, и параметра
# ?include_archived=1 не существует — если его передать, валидация молча
# его проигнорирует. Единственный способ увидеть архивный канал — этот список.
curl -X GET "https://api.aisar.app/v1/channels/archived" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 12,
      "name": "WhatsApp — старый номер",
      "account_name": "+12025550180",
      "status": "disconnected",
      "provider_key": "whatsapp",
      "setup_state": "ready",
      "archived_at": "2026-08-20T09:15:00.000000Z",
      "messages_count": 4213,
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" }
    }
  ]
}

Permanently delete an archived channel

Request

bash
curl -X DELETE "https://api.aisar.app/v1/channels/archived/12" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "message": "Archived channel purged."
  }
}

WhatsApp calling readiness (eligibility)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/whatsapp-calling" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "eligibility": {
      "calling": {
        "sip_status": "NOT_SET",
        "can_receive_call_sip": null,
        "active": false
      },
      "prerequisites": {
        "business_verified": false,
        "payment_ok": true,
        "name_approved": true,
        "number_registered": true
      },
      "blocking_issues": [
        {
          "key": "business_verification",
          "severity": "error",
          "title": "Business is not verified with Meta",
          "detail": "The business owner must complete Business Verification in Meta Business Manager.",
          "fix_url": "https://business.facebook.com/settings/security"
        }
      ],
      "can_enable": false,
      "name_status": "APPROVED",
      "checked_at": "2026-02-09T18:00:00Z"
    }
  }
}

Enable WhatsApp calling

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/3/whatsapp-calling/enable" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "ok": true,
    "error": null,
    "error_code": null,
    "outbound_trunk_provisioned": true,
    "eligibility": {
      "calling": {
        "sip_status": "ENABLED",
        "can_receive_call_sip": "AVAILABLE",
        "active": true
      },
      "prerequisites": {
        "business_verified": true,
        "payment_ok": true,
        "name_approved": true,
        "number_registered": true
      },
      "blocking_issues": [],
      "can_enable": true,
      "name_status": "APPROVED",
      "checked_at": "2026-02-09T18:05:00Z"
    }
  }
}

Channel types and providers catalog

Request

bash
curl -X GET "https://api.aisar.app/v1/channel-types?published=true" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": [
    {
      "id": 1,
      "key": "whatsapp",
      "name": "WhatsApp",
      "description": "WhatsApp через QR-сопряжение",
      "category": "messaging",
      "order_id": 1,
      "badge_label": "Popular",
      "badge_variant": "primary",
      "is_published": true,
      "capabilities": { "send_text": true, "send_media": true },
      "limits": {},
      "provider_config": {
        "mark_online_on_connect": {
          "value": true,
          "label": "Отмечать онлайн при подключении",
          "description": "Показывать аккаунт онлайн, пока канал подключён"
        }
      }
    }
  ]
}

Templates for the chat picker (non-WABA channel)

Request

bash
# Для WhatsApp/Telegram/Instagram и т.п. отдаются опубликованные шаблоны
# компании того же channel_type — без привязки к каналу (channel_id: null)
# и без модерации Meta (meta_status: null). Фронт вставляет body как готовый
# текст в поле ввода, а не отправляет через POST /messages/send-template.
curl -X GET "https://api.aisar.app/v1/channels/3/templates/sendable" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": [
    {
      "id": 58,
      "company_id": 1,
      "template_group_id": 58,
      "channel_type_id": 1,
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" },
      "channel": null,
      "name": "greeting",
      "slug": "greeting",
      "status": "published",
      "meta_status": null,
      "meta_status_reason": null,
      "last_synced_at": null,
      "language": "ru",
      "category": null,
      "body": "Здравствуйте, {{first_name}}! Чем можем помочь?",
      "parameters": [
        { "id": 101, "name": "first_name", "type": "text", "sample_value": "Иван", "position": 1 }
      ],
      "buttons": [],
      "uses_count": 12
    }
  ]
}

Blocked numbers (WhatsApp Business/Cloud)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/3/whatsapp/blocked-users?limit=50" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "numbers": ["+12025550179"],
    "next_cursor": "CURSOR_1"
  }
}

Blocked numbers (QR-based WhatsApp)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/7/whatsapp/blocked-users" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "numbers": ["12125550175@s.whatsapp.net"],
    "next_cursor": null
  }
}

Channel mutation unavailable to a family company (403)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/91/pause" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "message": "This area is not available for your account type.",
    "error": "company_type_forbidden"
  }
}

A team block is refused (422)

Request

bash
curl -X PUT "https://api.aisar.app/v1/channels/3/access" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "model_type": "team", "model_id": 12, "rule": "block" }
    ]
  }'

Response

json
{
  "data": {
    "message": "The given data was invalid.",
    "errors": {
      "rules.0.rule": ["A team can only be allowed on a channel. To keep someone out, block that user personally."]
    }
  }
}

Only an admin may change channel access (403)

Request

bash
curl -X PUT "https://api.aisar.app/v1/channels/3/access" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "model_type": "team", "model_id": 12, "rule": "allow" }
    ]
  }'

Response

json
{
  "data": {
    "message": "Only a company admin can change channel access or team membership while team access rules are enforced.",
    "code": "channel_access_admin_only"
  }
}

Connecting WhatsApp Business with a team picker

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/meta/complete-signup" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "AQB...",
    "name": "Продажи — WhatsApp",
    "allowed_team_ids": [12, 14]
  }'

Response

json
{
  "data": {
    "id": 91,
    "channel_type": "whatsapp_business",
    "status": "connected",
    "name": "Продажи — WhatsApp"
  }
}

Email: check a mailbox (test-connection)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/email/test-connection" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "imap_smtp",
    "email": "zakaz@example.com",
    "username": "zakaz@example.com",
    "password": "app-password-from-the-provider",
    "imap_host": "imap.example.com",
    "imap_port": 993,
    "smtp_host": "smtp.example.com",
    "smtp_port": 465
  }'

Response

json
{
  "data": {
    "imap": { "ok": true, "code": null },
    "smtp": { "ok": false, "code": "email_auth_failed" },
    "sent_folder": "Sent"
  }
}

Email: create the channel (complete-signup)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/email/complete-signup" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "imap_smtp",
    "email": "zakaz@example.com",
    "username": "zakaz@example.com",
    "password": "app-password-from-the-provider",
    "imap_host": "imap.example.com",
    "imap_port": 993,
    "smtp_host": "smtp.example.com",
    "smtp_port": 465,
    "display_name": "Example, отдел заказов",
    "signature_text": "С уважением, отдел заказов",
    "save_to_sent": true,
    "initial_sync_days": 3,
    "poll_interval_seconds": 180
  }'

Response

json
{
  "data": {
    "id": 41,
    "company_id": 1,
    "provider_key": "imap_smtp",
    "name": "zakaz@example.com",
    "account_name": "zakaz@example.com",
    "status": "connected",
    "setup_state": "ready",
    "channel_type": { "id": 14, "key": "email", "name": "Email" }
  }
}

Email: mailbox already connected (409) and a failed check (422)

Request

bash
# 409: этот адрес уже подключён в компании
# 422: пароль не принят, тело ответа ниже

Response

json
{
  "data": {
    "message": "The mailbox check failed.",
    "code": "email_auth_failed",
    "imap": { "ok": false, "code": "email_auth_failed" },
    "smtp": { "ok": true, "code": null },
    "sent_folder": null
  }
}

Email: mailbox settings (GET email-settings)

Request

bash
curl -X GET "https://api.aisar.app/v1/channels/41/email-settings" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Response

json
{
  "data": {
    "channel_id": 41,
    "provider": "imap_smtp",
    "email_address": "zakaz@example.com",
    "display_name": "Example, отдел заказов",
    "username": "zakaz@example.com",
    "imap_host": "imap.example.com",
    "imap_port": 993,
    "imap_security": "ssl",
    "smtp_host": "smtp.example.com",
    "smtp_port": 465,
    "smtp_security": "ssl",
    "sent_folder": "Sent",
    "save_to_sent": true,
    "quote_on_reply": true,
    "poll_interval_seconds": 180,
    "initial_sync_days": 3,
    "signature_text": "С уважением, отдел заказов",
    "filters": { "skip_bulk": true, "skip_auto_replies": false },
    "last_polled_at": "2026-10-09T12:00:00+00:00",
    "last_success_at": "2026-10-09T12:00:00+00:00",
    "consecutive_failures": 0,
    "last_error_code": null
  }
}

Email: change mailbox settings (PATCH email-settings)

Request

bash
curl -X PATCH "https://api.aisar.app/v1/channels/41/email-settings" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "poll_interval_seconds": 120, "filters": { "skip_auto_replies": true } }'

Response

json
{
  "data": {
    "channel_id": 41,
    "poll_interval_seconds": 120,
    "filters": { "skip_bulk": true, "skip_auto_replies": true }
  }
}

Email: update the password (reconnect)

Request

bash
curl -X POST "https://api.aisar.app/v1/channels/41/reconnect" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "password": "new-app-password" }'

Response

json
{
  "data": {
    "id": 41,
    "status": "connected",
    "provider_key": "imap_smtp"
  }
}