Bot API — bots for developers

How to build a bot for MRIYU: token, messages, buttons, photos and files, bots in groups and channels, webhooks, limits and ready examples.

A bot is a page run by a program. People chat with it in the messenger, and your code replies: it receives messages through the Bot API and sends answers. If you've built Telegram bots, everything will feel familiar.

What a bot can do:

  • reply to messages and commands (/start, /help and your own);
  • send messages with buttons: an answer to the bot, a link to a website, or opening a game / Mini App on MRIYU;
  • edit and delete its own messages;
  • send photos and files;
  • send notifications to its subscribers;
  • work in group chats and channels: answer commands, greet newcomers, moderate, publish posts (auto-posting);
  • show a menu button that opens your game or Mini App.

A bot can only write to people who tapped Start or wrote to it themselves. A bot can't message a stranger first — that's why MRIYU has no bot spam.

Quick start in 5 minutes

  1. Open Developers in the left menu (the item is hidden — turn it on in Menu items, see Settings and menu, or open mriyu.com/developers?bots directly) → the Bots tab.
  2. Click New bot: enter a name, an address (Latin letters, digits, _, must end with bot, e.g. weather_bot) and a few words about the bot.
  3. Copy the token — it's shown only once. The token is the bot's password.
  4. In the Setup tab, copy the ready code (your token is already filled in) and run it.
  5. Open mriyu.com/<bot address>, tap Open bot → Start and write something — the bot will reply.

How requests work

Every method lives at:

https://mriyu.com/api/bot/v1/<method>
  • Send a POST with a JSON body (Content-Type: application/json). Methods without parameters can also be called with GET.
  • Put the token in the Authorization: Bearer <token> header. The X-Bot-Token header or a ?token= parameter also work (not recommended: a URL with the token can end up in logs).
  • The answer is always JSON.

Success:

{"ok": true, "result": { ... }}

Error:

{"ok": false, "error_code": 403, "description": "Пользователь не нажимал «Начать» или остановил бота."}
Code When
400 Invalid parameters (description says which).
401 Invalid token, or the bot is blocked by the administration.
403 You can't write to this person: they didn't tap Start, stopped the bot or blocked it.
404 No such method, user or message.
409 getUpdates while a webhook is on.
422 Invalid buttons (details in description).
429 Too many requests — wait and retry.

Error texts in description are in Russian, like the rest of the site's API.

How a bot receives messages

Everything that happens to a bot arrives as updates. There are two ways to get them — pick one.

Way 1. getUpdates — no website needed

Your code asks every 1–3 seconds: "anything new?". Any computer, VPS or hosting will do — no public address required.

  • Updates are kept for 24 hours.
  • Pass offset = the update_id of the last processed update + 1 — everything before it counts as received and won't come again.

Way 2. Webhook — messages arrive instantly

MRIYU sends every update to your address with a POST request. You need a website with https.

  • Turn it on with setWebhook, or in the dashboard → bot → Settings → Webhook. The Test webhook button sends a test update {"update_id": 0, "type": "ping"}.
  • If you set a secret, it arrives in the X-Mriyu-Bot-Secret header — check it so nobody can send you fake updates.
  • Your server must answer with 2xx within 5 seconds. Answer right away and do slow work afterwards.
  • No answer or an error — delivery stops and retries a minute later, keeping the order of updates. The error is shown in the dashboard and in getWebhookInfo.
  • While a webhook is on, getUpdates is unavailable (error 409).

Update types

Each update is JSON with update_id (increasing) and type. The from field is who did it: {"id": 15, "name": "Olya Petrenko", "username": "olya"}.

type When it arrives Fields
start Someone tapped Start from, payload — the parameter from a ?start= link, or null
message Someone wrote to the bot from, message
button Tapped a button with data under the bot's message from, button: {message_id, data}
stop Tapped Stop bot from
chat_message A message in a group chat or channel the bot is in (see "Bots in groups") chat, from, message
chat_added The bot was added to a chat or channel chat, by — who added it
chat_removed The bot was removed from a chat chat, by
chat_join A new member joined the chat chat, user, by — who invited them, or null
chat_leave A member left or was removed chat, user, by

The chat field is {"id": 7, "title": "Our block", "type": "chat"} (type: chat — group chat, channel — channel). A tap on a button under the bot's message in a group arrives as a button update with a chat field.

message fields:

Field Meaning
id Message number
text Text
kind Kind: text (the bot receives text messages)
command If the text starts with /: {"name": "forecast", "args": "Kyiv"} for /forecast Kyiv, otherwise null
reply_to The message it replies to, or null
date Time, unix seconds

Example update:

{
  "update_id": 1042,
  "type": "message",
  "from": {"id": 15, "name": "Olya Petrenko", "username": "olya"},
  "message": {"id": 98761, "text": "/forecast Kyiv", "kind": "text",
              "command": {"name": "forecast", "args": "Kyiv"}, "reply_to": null, "date": 1760524800}
}

Methods

getMe

Check the token and get info about the bot. No parameters. Returns id, name, username, about, verified (the Official check mark), webhook (whether it's on) and subscribers (how many people tapped Start).

sendMessage

Send a message to a person.

Parameter Required Meaning
user_id yes Who to — from.id from an update
text yes Up to 4000 characters. Same formatting as the messenger: **bold**, __italic__, links [text](https://…), monospace in backticks
buttons no Buttons under the message (see below)
reply_to no The message the bot replies to
silent no true — no notification sound

Returns the sent message: {"id", "user_id", "text", "buttons", "date", "edited"}. Keep the id to edit the message later.

editMessage

Edit your message: message_id plus new text and/or buttons. Pass "buttons": null to remove the buttons. Handy after a button tap: "Subscribe?" → "Subscribed ✅".

deleteMessage

Delete your message: message_id.

getUpdates

Fetch updates (without a webhook only). Parameters: offset — starting update_id (everything before counts as received), limit — how many, up to 100 (default 100). Returns an array of updates.

setWebhook / deleteWebhook / getWebhookInfo

  • setWebhook: url (https) and an optional secret (8–128 characters: Latin letters, digits, _, -).
  • deleteWebhook: turn the webhook off and go back to getUpdates.
  • getWebhookInfo: url, has_secret, pending (waiting for delivery), last_error, last_error_at.

setCommands / getCommands

The command list, shown in the chat under the Commands button. Up to 30 commands: {"command": "forecast", "description": "Tomorrow's forecast"}. A command name is Latin letters, digits and _, up to 32 characters, without /. You can also set this in the dashboard.

getUser

Info about someone who wrote to the bot: user_id → id, name, username, avatar, started (subscribed right now).

sendPhoto / sendDocument

A photo or file — to a person (user_id) or a chat (chat_id).

Parameter Required Meaning
user_id or chat_id yes Who or where to
file or url yes The file itself (multipart/form-data request) or an https link — MRIYU downloads it
caption no Caption up to 1000 characters
buttons no Buttons; in multipart/form-data — as a JSON string
reply_to, silent no Same as sendMessage

sendPhoto accepts images only (jpg, png, webp, gif). The size limit is the site's upload limit. Programs, scripts and HTML can't be sent. The explicit-image filter applies in groups and channels.

curl -X POST https://mriyu.com/api/bot/v1/sendPhoto \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F user_id=15 -F caption="Tomorrow's weather" -F file=@forecast.png

getSubscribers

Who is subscribed to the bot — for broadcasts. 1000 ids per page: {"user_ids": [...], "next": 5012}. If next isn't null, request the next page with after = next.

Bots in groups and channels

A bot can be added to a group chat or channel: the owner or an admin opens Participants → Add bot and enters the bot's address. The developer can forbid this: dashboard → bot → Settings → untick Can be added to groups and channels.

  • In a group chat the bot is a regular member. The owner can make it an admin (Make admin) so it can moderate.
  • In a channel the bot is added as an admin straight away, so it can publish posts (auto-posting).

What a bot sees in a group

By default — like Telegram's privacy mode — the bot only receives:

  • commands: /rules, /help Kyiv (/start@other_bot goes only to other_bot);
  • mentions: messages containing @bot_address;
  • replies to its own messages.

If the bot needs to see everything (for example, to delete spam), turn on Sees all messages in groups in the dashboard. People in the chat aren't told about it — only turn it on if you really need it.

Group methods

Method Parameters What it does
getChats — Chats and channels the bot is in: [{id, title, type, role}]
getChat chat_id The chat: id, title, type, the bot's role, members
sendChatMessage chat_id, text, buttons, reply_to, silent A message to the chat; to a channel — only if the bot is an admin
editChatMessage chat_id, message_id, text, buttons Edit your message
deleteChatMessage chat_id, message_id Delete your message; someone else's — if the bot is an admin
kickChatMember chat_id, user_id Remove a member (the bot must be an admin; the owner and admins can't be removed)
sendPhoto, sendDocument chat_id instead of user_id A photo or file to the chat

sendChatMessage returns {"id", "chat_id", "kind", "text", "url", "buttons", "date"}.

Example: chat guard (Node.js)

An admin bot with "Sees all messages": greets newcomers, deletes links and answers /rules.

async function handle(update) {
  if (update.type === 'chat_join') {
    await call('sendChatMessage', {chat_id: update.chat.id, text: 'Hi, ' + update.user.name + '! Rules — /rules'});
  }
  if (update.type === 'chat_message') {
    const {chat, message} = update;
    if (/https?:\/\//i.test(message.text)) {
      await call('deleteChatMessage', {chat_id: chat.id, message_id: message.id});
      await call('sendChatMessage', {chat_id: chat.id, text: 'Links are not allowed in this chat.'});
    } else if (message.command?.name === 'rules') {
      await call('sendChatMessage', {chat_id: chat.id, text: '1. No spam\n2. No insults', reply_to: message.id});
    }
  }
}

Example: auto-posting to a channel (Python)

import requests

HEADERS = {'Authorization': 'Bearer YOUR_TOKEN'}
API = 'https://mriyu.com/api/bot/v1/'

chats = requests.post(API + 'getChats', headers=HEADERS).json()['result']
channel = next(c for c in chats if c['type'] == 'channel')
requests.post(API + 'sendPhoto', headers=HEADERS, data={
    'chat_id': channel['id'], 'caption': 'News of the day', 'url': 'https://example.com/today.jpg',
})

Buttons

buttons is an array of rows, each row an array of buttons. Up to 8 rows, 1 to 4 buttons per row, button text up to 40 characters. Each button is one of three kinds:

Button What happens on tap
{"text": "Yes", "data": "sub:yes"} The bot gets a button update with this data (up to 64 bytes). Reply with a new message or edit this one (editMessage).
{"text": "Website", "url": "https://example.com"} Opens the link (https only) in a new tab.
{"text": "Play", "app": "my-game"} Opens a MRIYU game or Mini App by its address.
"buttons": [
  [{"text": "✅ Yes", "data": "sub:yes"}, {"text": "❌ No", "data": "sub:no"}],
  [{"text": "🎮 Open game", "app": "my-game"}]
]

The bot's public page

A bot has its own page — mriyu.com/<bot address>. Set it up in the dashboard → bot → Settings → Public page:

  • cover and avatar;
  • status — a line under the name (up to 140 characters);
  • category — News, Weather, Games, Tools, Shopping, Education, Entertainment, Support, Groups or Other;
  • website — a link to your site;
  • What the bot can do — a description shown on the page and in an empty chat.

The page also shows the commands, how many people use the bot, the developer, the Open bot button and the menu button.

Commands, menu button and start links

  • Commands. Above the message field in a bot chat there's a Commands button — the list from setCommands. Tapping sends the command; you get a message with command filled in.
  • Menu button. In the dashboard → Settings, enter a game or Mini App address (e.g. my-game) and the button text — a button that opens it appears next to Commands.
  • Start links. mriyu.com/weather_bot?start=promo42 — after Start you get a start update with "payload": "promo42". The parameter is up to 64 characters: Latin letters, digits, _, -. Handy for referral links and ads.

Limits

What How much
Messages from a bot 30 per second in total, 20 per minute to one person, 20 per minute to one chat
API requests from one IP 600 per minute
Update storage 24 hours
Webhook answer 5 seconds
Bots per developer 20

Over the limit — answer 429: wait and retry. For broadcasts to many people, send 25–30 messages per second.

Ready examples

Node.js (18+), no server — getUpdates

const TOKEN = 'YOUR_TOKEN';
const call = (method, data = {}) => fetch('https://mriyu.com/api/bot/v1/' + method, {
  method: 'POST',
  headers: {'Authorization': 'Bearer ' + TOKEN, 'Content-Type': 'application/json'},
  body: JSON.stringify(data),
}).then((r) => r.json());

let offset = 0;
async function loop() {
  const {ok, result} = await call('getUpdates', {offset});
  if (ok) {
    for (const update of result) {
      offset = update.update_id + 1;
      if (update.type === 'start') {
        await call('sendMessage', {
          user_id: update.from.id,
          text: 'Hi, **' + update.from.name + '**! Subscribe to the forecast?',
          buttons: [[{text: 'Yes', data: 'yes'}, {text: 'No', data: 'no'}]],
        });
      } else if (update.type === 'button') {
        await call('editMessage', {
          message_id: update.button.message_id,
          text: update.button.data === 'yes' ? 'Subscribed ✅' : 'OK, maybe later.',
          buttons: null,
        });
      } else if (update.type === 'message') {
        await call('sendMessage', {user_id: update.from.id, text: 'You wrote: ' + update.message.text});
      }
    }
  }
  setTimeout(loop, 1500);
}
loop();

PHP — webhook

<?php
// File https://example.com/mriyu-bot.php — put its address into setWebhook.
const TOKEN = 'YOUR_TOKEN';
const SECRET = 'YOUR_WEBHOOK_SECRET';

if (($_SERVER['HTTP_X_MRIYU_BOT_SECRET'] ?? '') !== SECRET) { http_response_code(403); exit; }
$update = json_decode(file_get_contents('php://input'), true);
http_response_code(200);

function call(string $method, array $data): array {
    $ch = curl_init('https://mriyu.com/api/bot/v1/' . $method);
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . TOKEN, 'Content-Type: application/json'],
        CURLOPT_POSTFIELDS => json_encode($data),
    ]);
    return json_decode(curl_exec($ch), true);
}

