Skip to content
AISARAISAR

Sending messages from your system

A complete guide to sending messages from an external system (an AI assistant, a chatbot, a CRM): a step-by-step walkthrough (find a contact, pick a channel, send a text or a WhatsApp template, receive replies over a webhook) plus a detailed endpoint reference — channels, media and voice uploads, sending text and templates.

A typical integration — an AI assistant, a chatbot, a CRM, or an automation script — needs to send outbound messages through channels connected to AISAR (WhatsApp, Telegram, Instagram, etc.) and receive replies from contacts. This walkthrough takes you from a token to a full "sent → received a reply" loop, and ends with a detailed reference for each sending endpoint.

Every request uses the company's Bearer token (see the Authentication guide) and runs in that company's context:

text
Authorization: Bearer YOUR_API_TOKEN

Step 1 — pick a channel

Fetch the company's channels to find the channel_id you'll send through:

bash
curl "https://api.aisar.app/v1/channels?status=connected" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
Response (abridged)
{
  "data": [
    {
      "id": 57,
      "name": "WhatsApp test",
      "account_name": "+12025550187",
      "status": "connected",
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" },
      "message_operations": { "can_create": true },
      "limits": { "max_text_length": 4096 }
    }
  ]
}

Before sending, check that status === "connected" and message_operations.can_create === true. The full list of query params and response fields for /channels is in the "GET /channels" section below.

Step 2 — find or create a contact (optional)

Looking up or creating a contact ahead of time is not required: /messages/send finds an existing conversation by recipient or creates a new contact, thread and conversation on its own. Look a contact up in advance only if you need to check whether it already exists, or to attach extra fields (email, tags, etc.) before the first send.

Search for a contact
curl "https://api.aisar.app/v1/contacts?search=%2B12025550187" \
  -H "Authorization: Bearer YOUR_API_TOKEN"
Create a contact
curl -X POST https://api.aisar.app/v1/contacts \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Иван Иванов",
    "phones": [{ "phone": "+12025550187", "type": "mobile" }]
  }'

Step 3 — send a text message

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": 57,
    "recipient": "+12025550187",
    "message_content": "Здравствуйте! Чем могу помочь?"
  }'
Success response
{
  "success": true
}

recipient is the recipient identifier in the format the channel expects: an E.164 number for WhatsApp/SMS, a username or chat_id for Telegram Bot, an IGSID for Instagram. To send to a WhatsApp group, pass the group JID (<id>@g.us) — the @g.us suffix is recognized automatically.

The token owner needs the conversation.create permission — without it the response is 403 Forbidden.

To attach media, first upload the file via POST /v1/uploads (get a media_id), then pass it in attachments:

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": 57,
    "recipient": "+12025550187",
    "message_content": "Смотрите фото",
    "attachments": [{ "media_id": "med_01HZABCDEFGHIJKLMNOPQRSTUV" }]
  }'

Upload details are in the "POST /uploads" section (regular files, idempotency, broadcasting one media asset to many contacts) and "POST /uploads/voice" (voice notes) below. The full field list for /messages/send (is_group, is_silent, attachment limits) is in the "POST /messages/send" section.

Step 4 — send a WhatsApp template and the 24-hour window

WhatsApp Business does not allow sending arbitrary messages to a contact who hasn't messaged you in the last 24 hours — the so-called "customer service window". To start a conversation outside that window, or to run a broadcast, use a Meta-approved template via /messages/send-template.

First, fetch the list of approved templates:

bash
curl "https://api.aisar.app/v1/templates?channel_id=57&metaStatuses[]=approved" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Only a template with meta_status: "approved" can be sent. Then send it to the recipient:

bash
curl -X POST https://api.aisar.app/v1/messages/send-template \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "channel_id": 57,
    "recipient": "+12025550187",
    "template_id": 42,
    "params": { "name": "Иван", "order_id": "A-100" }
  }'
Success response
{
  "success": true,
  "message_id": 12345
}

params fills the template's variables: as a named map ({{name}}) or by position — an array or a map with numeric keys ({{1}}, {{2}}). The same conversation.create permission is required; the channel must be of type whatsapp_business. The full template parameter reference (/templates filters, the header_media_url, header_media_id, header_media_filename, thread_id, is_silent fields, the display structure and quick-reply buttons) is in the "GET /templates" and "POST /messages/send-template" sections below.

Step 5 — receive replies over a webhook

To react to a contact's replies (a conversational flow, e.g. with an AI assistant), subscribe to the message.created event — see the Webhooks guide. A typical loop: receive message.created with direction: "inbound" → process the text → call /messages/send or /messages/send-template to reply.

