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) and a retried request with the same key returns the same message instead of creating a duplicate.
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).
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"}. The check runs against the target thread's conversation_id (resolved automatically for /messages/send and /forward) rather than the recipient directly.
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).
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, or a non-existent one, 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}, 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).
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.
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); 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, contact_blocked (the recipient is blocked — see "Contact blocking" above).
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." }.
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 (before_message_id/after_message_id) pagination. | List messages with filters (conversation/thread/channel/direction) and page- or cursor-based (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. |
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": "+77001234567",
"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": "+77001234567",
"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 }
}
}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 }
}
}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.",
"channel_id": 3,
"channel_type": "whatsapp"
}
],
"pagination": { "page": 1, "perPage": 20, "total": 1, "lastPage": 1 },
"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.",
"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 },
"total_matches": 2
}
}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?signature=PLACEHOLDER",
"thumbnail_url": "https://api.aisar.app/v1/messages/1001/attachments/55/media?w=640&signature=PLACEHOLDER",
"filename": "photo.jpg",
"mime_type": "image/jpeg"
}
]
}
}
}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,
"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": "+77001234567",
"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": "+77001234567",
"message_content": "Здравствуйте!"
}'Response
{
"message": "This contact is blocked; the message was not sent.",
"error_code": "contact_blocked"
}