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,/helpand 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
- 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?botsdirectly) → the Bots tab. - Click New bot: enter a name, an address (Latin letters, digits,
_, must end withbot, e.g.weather_bot) and a few words about the bot. - Copy the token — it's shown only once. The token is the bot's password.
- In the Setup tab, copy the ready code (your token is already filled in) and run it.
- 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
POSTwith a JSON body (Content-Type: application/json). Methods without parameters can also be called withGET. - Put the token in the
Authorization: Bearer <token>header. TheX-Bot-Tokenheader 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
descriptionare 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= theupdate_idof 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-Secretheader — 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,
getUpdatesis unavailable (error409).
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 optionalsecret(8–128 characters: Latin letters, digits,_,-).deleteWebhook: turn the webhook off and go back togetUpdates.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_botgoes only toother_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 amessagewithcommandfilled 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 astartupdate 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-Secretin 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.