Messages
Reading a conversation, replying to a visitor, idempotency and the origin field.
On this page
The conversation #
| Field | Type | Req. | Description |
|---|---|---|---|
| cursor | RFC 3339 | no | continuation of the previous page |
| limit | int | no | 1…100, 25 by default |
{"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 #
| Field | Type | Req. | Description |
|---|---|---|---|
| id | uuid | yes | message id |
| direction | string | yes | incoming — the visitor, outgoing — support |
| senderType | string | yes | customer, admin, system |
| content | string | yes | the text |
| entities[] | object | no | text markup as ranges, see the Text formatting section; an empty list for plain text |
| messageType | string | yes | text, image, document, audio, video |
| origin | string | yes | where it was written: widget, telegram, api, bitrix24, amocrm, retailcrm, unknown |
| createdAt | RFC 3339 | yes | when it was recorded |
| editedAt | RFC 3339|null | no | when it was edited |
| attachments[] | object | no | attachments, 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 #
| Field | Type | Req. | Description |
|---|---|---|---|
| content | string | no | text up to 4096 characters; required unless file is given |
| entities[] | object | no | text markup: bold, italic, underline, strikethrough, monospace, quote and a link with a caption — see the section below |
| file.url | string | no | a link to the file: we download it ourselves, up to 50 MB |
| file.name | string | no | the file name for the conversation |
| file.type | string | no | image, document, audio, video |
| file_url, file_name, file_type | string | no | the same as flat fields — for integration builders that cannot send nested objects |
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"}}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.
| Field | Type | Req. | Description |
|---|---|---|---|
| type | string | yes | bold, italic, underline, strikethrough, code, pre, blockquote, text_link |
| offset | int | yes | the start of the range from the beginning of the text, in UTF-16 units |
| length | int | yes | the length of the range in the same units |
| url | string | no | the address for type: text_link; http, https, mailto and tel schemes are allowed |
| language | string | no | the code block language for type: pre |
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.