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
MethodPathWhat it does
GET/meThe bot's account
GET/chatsChats the bot is in
POST/messagesSend a message
POST/mediaUpload a picture, a video or a file
POST/reactionsAdd or remove a reaction
POST/channelsCreate the bot's own channel
GET/updatesGet updates (long polling)
POST, DELETE/webhookSet or remove a webhook
PUT, GET, DELETE/commandsThe 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 fieldTypeMeaning
user_idintegerThe bot's account id
usernamestringUsername without @
display_namestringThe name shown in chats
token_namestringThe token's name; bot for a token from @marxbot
is_botbooleantrue 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 fieldTypeMeaning
idintegerChat id, chat_id in other methods
typestringdirect, group or channel
titlestring or nullTitle; 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 fieldTypeMeaning
chat_idinteger, requiredChat id
kindstringtext (default), image, video, file or poll
bodystringText, up to 8192 characters; not empty for text
media_idintegerFor image, video, file: an id from POST /media by the same bot
captionstringCaption of a picture, video or file, up to 8192 characters
file_name, file_sizestring, integerFile name and size for file
questionstringFor poll: the question, up to 300 characters
optionsarray of stringsFor poll: 2 to 10 options, each up to 100 characters; empty ones are dropped
anonymous, multiple, revotebooleanFor poll: anonymous (default yes), several answers (no), may change the vote (yes)
client_idstringUp 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_msintegerReturned in the response as is
Response fieldMeaning
message_idMessage id
duplicatetrue for a repeat by client_id; the response then holds only message_id and duplicate
messageThe message, the same object as in an update
composed_at_msThe 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

Statusdetail
400an empty post says nothing, a poll needs a question and two options, a image post needs a media_id
403this account cannot post here: the bot may not post in this chat
404chat not found: no such chat, or the bot is not in it
422not 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 fieldMeaning
fileThe 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/…
kindimage (default), video or file
duration_msFor a video: its length in milliseconds
posterFor 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 fieldTypeMeaning
message_idinteger, requiredMessage id
emojistring, requiredAn 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.

FieldMeaning
titleRequest: the title, 1 to 128 characters
idResponse: the channel's id
createdResponse: 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.

ParameterDefaultMeaning
offset0The id of the first update wanted. Every update with a lower update_id counts as received and is deleted
limit100How many updates to return, 1 to 100
timeout0How 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 fieldMeaning
urlRequired. https:// only, up to 512 characters; the address must be public
secretUp 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.