tgbotrs 0.2.0

A fully-featured, auto-generated Telegram Bot API library for Rust. All 285 types and 165 methods - strongly typed, fully async.
Documentation

Crates.io docs.rs CI API Sync

Bot API Rust Coverage Downloads License

All types and 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


✨ Features

🤖 Complete API Coverage

  • All Telegram Bot API types and methods
  • Fully async implementations
  • All union types represented as Rust enums
  • Builder structs for optional parameters

🔄 Auto Generated

  • Code generated from official API specification
  • Automatic updates when Telegram releases new API versions
  • CI pipeline regenerates and opens update pull requests
  • Always stays in sync with Telegram

🦀 Idiomatic Rust

  • Built with async/await using Tokio
  • Accepts i64 or @username for ChatId
  • Uses Option<T> for optional fields
  • Recursive types handled safely with Box<T>

🛡️ Type Safety

  • Strong compile-time guarantees
  • Typed InputFile for file uploads
  • Unified ReplyMarkup enum for keyboards
  • Typed InputMedia enum for media groups

📡 Flexible HTTP Layer

  • Uses reqwest HTTP backend
  • Supports custom Bot API servers
  • Built-in multipart file uploads
  • Configurable timeouts

📬 Built-in Polling

  • Long polling dispatcher included
  • Spawns Tokio task per update
  • Configurable limit and timeout
  • Clean concurrent update handling

🌐 Webhook Support

  • Built-in WebhookServer with axum
  • Same handler interface as Poller
  • Validates secret token
  • Spawns Tokio task per update
  • Or use manual webhook with your own HTTP server

📦 Installation

Add to your Cargo.toml:

[dependencies]
tgbotrs = "0.2"
tokio   = { version = "1", features = ["full"] }

Requirements: Rust 1.75+ and Tokio async runtime


🚀 Quick Start

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"));

    let msg = bot.send_message(123456789i64, "Hello from tgbotrs! 🦀", None).await?;
    println!("📨 Sent message #{}", msg.message_id);

    Ok(())
}

📖 Examples

🔁 Echo Bot Long Polling

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();
}

💬 Formatted Messages

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?;

🎹 Inline Keyboards

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()
            },
        ],
    ],
};

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?;

⚡ Callback Queries

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;

        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;
            }
        }
    })
});

⌨️ Reply Keyboards

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()
            },
        ],
    ],
    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 Photos & Files

use tgbotrs::{InputFile, gen_methods::SendPhotoParams};

let params = SendPhotoParams::new()
    .caption("Look at this! 📷".to_string())
    .parse_mode("HTML".to_string());

// Reference a file already on Telegram's servers (fastest)
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?;

🎬 Media Groups

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?;

📊 Polls

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() },
];

bot.send_poll(chat_id, "Best language for bots?", options, Some(SendPollParams::new().is_anonymous(false))).await?;

🏪 Inline Queries

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()),
        ..Default::default()
    }),
];

bot.answer_inline_query(query.id.clone(), results, None).await?;

🛒 Payments & Telegram Stars

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?;

🔔 Webhooks

tgbotrs supports two webhook approaches: a built-in server (zero boilerplate) or a manual setup using your own HTTP framework.


⚡ Built-in WebhookServer

Enable the feature flag:

[dependencies]
tgbotrs = { version = "0.2", features = ["webhook"] }
tokio   = { version = "1", features = ["full"] }

Then use WebhookServer it uses the same UpdateHandler interface as Poller:

use tgbotrs::{Bot, UpdateHandler, WebhookServer};

#[tokio::main]
async fn main() {
    let bot = Bot::new(std::env::var("BOT_TOKEN").unwrap()).await.unwrap();

    let handler: UpdateHandler = Box::new(|bot, update| {
        Box::pin(async move {
            let Some(msg) = update.message else { return };
            let _ = bot.send_message(msg.chat.id, "Received via webhook! 🚀", None).await;
        })
    });

    WebhookServer::new(bot, handler)
        .port(8080)
        .path("/webhook")
        .secret_token("my_secret")        // validates X-Telegram-Bot-Api-Secret-Token
        .max_connections(40)
        .drop_pending_updates()
        .start("https://yourdomain.com")  // registers setWebhook + starts axum server
        .await
        .unwrap();
}

Internally this:

  • Calls setWebhook with Telegram
  • Starts an axum HTTP server
  • Spawns each update as a Tokio task
  • Returns 200 OK immediately so Telegram doesn't retry

