Skip to content
AISARAISAR
REST API

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.

FieldDescription
referral.source_urlURL of the ad or post; WhatsApp only
referral.source_typead or post
referral.source_idAd/post ID (ad_id for Instagram)
referral.headlineAd headline text
referral.bodyAd body text; WhatsApp only
referral.ctwa_clidClick tracking ID for precise attribution; WhatsApp only
referral.media_typeimage or video; WhatsApp only
referral.image_urlURL of the ad image (if applicable)
referral.video_urlURL of the ad video (if applicable)
referral.post_idID of the post the ad ran from; Instagram only
referral.refDeep-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

MethodPath
POST/v1/messages/send

Send a message by channel and recipient (creates the thread if needed).

POST/v1/messages/send-template

Send an approved WhatsApp template by channel and recipient (or thread_id).

GET/v1/messages

List messages with filters (conversation/thread/channel/direction) and page- or cursor-based (before_message_id/after_message_id) pagination.

GET/v1/messages/search

Full-text message search (`conversation_id` is optional — omit it to search the whole company).

POST/v1/messages

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.

PATCH/v1/messages/{message}

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).

GET/v1/messages/{message}/around

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}/attachments

List a message's attachments.

POST/v1/messages/{message}/attachments

Attach a file to an already-created message.

GET/v1/messages/{message}/statuses

Delivery status history (sent/delivered/read/failed).

POST/v1/messages/{message}/statuses

Record a delivery status for a message.

GET/v1/messages/{message}/reactions

List emoji reactions on a message.

POST/v1/messages/{message}/reactions

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}/reactions

Remove the company's outbound reaction from a message (idempotent; no reaction id needed — one per message).

POST/v1/messages/{message}/like

Like an Instagram comment (Instagram Business comment threads only; the message must have an external id).

POST/v1/messages/{message}/forward

Forward a message (copy-send) into an existing thread or cross-channel by contact.

POST/v1/messages/{message}/attachments/{attachment}/fetch

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}/media

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/uploads

Upload a file (up to 64 MB) and get a media reference to attach to a message.

POST/v1/uploads/voice

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

bash
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

json
{
  "success": true
}

Send a sticker from the library

Request

bash
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

json
{
  "success": true
}

Quoted reply

Request

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
curl -X GET "https://api.aisar.app/v1/messages/search?q=заказ" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "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

bash
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

json
{
  "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

bash
curl -X POST "https://api.aisar.app/v1/uploads" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@invoice-1045.pdf"

Response

json
{
  "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

bash
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

bash
curl -X POST "https://api.aisar.app/v1/messages/1001/attachments/55/fetch" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "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

bash
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

bash
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

json
{
  "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

bash
curl -X DELETE "https://api.aisar.app/v1/messages/1001/reactions" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "data": {
    "message": "Message reaction removed."
  }
}

Delete and revoke for the recipient (mode in body)

Request

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
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

json
{
  "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

bash
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

json
{
  "message": "Несколько подключённых каналов подходят этому контакту — укажите target_channel_id.",
  "error_code": "channel_required"
}

Upload a voice note

Request

bash
curl -X POST "https://api.aisar.app/v1/uploads/voice" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -F "file=@voice-note.webm"

Response

json
{
  "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

bash
curl -X POST "https://api.aisar.app/v1/messages/1001/like" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Accept: application/json"

Response

json
{
  "message": "Comment liked."
}

Send with an Idempotency-Key header (safe retry after a network failure)

Request

bash
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

json
{
  "success": true
}

A message that started from an ad (referral)

Request

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

Response

json
{
  "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

bash
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

json
{
  "message": "This contact is blocked; the message was not sent.",
  "error_code": "contact_blocked"
}