Methods
Every method is under https://api.bot.marx.moscow/api/bot/v1/ and takes the header Authorization: Bearer marx_pat_….
In the examples $MARX_TOKEN is the bot's token, $CHAT_ID a chat id from GET /chats or from an update, $MESSAGE_ID a message id. Set them in the shell first:
export MARX_TOKEN=marx_pat_...
export CHAT_ID=812
export MESSAGE_ID=90211
| Method | Path | What it does |
|---|---|---|
GET | /me | The bot's account |
GET | /chats | Chats the bot is in |
POST | /messages | Send a message |
POST | /media | Upload a picture, a video or a file |
POST | /reactions | Add or remove a reaction |
POST | /channels | Create the bot's own channel |
GET | /updates | Get updates (long polling) |
POST, DELETE | /webhook | Set or remove a webhook |
PUT, GET, DELETE | /commands | The bot's commands in the app's menu |
GET /me
The account the token belongs to. Useful to check the token at start-up.
| Response field | Type | Meaning |
|---|---|---|
user_id | integer | The bot's account id |
username | string | Username without @ |
display_name | string | The name shown in chats |
token_name | string | The token's name; bot for a token from @marxbot |
is_bot | boolean | true for a bot |
curl https://api.bot.marx.moscow/api/bot/v1/me \
-H "Authorization: Bearer $MARX_TOKEN"
{"user_id": 41207, "username": "dacha_weather_bot", "display_name": "Dacha weather", "token_name": "bot", "is_bot": true}
GET /chats
Every chat the bot is in, in the order of the app's chat list.
| Item field | Type | Meaning |
|---|---|---|
id | integer | Chat id, chat_id in other methods |
type | string | direct, group or channel |
title | string or null | Title; null for a direct chat |
curl https://api.bot.marx.moscow/api/bot/v1/chats \
-H "Authorization: Bearer $MARX_TOKEN"
[{"id": 812, "type": "group", "title": "Dacha"}, {"id": 977, "type": "direct", "title": null}]
POST /messages
Send a message to a chat where the bot is a member allowed to post. Group members may post by default; in a channel only administrators may. A bot cannot start a direct chat: one appears when a person writes to the bot.
| Request field | Type | Meaning |
|---|---|---|
chat_id | integer, required | Chat id |
kind | string | text (default), image, video, file or poll |
body | string | Text, up to 8192 characters; not empty for text |
media_id | integer | For image, video, file: an id from POST /media by the same bot |
caption | string | Caption of a picture, video or file, up to 8192 characters |
file_name, file_size | string, integer | File name and size for file |
question | string | For poll: the question, up to 300 characters |
options | array of strings | For poll: 2 to 10 options, each up to 100 characters; empty ones are dropped |
anonymous, multiple, revote | boolean | For poll: anonymous (default yes), several answers (no), may change the vote (yes) |
client_id | string | Up to 64 characters. A repeat with the same client_id within an hour creates no second message and does not count against the limit |
composed_at_ms | integer | Returned in the response as is |
| Response field | Meaning |
|---|---|
message_id | Message id |
duplicate | true for a repeat by client_id; the response then holds only message_id and duplicate |
message | The message, the same object as in an update |
composed_at_ms | The value from the request, or null |
Text
curl https://api.bot.marx.moscow/api/bot/v1/messages \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{"chat_id": $CHAT_ID, "body": "Tomorrow +18, no rain", "client_id": "forecast-2026-10-05"}
EOF
{
"message_id": 90230,
"duplicate": false,
"message": {
"id": 90230,
"chat_id": 812,
"sender_id": 41207,
"author": {"id": 41207, "display_name": "Dacha weather", "username": "dacha_weather_bot", "avatar_key": null, "avatar_url": null, "is_bot": true},
"seq": 5121,
"type": "text",
"body": "Tomorrow +18, no rain",
"caption": null,
"created_at": "2026-10-05T07:00:00.412Z",
"media_url": null,
"object_key": null,
"media": null,
"attachments": [],
"edited_at": null,
"call_status": null,
"call_duration": null,
"reply_to_message_id": null,
"quote": null,
"views": 0,
"comment_count": 0,
"poll": null,
"forwarded_from_user_id": null,
"forwarded_from_chat_id": null,
"forwarded_from_name": null,
"about_user_id": null,
"about_name": null,
"reactions": [],
"duration_ms": null,
"file_name": null,
"file_size": null
},
"composed_at_ms": null
}
A repeat with the same client_id
curl https://api.bot.marx.moscow/api/bot/v1/messages \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{"chat_id": $CHAT_ID, "body": "Tomorrow +18, no rain", "client_id": "forecast-2026-10-05"}
EOF
{"message_id": 90230, "duplicate": true}
Poll
curl https://api.bot.marx.moscow/api/bot/v1/messages \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{"chat_id": $CHAT_ID, "kind": "poll", "question": "When do we go?", "options": ["Saturday", "Sunday"]}
EOF
{
"message_id": 90231,
"duplicate": false,
"message": {
"id": 90231,
"chat_id": 812,
"sender_id": 41207,
"author": {"id": 41207, "display_name": "Dacha weather", "username": "dacha_weather_bot", "avatar_key": null, "avatar_url": null, "is_bot": true},
"seq": 5122,
"type": "poll",
"body": "{\"question\": \"When do we go?\", \"options\": [\"Saturday\", \"Sunday\"], \"closed\": false}",
"caption": null,
"created_at": "2026-10-05T07:00:02.035Z",
"media_url": null,
"object_key": null,
"media": null,
"attachments": [],
"edited_at": null,
"call_status": null,
"call_duration": null,
"reply_to_message_id": null,
"quote": null,
"views": 0,
"comment_count": 0,
"poll": {
"question": "When do we go?",
"closed": false,
"my_vote": null,
"my_votes": [],
"total": 0,
"voters": 0,
"anonymous": true,
"multiple": false,
"revote": true,
"can_close": true,
"recent_voters": [],
"options": [{"label": "Saturday", "count": 0}, {"label": "Sunday", "count": 0}]
},
"forwarded_from_user_id": null,
"forwarded_from_chat_id": null,
"forwarded_from_name": null,
"about_user_id": null,
"about_name": null,
"reactions": [],
"duration_ms": null,
"file_name": null,
"file_size": null
},
"composed_at_ms": null
}
Errors
| Status | detail |
|---|---|
| 400 | an empty post says nothing, a poll needs a question and two options, a image post needs a media_id |
| 403 | this account cannot post here: the bot may not post in this chat |
| 404 | chat not found: no such chat, or the bot is not in it |
| 422 | not your attachment, unknown attachment: the media_id was uploaded by someone else or does not exist |
POST /media
Upload a picture, a video or a file, then send it with POST /messages and its media_id. The body is multipart/form-data.
| Form field | Meaning |
|---|---|
file | The file, required. A picture up to 50 MB, a video up to 320 MB, a file up to 50 MB. For image and video the part's Content-Type must be image/… or video/… |
kind | image (default), video or file |
duration_ms | For a video: its length in milliseconds |
poster | For a video: a cover frame, a picture |
The response: media_id, size in bytes, width and height for a picture (null otherwise). A refusal is 400 with the reason in detail, for example not a readable image.
curl https://api.bot.marx.moscow/api/bot/v1/media \
-H "Authorization: Bearer $MARX_TOKEN" \
-F kind=image -F "file=@map.png;type=image/png"
{"media_id": 5521, "size": 183204, "width": 1280, "height": 720}
Then:
{"chat_id": 812, "kind": "image", "media_id": 5521, "caption": "Rain map"}
POST /reactions
Add a reaction to a message in a chat the bot is in. The same request again removes it.
| Request field | Type | Meaning |
|---|---|---|
message_id | integer, required | Message id |
emoji | string, required | An emoji, 1 to 16 characters |
curl https://api.bot.marx.moscow/api/bot/v1/reactions \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{"message_id": $MESSAGE_ID, "emoji": "👍"}
EOF
{"ok": true}
No such message, or the bot is not in its chat: 404 message not found.
POST /channels
The channel owned by the bot with this title. If the bot already has a channel with this title, that one is returned; otherwise a new one is created. People are added to the channel from the app.
| Field | Meaning |
|---|---|
title | Request: the title, 1 to 128 characters |
id | Response: the channel's id |
created | Response: true if this request created the channel |
curl https://api.bot.marx.moscow/api/bot/v1/channels \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Dacha weather"}'
{"id": 1304, "title": "Dacha weather", "created": true}
GET /updates
New messages for the bot. How it works: Long polling.
| Parameter | Default | Meaning |
|---|---|---|
offset | 0 | The id of the first update wanted. Every update with a lower update_id counts as received and is deleted |
limit | 100 | How many updates to return, 1 to 100 |
timeout | 0 | How many seconds to wait when there are none, 0 to 50 |
curl "https://api.bot.marx.moscow/api/bot/v1/updates?offset=0&timeout=30" \
-H "Authorization: Bearer $MARX_TOKEN"
[
{
"update_id": 7,
"chat": {"id": 812, "type": "group", "title": "Dacha"},
"message": {
"id": 90229,
"chat_id": 812,
"sender_id": 40011,
"author": {"id": 40011, "display_name": "Anna", "username": "anna", "avatar_key": null, "avatar_url": null, "is_bot": false},
"seq": 5120,
"type": "text",
"body": "/today@dacha_weather_bot",
"caption": null,
"created_at": "2026-10-05T06:59:58.101Z",
"media_url": null,
"object_key": null,
"media": null,
"attachments": [],
"edited_at": null,
"call_status": null,
"call_duration": null,
"reply_to_message_id": null,
"quote": null,
"views": 0,
"comment_count": 0,
"poll": null,
"forwarded_from_user_id": null,
"forwarded_from_chat_id": null,
"forwarded_from_name": null,
"about_user_id": null,
"about_name": null,
"reactions": [],
"duration_ms": null,
"file_name": null,
"file_size": null
}
}
]
A webhook is set: 409 webhook is set.
POST /webhook, DELETE /webhook
POST sets the address Marx posts every update to; GET /updates answers 409 from then on. DELETE removes the webhook; updates not yet posted stay and come through GET /updates. Details: Webhook.
| Request field | Meaning |
|---|---|
url | Required. https:// only, up to 512 characters; the address must be public |
secret | Up to 256 characters of A-Z, a-z, 0-9, _, -. Sent in the X-Marx-Bot-Secret header of every post |
curl https://api.bot.marx.moscow/api/bot/v1/webhook \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://bot.example.org/marx", "secret": "Zr4q-8NnYtV2"}'
{"ok": true, "url": "https://bot.example.org/marx"}
curl -X DELETE https://api.bot.marx.moscow/api/bot/v1/webhook \
-H "Authorization: Bearer $MARX_TOKEN"
{"ok": true}
An address that is not accepted: 400 with the reason, such as only https is allowed, the address is not public, the host does not resolve.
PUT /commands, GET /commands, DELETE /commands
The command list the app shows in the menu of a chat with the bot. PUT replaces the whole list, DELETE clears it. Rules: Commands.
curl -X PUT https://api.bot.marx.moscow/api/bot/v1/commands \
-H "Authorization: Bearer $MARX_TOKEN" \
-H "Content-Type: application/json" \
-d @- <<EOF
{"commands": [
{"command": "today", "description": "Weather today"},
{"command": "week", "description": "Forecast for the week"}
]}
EOF
{"commands": [{"command": "today", "description": "Weather today"}, {"command": "week", "description": "Forecast for the week"}]}
curl https://api.bot.marx.moscow/api/bot/v1/commands \
-H "Authorization: Bearer $MARX_TOKEN"
{"commands": [{"command": "today", "description": "Weather today"}, {"command": "week", "description": "Forecast for the week"}]}
curl -X DELETE https://api.bot.marx.moscow/api/bot/v1/commands \
-H "Authorization: Bearer $MARX_TOKEN"
A list that fails the rules: 400, with the item and the problem in detail, for example commands[1].command: must match ^[a-z0-9_]{1,32}$, without the /. A person's token instead of a bot's: 403 this account is not a bot.