After every send (text or template), subscribed webhook integrations receive message.created with direction: "outbound", followed by message.status.updated as the delivery status progresses (sent → delivered → read, or failed).

When a contact taps a quick-reply button on a sent template, the tap arrives as a regular inbound message with type: "text" and the button's label — handle it the same way as any other message.created event.

Full cycle

text
1. Внешняя система → GET /v1/channels
       (узнать channel_id)
2. Внешняя система → POST /v1/messages/send
       { channel_id, recipient, message_content }
3. AISAR создаёт/находит диалог, создаёт сообщение
4. AISAR → POST {webhook_url}  (event: message.created, direction: outbound)
5. Коннектор доставляет сообщение в мессенджер
6. AISAR → POST {webhook_url}  (event: message.status.updated)
7. Контакт отвечает → AISAR → POST {webhook_url}  (event: message.created, direction: inbound)
8. Внешняя система обрабатывает ответ → снова POST /v1/messages/send

Endpoint reference

Below is a detailed reference for each sending endpoint: request fields, response shape, and edge cases. It complements the step-by-step walkthrough above; every request uses the same company Bearer token and runs in that company's context.

GET /channels

Fetch the channels available to the current user within the company — use it to find the channel_id you'll send through.

Query paramDescription
typeFilter by channel type key: whatsapp, whatsapp_business, telegram, telegram_bot, telegram_business, instagram_business, threads, facebook_messenger, live_chat, sip_telephony
statusFilter by status: connected, disconnected, paused, etc.
setup_stateFilter by setup state: Draft, Setup, Active, Paused
Response (abridged)
{
  "data": [
    {
      "id": 57,
      "name": "WhatsApp test",
      "account_name": "+13125550141",
      "status": "connected",
      "message_operations": {
        "can_create": true,
        "can_edit": true,
        "edit_window_seconds": 900,
        "revoke_window_seconds": 172800
      },
      "limits": {
        "max_text_length": 4096,
        "file_limits": { "image": { "max_size_mb": 16 }, "video": { "max_size_mb": 64 } }
      },
      "channel_type": { "id": 1, "key": "whatsapp", "name": "WhatsApp" },
      "broadcast_rate_limits": { "daily_limit": 1500, "messages_per_minute": 15 }
    }
  ]
}

Before sending, check that status === "connected" and message_operations.can_create === true.

POST /uploads

Uploads a file to AISAR and returns a compact media_id you then pass to /messages/send. Delivery to the messenger is handled by the connector without re-downloading. Two modes are supported.

Mode A — multipart upload

http
POST /v1/uploads
Authorization: Bearer YOUR_API_TOKEN
Content-Type: multipart/form-data

file=@photo.jpg

Maximum 64 MB. Allowed extensions: jpg, jpeg, png, gif, webp, bmp, svg, mp4, mov, avi, wmv, webm, mkv, mp3, ogg, wav, aac, flac, m4a, opus, amr, pdf, doc, docx, xls, xlsx, csv, txt, rtf, ppt, pptx, zip, rar, 7z, gz, json, xml.

Mode B — upload by URL

http
POST /v1/uploads
Authorization: Bearer YOUR_API_TOKEN
Content-Type: application/json

{ "url": "https://cdn.example.com/photo.jpg" }

AISAR downloads the file once, stores it, and returns a media_id. The URL must be http/https, the size ≤ 64 MB, download timeout 20 sec.

Response (both modes)
{
  "media_id": "med_01HZABCDEFGHIJKLMNOPQRSTUV",
  "type": "image",
  "filename": "photo.jpg",
  "mime_type": "image/jpeg",
  "size": 123456,
  "width": null,
  "height": null,
  "duration": null,
  "expires_at": "2026-05-16T10:30:00.000000Z"
}
The result is idempotent by content (SHA-256 within the company) — re-uploading the same file (multipart or URL) returns the same media_id; the file isn't duplicated in storage. A broadcast of one image to 100 contacts needs only one POST /uploads, even if you accidentally call it twice.

Requires the conversation.view permission.

POST /uploads/voice

Uploads an audio recording and transcodes it to OGG/Opus — the format WhatsApp and Telegram render as a voice note rather than an attached file. This is the only correct way to send a voice message via the API: a plain /uploads call stores the file as-is (type audio), and the recipient gets an audio file, not a voice note.

http
POST /v1/uploads/voice
Authorization: Bearer YOUR_API_TOKEN
Content-Type: multipart/form-data

file=@voice.m4a