For local testing run: ngrok http 8080 and use the generated HTTPS URL as your webhook URL.


Manual Webhook (bring your own server)

If you already run axum, actix-web, or another HTTP framework, register the webhook manually and handle the JSON body yourself:

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();

    // Register webhook once on startup
    bot.set_webhook(
        "https://yourdomain.com/webhook",
        Some(
            SetWebhookParams::new()
                .secret_token("my_secret".to_string())
                .allowed_updates(vec!["message".into(), "callback_query".into()]),
        ),
    )
    .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 so Telegram gets a fast 200 OK
    tokio::spawn(async move {
        if let Some(msg) = update.message {
            let _ = bot.send_message(msg.chat.id, "Hello!", None).await;
        }
    });

    StatusCode::OK
}

Which to use

Built-in WebhookServer Manual
Zero boilerplate
Secret token validation ✅ built-in ✅ manual
Custom middleware / routing
Works with existing server
Feature flag needed webhook

See examples/webhook/ for a full working example with .env configuration.


🌐 Local Bot API Server

let bot = Bot::with_api_url("YOUR_TOKEN", "http://localhost:8081").await?;

🛠️ Error Handling

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),
}

🔧 API Reference

Bot

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

ChatId

Anywhere ChatId is expected, you can pass:

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?;

InputFile

InputFile::file_id("AgACAgIAAxkBAAI...")   // Already on Telegram's servers (fastest)
InputFile::url("https://example.com/image.png")  // Telegram downloads from URL
InputFile::memory("photo.jpg", bytes)      // Upload raw bytes directly

ReplyMarkup

ReplyMarkup::InlineKeyboard(InlineKeyboardMarkup { .. })
ReplyMarkup::ReplyKeyboard(ReplyKeyboardMarkup { .. })
ReplyMarkup::ReplyKeyboardRemove(ReplyKeyboardRemove { remove_keyboard: true, .. })
ReplyMarkup::ForceReply(ForceReply { force_reply: true, .. })

Poller

Poller::new(bot, handler)
    .timeout(30)
    .limit(100)
    .allowed_updates(vec![
        "message".into(),
        "callback_query".into(),
        "inline_query".into(),
    ])
    .start()
    .await?;

BotError

pub enum BotError {
    Http(reqwest::Error),
    Json(serde_json::Error),
    Api {
        code: i64,
        description: String,
        retry_after: Option<i64>,        // Flood-wait seconds (code 429)
        migrate_to_chat_id: Option<i64>, // Migration target (code 400)
    },
    InvalidToken,
    Other(String),
}

error.is_api_error_code(429)   // -> bool
error.flood_wait_seconds()     // -> Option<i64>

Builder Pattern

Every method with optional parameters has a *Params struct with a fluent builder:

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() });

🔄 Auto-Codegen

tgbotrs automatically stays in sync with the official API. The spec is sourced from tgapis/x, which scrapes the official Telegram Bot API page every 6 hours. When a new version is detected, regeneration kicks off immediately.

Regenerate Manually

# Pull latest spec
curl -sSf https://raw.githubusercontent.com/tgapis/x/data/botapi.json -o api.json

# Run codegen (no pip installs needed)
python3 codegen/codegen.py api.json tgbotrs/src/

# Rebuild
cargo build

🤝 Contributing

Report issues:

Development workflow:

git clone https://github.com/ankit-chaubey/tgbotrs && cd tgbotrs

cargo build --workspace
cargo test --workspace
cargo clippy --workspace -- -D warnings
cargo fmt --all

# Regenerate from latest spec
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.rs

PR guidelines:

  • One concern per PR
  • Run cargo fmt and cargo clippy before submitting
  • Never edit gen_types.rs or gen_methods.rs directly — edit codegen.py instead
  • Add examples for any new helpers

📜 Changelog

See CHANGELOG.md for the full release history.


👤 Author

tgbotrs was built and is maintained by Ankit Chaubey.

Started as a personal tool in 2024 to address limitations in existing Rust Telegram libraries, refined over two years, and made public for the community.


🙏 Credits

Special thanks to Paul / PaulSonOfLars for the auto-generation approach was directly inspired by his Go library gotgbot


📄 License

MIT License © 2026 Ankit Chaubey


If tgbotrs saved you time, a ⭐ on GitHub means a lot!