if (($update['type'] ?? '') === 'message') {
    $command = $update['message']['command']['name'] ?? null;
    $text = $command === 'help' ? 'I can do: /forecast <city>' : 'Type /help';
    call('sendMessage', ['user_id' => $update['from']['id'], 'text' => $text]);
}

Python 3 — getUpdates

import time, requests

TOKEN = 'YOUR_TOKEN'
API = 'https://mriyu.com/api/bot/v1/'
HEADERS = {'Authorization': 'Bearer ' + TOKEN}

def call(method, **data):
    return requests.post(API + method, json=data, headers=HEADERS, timeout=10).json()

offset = 0
while True:
    answer = call('getUpdates', offset=offset)
    for update in answer.get('result', []):
        offset = update['update_id'] + 1
        if update['type'] == 'message':
            call('sendMessage', user_id=update['from']['id'], text='Echo: ' + update['message']['text'])
    time.sleep(1.5)

curl — check the token and send a message

curl -X POST https://mriyu.com/api/bot/v1/getMe -H "Authorization: Bearer YOUR_TOKEN"

curl -X POST https://mriyu.com/api/bot/v1/sendMessage \
  -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json" \
  -d '{"user_id": 15, "text": "Hello!", "buttons": [[{"text": "Website", "url": "https://example.com"}]]}'

