Messages
Messages are the core unit of communication inside a thread. The API lets you read message history, search by text, send new outbound messages (text, attachments, voice notes), edit or delete an operator's recent messages, add reactions, and track delivery statuses.
Entry points and idempotency
POST /messages/send and POST /messages/send-template are the most common entry points — they resolve or create the right thread from a channel + recipient pair automatically and generate the idempotency key themselves (they do not accept a client-supplied body idempotency_key). Client-key idempotency is available on the direct POST /messages and on POST /messages/{message}/forward: pass an idempotency_key (up to 100 chars). The key may not start with a prefix reserved for the platform's own server-side integrations (b24-ol:, b24robot:, amo-chat:) — such a value returns 422. A retried request with the same key returns the same message only when the retry has the same thread and the same author AND that message is visible to the caller (the same rule GET /messages/{id} uses); in every other case — the key is already held by a message of a different thread, a different author, or one hidden from you by channel-access rules — the response is 409 {"message": "...", "error_code": "idempotency_key_conflict"}, without any data about the existing message. The key is optional: without it, the server generates a random one on every call — retries then do NOT collapse, each one creates and delivers a separate message, so a client with automatic network retries should pass the key explicitly. The string "0" is an ordinary valid key value, not an empty one.
Separately, POST /messages/send and POST /messages/send-template accept an optional Idempotency-Key HTTP header (up to 128 chars) — a middleware-level mechanism unrelated to the body idempotency_key parameter above. A retried request with the same header value (scoped by key + endpoint + user) within 24 hours does not re-send the message and instead returns the cached response from the first request; reusing the key with a different request body returns 422. The header is optional — omitting it leaves the endpoints' behavior unchanged; it exists to protect clients with automatic network retries (e.g. the mobile app after a dropped connection).
Threads of deleted channels (channel_archived)
Without channel_id, the direct POST /messages sends through the thread's channel. If that channel has been deleted (including deleted with history kept: the correspondence stays readable but nothing can be sent through it), since 2026-10-12 the request is refused immediately with 422 {"message": "...", "error_code": "channel_archived"} and no message is created. Previously such a message was stored and then failed with reason no_runtime_instance. To reply to such a contact, pass channel_id of an active channel of a compatible type in the same request (for example, another WhatsApp number): the message goes out through it into that channel's thread with the same contact, which is created if needed. POST /messages/{message}/forward returns the same channel_archived code when the target thread belongs to a deleted channel. Active channels are listed by GET /channels.
Allowed message types (message_type)
The direct POST /messages accepts message_type only from a closed list: text, image, video, audio, voice, document, file, sticker, media, location, contact — any other value (including template) returns 422 in data.errors.message_type. template does still occur on inbound messages: that is how business templates sent to a personal WhatsApp channel arrive. Templates are sent exclusively through POST /messages/send-template — the direct POST /messages does not accept them, since a plain string message_content cannot carry a template name and its parameters. Service message types (an internal note, a call card, a call-permission request, system and interactive messages) are created only by the platform itself, through its own internal handlers, never through this endpoint. When message_type is omitted or equals text and the request carries attachments, the message type is instead taken from attachments[0].type — so the attachment automatically determines that the message is actually image/document/sticker/etc., not plain text.
Since 2026-10-07 every items[] entry of GET /messages/search carries three more fields: contact_id (the thread contact id or null), contact_photo (the contact photo URL or null) and is_group (boolean: the thread or the contact account is a group chat). They let a result be rendered as a row with an avatar without a contact lookup.
Since 2026-10-07 the message object (MessageResource) contains deleted_via and deleted_at. deleted_via says who or what deleted the message: contact (the contact deleted it on their side), revoke (an operator recalled it for everyone), local (hidden in AISAR only), system, ai_agent, safety, or null when the message is not deleted; for older deleted inbound messages with no recorded mode it is contact. deleted_at is the ISO-8601 deletion time or null.
is_deleted on message creation only accepts false (including implicitly, when the field is omitted) — true returns 422: a message cannot be born already deleted, use DELETE /messages/{message} to delete one.
Channel access (since 2026-09-26)
A message by id (GET /messages/{id}, edit, delete, statuses, attachments, reactions, comment like) on a channel that channel_access rules close to the user answers 404, like a message of another company. Sending through such a channel is refused exactly like a channel of another company: POST /messages/send and POST /messages/send-template — 422 "Invalid channel for current company.", the direct POST /messages — 422 "Invalid thread for current company." / "Invalid channel for current company.", and quoting an unreachable message — "Invalid quoted message for current company.". For calls the same refusal is a 404 (see the Calls group).
Outbound message recipient
The direct POST /messages only creates outbound messages: direction is a required field and only accepts outbound — any other value returns 422. The recipient of an outbound message is always the contact account of the thread the message ends up in. If recipient_contact_account_id names a DIFFERENT account of the SAME conversation contact (a second number of the same customer, for example), the message is rerouted to that account's own thread — an existing one, or one created on the fly for the same conversation+channel pair; an account of a DIFFERENT contact still returns 422 "Invalid contact account for current company.". The external_thread_id field in the request body is ignored — the value that ends up on the message always comes from the thread it actually landed in, not from the request body.
external_message_id is the provider-assigned id the server writes back only AFTER a successful send — a client never sets it. On the direct POST /messages, any non-empty value for this field returns 422 (an empty or omitted one is not an error); on POST /messages/{message}/forward the field is not read from the request body at all, so a value sent there simply has no effect. Any outbound message created through the API via either of these two routes always gets an author (author_user_id — the calling user) and is queued for delivery — an "authorless echo" cannot be created through them.
Contact blocking
When the recipient is a blocked contact (blocked_at set; blocked via POST /contacts/{contact}/block, see the "Contacts" group), an outbound message is refused before it is created: POST /messages/send, the direct POST /messages, and POST /messages/{message}/forward all return 422 {"message": "This contact is blocked; the message was not sent.", "error_code": "contact_blocked"}. BOTH contacts are checked: the target thread's own contact (conversation_id; resolved automatically for /messages/send and /forward) AND the actual recipient's contact when it differs (see "Outbound message recipient" above, recipient_contact_account_id) — either one being blocked is enough to refuse the send.
Channels that cannot write first
A channel with can_start_conversation: false (currently only Telegram Business, see the "Channels" group) only replies to people who wrote first and does not open a conversation with a contact this channel has no thread with yet. Such a request is refused before any message is created, with 422 and error_code: channel_cannot_start_conversation. For POST /messages/send the body is wrapped: {"data": {"message": "…", "error_code": "channel_cannot_start_conversation"}}, and a contact created for the refused call is not kept. A cross-channel POST /messages/{message}/forward answers with the flat {"message": "…", "error_code": "channel_cannot_start_conversation"}, like its other refusals. The direct POST /messages refuses with the same flat body when channel_id or recipient_contact_account_id would move the message into a new thread of such a channel. POST /dialogs and thread creation through POST /conversations/{conversation}/threads give the same refusal (see the "Conversations" group). A thread counts as existing if the channel has a thread with the same contact account in any conversation of the company, so a reply after the previous conversation was closed goes through.
Origin flags of Telegram Business messages
A message's metadata object in REST responses contains three boolean origin flags. They are true only on Telegram Business channel messages and always false on other channels. sent_from_device marks an outgoing message the account owner typed in the Telegram app itself rather than in AISAR. sent_by_business_bot marks an outgoing message the channel's bot sent on behalf of the account. is_from_offline marks an automatic message from Telegram itself: a greeting or away message set up in Telegram Business. These flags tell replies from the phone and Telegram's auto-replies apart from operator replies sent from AISAR. The message.* webhook events do not carry them. The keys are reserved: sent by a client in the metadata of a direct POST /messages, they are silently stripped (see "Reserved metadata keys" below), so the marks cannot be forged.
Reserved metadata keys (since 2026-09-27)
The direct POST /messages and POST /conversations/{conversation}/notes (see the "Conversations" group) accept an arbitrary metadata object — an integrator's own keys (order_id, a CRM record reference, etc.) are stored as-is. Some key names are used by the server itself as an internal channel (for example raw_payload, pending_echo, echo_external_ids, is_history_sync, bitrix24, amocrm, is_ai_response, ai_agent_instance_id, voice_call_id, mentions and other dedup/attribution markers) — since 2026-09-25/27 (a security review against SSRF via metadata.raw_payload) such keys are silently stripped from the request body before validation and never reach the stored message or note; the request itself is not rejected and returns its normal success response. The reserved-name list is a denylist, not a whitelist — it is not published in full and may grow.
The my_ai_rating field (good, bad or null) is present on messages in conversation lists and the conversation card: the rating of the current user on an AI agent answer; set and cleared through POST/DELETE /conversations/{conversation}/messages/{message}/ai-rating (see the "AI agents and voice" section of the MCP guide). It is absent from the message WebSocket broadcast: the channel is shared by the whole company while the rating is personal.
Listing and pagination
GET /messages filters by conversation_id/thread_id/channel_id/direction/message_type (for the canonical list of message_type values, see the message.type field of the message.created event in the webhook events reference) and paginates either page-by-page (page/perPage) or by cursor — pass before_message_id to walk older messages (the scroll-up direction of a chat history) or after_message_id for newer ones; the legacy aliases before_id/after_id, limit (=perPage) and order (=sortDir) are also accepted, sortBy is one of id|timestamp|created_at|updated_at (default timestamp) and in cursor mode the response keeps the {items, pagination} shape but returns total/lastPage as null (the COUNT is skipped).
cursor (since 2026-09-23) is an opaque history cursor and the recommended way to page through a long history. Every GET /messages response carries data.pagination.next_cursor (the same value is repeated in data.meta.next_cursor): a string for the next page, or null when there are no more messages. Pass it as cursor in the next request with the same filters and the same sortDir, without page. The cursor is keyed on the very order the list is sorted by, COALESCE(timestamp, created_at) plus id, so unlike before_message_id/after_message_id it does not cut short a history whose ids do not follow time (imported correspondence, for example: large ids on old messages). The cursor is issued and accepted only with sortBy=timestamp (the default); with any other sortBy, next_cursor is null and a supplied cursor returns 422. cursor cannot be combined with page, before_message_id or after_message_id (422); a string the API did not issue also returns 422. Do not parse the cursor, its format may change. A page fetched by cursor runs in cursor mode: total/lastPage are null. The existing page, before_message_id and after_message_id parameters keep working as before.
GET /messages also supports a text filter search (the same ILIKE over message_content/external_message_id/external_thread_id) and its official alias q (since 2026-08-20): if only q is sent, its value is copied into search; if both are sent, the explicit search wins. Before that date an unrecognized q here was silently dropped — the request looked like it worked but found nothing; prefer the explicit search name for new integrations.
Search (`GET /messages/search`)
GET /messages/search — the required q (string, 2 to 255 chars) searches message text (ILIKE over message_content). conversation_id is optional (since 2026-08-20): given, the search runs inside that one conversation (a conversation from another company, a non-existent one, one closed by channel-access rules, or one lying entirely on a channel hidden from an 18+ child's family, is 404); omitted, the search runs company-wide, scoped to the channels the user can access, and every result then carries its own conversation_id — this is how you find a conversation that otherwise is not visible in any list. Results are sorted newest first (created_at desc). Pagination is page/perPage (5–100, default 20). The response is { "data": { items[], pagination{page,perPage,total,lastPage,has_more}, total_matches } }, where each items[] is {message_id, conversation_id, thread_id, created_at, snippet, query, contact_name, channel_id, channel_type}; the last three fields (since 2026-08-20) are contact_name (the thread's contact readable name, string or null), channel_id, and channel_type (the channel type key, e.g. whatsapp/telegram, or null) — added so a company-wide hit can be rendered as a standalone row without an extra contact/channel lookup. snippet (since 2026-08-22) is a text window centered on the MATCH itself (case-insensitive, up to ~240 chars, with ... at whichever edge is truncated), not the first characters of the message — before that date the snippet always started at the beginning of the message, so a match late in a long message could be invisible; when there is no textual match (e.g. the hit came from another field, or the message is media with no caption), the previous behavior applies (the first ~240 chars). The field's format is unchanged — only its content improved. The route carries a heavier rate limit (throttle:api-token-heavy, 120 requests/min both for programmatic tokens and — since 2026-08-22 — for regular session-authenticated web-client users; session clients previously had no limit at all here).
with_total (optional boolean, defaults to true, since 2026-09-05) controls whether pagination runs an exact COUNT(*). By default (or with an explicit with_total=1) nothing changes: total and lastPage in the response are exact. With with_total=0 the request switches to a count-free pagination mode — total and lastPage come back null, and the COUNT(*) itself (up to ~3.4s of a ~3.7s response on a large company) is skipped entirely. The has_more field (boolean) is always present regardless of with_total, and it is the only reliable "is there a next page" signal — do not rely on the "the page came back full" heuristic: it false-positives whenever the number of matching messages 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 search returns no messages of contacts hidden from the working inbox (POST /v1/contacts/{contact}/hide-from-inbox) — neither company-wide nor with an explicit conversation_id. Such a conversation can still be read directly: GET /messages?conversation_id=....
Quoted replies
To send a quoted reply, use the direct POST /messages with quoted_message_id (the internal id of the quoted message) or quoted_external_message_id — /messages/send does not accept it.
Ad attribution (referral)
The message object (GET /messages, /messages/{message}, /messages/{message}/around) carries a referral field — the same ad-attribution data as the message.created webhook (its data.referral object), just without the event envelope. null for regular messages; populated on the first message of a conversation started from an ad, only for the whatsapp/whatsapp_business (Click-to-WhatsApp) and instagram_business (click-to-Direct ads) channels. The field set depends on the channel: WhatsApp supplies source_url, ctwa_clid, body, media_type; Instagram supplies post_id and ref. Common to both: source_type, source_id, headline, image_url, video_url. See the webhook events reference (message.created event) for the full per-field description.
| Field | Description |
|---|---|
referral.source_url | URL of the ad or post; WhatsApp only |
referral.source_type | ad or post |
referral.source_id | Ad/post ID (ad_id for Instagram) |
referral.headline | Ad headline text |
referral.body | Ad body text; WhatsApp only |
referral.ctwa_clid | Click tracking ID for precise attribution; WhatsApp only |
referral.media_type | image or video; WhatsApp only |
referral.image_url | URL of the ad image (if applicable) |
referral.video_url | URL of the ad video (if applicable) |
referral.post_id | ID of the post the ad ran from; Instagram only |
referral.ref | Deep-link label (ig.me/...?ref=) when set; Instagram only |
Attachments
Attachments are sent in two steps: first upload the file through POST /uploads (up to 64 MB — image, video, audio, document), then reference the returned media_id/storage_path when creating the message or via POST /messages/{message}/attachments; the source is validated and must belong to your company, otherwise a flat 422 {error_code: attachment_source_invalid} is returned (re-upload).
Inbound attachments first carry the provider's runtime/CDN url, which can expire within hours (e.g. Instagram) — call POST /messages/{message}/attachments/{attachment}/fetch to pull the bytes into local storage, after which the attachment exposes a stable signed url and GET .../attachments/{attachment}/media streams those bytes.
fetch also works for Instagram-channel (meta_instagram) attachments — ig_post/ig_reel/story attachments are resolved through the Graph API/CDN the same way the webhook handler does when the message first arrives. For attachments that have no downloadable media at all (type is template, ephemeral, or unsupported_type — a generic-template card, a self-destructing attachment, or an unrecognized one), fetch immediately returns 422 {message: "...", error_code: "attachment_not_fetchable"} instead of eventually surfacing a 502 — calling fetch on these types is pointless, they never get a storage_path.
thumbnail_url (since 2026-08-24) is now populated automatically for locally-stored image attachments, except image/svg+xml (not rasterizable, and not served inline for security reasons) — it is the same signed .../media route with a w=640 query param, returning a webp derivative instead of the original. The field used to be almost always null; a provider-supplied thumbnail_url (e.g. from Instagram's lookaside CDN), when already present, takes priority and is not overridden. On GET .../attachments/{attachment}/media, the w param only accepts 256 or 640 (a closed whitelist) — any other value, or no w at all, returns the original as before; w is ignored for non-images. As of the same date, MIME types unsafe to render inline (svg, html, xml, txt, etc.) are served as Content-Type: application/octet-stream with Content-Disposition: attachment instead of inline — a guard against XSS via a message with an attacker-declared mime_type on the session-cookie origin. The route carries a throttle:signed-media rate limit (2000 requests/minute per IP).
Editing and deleting
Editing (PATCH) and deletion (DELETE) are bounded by channel windows: gate the edit and revoke-for-everyone controls on the pre-computed per-message can_edit_until / can_revoke_until fields instead of re-deriving per-channel rules — each is an ISO-8601 deadline while the action is allowed, or null when it is not (inbound, already-deleted, non-text for edit, a channel that does not support the action such as whatsapp_business for revoke, or an expired window).
DELETE soft-deletes by default (nulls the text); mode=revoke also revokes it for the recipient where the channel supports provider-side deletion (delete_scope='provider': QR-based WhatsApp, Telegram, Instagram) — WhatsApp Cloud (whatsapp_business) cannot revoke and returns 422. Send mode in the JSON body rather than the query string: some proxies drop query params on DELETE and would silently downgrade a revoke to a local delete.
Marking an inbound message read
POST /messages/{message}/statuses records a delivery status for a message; for a client token (session, PAT, embed) it only accepts status_name: "read" — any other value returns 422 explaining that delivery statuses (queued/sent/delivered/failed) can only be set by the channel connector itself. A status can only be recorded on an INBOUND message (direction: "inbound") — trying to mark your own outbound message as read returns the same 422; a message the caller cannot reach (another company, a channel closed by access rules, a dialog entirely on a channel hidden from an 18+ family) returns 404 — checked before the status rule. The optional status_timestamp may not name a moment in the future (a 5-minute clock-skew allowance, the same one the thread's last_message_at uses) and is stored in UTC. The external_message_id and status_details fields belong to the provider connector — a client may not send them (422, prohibited). The optional payload must be a flat object: at most 20 keys, each key name up to 100 characters, each value a string up to 1000 characters, a number, a bool, or null; it is only stored (neither the webhook nor the response reads it back) — it never unpacks into anything, it just sits in the database. The route is capped at a named rate limit of 60 requests per minute per user (429 when exceeded).
Reactions
POST /messages/{message}/reactions takes {reaction: "<emoji>"} (required, up to 32 chars) and reacts on behalf of the company (the author defaults to the authenticated user; explicit author_user_id/author_contact_account_id take priority and are validated against the company) — a channel allows one outbound reaction per message, so a new call replaces the previous one. DELETE /messages/{message}/reactions removes the company's outbound reaction from a message — no reaction id is needed (a channel carries at most one), and the call is idempotent (200 even when there is nothing to remove). Both actions broadcast the message.reaction.updated webhook (action: "added" / "removed").
Forwarding
POST /messages/{message}/forward performs a copy-send: it creates a new outbound message with the same content and attachments, either into an existing thread (target_thread_id) or cross-channel by contact (target_contact_account_id, optionally with an explicit target_channel_id); only a message with content to copy is forwarded — a message_type of text, image, video, audio, voice, document, file, sticker, location, or contact. A service message (an internal note, a call card, a call-permission request, a system or interactive message) or a media message cannot be forwarded — it returns 422 message_not_forwardable. The optional idempotency_key follows the same rules as on the direct POST /messages (see "Entry points and idempotency" above): a reserved server prefix returns 422; a retry with the same key, same thread/author and a visible message re-returns the same forward, otherwise 409 idempotency_key_conflict. On refusal it returns 422 with an error_code from the set message_deleted, message_not_forwardable, channel_not_textable, content_type_unsupported, wa_window_closed, channel_incompatible, channel_not_connected, no_compatible_channel, channel_required, channel_cannot_start_conversation (the channel cannot write first — see "Channels that cannot write first" above), contact_blocked (the recipient is blocked — see "Contact blocking" above), attachment_not_available (since 2026-09-27: the source message's attachment had not been downloaded into local storage yet, and fetching it on the fly before forwarding failed — the message is not created at all, unlike the previous behavior of silently forwarding without the media). Unlike the direct POST /messages, this route never reads external_message_id from the request body at all — a value sent there simply has no effect (not an error, but not stored either).
Voice notes
Voice notes are uploaded through a dedicated path POST /uploads/voice: it accepts a recorded audio or video file (multipart file field, up to 32 MB, mimetypes webm/ogg/mp4/mpeg/wav/aac/m4a) and transcodes it to voice-OGG (Opus), returning a flat object with media_id/storage_path in the same shape as POST /uploads — reference that media_id when sending and the recipient sees a proper voice note (ptt) rather than a plain audio file; a transcoding failure returns 422.
Sending a sticker
A sticker from the company's library (see the "Stickers" group) is sent as a regular attachment, but via attachments[].sticker_id instead of attachments[].media_id — reference the sticker's id from GET /v1/sticker-packs. Each attachments[] row must carry exactly one of the two fields — both at once, or neither, returns 422. You do not need to set attachments[].type for a sticker — it is set automatically (sticker); POST /v1/messages/send and the direct POST /v1/messages accept sticker_id the same way.
Liking an Instagram comment
POST /messages/{message}/like likes a comment: it works only for Instagram Business comment threads (thread.is_group + a channel of type instagram_business) and requires the message to have an external id, otherwise 422; on success it returns a flat { "message": "Comment liked." }.
Temporary restriction of an instagram account
For a channel of type instagram (personal account) POST /messages may answer 409 with a flat body (no data wrapper): { "message": "Instagram has temporarily restricted this account.", "error_code": "ig_frozen", "frozen_until": "2026-10-06T10:00:00Z" } and a Retry-After header (seconds until the restriction ends). Instagram has temporarily restricted the account: retry the send after frozen_until. The same window is visible in the channel state.
Live-operator reply on Instagram and Messenger: human_agent
For Instagram (Meta API) and Facebook Messenger the regular reply window is 24 hours after the customer's last message. A live operator's reply can be sent within 7 days (Meta's HUMAN_AGENT tag). The optional boolean human_agent field on POST /messages and POST /messages/{message}/forward states that a person typed the message. Users of the web app, the mobile app and the embedded chat are treated as humans automatically and do not send the field. Integrations using an API token (and the server-side embed token) must pass human_agent: true explicitly, and only when a person really wrote the message; sending it for bots or automation is forbidden (Meta policy, abuse can get the feature disabled). The human_agent and human_agent_source keys in metadata are reserved by the server: values supplied by a client are discarded. On forward, the mark is applied only when the source message is a manual operator reply. Operator replies from Bitrix24 Open Lines and amoCRM chats get the mark only when the CRM user is mapped to an AISAR user in the integration settings: by mapping a user, the company confirms that a person, not a robot or business process, writes under that account. The feature is switched on by a platform-side flag and may still be off, in which case the regular 24-hour window applies. For the conversation's window state see meta_window_state in the "Conversations" group.
Email messages
Every message object carries an email field: for letters of the email channel it is a block with the letter envelope, for every other message it is null. The block fields: subject (up to 998 characters, null when there is none), from ({ address, name }), to, cc, reply_to (lists of { address, name }; the realtime representation of a message carries at most 5 addresses per list, the regular one up to 20), sender_auth (pass, fail or unknown: whether the mail server confirmed the sender via DMARC), is_automated (an automated letter), auto_class (the class of the letter: bounce, auto_reply, bulk, self, spam or human), bounce ({ status, diagnostic_code } for a non-delivery report; diagnostic_code is returned only when it is at most 200 characters), stub (too_large for a letter over 25 MB and unprocessable for a letter that could not be parsed: such messages have no body, the chat shows a placeholder), has_html, has_quoted, attachments_skipped (how many attachments were not loaded beyond the limit of 50 per letter). The letter body and the Message-ID, References and Authentication-Results headers are not part of the block. The AI agent, automations and auto-replies do not fire on letters whose auto_class is not human or whose sender_auth is fail.
Sending a letter: a regular POST /messages/send or POST /messages into a thread of an email channel. Of metadata.email a client may set only subject (a single line, up to 255 characters); everything else (message_id, in_reply_to, references, bounce, class, etc.) is written by the server and anything passed is silently dropped, so a letter chain cannot be forged. A reply needs no subject: the server sets "Re: …" from the original subject, and the reply stays in the same thread and goes to the sender only (Cc addresses are not added). For the first letter to a contact (the thread has no letters yet) metadata.email.subject is required: otherwise 422 on the metadata.email.subject field. The mailbox signature is appended to the outgoing letter. Such a thread can be created with POST /conversations/{conversation}/threads and contact_email_id (the "Conversations" group). Delivery failures arrive as a message status with a code: email_auth_failed (the server rejected the password, the channel moves to expired), email_host_blocked, email_message_too_large, email_attachment_too_large, email_recipient_invalid, email_header_invalid, email_subject_required, email_bounced (the recipient server returned the letter), email_auto_rate_capped (the automatic-letter limit of the mailbox is exceeded; staff replies are not limited).
GET /messages/{message}/email-body returns the whole letter: data = { html, text, has_remote_images, has_quoted_text, html_too_complex, cid_map }. html is sanitized HTML (scripts, forms and active content removed) or null, text is the plain-text version. External images are switched off: in the HTML they stay as data-remote-src pointing at the proxy GET /email/remote-image?t=... (a signed link bound to the user and the message, valid for 10 minutes). Inline cid: images are data-cid, and cid_map maps a content id to a short-lived signed attachment link. html_too_complex: true means the letter is too complex to render with its layout and text should be shown. Access is the same as reading a message by id (the conversation.view permission, channel and thread visibility); an inaccessible message and a message without a stored letter answer the same way: 404.
GET /email/remote-image?t=... returns the bytes of an image from a letter through the AISAR proxy, so the sender does not see the reader's address. The t token comes only from email-body; on every call the signature and expiry of the token are re-checked, along with that it was issued to this very user, that the message is visible to them and that the image address is present in the stored letter (otherwise 403 or 404). A non-image, an oversized file or an unreachable address produce an error with data.code: email_image_not_an_image, email_image_too_large, email_image_blocked, email_image_unreachable, email_image_upstream_error. A user can run at most 4 downloads at a time (429 email_image_busy with Retry-After), and the limit of 120 requests per minute per user and 600 per company applies as well. The response carries the headers Content-Security-Policy: default-src 'none'; sandbox, X-Content-Type-Options: nosniff and Cache-Control: private, max-age=600.
Endpoints
| Method | Path | Summary |
|---|---|---|
| POST | /v1/messages/sendSend a message by channel and recipient (creates the thread if needed). | Send a message by channel and recipient (creates the thread if needed). |
| POST | /v1/messages/send-templateSend an approved WhatsApp template by channel and recipient (or thread_id). | Send an approved WhatsApp template by channel and recipient (or thread_id). |
| GET | /v1/messagesList messages with filters (conversation/thread/channel/direction) and page- or cursor-based (cursor from next_cursor, before_message_id/after_message_id) pagination. | List messages with filters (conversation/thread/channel/direction) and page- or cursor-based (cursor from next_cursor, before_message_id/after_message_id) pagination. |
| GET | /v1/messages/searchFull-text message search (`conversation_id` is optional — omit it to search the whole company). | Full-text message search (`conversation_id` is optional — omit it to search the whole company). |
| POST | /v1/messagesCreate a message directly, including a quoted reply via quoted_message_id (usually /messages/send is used instead). | Create a message directly, including a quoted reply via quoted_message_id (usually /messages/send is used instead). |
| GET | /v1/messages/{message}Get a message by id. | Get a message by id. |
| PATCH | /v1/messages/{message}Edit the text of your own outbound message (within the channel's edit window). | Edit the text of your own outbound message (within the channel's edit window). |
| DELETE | /v1/messages/{message}Delete a message (soft delete; mode=revoke in the JSON body revokes it for the recipient where the channel supports it, except whatsapp_business). | Delete a message (soft delete; mode=revoke in the JSON body revokes it for the recipient where the channel supports it, except whatsapp_business). |
| GET | /v1/messages/{message}/aroundA window of messages around a given message (for jump-to-context navigation); since 2026-08-22 under the same heavier rate limit as /messages/search (throttle:api-token-heavy). | A window of messages around a given message (for jump-to-context navigation); since 2026-08-22 under the same heavier rate limit as /messages/search (throttle:api-token-heavy). |
| GET | /v1/messages/{message}/attachmentsList a message's attachments. | List a message's attachments. |
| POST | /v1/messages/{message}/attachmentsAttach a file to an already-created message. | Attach a file to an already-created message. |
| GET | /v1/messages/{message}/statusesDelivery status history (sent/delivered/read/failed). | Delivery status history (sent/delivered/read/failed). |
| POST | /v1/messages/{message}/statusesRecord a delivery status for a message. | Record a delivery status for a message. |
| GET | /v1/messages/{message}/reactionsList emoji reactions on a message. | List emoji reactions on a message. |
| POST | /v1/messages/{message}/reactionsAdd a reaction {reaction:"<emoji>"} to a message on behalf of the company (one outbound reaction per message — a new one replaces the previous). | Add a reaction {reaction:"<emoji>"} to a message on behalf of the company (one outbound reaction per message — a new one replaces the previous). |
| DELETE | /v1/messages/{message}/reactionsRemove the company's outbound reaction from a message (idempotent; no reaction id needed — one per message). | Remove the company's outbound reaction from a message (idempotent; no reaction id needed — one per message). |
| POST | /v1/messages/{message}/likeLike an Instagram comment (Instagram Business comment threads only; the message must have an external id). | Like an Instagram comment (Instagram Business comment threads only; the message must have an external id). |
| POST | /v1/messages/{message}/forwardForward a message (copy-send) into an existing thread or cross-channel by contact. | Forward a message (copy-send) into an existing thread or cross-channel by contact. |
| POST | /v1/messages/{message}/attachments/{attachment}/fetchLazily fetch an attachment's media from the runtime/Instagram Graph API into local storage; for undownloadable types (template/ephemeral/unsupported_type) returns 422 attachment_not_fetchable. | Lazily fetch an attachment's media from the runtime/Instagram Graph API into local storage; for undownloadable types (template/ephemeral/unsupported_type) returns 422 attachment_not_fetchable. |
| GET | /v1/messages/{message}/attachments/{attachment}/mediaStream an attachment's media from local storage; the optional signed w=256|640 param returns a webp preview of the image instead of the original. | Stream an attachment's media from local storage; the optional signed w=256|640 param returns a webp preview of the image instead of the original. |
| POST | /v1/uploadsUpload a file (up to 64 MB) and get a media reference to attach to a message. | Upload a file (up to 64 MB) and get a media reference to attach to a message. |
| POST | /v1/uploads/voiceUpload a voice note: transcodes audio/video to voice-OGG (Opus) and returns a media_id to attach as a voice message. | Upload a voice note: transcodes audio/video to voice-OGG (Opus) and returns a media_id to attach as a voice message. |
| GET | /v1/messages/{message}/email-bodyEmail: the whole letter — sanitized html, text, has_remote_images, has_quoted_text, html_too_complex and cid_map; external images are off and go through a signed proxy link. | Email: the whole letter — sanitized html, text, has_remote_images, has_quoted_text, html_too_complex and cid_map; external images are off and go through a signed proxy link. |
| GET | /v1/email/remote-imageEmail: an image from a letter through the AISAR proxy using the signed token t from email-body (10 minutes, bound to the user and the message). | Email: an image from a letter through the AISAR proxy using the signed token t from email-body (10 minutes, bound to the user and the message). |
Examples
Send a text message
Request
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте! Ваш заказ №1045 передан в доставку."
}'Response
{
"success": true
}Send a sticker from the library
Request
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"attachments": [
{ "sticker_id": 87 }
]
}'Response
{
"success": true
}Quoted reply
Request
curl -X POST "https://api.aisar.app/v1/messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_type": "text",
"message_content": "Да, доставим сегодня до 18:00.",
"quoted_message_id": 998
}'Response
{
"data": {
"message": "Message created.",
"item": {
"id": 1005,
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Да, доставим сегодня до 18:00." },
"quoted_message_id": 998,
"is_deleted": false,
"created_at": "2026-02-09T18:10:00Z"
}
}
}List messages in a conversation
Request
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"id": 1001,
"conversation_id": 15,
"thread_id": 33,
"channel_id": 3,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"display_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_deleted": false,
"timestamp": "2026-02-09T18:00:00Z",
"created_at": "2026-02-09T18:00:00Z",
"updated_at": "2026-02-09T18:00:00Z",
"can_revoke_until": "2026-02-09T18:15:00Z",
"can_edit_until": "2026-02-09T18:15:00Z"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "next_cursor": null },
"meta": { "next_cursor": null }
}
}Scroll history up (before_message_id cursor)
Request
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&before_message_id=998&perPage=20&sortDir=desc" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{ "id": 997, "direction": "inbound", "message_type": "text", "message_content": { "text": "Здравствуйте, подскажите статус заказа?" } },
{ "id": 996, "direction": "outbound", "message_type": "text", "message_content": { "text": "Добрый день! Секунду, проверяю." } }
],
"pagination": { "page": 1, "perPage": 20, "total": null, "lastPage": null, "next_cursor": null },
"meta": { "next_cursor": null }
}
}Paging through history with cursor
Request
curl -X GET "https://api.aisar.app/v1/messages?conversation_id=15&perPage=5&cursor=MjAyNi0wMi0wOVQxNzo1ODowMC4wMDAwMDBafDk5Ng" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{ "id": 995, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:55:00Z", "message_content": { "text": "Добрый день, заказ ещё в пути?" } },
{ "id": 994, "direction": "outbound", "message_type": "text", "timestamp": "2026-02-09T17:50:30Z", "message_content": { "text": "Уточняю у склада." } },
{ "id": 12870, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:45:00Z", "message_content": { "text": "Сообщение из импортированной истории" } },
{ "id": 952, "direction": "inbound", "message_type": "text", "timestamp": "2026-02-09T17:42:00Z", "message_content": { "text": "Спасибо." } },
{ "id": 951, "direction": "outbound", "message_type": "text", "timestamp": "2026-02-09T17:40:12Z", "message_content": { "text": "Заказ передан курьеру." } }
],
"pagination": { "page": 1, "perPage": 5, "total": null, "lastPage": null, "next_cursor": "MjAyNi0wMi0wOVQxNzo0MDoxMi4wMDAwMDBafDk1MQ" },
"meta": { "next_cursor": "MjAyNi0wMi0wOVQxNzo0MDoxMi4wMDAwMDBafDk1MQ" }
}
}Search messages within a conversation
Request
curl -X GET "https://api.aisar.app/v1/messages/search?conversation_id=15&q=заказ" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"message_id": 1001,
"conversation_id": 15,
"thread_id": 33,
"created_at": "2026-02-09T18:00:00Z",
"snippet": "...Ваш заказ №1045 передан в доставку...",
"query": "заказ",
"contact_name": "Aigerim K.",
"contact_id": 88,
"contact_photo": null,
"is_group": false,
"channel_id": 3,
"channel_type": "whatsapp"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1, "has_more": false },
"total_matches": 1
}
}Search across the whole company (conversation_id omitted)
Request
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"message_id": 1001,
"conversation_id": 15,
"thread_id": 33,
"created_at": "2026-02-09T18:00:00Z",
"snippet": "...Ваш заказ №1045 передан в доставку...",
"query": "заказ",
"contact_name": "Aigerim K.",
"contact_id": 88,
"contact_photo": null,
"is_group": false,
"channel_id": 3,
"channel_type": "whatsapp"
},
{
"message_id": 2044,
"conversation_id": 27,
"thread_id": 61,
"created_at": "2026-02-08T11:20:00Z",
"snippet": "...уточните, пожалуйста, номер заказа...",
"query": "заказ",
"contact_name": "Jane Roe",
"channel_id": 9,
"channel_type": "telegram"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 2, "lastPage": 1, "has_more": false },
"total_matches": 2
}
}Search without an exact total (`with_total=0`)
Request
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ&with_total=0&perPage=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{
"message_id": 2044,
"conversation_id": 27,
"thread_id": 61,
"created_at": "2026-02-08T11:20:00Z",
"snippet": "...уточните, пожалуйста, номер заказа...",
"query": "заказ",
"contact_name": "Jane Roe",
"channel_id": 9,
"channel_type": "telegram"
}
],
"pagination": { "page": 1, "perPage": 20, "total": null, "lastPage": null, "has_more": true },
"total_matches": null
}
}A window of messages around a given one
Request
curl -X GET "https://api.aisar.app/v1/messages/1001/around?before=20&after=20" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"items": [
{ "id": 998, "direction": "inbound", "message_type": "text", "message_content": { "text": "Добрый день! Где мой заказ №1045?" } },
{ "id": 1001, "direction": "outbound", "message_type": "text", "message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." } }
],
"anchor_message_id": 1001,
"has_older": true,
"has_newer": false
}
}Upload a file for an attachment
Request
curl -X POST "https://api.aisar.app/v1/uploads" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@invoice-1045.pdf"Response
{
"media_id": "med_9f3a2b7c",
"type": "document",
"storage_disk": "local",
"storage_path": "10/attachments/8b1e2c4f-0a3d-4e57-9c21-1f2a3b4c5d6e.pdf",
"filename": "invoice-1045.pdf",
"mime_type": "application/pdf",
"size": 245113,
"width": null,
"height": null,
"duration": null,
"expires_at": "2026-03-11T18:00:00Z"
}Attach an uploaded file to a message
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"type": "document",
"media_id": "med_9f3a2b7c",
"filename": "invoice-1045.pdf",
"mime_type": "application/pdf",
"size": 245113
}'Lazily fetch an inbound attachment's media
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments/55/fetch" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"item": {
"id": 1001,
"direction": "inbound",
"message_type": "image",
"attachments": [
{
"id": 55,
"type": "image",
"url": "https://api.aisar.app/v1/messages/1001/attachments/55/media?company=3&signature=PLACEHOLDER",
"thumbnail_url": "https://api.aisar.app/v1/messages/1001/attachments/55/media?company=3&w=640&signature=PLACEHOLDER",
"filename": "photo.jpg",
"mime_type": "image/jpeg",
"width": 1280,
"height": 960
}
]
}
}
}Edit an outbound message
Request
curl -X PATCH "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "message_content": "Здравствуйте! Ваш заказ №1045 уже в пути." }'React to a message
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/reactions" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "reaction": "👍" }'Response
{
"data": {
"message": "Message reaction created.",
"item": {
"id": 77,
"message_id": 1001,
"author_user_id": 8,
"author_name": "Азиз Каримов",
"reaction": "👍",
"created_at": "2026-02-09T18:12:00Z"
}
}
}Remove a reaction from a message
Request
curl -X DELETE "https://api.aisar.app/v1/messages/1001/reactions" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"message": "Message reaction removed."
}
}Delete and revoke for the recipient (mode in body)
Request
curl -X DELETE "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "revoke", "reason": "Отправлено по ошибке" }'Response
{
"data": {
"message": "Message deleted.",
"item": {
"id": 1001,
"direction": "outbound",
"is_deleted": true,
"deleted_via": "revoke",
"deleted_at": "2026-10-07T09:12:00+00:00",
"can_revoke_until": null,
"can_edit_until": null
}
}
}Revoke not supported on whatsapp_business (422)
Request
curl -X DELETE "https://api.aisar.app/v1/messages/1001" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "mode": "revoke" }'Response
{
"message": "This channel does not support revoking a sent message for the recipient. Provider-side revoke (\"delete for everyone\") is not available on API-based channels such as WhatsApp Cloud (whatsapp_business). Omit \"mode\" for a local delete."
}Forward into an existing thread
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "target_thread_id": 45 }'Response
{
"data": {
"message": "Message forwarded.",
"item": {
"id": 1002,
"thread_id": 45,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_forwarded": true,
"created_at": "2026-02-09T18:05:00Z"
}
}
}Cross-channel forward by contact
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_contact_account_id": 42,
"target_channel_id": 5
}'Response
{
"data": {
"message": "Message forwarded.",
"item": {
"id": 1003,
"thread_id": 51,
"channel_id": 5,
"direction": "outbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте! Ваш заказ №1045 передан в доставку." },
"is_forwarded": true,
"created_at": "2026-02-09T18:06:00Z"
}
}
}Forward rejected: explicit channel required (422)
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/forward" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "target_contact_account_id": 42 }'Response
{
"message": "Несколько подключённых каналов подходят этому контакту — укажите target_channel_id.",
"error_code": "channel_required"
}Upload a voice note
Request
curl -X POST "https://api.aisar.app/v1/uploads/voice" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-F "file=@voice-note.webm"Response
{
"media_id": "med_01JBQZ8VN2K3M4N5P6Q7R8S9T0",
"type": "audio",
"storage_disk": "local",
"storage_path": "10/attachments/7c1e2a4f-0b3d-4e57-9c21-2f3a4b5c6d7e.ogg",
"filename": "voice.ogg",
"mime_type": "audio/ogg",
"size": 18342,
"duration": 7,
"expires_at": "2026-03-11T18:00:00Z"
}Like an Instagram comment
Request
curl -X POST "https://api.aisar.app/v1/messages/1001/like" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"message": "Comment liked."
}Send with an Idempotency-Key header (safe retry after a network failure)
Request
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3f9c2b7e-8b7a-4e2d-9c3f-7a1b2c3d4e5f" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте! Ваш заказ №1045 передан в доставку."
}'Response
{
"success": true
}A message that started from an ad (referral)
Request
curl -X GET "https://api.aisar.app/v1/messages/1006" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"Response
{
"data": {
"item": {
"id": 1006,
"conversation_id": 22,
"thread_id": 48,
"channel_id": 9,
"direction": "inbound",
"message_type": "text",
"message_content": { "text": "Здравствуйте, интересует товар из рекламы" },
"referral": {
"source_type": "ad",
"source_id": "120210000000000",
"headline": "Летняя коллекция — скидка 20%",
"image_url": "https://scontent.xx.fbcdn.net/example.jpg",
"post_id": "17900000000000000",
"ref": "summer_sale"
},
"created_at": "2026-02-09T18:00:00Z"
}
}
}Send to a blocked contact is refused (422)
Request
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channel_id": 3,
"recipient": "+12025550187",
"message_content": "Здравствуйте!"
}'Response
{
"message": "This contact is blocked; the message was not sent.",
"error_code": "contact_blocked"
}Mark an inbound message as read
Request
curl -X POST "https://api.aisar.app/v1/messages/998/statuses" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "status_name": "read" }'Response
{
"data": {
"message": "Message status recorded.",
"item": {
"id": 214,
"message_id": 998,
"status_name": "read",
"status_timestamp": "2026-02-09T18:11:00Z",
"created_at": "2026-02-09T18:11:00Z"
}
}
}A refused idempotency_key replay (409)
Request
curl -X POST "https://api.aisar.app/v1/messages" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"conversation_id": 15,
"thread_id": 33,
"direction": "outbound",
"message_content": "Здравствуйте!",
"idempotency_key": "order-1045-followup"
}'Response
{
"message": "This idempotency key has already been used for a different message.",
"error_code": "idempotency_key_conflict"
}Email: the whole letter (email-body)
Request
curl -X GET "https://api.aisar.app/v1/messages/9001/email-body" \
-H "Authorization: Bearer YOUR_API_TOKEN"Response
{
"data": {
"html": "<div>Здравствуйте! Нужен счёт на 20 мешков.</div><img data-remote-src=\"https://api.aisar.app/v1/email/remote-image?t=eyJ...\">",
"text": "Здравствуйте! Нужен счёт на 20 мешков.",
"has_remote_images": true,
"has_quoted_text": false,
"html_too_complex": false,
"cid_map": {}
}
}Email: the email block of a message and a first letter with a subject
Request
curl -X POST "https://api.aisar.app/v1/messages/send" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"thread_id": 77,
"message_type": "text",
"message_content": { "text": "Добрый день! Высылаем коммерческое предложение." },
"metadata": { "email": { "subject": "Коммерческое предложение" } }
}'Response
{
"data": {
"id": 9002,
"thread_id": 77,
"direction": "outbound",
"message_type": "text",
"email": {
"subject": "Коммерческое предложение",
"from": { "address": "zakaz@example.com", "name": "Example, отдел заказов" },
"to": [{ "address": "client@example.org", "name": null }],
"cc": [],
"reply_to": [],
"sender_auth": null,
"is_automated": false,
"auto_class": null,
"bounce": null,
"stub": null,
"has_html": false,
"has_quoted": false,
"attachments_skipped": 0
}
}
}