Build bots for YukiChat in Python
yukibot is the official Python library for the YukiChat Bot API. If you have used pyTelegramBotAPI (telebot), you already know it.
Quick start
1. In YukiChat, open a chat with @YukiFather and send /newbot MyCoolBot. The name must end with bot. You'll get a token like yk_bot_β¦. Keep it secret.
2. Install the library:
pip install -U yukibot
3. Write your bot:
from yukibot import YukiBot
bot = YukiBot("yk_bot_YOUR_TOKEN")
@bot.message_handler(commands=["start"])
def start(message):
bot.reply_to(message, f"Hi @{message.sender.username}! π")
@bot.message_handler(func=lambda m: True)
def echo(message):
bot.send_message(message.chat.id, message.text_or_caption)
bot.polling()
Run it, then message your bot in YukiChat. To use it in a group, add it as a member.
Handlers
Decorate functions to handle updates. For each update, handlers are tried in the order they were defined and the first match runs, so put specific handlers before catch-alls.
@bot.message_handler(commands=["start", "help"]) # /start, /start@YourBot, /start args (not /startgame)
@bot.message_handler(content_types=["photo", "video"]) # text, photo, video, file, audio, voice, video_note, sticker
@bot.message_handler(regexp=r"(?i)hello") # matched against text or caption
@bot.message_handler(chat_types=["dm"]) # dm, group, channel
@bot.message_handler(func=lambda m: len(m.text or "") > 100)
@bot.edited_message_handler() # someone edited a message
@bot.callback_query_handler(data="like") # a button was pressed (exact data or a compiled regex)
@bot.poll_handler() # vote counts changed in your poll
@bot.poll_answer_handler() # who voted for what (polls sent with is_anonymous=False)
@bot.chat_member_handler() # someone joined or left a chat the bot is in
@bot.my_chat_member_handler() # the bot itself was added/removed (also when someone opens a DM with it)
@bot.message_reaction_handler() # someone reacted to a message
Useful message helpers: message.command ("start"), message.args (text after the command), message.text_or_caption and message.content_type.
@bot.message_handler(commands=["echo"])
def echo(m):
bot.reply_to(m, m.args or "Usage: /echo <text>")
@bot.exception_handler
def on_error(exc, update):
print("handler failed:", exc) # without this, errors are logged and the bot keeps running
Buttons
Attach an inline keyboard to any message with reply_markup. Each button has a text and either callback_data (sent to your bot when pressed) or a url (opens a link).
kb = {"inline_keyboard": [
[{"text": "β", "callback_data": "dec"}, {"text": "β", "callback_data": "inc"}],
[{"text": "Open YukiChat", "url": "https://yukichat.lol"}],
]}
bot.send_message(chat_id, "Counter: 0", reply_markup=kb)
@bot.callback_query_handler(func=lambda q: q.data in ("inc", "dec"))
def press(q):
n = int(q.message.text.split(": ")[1]) + (1 if q.data == "inc" else -1)
bot.edit_message_text(f"Counter: {n}", q.message.message_id, reply_markup=kb)
bot.answer_callback_query(q.id, f"Now {n}") # small toast
# bot.answer_callback_query(q.id, "Saved!", show_alert=True) # dialog instead
Limits: up to 20 rows, 8 buttons per row, 100 buttons in total. Button text can be 1β64 characters and callback_data 1β256 bytes. Answer a press within 10 minutes, and only once.
Sending files
Every media method accepts an http(s) URL, a file path, bytes or an open file. Local files are uploaded to YukiChat for you.
bot.send_photo(chat_id, "cat.jpg", caption="Cat π±")
bot.send_photo(chat_id, "https://example.com/cat.jpg")
bot.send_document(chat_id, open("report.pdf", "rb"))
bot.send_voice(chat_id, "note.ogg") # converted to a playable voice message
bot.send_video_note(chat_id, "round.mp4") # converted to a round video
bot.send_media_group(chat_id, [ # sent as one album (1β10 items)
{"type": "photo", "media": "a.jpg", "caption": "first"},
{"type": "photo", "media": "https://example.com/b.jpg"},
])
info = bot.upload_file("big.zip") # just store it: {"url", "name", "size", "mime"}
Polls
bot.send_poll(chat_id, "Best pet?", ["π± Cat", "πΆ Dog"], is_anonymous=False)
@bot.poll_answer_handler()
def voted(a):
print(a.user.username, "chose", a.option_ids)
bot.stop_poll(message_id) # closes it and returns the final counts
Polls appear as buttons that show live counts. Tapping your choice again takes your vote back. Set allows_multiple_answers=True to let people pick several options.
Groups & admin rights
A bot in a group has the same rights as a person with the same role. Add it as a member to read and send messages. Make it an admin to moderate, and give it only the rights it needs.
| Right | Needed for |
|---|---|
delete_messages | delete_message on other people's messages (a bot can always delete its own) |
ban_users | ban_chat_member, kick_chat_member, unban_chat_member, restrict_chat_member, set_chat_permissions |
pin_messages | pin_chat_message, unpin_chat_message, unpin_all_chat_messages |
change_info | set_chat_title, set_chat_description, set_chat_photo, set_chat_theme, set_chat_background |
invite_users | export_chat_invite_link |
add_admins | promote_chat_member (the bot can only grant rights it has itself) |
post_messages | posting in channels |
import time
@bot.message_handler(commands=["mute"], chat_types=["group"])
def mute(m):
if not m.reply_to_message:
return bot.reply_to(m, "Reply to someone with /mute")
user_id = m.reply_to_message["from"]["id"]
bot.restrict_chat_member(m.chat.id, user_id, {"can_send_messages": False},
until_date=int(time.time()) + 600) # 10 minutes
bot.reply_to(m, "π Muted for 10 minutes")
Nobody can ban, demote or restrict the owner. Admins can't be restricted; demote them first. Group slow mode and member permissions apply to bots that aren't admins.
Per-user storage
Store any JSON value (up to 64 KB) per user on the server, so simple bots don't need a database.
bot.set_bot_data(user_id, "settings", {"lang": "en", "score": 10})
bot.get_bot_data(user_id, "settings") # β {"lang": "en", "score": 10}, or None
bot.delete_bot_data(user_id, "settings")
Webhooks
Instead of polling, YukiChat can send each update to your server as a POST with a JSON body.
bot.set_webhook("https://example.com/yuki", secret_token="long-random-string")
# Flask example
from flask import Flask, request
app = Flask(__name__)
@app.post("/yuki")
def hook():
ok = bot.process_webhook(request.get_json(), "long-random-string", request.headers)
return ("", 200) if ok else ("", 403)
- Every request carries the header
X-YukiBot-Api-Secret-Token(andX-Telegram-Bot-Api-Secret-Tokenfor ported code). Always check it. - Updates arrive one by one, in order. Reply with any
2xxstatus. Failed deliveries are retried (3 tries, then again every minute) and kept for 24 hours. - The URL must be a public
http(s)address. Local and private network addresses are refused. - While a webhook is set,
get_updatesfails with409.bot.polling()removes the webhook for you.
Errors & limits
When the API refuses a request, the library raises YukiAPIError with .error_code, .description and .retry_after. Network errors, 429 and 5xx answers are retried automatically: YukiBot(token, retries=3, timeout=30).
from yukibot import YukiAPIError
try:
bot.ban_chat_member(chat_id, user_id)
except YukiAPIError as e:
bot.send_message(chat_id, f"Can't ban: {e.description}") # e.error_code == 403
| Code | Meaning |
|---|---|
400 | Bad Request: a missing or invalid parameter, or the message/chat wasn't found |
401 | Unauthorized: wrong or revoked token |
403 | Forbidden: the bot isn't in the chat or lacks an admin right |
404 | Method doesn't exist |
409 | getUpdates was called while a webhook is set |
429 | Too many requests. Wait retry_after seconds |
501 | Method not supported by YukiChat yet |
| Limit | Value |
|---|---|
| API calls | 30 per second per bot (bursts up to 60) |
| Messages into one chat | bursts of 20, then 1 per second |
| Text / caption | 8192 / 2048 characters |
| Pending updates | kept for 24 hours, at most 1000 per bot |
getUpdates | up to 100 updates per call, long-poll timeout up to 50 s |
| Self-destructing messages | expires_in from 1 s to 1 year |
Objects & updates
Every object is a dict that also supports dot access, and missing keys read as None. Because from is a Python keyword, the author is message.sender.
Message
{
"message_id": 812, "date": 1792000000, "type": "photo",
"chat": {"id": 148, "type": "group", "title": "Friends", "username": null},
"from": {"id": 150, "username": "alice", "is_bot": false},
"text": "β¦", // text messages only
"photo": "https://β¦", "media": "https://β¦", "caption": "β¦", // media messages
"reply_to_message": {"message_id": 800, "text": "β¦", β¦},
"media_group_id": "g_β¦", "edit_date": 1792000100, "reply_markup": {β¦}
}
The media field is named after the type: photo, video, document (type file), audio, voice, video_note or sticker. media always holds the same URL. Voice and audio messages also include duration.
Update kinds
| Key | Contents |
|---|---|
message | A new message from a person (Message) |
edited_message | An edited text message (Message) |
callback_query | id, from, message, data |
poll | id, question, options[{text, voter_count}], total_voter_count, is_closed |
poll_answer | poll_id, user, option_ids |
chat_member / my_chat_member | chat, user, event ("join" or "leave"), date |
message_reaction | chat, message_id, user, new_reaction, reactions (counts) |
Chat types are dm, group, channel and saved.
Raw HTTP API
You can call the API from any language. The base URL is https://vps.yukichat.lol:8443.
# token in a header (recommended: it never appears in URLs or logs)
curl -X POST https://vps.yukichat.lol:8443/botapi/sendMessage \
-H "Authorization: Bearer yk_bot_YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chat_id": 148, "text": "Hello"}'
# Telegram-style URL also works
curl https://vps.yukichat.lol:8443/botyk_bot_YOUR_TOKEN/getMe
- Method names are camelCase (
sendMessage,getUpdatesβ¦). Use GET or POST, and send parameters as JSON, as a form, or asmultipart/form-datato upload files. - Every answer is
{"ok": true, "result": β¦}or{"ok": false, "error_code": 403, "description": "Forbidden: β¦"}, with the same HTTP status. getUpdates: passoffset= lastupdate_id+ 1 to confirm what you've handled, andtimeout(seconds) for long polling.uploadFile: send the raw body with anX-File-Nameheader, or multipart fieldfile.
How it works
When someone sends a message, presses a button, joins or reacts in a chat with your bot, the server puts an update in that bot's queue. get_updates returns them in order and waits briefly when the queue is empty. With a webhook, the server pushes them to your URL. Updates are removed once you confirm them with offset or your webhook answers 2xx.
Upgrading from 1.x
1.x code keeps working: the camelCase methods (sendMessage, editMessageTextβ¦) still exist. Things to know:
- Method names are now snake_case (
send_message). The old names are aliases. sendChatAction(chat_id, action)now needs the chat id.content_type='photo'still works, but prefercontent_types=['photo'].- Commands match exactly:
commands=['start']no longer fires on/startgame. - The client-side cache is gone, so admin checks are never stale.
- Removed, because the app never supported them:
answerInlineQuery,answerWebAppQuery,sendSystemNotification,verifyIdentity,setChatEmojiEffect,setBotMenu,stopMessageLiveUpdate, and the bot call methods. They return501.
Demo bot
A complete bot with a menu, a counter, a poll, notes, moderation and welcome messages:
import os, random, time
from yukibot import YukiBot, YukiAPIError
bot = YukiBot(os.environ["YUKI_TOKEN"], skip_pending=True)
MENU = {"inline_keyboard": [[{"text": "π² Dice", "callback_data": "dice"},
{"text": "π YukiChat", "url": "https://yukichat.lol"}]]}
@bot.message_handler(commands=["start"])
def start(m):
bot.reply_to(m, f"Hi @{m.sender.username}!", reply_markup=MENU)
@bot.callback_query_handler(data="dice")
def dice(q):
bot.answer_callback_query(q.id)
bot.send_chat_action(q.message.chat.id, "typing")
bot.send_message(q.message.chat.id, f"π² {random.randint(1, 6)}")
@bot.message_handler(commands=["remember"])
def remember(m):
bot.set_bot_data(m.sender.id, "note", m.args)
bot.set_message_reaction(m.message_id, "π")
@bot.message_handler(commands=["recall"])
def recall(m):
bot.reply_to(m, bot.get_bot_data(m.sender.id, "note") or "Nothing saved yet")
@bot.message_handler(commands=["secret"])
def secret(m):
bot.send_message(m.chat.id, "π€« Gone in 15 seconds", expires_in=15)
@bot.message_handler(content_types=["photo"])
def photo(m):
bot.set_message_reaction(m.message_id, "π₯")
@bot.chat_member_handler()
def welcome(e):
if e.event == "join" and not e.user.is_bot:
bot.send_message(e.chat.id, f"Welcome, @{e.user.username}! π")
bot.set_my_commands([{"command": "start", "description": "Menu"},
{"command": "remember", "description": "Save a note"},
{"command": "recall", "description": "Show your note"}])
bot.polling()
Method reference
All methods of YukiBot. The camelCase name in grey is the raw API method, which also works as a Python alias. ADMIN = needs an admin right in the chat.
Changelog
2.0.0 October 15, 2026
NEWLong polling with instant delivery; webhooks with a secret header, ordered delivery and retries.
NEWHandlers for edits, button presses, polls, poll answers, joins/leaves, the bot's own membership and reactions. Added exception_handler.
NEWFile uploads from paths, bytes or open files; albums; upload_file.
NEWReal polls with vote counts; stop_poll. Buttons can open URLs, and answer_callback_query shows a toast or dialog.
NEWget_me, unban_chat_member, kick_chat_member, unpin_all_chat_messages, delete_chat_photo, get_my_commands, delete_my_commands, delete_bot_data, delete_sticker_set, get_user.
CHANGEDsnake_case method names (camelCase kept as aliases). The token is sent in a header. YukiAPIError replaces the bare Exception. Automatic retries and timeouts. The cache was removed.
FIXEDA new bot no longer receives the whole chat history, and bots no longer see their own messages (echo loops). Commands match exactly. Edits and deletes show up live without chat_id. set_my_commands works without a scope. Documents and video notes have the right types.
FIXEDAdmin methods now check rights: bots can't kick the owner, and restricting someone actually stops them posting.
1.1.1 June 24, 2026
FIXEDUpdated the API URL.
1.1.0 April 26, 2026
NEWMedia, chat management, stickers, profile, storage and history methods; expires_in for self-destructing messages.