tgbotrs 0.1.4

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 Types Methods Coverage Downloads License

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


✨ Features

πŸ€– Complete API Coverage

  • All 285 types β€” structs, enums, markers
  • All 165 methods β€” fully async
  • All 21 union types as Rust enums
  • 100 optional params structs with builder pattern

πŸ”„ Auto-Generated & Always Fresh

  • Generated from the official spec
  • Daily automated check for API updates
  • PR auto-opened on every new API version
  • Zero manual work to stay up-to-date

πŸ¦€ Idiomatic Rust

  • Fully async/await with Tokio
  • Into<ChatId> β€” accepts i64 or "@username"
  • Into<String> on all text params
  • Option<T> for all optional fields
  • Box<T> to break recursive type cycles

πŸ›‘οΈ Fully Type-Safe

  • ChatId β€” integer or username, no stringly typing
  • InputFile β€” file_id / URL / raw bytes
  • ReplyMarkup β€” unified enum for all 4 keyboard types
  • InputMedia β€” typed enum for media groups
  • Compile-time guarantees on every API call

πŸ“‘ Flexible HTTP Layer

  • Custom API server support (local Bot API)
  • Multipart file uploads built-in
  • Configurable timeout
  • Flood-wait aware error handling
  • reqwest backend

πŸ“¬ Built-in Polling

  • Long-polling dispatcher included
  • Spawns a Tokio task per update
  • Configurable timeout, limit, allowed_updates
  • Clean concurrent update processing

πŸ“¦ Installation

Add to your Cargo.toml:

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

Requirements: Rust 1.75+ Β· 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"));
    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(())
}

πŸ“– Examples

πŸ” Echo Bot β€” Long Polling

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

πŸ’¬ Formatted Messages

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

🎹 Inline Keyboards

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

⚑ Callback Queries

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

⌨️ Reply Keyboards

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 Photos & Files

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

🎬 Media Groups

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

πŸ“Š Polls

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

πŸͺ Inline Queries

Handle @yourbot query inline mode from any chat.

use tgbotrs::types::{
    InlineQueryResult, InlineQueryResultArticle,
    InputMessageContent, InputTextMessageContent,
};

let results = vec![
    InlineQueryResult::Article(InlineQueryResultArticle {
        r#type: "article".into(),
        id: "1".into(),
        title: "Hello World".into(),
        input_message_content: InputMessageContent::Text(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

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

πŸ”” Webhooks

Register a webhook URL so Telegram pushes updates to your server instead of you polling.

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


🌐 Local Bot API Server

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

πŸ› οΈ Error Handling

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

πŸ”§ API Reference

Bot β€” Core Struct

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 β€” Flexible Chat Identifier

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

InputFile β€” File Sending

// 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)

ReplyMarkup β€” All Keyboard Types

// 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 β€” Long Polling Dispatcher

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

BotError β€” Error Variants

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>

Builder Pattern for Optional Params

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

πŸ“Š Coverage Statistics

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 β€”

πŸ”„ Auto-Codegen

tgbotrs is the only Rust Telegram library that automatically stays in sync with the official API spec via GitHub Actions β€” no manual updates, no lag.

How It Works

Every Day at 08:00 UTC
        β”‚
        β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  Fetch latest   β”‚  ← github.com/ankit-chaubey/api-spec
  β”‚  api.json spec  β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β–Ό
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚  Compare with   │── No change? ──► Stop βœ…
  β”‚  pinned version β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚ 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    β”‚
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Regenerate Manually

# 1. Pull latest spec
curl -o api.json \
  https://raw.githubusercontent.com/ankit-chaubey/api-spec/main/api.json

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

# 3. Rebuild
cargo build

GitHub Actions Workflows

Workflow Trigger Purpose
auto-regenerate.yml ⏰ Daily 08:00 UTC + manual Spec sync β†’ diff β†’ codegen β†’ PR
ci.yml Every push / PR Build, test, lint on 3 OS Γ— 2 Rust versions
release.yml PR merged β†’ main Semver bump β†’ tag β†’ crates.io publish
notify.yml After regen GitHub Issue with full change summary

🀝 Contributing

Contributions are welcome!

Report issues:

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

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


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)


πŸ™ Thanks & Credits

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
ankit-chaubey / api-spec Machine-readable spec used as the codegen source

πŸ“„ License

MIT License Β© 2026 Ankit Chaubey


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