A fully-featured, auto-generated Telegram Bot API library for Rust π¦
All 285 types and 165 methods of the Telegram Bot API β
strongly typed, fully async, automatically kept in sync with every official release.
π¦ Install Β· π Quick Start Β· π Examples Β· π§ API Reference Β· π Auto-Codegen Β· π docs.rs
|
π€ Complete API Coverage
|
π Auto-Generated & Always Fresh
|
|
π¦ Idiomatic Rust
|
π‘οΈ Fully Type-Safe
|
|
π‘ Flexible HTTP Layer
|
π¬ Built-in Polling
|
Add to your Cargo.toml:
[package]
name = "mybot"
version = "0.1.0"
edition = "2021"
[dependencies]
tgbotrs = ">=0.1.5"
tokio = { version = "1", features = ["full"] }Requirements: Rust
1.75+Β· Tokio async runtime
use tgbotrs::Bot;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let bot = Bot::new("YOUR_BOT_TOKEN").await?;
println!("β
Running as @{}", bot.me.username.as_deref().unwrap_or("unknown"));
println!(" ID: {}", bot.me.id);
// chat_id accepts i64, negative group IDs, or "@username"
let msg = bot.send_message(123456789i64, "Hello from tgbotrs! π¦", None).await?;
println!("π¨ Sent message #{}", msg.message_id);
Ok(())
}The simplest possible bot. Receives every message and echoes it back.
use tgbotrs::{Bot, Poller, UpdateHandler};
#[tokio::main]
async fn main() {
let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap())
.await
.expect("Invalid token");
println!("π€ @{} is running...", bot.me.username.as_deref().unwrap_or(""));
let handler: UpdateHandler = Box::new(|bot, update| {
Box::pin(async move {
let Some(msg) = update.message else { return };
let Some(text) = msg.text else { return };
let _ = bot.send_message(msg.chat.id, text, None).await;
})
});
Poller::new(bot, handler)
.timeout(30)
.limit(100)
.start()
.await
.unwrap();
}Send HTML or MarkdownV2 formatted messages with optional settings.
use tgbotrs::gen_methods::SendMessageParams;
let params = SendMessageParams::new()
.parse_mode("HTML".to_string())
.disable_notification(true);
bot.send_message(
"@mychannel",
"<b>Bold</b> Β· <i>Italic</i> Β· <code>code</code> Β· <a href='https://example.com'>Link</a>",
Some(params),
).await?;Buttons embedded inside messages. Perfect for interactive menus.
use tgbotrs::{ReplyMarkup, gen_methods::SendMessageParams};
use tgbotrs::types::{InlineKeyboardButton, InlineKeyboardMarkup};
let keyboard = InlineKeyboardMarkup {
inline_keyboard: vec![
vec![
InlineKeyboardButton {
text: "β
Accept".into(),
callback_data: Some("accept".into()),
..Default::default()
},
InlineKeyboardButton {
text: "β Decline".into(),
callback_data: Some("decline".into()),
..Default::default()
},
],
vec![
InlineKeyboardButton {
text: "π Visit Website".into(),
url: Some("https://ankitchaubey.in".into()),
..Default::default()
},
],
],
};
let params = SendMessageParams::new()
.parse_mode("HTML".to_string())
.reply_markup(ReplyMarkup::InlineKeyboard(keyboard));
bot.send_message(chat_id, "<b>Make a choice:</b>", Some(params)).await?;Handle button taps from inline keyboards. Always acknowledge with answer_callback_query.
use tgbotrs::gen_methods::{AnswerCallbackQueryParams, EditMessageTextParams};
use tgbotrs::types::MaybeInaccessibleMessage;
let handler: UpdateHandler = Box::new(|bot, update| {
Box::pin(async move {
let Some(cq) = update.callback_query else { return };
let data = cq.data.as_deref().unwrap_or("");
// Always acknowledge β dismisses the loading spinner
let _ = bot
.answer_callback_query(
cq.id.clone(),
Some(
AnswerCallbackQueryParams::new()
.text(format!("You chose: {}", data))
.show_alert(false),
),
)
.await;
// Edit the original message in-place
if let Some(msg) = &cq.message {
if let MaybeInaccessibleMessage::Message(m) = msg.as_ref() {
let edit_params = EditMessageTextParams::new()
.chat_id(m.chat.id)
.message_id(m.message_id)
.parse_mode("HTML".to_string());
let _ = bot
.edit_message_text(
format!("β
You selected: <b>{}</b>", data),
Some(edit_params),
)
.await;
}
}
})
});Custom keyboard shown at the bottom of the screen. Great for persistent menu buttons.
use tgbotrs::{ReplyMarkup, gen_methods::SendMessageParams};
use tgbotrs::types::{KeyboardButton, ReplyKeyboardMarkup};
let keyboard = ReplyKeyboardMarkup {
keyboard: vec![
vec![
KeyboardButton {
text: "π Share Location".into(),
request_location: Some(true),
..Default::default()
},
KeyboardButton {
text: "π± Share Contact".into(),
request_contact: Some(true),
..Default::default()
},
],
vec![
KeyboardButton { text: "π Home".into(), ..Default::default() },
KeyboardButton { text: "βοΈ Settings".into(), ..Default::default() },
],
],
resize_keyboard: Some(true),
one_time_keyboard: Some(true),
..Default::default()
};
let params = SendMessageParams::new()
.reply_markup(ReplyMarkup::ReplyKeyboard(keyboard));
bot.send_message(chat_id, "Use the keyboard below π", Some(params)).await?;Send files by file_id, URL, or raw bytes from disk.
use tgbotrs::{InputFile, gen_methods::SendPhotoParams};
let params = SendPhotoParams::new()
.caption("Look at this! π·".to_string())
.parse_mode("HTML".to_string());
// Fastest β already on Telegram's servers
bot.send_photo(chat_id, "AgACAgIAAxkBAAI...", Some(params.clone())).await?;
// Let Telegram download from a URL
bot.send_photo(chat_id, "https://example.com/photo.jpg", Some(params.clone())).await?;
// Upload raw bytes from disk
let data = tokio::fs::read("photo.jpg").await?;
bot.send_photo(chat_id, InputFile::memory("photo.jpg", data), Some(params)).await?;Send multiple photos or videos as an album in a single message.
use tgbotrs::InputMedia;
use tgbotrs::types::{InputMediaPhoto, InputMediaVideo};
let media = vec![
InputMedia::Photo(InputMediaPhoto {
r#type: "photo".into(),
media: "AgACAgIAAxkBAAI...".into(),
caption: Some("First photo πΈ".into()),
..Default::default()
}),
InputMedia::Video(InputMediaVideo {
r#type: "video".into(),
media: "BAACAgIAAxkBAAI...".into(),
caption: Some("A video π¬".into()),
..Default::default()
}),
];
bot.send_media_group(chat_id, media, None).await?;Send polls β regular or quiz style.
use tgbotrs::gen_methods::SendPollParams;
use tgbotrs::types::InputPollOption;
let options = vec![
InputPollOption { text: "π¦ Rust".into(), ..Default::default() },
InputPollOption { text: "πΉ Go".into(), ..Default::default() },
InputPollOption { text: "π Python".into(), ..Default::default() },
];
let params = SendPollParams::new().is_anonymous(false);
bot.send_poll(chat_id, "Best language for bots?", options, Some(params)).await?;Handle @yourbot query inline mode from any chat.
use tgbotrs::types::{
InlineQueryResult, InlineQueryResultArticle,
InputMessageContent, InputTextMessageContent,
};
let results = vec![
InlineQueryResult::InlineQueryResultArticle(InlineQueryResultArticle {
r#type: "article".into(),
id: "1".into(),
title: "Hello World".into(),
input_message_content: InputMessageContent::InputTextMessageContent(InputTextMessageContent {
message_text: "Hello from inline mode! π".into(),
..Default::default()
}),
description: Some("Send a greeting".into()),
reply_markup: None,
url: None,
thumbnail_url: None,
thumbnail_width: None,
thumbnail_height: None,
}),
];
bot.answer_inline_query(query.id.clone(), results, None).await?;Send invoices using Telegram Stars (XTR) or payment providers.
use tgbotrs::gen_methods::SendInvoiceParams;
use tgbotrs::types::LabeledPrice;
let prices = vec![
LabeledPrice { label: "Premium Plan".into(), amount: 999 },
];
bot.send_invoice(
chat_id,
"Premium Access",
"30 days of unlimited features",
"payload_premium_30d",
"XTR", // Telegram Stars
prices,
None,
).await?;Register a webhook URL so Telegram pushes updates to your server instead of you polling. Read detailed webhook bot examples!
use tgbotrs::gen_methods::SetWebhookParams;
// Register webhook
let params = SetWebhookParams::new()
.max_connections(100i64)
.allowed_updates(vec!["message".into(), "callback_query".into()])
.secret_token("my_secret_token".to_string());
bot.set_webhook("https://mybot.example.com/webhook", Some(params)).await?;Full webhook server with axum:
# Cargo.toml
[dev-dependencies]
axum = "0.7"use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use std::sync::Arc;
use tgbotrs::{gen_methods::SetWebhookParams, types::Update, Bot};
struct AppState { bot: Bot }
#[tokio::main]
async fn main() {
let bot = Bot::new("YOUR_BOT_TOKEN").await.unwrap();
bot.set_webhook(
"https://yourdomain.com/webhook",
Some(SetWebhookParams::new()),
)
.await
.unwrap();
let app = Router::new()
.route("/webhook", post(handle_update))
.with_state(Arc::new(AppState { bot }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn handle_update(
State(state): State<Arc<AppState>>,
Json(update): Json<Update>,
) -> StatusCode {
let bot = state.bot.clone();
// Spawn immediately β return 200 fast or Telegram will retry
tokio::spawn(async move {
if let Some(msg) = update.message {
let _ = bot
.send_message(msg.chat.id, "Received via webhook! π", None)
.await;
}
});
StatusCode::OK
}For local testing:
ngrok http 8080β use the ngrok URL as your webhook
Point the bot at a self-hosted Telegram Bot API server for higher file size limits and faster speeds.
let bot = Bot::with_api_url("YOUR_TOKEN", "http://localhost:8081").await?;Structured errors with helpers for flood-wait and common API errors.
use tgbotrs::BotError;
match bot.send_message(chat_id, "Hello!", None).await {
Ok(msg) => println!("β
Sent: #{}", msg.message_id),
Err(BotError::Api { code: 403, .. }) => {
eprintln!("π« Bot was blocked by user");
}
Err(BotError::Api { code: 400, description, .. }) => {
eprintln!("β οΈ Bad request: {}", description);
}
Err(e) if e.is_api_error_code(429) => {
if let Some(secs) = e.flood_wait_seconds() {
println!("β³ Flood wait: {} seconds", secs);
tokio::time::sleep(std::time::Duration::from_secs(secs as u64)).await;
}
}
Err(e) => eprintln!("β Unexpected error: {}", e),
}pub struct Bot {
pub token: String, // Bot token from @BotFather
pub me: User, // Populated via getMe on creation
pub api_url: String, // Default: https://api.telegram.org
}| Constructor | Description |
|---|---|
Bot::new(token) |
Create bot, calls getMe, verifies token |
Bot::with_api_url(token, url) |
Create with a custom/local API server |
Bot::new_unverified(token) |
Create without calling getMe |
Anywhere ChatId is expected, you can pass any of these:
bot.send_message(123456789i64, "user by numeric id", None).await?;
bot.send_message(-100123456789i64, "group or channel", None).await?;
bot.send_message("@channelname", "by username", None).await?;
bot.send_message(ChatId::Id(123), "explicit wrapper", None).await?;// Reference a file already on Telegram's servers (fastest)
InputFile::file_id("AgACAgIAAxkBAAI...")
// Let Telegram download from a URL
InputFile::url("https://example.com/image.png")
// Upload raw bytes directly
let data = tokio::fs::read("photo.jpg").await?;
InputFile::memory("photo.jpg", data)// Inline keyboard β buttons inside messages
ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup { .. })
// Reply keyboard β custom keyboard at bottom of screen
ReplyMarkup::ReplyKeyboard(ReplyKeyboardMarkup { .. })
// Remove the reply keyboard
ReplyMarkup::ReplyKeyboardRemove(ReplyKeyboardRemove { remove_keyboard: true, .. })
// Force the user to reply to a message
ReplyMarkup::ForceReply(ForceReply { force_reply: true, .. })Poller::new(bot, handler)
.timeout(30) // Seconds to long-poll (0 = short poll)
.limit(100) // Max updates per request (1β100)
.allowed_updates(vec![ // Filter which update types to receive
"message".into(),
"callback_query".into(),
"inline_query".into(),
])
.start()
.await?;pub enum BotError {
Http(reqwest::Error), // Network / HTTP transport error
Json(serde_json::Error), // Serialization error
Api {
code: i64, // Telegram error code (400, 403, 429β¦)
description: String, // Human-readable message
retry_after: Option<i64>, // Flood-wait seconds (code 429)
migrate_to_chat_id: Option<i64>, // Migration target (code 400)
},
InvalidToken, // Token missing ':'
Other(String), // Catch-all
}
// Helper methods
error.is_api_error_code(429) // β bool
error.flood_wait_seconds() // β Option<i64>Every method with optional parameters has a *Params struct with a fluent builder API:
// Pattern: MethodNameParams::new().field(value).field(value)
let params = SendMessageParams::new()
.parse_mode("HTML".to_string())
.disable_notification(true)
.protect_content(false)
.message_thread_id(123i64)
.reply_parameters(ReplyParameters { message_id: 42, ..Default::default() })
.reply_markup(ReplyMarkup::ForceReply(ForceReply {
force_reply: true,
input_field_placeholder: None,
selective: None,
}));| Category | Count | Status |
|---|---|---|
| Total Types | 285 | β 100% |
| β³ Struct types | 257 | β |
| β³ Union / Enum types | 21 | β |
| β³ Marker types | 7 | β |
| Total Methods | 165 | β 100% |
β³ set* methods |
30 | β |
β³ get* methods |
29 | β |
β³ send* methods |
22 | β |
β³ edit* methods |
12 | β |
β³ delete* methods |
11 | β |
| β³ Other methods | 61 | β |
| Optional params structs | 100 | β |
| Lines auto-generated | ~11,258 | β |
tgbotrs is the only Rust Telegram library that automatically stays in sync with the official API β no manual updates, no lag.
The spec is sourced from tgapis/x (data branch, botapi.json), which scrapes and parses the official Telegram Bot API page and auto-updates every 6 hours. When a new API version is detected, the tgapis/x pipeline dispatches a trigger to this repo β regeneration kicks off immediately, no cron required.
tgapis/x scrapes Telegram Bot API
every 6 hours β pushes botapi.json
to the data branch
β
βΌ
βββββββββββββββββββββββ
β tgapis/x pipeline βββ No change? βββΊ Stop β
β detects new versionβ
ββββββββββββ¬βββββββββββ
β Changed!
β repository_dispatch β
β event: x-data-updated
βΌ
βββββββββββββββββββββββ
β Fetch botapi.json β β raw.githubusercontent.com/tgapis/x/data/botapi.json
β (always latest) β
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Compare with βββ No change? βββΊ Stop β
β pinned api.json β
ββββββββββββ¬βββββββββββ
β Changed!
βΌ
βββββββββββββββββββββββ
β diff_spec.py β β Semantic diff (added/removed types & methods)
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β codegen.py β β Pure Python, zero pip dependencies
β β Generates gen_types.rs + gen_methods.rs
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β validate.py β β Verify 100% coverage
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β Open PR with β β Rich report: summary table, per-field diff
β full report β New/removed items, checklist
ββββββββββββ¬βββββββββββ
β
βΌ
βββββββββββββββββββββββ
β On PR merge: β
β β’ Bump semver β
β β’ Git tag β
β β’ GitHub Release β
β β’ crates.io β
βββββββββββββββββββββββ
# 1. Pull latest spec from tgapis/x data branch into repo root
curl -sSf https://raw.githubusercontent.com/tgapis/x/data/botapi.json -o api.json
# 2. Run codegen (no pip installs needed)
python3 codegen/codegen.py api.json tgbotrs/src/
# 3. Rebuild
cargo build| Workflow | Trigger | Purpose |
|---|---|---|
auto-regenerate.yml |
π‘ Pipeline dispatch from tgapis/x + manual |
Spec sync β diff β codegen β PR |
ci.yml |
Every push / PR | Build, test, lint on 3 OS Γ 2 Rust versions (spec fetched live from tgapis/x) |
release.yml |
PR merged β main | Semver bump β tag β crates.io publish |
notify.yml |
After regen | GitHub Issue with full change summary |
Contributions are welcome!
Report issues:
- π Bug β open a bug report
- π‘ Feature β open a feature request
- π Security β email ankitchaubey.dev@gmail.com directly
Development workflow:
git clone https://github.com/ankit-chaubey/tgbotrs && cd tgbotrs
cargo build --workspace # Build everything
cargo test --workspace # Run tests
cargo clippy --workspace -- -D warnings # Lint
cargo fmt --all # Format
# Pull the latest spec from tgapis/x into repo root and regenerate
curl -sSf https://raw.githubusercontent.com/tgapis/x/data/botapi.json -o api.json
python3 codegen/codegen.py api.json tgbotrs/src/
# Validate 100% coverage
python3 .github/scripts/validate_generated.py \
api.json tgbotrs/src/gen_types.rs tgbotrs/src/gen_methods.rsPR guidelines:
- One concern per PR
- Always run
cargo fmtandcargo clippybefore submitting - Never edit
gen_types.rsorgen_methods.rsdirectly β editcodegen.pyinstead - Add examples for any new helpers
See CHANGELOG.md for the full release history.
Developed by Ankit Chaubey
tgbotrs started as a personal tool.
I was constantly running into limitations, missing features, and unsupported things,
so in 2024 I decided to build my own solution.
After using tgbotrs for a long time (2024-26) and refining it along the way,
I felt it could be useful for others too β so I made it public.
If this helps you in any way, feel free to β star it or π΄ fork it π
Developed and maintained by Ankit Chaubey (@ankit-chaubey)
Rust engineer Β· Open-source builder Β· Telegram & systems enthusiast
Special thanks to Paul / PaulSonOfLars β the auto-generation approach at the heart of this library was directly inspired by his excellent Go library gotgbot and api-spec gen. Seeing how clean and maintainable a fully-generated, strongly-typed Telegram library can be was the spark for building tgbotrs.
| Telegram | The Bot API this library implements |
| PaulSonOfLars / gotgbot | Inspiration for the codegen-first approach |
| tgapis/x | Machine-readable spec source β auto-updated every 6 hours |
MIT License Β© 2026 Ankit Chaubey
If tgbotrs saved you time, a β on GitHub means a lot!