Accepts audio/webm, audio/ogg, audio/mp4, audio/mpeg, audio/wav, audio/aac, audio/x-m4a (and video/webm — the browser-recording container). The server transcodes the recording to OGG/Opus itself.

Response
{
  "media_id": "med_01HZABCDEFGHIJKLMNOPQRSTUV",
  "type": "audio",
  "filename": "voice.ogg",
  "mime_type": "audio/ogg",
  "size": 24576,
  "duration": 7,
  "expires_at": "2026-05-16T10:30:00.000000Z"
}
The type field in the response is audio (it's a media asset). To make the message send as a voice note, set attachments[].type: "voice" when sending (see below). Voice notes are supported on WhatsApp (personal), WhatsApp Business, Telegram (personal), and Telegram Bot; on other channels the attachment goes out as a regular audio file. Requires the conversation.view permission.

POST /messages/send

Sends an outbound message on behalf of a channel to a specific recipient. If the conversation doesn't exist yet, it will be created.

FieldTypeRequiredDescription
channel_idintegerYesThe sending channel ID (from /channels)
recipientstringYesThe recipient identifier in the format the channel expects: an E.164 number, a Telegram username, an IGSID, etc. For WhatsApp groups — the group JID <id>@g.us. Max 255 chars.
is_groupbooleanNotrue — the recipient is a WhatsApp group. Not required if recipient is already in the <id>@g.us form (the suffix is recognized automatically). Defaults to false.
is_silentbooleanNotrue — a "silent" send: the operator's unread counter is not reset. Defaults to false. Useful for programmatic/service messages.
message_contentstringConditionalMessage text, max 65,535 chars (the channel's actual limit is in limits.max_text_length from /channels). Required if attachments is absent.
attachmentsarrayConditional1–10 attachments. Required if message_content is absent. Each item: media_id (required) + optional type (image/video/audio/voice/document/file).
Example — text
{
  "channel_id": 42,
  "recipient": "+14155550148",
  "message_content": "Здравствуйте! Чем могу помочь?"
}
Example — WhatsApp group
{
  "channel_id": 42,
  "recipient": "120363398017778174@g.us",
  "message_content": "Всем привет в группе!"
}

Where to get the group identifier: the JID <id>@g.us arrives in inbound-message webhooks and is available in GET /v1/conversations — the group thread's threads[].contact_account.external_id (threads[].is_group: true). Constraints: groups are only supported on WhatsApp channels; the channel's account must be a member of the group; a contradictory request (recipient ends with @g.us but is_group: false) is rejected with 422.

Success response
{ "success": true }

Behaviour. If a conversation with the recipient already exists on this channel it is reused; otherwise a new one is created (contact account, thread, conversation). After the message is created, subscribed webhook integrations receive message.created with direction: outbound. The endpoint creates the message synchronously, but delivery to the messenger runs asynchronously through the connector queue — the final delivery status arrives as message.status.updated. Requires the conversation.create permission, otherwise 403 Forbidden.

GET /templates

Fetches the company's message templates — use it to find Meta-approved WhatsApp Business templates and their id for /messages/send-template.

Query paramDescription
channel_idFilter by a specific WhatsApp Business channel
metaStatuses[]Meta approval status: approved, rejected, pending. For sendable templates use metaStatuses[]=approved
channelTypeChannel type key (e.g. whatsapp_business)
languageBCP-47, e.g. en, ru
searchSearch by name/text/slug/labels
sortBy / sortDirname, status, created_at, updated_at, uses_count, language; asc/desc
Response (abridged)
{
  "data": {
    "items": [
      {
        "id": 42,
        "name": "order_update",
        "meta_status": "approved",
        "language": "en",
        "body": "Здравствуйте, {{name}}! Ваш заказ {{order_id}} готов.",
        "parameters": [
          { "name": "name", "type": "text", "sample_value": "Иван", "position": 1 },
          { "name": "order_id", "type": "text", "sample_value": "A-100", "position": 2 }
        ],
        "channel_type": { "id": 1, "key": "whatsapp_business", "name": "WhatsApp Business" },
        "channel": { "id": 57, "name": "WhatsApp test", "account_name": "+13125550141" }
      }
    ],
    "pagination": { "page": 1, "perPage": 15, "total": 1, "lastPage": 1 }
  }
}

meta_status is the Meta review status; only `approved` can be sent. body carries the placeholders ({{name}} or {{1}}) that tell you which params /messages/send-template needs. The ready answer is the send_params field: a list of { "key": "1", "component": "body" } in the order the server reads a params array (header → body → URL buttons). Meta may return a template created with a named variable as positional; key is then "1", and that is the key to send. Requires the template.view permission.

To avoid polling this endpoint constantly, subscribe to the template.approved and template.rejected webhook events — they fire when meta_status changes, letting you invalidate a cache.

POST /messages/send-template

Sends an approved WhatsApp Business template to a recipient (recipient) or into an existing thread (thread_id) — provide exactly one of the two — for initiating conversations outside the 24-hour window and for broadcasts (call once per recipient). If a conversation with the recipient doesn't exist yet, it will be created.

FieldTypeRequiredDescription
channel_idintegerYesMust be of type whatsapp_business
recipientstringConditional (required_without:thread_id)E.164 number, max 255 chars
thread_idintegerConditional (required_without:recipient)ID of an existing thread; an alternative to recipient — provide exactly one of the two (min: 1)
template_idintegerYesTemplate ID from /templates, must belong to the company and have meta_status = approved
paramsobject/arrayNoTemplate variable values (see below). Omit for templates without variables.
header_media_urlstring (url)NoFor templates with a media header (image/video/document)
header_media_idstringNoAlternative to header_media_url — the media_id returned by POST /uploads; doesn't require a permanent public file link. If both fields are passed, header_media_id wins
header_media_filenamestringNoFile name for a document header (max 240 chars); ignored for image/video
is_silentbooleanNoSame as /messages/send — does not reset the unread counter

For a document header, WhatsApp shows the recipient a file name rather than rendering the file itself — without one the client sees "Untitled". If header_media_filename is omitted, a name is filled in automatically, in order of trust: the stored name of the AISAR attachment, when header_media_url points at our signed GET /messages/{message}/attachments/{attachment}/media route (lookup scoped to the template's company), then the file name spelled out in the link's own path (e.g. .../files/deck.pdf → deck.pdf). If no name can be resolved, the document ships without one.

header_media_id attaches a media header without keeping a permanent link to the file. A file uploaded via POST /uploads has no public address Meta could fetch, so AISAR uploads it to the Meta Media API itself at send time and passes the header by Meta's own media_id. The uploaded copy is reused for up to 14 days (keyed by sending phone number + file checksum), so a broadcast sending the same file doesn't re-upload it for every recipient. The filename for a document header is taken from header_media_filename when provided, otherwise from the filename stored at upload time in /uploads. If header_media_id isn't found or belongs to another company, the request fails with 422; if the asset is gone by the time the message actually sends, or Meta refuses the upload, the send fails outright (the message never ships with an empty header) — the outcome arrives as a message.status.updated event with status failed.

The server builds the Meta components structure from the template definition for you. params values are passed one of two ways: a named map for named placeholders {{name}} ({ "name": "Иван", "order_id": "A-100" }), or positional for numeric placeholders {{1}}, {{2}} — a map with numeric keys or a plain array (["Иван", "A-100"], values in order: header → body → buttons).

An authentication template (a verification code with a "Copy code" button) takes the code once: { "1": "761014" } or ["761014"]. The server repeats the same code into the button itself, as Meta requires.

Example request
{
  "channel_id": 57,
  "recipient": "+14155550148",
  "template_id": 42,
  "params": { "name": "Иван", "order_id": "A-100" }
}
Success response
{
  "success": true,
  "message_id": 12345
}

Behaviour. Sending is only allowed for whatsapp_business channels and templates with meta_status = approved — otherwise 422. If a variable's value is missing, 422 lists the missing parameters. The sent message keeps the rendered template structure in message_content.display (visible in GET /messages): header, body, footer, buttons[] — each button is { "type": "quick_reply" | "url" | "phone", "label": "...", "value": "..." }; for a document header, header also carries filename — the same name the recipient sees in WhatsApp (either the explicit header_media_filename or the automatically resolved one). display is the template snapshot as is, with its {{…}} placeholders; the text the recipient actually saw is next to it in display.rendered: header, body, footer, buttons[] with the values filled in (for example, the "Copy code" button link already carrying the code). When a contact taps a quick-reply button, the tap arrives back as an inbound message of type: "text" carrying the button's label and content.interactive_reply_payload. Requires the conversation.create permission.

Errors and status codes

CodeCauseHow to handle
401Invalid or missing tokenCheck Authorization: Bearer {token}
403The token owner lacks the conversation.create permissionGrant the token owner the conversation.create permission
404Company not found (the token isn't bound to a company)Verify the token is bound to a company
422Validation error, the channel doesn't belong to the company, the template isn't approved, or header_media_id wasn't found or belongs to another companyCheck the request body and template status
429Rate limit exceededRetry with exponential backoff

For the full error format, see the Errors guide.