Security and rules

  • The token is the bot's password. Don't publish it in open source code or on the client (in a browser or app). If it leaks: dashboard → bot → Setup → New token — the old one stops working immediately.
  • Check X-Mriyu-Bot-Secret in your webhook.
  • A bot receives the name and page address of the people who write to it. Don't collect more than you need and don't share this data with third parties.
  • Spam and unsolicited broadcasts, fraud, collecting passwords or payment details and 18+ content are forbidden.
  • Users can report a bot (in the chat ⋯ → Report bot). For violations the administration blocks the bot — the token stops working and the reason is shown in the dashboard.
  • Bots checked by the administration get the Official check mark.

Not available yet

  • Voice and video messages from a bot.
  • Payments through a bot.

FAQ

The bot doesn't reply. Check the token (getMe). Without a webhook — is your getUpdates code running? With a webhook — getWebhookInfo: a last_error means your server didn't answer 2xx within 5 seconds.

403 when sending. The person didn't tap Start, stopped the bot or blocked it. Wait until they write first.

Updates come again. With getUpdates, pass offset = last update_id + 1. With a webhook, answer 2xx — otherwise the update is sent again.

How do I broadcast? getSubscribers → sendMessage for each, no faster than 30 per second. For a large audience a channel is easier: add the bot as an admin and publish with sendChatMessage.

The bot ignores messages in a group. Without "Sees all messages" it only gets commands, @bot_address mentions and replies to its messages.

403 when deleting a message in a group. A bot can delete other people's messages and remove members only if the chat owner made it an admin.

See also: Bots for users, For game developers.