Messages

Reading a conversation, replying to a visitor, idempotency and the origin field.

The conversation #

GET /api/v1/conversations/{id}/messages conversations:read
FieldTypeReq.Description
cursorRFC 3339nocontinuation of the previous page
limitintno1…100, 25 by default
Response
{"data": [{
  "id": "7735af47-2e36-45fd-9543-83f67b6d7eef",
  "direction": "outgoing",
  "senderType": "admin",
  "content": "Your order is on the way",
  "entities": [{"type": "bold", "offset": 0, "length": 4}],
  "messageType": "text",
  "origin": "api",
  "createdAt": "2026-08-22T09:15:04.281Z",
  "editedAt": null,
  "attachments": []
}], "hasMore": false}

The message object #

FieldTypeReq.Description
iduuidyesmessage id
directionstringyesincoming — the visitor, outgoing — support
senderTypestringyescustomer, admin, system
contentstringyesthe text
entities[]objectnotext markup as ranges, see the Text formatting section; an empty list for plain text
messageTypestringyestext, image, document, audio, video
originstringyeswhere it was written: widget, telegram, api, bitrix24, amocrm, retailcrm, unknown
createdAtRFC 3339yeswhen it was recorded
editedAtRFC 3339|nullnowhen it was edited
attachments[]objectnoattachments, see the Attachments section

🔴 origin exists so you do not loop: receiving an event about your own reply, your system must not send it a second time.

Reply to a visitor #

POST /api/v1/conversations/{id}/messages conversations:write
FieldTypeReq.Description
contentstringnotext up to 4096 characters; required unless file is given
entities[]objectnotext markup: bold, italic, underline, strikethrough, monospace, quote and a link with a caption — see the section below
file.urlstringnoa link to the file: we download it ourselves, up to 50 MB
file.namestringnothe file name for the conversation
file.typestringnoimage, document, audio, video
file_url, file_name, file_typestringnothe same as flat fields — for integration builders that cannot send nested objects
Request
POST /api/v1/conversations/6c82f8e2-…/messages
Idempotency-Key: reply-4417
Content-Type: application/json

{"content": "Your order is on the way",
 "file": {"url": "https://example.com/act.pdf", "name": "Invoice.pdf", "type": "document"}}
Response
201 {"data": {"id": "7735af47-…", "content": "Your order is on the way",
                "createdAt": "2026-08-22T09:15:04.281Z", "direction": "outgoing", "status": "queued"}}
200 {"data": {"id": "7735af47-…", "duplicate": true}}

The file is fetched before the message is written: an unreachable link will not leave an empty reply in the conversation. Requests to your address bypass proxies and never go into an internal network. An attachment can also be sent as the flat fields file_url, file_name, file_type — that suits integration builders; the nested file object wins. A file name or type without a link is a validation_error rather than a silent text-only send: otherwise the scenario would believe the attachment went out.

Text formatting #

In the chat window the text is shown with markup: bold, italic, underline, strikethrough, monospace, a quote and a link hidden behind a short caption instead of a long address.

The operator has nothing to learn for that: they format the reply with the usual Telegram tools (select the text → Bold, Link and so on), and the visitor sees the same. Through the API the markup is set with the entities field.

Markup is passed as ranges, not tags: we neither accept nor serve HTML in a conversation. A range says «from this character, this many characters, this style». Offsets are counted in UTF-16 units — the same way the Telegram Bot API and browser strings count them: an emoji takes two units, a Latin letter one.

FieldTypeReq.Description
typestringyesbold, italic, underline, strikethrough, code, pre, blockquote, text_link
offsetintyesthe start of the range from the beginning of the text, in UTF-16 units
lengthintyesthe length of the range in the same units
urlstringnothe address for type: text_link; http, https, mailto and tel schemes are allowed
languagestringnothe code block language for type: pre
Request
POST /api/v1/conversations/6c82f8e2-…/messages
Content-Type: application/json

{"content": "Your order is on the way, track it here",
 "entities": [
   {"type": "bold", "offset": 0, "length": 4},
   {"type": "text_link", "offset": 34, "length": 4, "url": "https://example.com/track/4417"}
 ]}

Ranges that cannot be shown are dropped while the text stays whole: an unknown type, a range outside the text and a link with a foreign scheme (javascript:, data:) silently become plain text. Ranges must not overlap — nest them the way Telegram does: a link inside bold, not half of it.

Idempotency-Key #

The header makes a retry safe: the connection can drop after the request was already accepted. Sending again with the same key returns 200 and duplicate: true instead of a second message.

Up to 128 characters from A-Z a-z 0-9 . _ : - are allowed. The key works within your access key: two different access keys with the same Idempotency-Key do not interfere.

What status: queued means #

The reply is accepted for delivery and goes into the visitor's window through the same queue as an operator reply from Telegram. There is no separate delivered flag in the API: the message.created event arrives on the usual terms, and whether the visitor saw the reply is reflected by the conversation's lastReadAt field.