Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Home

telegram-webapp-sdk is a type-safe, ergonomic Rust/WASM wrapper around the Telegram Web Apps JavaScript API. It lets you build Telegram Mini Apps entirely in Rust — with vanilla WebAssembly, or through first-class Yew and Leptos integrations.

API coverage

The crate tracks the Telegram WebApp API version 9.6, which is the latest Mini App surface exposed by telegram-web-app.js. Bot API has since advanced to 10.1, but versions 9.7–10.1 introduced no new WebApp methods, fields, or events, so the covered surface is complete. Bot API 9.5 added icon_custom_emoji_id for bottom buttons; 9.6 added WebApp.requestChat and the requestedChatSent / requestedChatFailed events — both are implemented.

The covered surface includes:

  • Buttons: Main, Secondary, Back, and Settings buttons
  • Dialogs: alert, confirm, popup, and QR-code scanner
  • Navigation, links, sharing, and invoices
  • Theme, colors, viewport, and safe-area insets
  • Storage: CloudStorage, DeviceStorage, and SecureStorage
  • Sensors: accelerometer, gyroscope, device orientation, and LocationManager
  • Biometric authentication and haptic feedback

For the full method-by-method checklist, see WEBAPP_API.md.

Feature flags

The crate is default = [] — enable only what you need.

FeatureWhat it enables
macrostelegram_app!, telegram_page!, and telegram_router! for boilerplate-free apps and routing
yewuse_telegram_context, reactive use_viewport / use_theme / use_safe_area hooks, and BottomButton / BackButton / SettingsButton components
leptosprovide_telegram_context, the same reactive use_* hooks, and matching Leptos components
mockA configurable mock Telegram.WebApp for local development and testing
fullAggregates macros, yew, leptos, and mock
telegram-webapp-sdk = { version = "0.11", features = ["leptos", "mock"] }

Documentation index

  • Installation — dependency line, feature flags, MSRV, and the wasm32 target
  • Quick Start — obtain the WebApp instance, ready()/expand(), read init data, and wire up a MainButton
  • WebApp API — a tour of the covered surface grouped by area, plus the *_with_callback vs async pattern
  • Framework Integration — Leptos and Yew hooks and components
  • Mock & Testing — the mock feature and wasm_bindgen_test
  • Examples — the demo app, vanilla example, bot, and full-stack integration

License

telegram-webapp-sdk is licensed under the MIT license.

Installation

Add the dependency

Add the crate to your Cargo.toml:

[dependencies]
telegram-webapp-sdk = "0.11"

Enable feature flags

The crate ships with no default features (default = []), so you opt into exactly what you need:

telegram-webapp-sdk = { version = "0.11", features = ["macros", "yew", "leptos", "mock"] }
FeaturePurpose
macrostelegram_app!, telegram_page!, and telegram_router! macros
yewYew context hook, reactive hooks, and system-button components
leptosLeptos context provider, reactive hooks, and system-button components
mockConfigurable mock Telegram.WebApp for local development
fullShortcut for macros + yew + leptos + mock

Typically you enable a single framework feature plus mock:

# Leptos app with local mock support
telegram-webapp-sdk = { version = "0.11", features = ["leptos", "mock"] }

Minimum Supported Rust Version

The MSRV is Rust 1.96 (edition 2024). Older toolchains are not supported.

rustup update stable
rustc --version   # must be >= 1.96

Target the browser

Telegram Mini Apps run as WebAssembly in the Telegram in-app browser, so you build for the wasm32-unknown-unknown target:

rustup target add wasm32-unknown-unknown

Bundling with Trunk or wasm-pack

Use a WASM bundler to produce the final .wasm + JS glue and an index.html.

Trunk (recommended for whole apps — this is what the demo uses):

cargo install trunk
trunk serve   # dev server with live reload
trunk build --release

A minimal index.html for Trunk pulls in Telegram’s script and your crate:

<!doctype html>
<html>
  <head>
    <script src="https://telegram.org/js/telegram-web-app.js"></script>
    <link data-trunk rel="rust" />
  </head>
  <body></body>
</html>

wasm-pack (for libraries or JS-driven integration):

cargo install wasm-pack
wasm-pack build --target web

Next steps

Quick Start

This page walks through the smallest useful Mini App: initialize the SDK, obtain the WebApp instance, signal readiness, read the launch data, and show a MainButton.

1. Initialize the SDK and get the instance

init_sdk() parses initData and themeParams from Telegram.WebApp and stores them in a global context. After that, obtain the live WebApp handle.

There are two accessors:

  • TelegramWebApp::instance() returns Option<TelegramWebApp> (None when not running inside Telegram) — handy for graceful degradation.
  • TelegramWebApp::try_instance() returns Result<TelegramWebApp, JsValue> — handy inside ?-using functions.
use telegram_webapp_sdk::{core::init::init_sdk, webapp::TelegramWebApp};
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn main() -> Result<(), JsValue> {
    init_sdk()?;

    let app = TelegramWebApp::try_instance()?;
    app.ready()?;   // tell Telegram the interface is initialized
    app.expand()?;  // expand the Mini App to full height

    Ok(())
}

If you prefer non-panicking startup, use try_init_sdk(), which returns Ok(false) when Telegram is not present:

use telegram_webapp_sdk::core::init::try_init_sdk;

match try_init_sdk() {
    Ok(true) => { /* running inside Telegram */ }
    Ok(false) => { /* regular browser — use a fallback */ }
    Err(e) => eprintln!("init failed: {e}")
}

2. Read init data and the user

Parsed launch data lives in the global TelegramContext. Access it through the closure-based getter:

use telegram_webapp_sdk::core::context::TelegramContext;

TelegramContext::get(|ctx| {
    if let Some(user) = &ctx.init_data.user {
        web_sys::console::log_1(&format!("Hello, {}", user.first_name).into());
    }
    let _ = ctx.init_data.auth_date;
    let _ = ctx.init_data.start_param.as_deref();
});

For server-side signature validation, grab the raw URL-encoded string and POST it to your backend (validate it there with your bot token, never on the client):

use telegram_webapp_sdk::TelegramWebApp;

let raw_init_data = TelegramWebApp::get_raw_init_data()?;
// POST /auth  { "init_data": raw_init_data }
Ok::<(), Box<dyn std::error::Error>>(())

3. A minimal MainButton

Set the label, show the button, and register a click callback. The callback returns an EventHandle you can later pass to remove_main_button_callback.

use telegram_webapp_sdk::webapp::TelegramWebApp;

fn run() -> Result<(), wasm_bindgen::JsValue> {
let app = TelegramWebApp::try_instance()?;

app.set_main_button_text("Send order")?;
app.set_main_button_color("#2481cc")?;
app.enable_main_button()?;
app.show_main_button()?;

let handle = app.set_main_button_callback(|| {
    if let Some(app) = TelegramWebApp::instance() {
        let _ = app.send_data("order-confirmed");
    }
})?;

// later, when the button is no longer needed:
app.remove_main_button_callback(handle)?;
app.hide_main_button()?;
Ok(())
}

Where to next

WebApp API

Almost every capability hangs off the TelegramWebApp handle (instance() / try_instance()), with a few free-function modules under telegram_webapp_sdk::api::* for sensors, storage, haptics, and theme. This page groups the surface by area. For the exhaustive checklist, see WEBAPP_API.md.

The *_with_callback vs async pattern

Every one-shot Telegram callback has two Rust siblings:

  • foo_with_callback(..., F) — synchronous registration; your closure runs when Telegram fires the result. Use it when you cannot .await (e.g. inside a non-async closure).
  • async fn foo(...) — returns the natural Rust type via .await. Prefer this in production code.
use telegram_webapp_sdk::webapp::TelegramWebApp;

async fn run() -> Result<(), wasm_bindgen::JsValue> {
let app = TelegramWebApp::try_instance()?;

let confirmed: bool = app.show_confirm("Send the order?").await?;
let scanned: String = app.show_scan_qr_popup("Scan a QR code").await?;
let granted: bool = app.request_write_access().await?;
let _ = (confirmed, scanned, granted);
Ok(())
}

The same dual applies to share_message, request_chat, check_home_screen_status, set_emoji_status, request_emoji_status_access, open_invoice, download_file, read_text_from_clipboard, show_popup, and invoke_custom_method.

Buttons

Main and Secondary bottom buttons share a BottomButton selector enum (BottomButton::Main / BottomButton::Secondary):

use telegram_webapp_sdk::webapp::{BottomButton, BottomButtonParams, TelegramWebApp};

fn run() -> Result<(), wasm_bindgen::JsValue> {
let app = TelegramWebApp::try_instance()?;

app.set_bottom_button_text(BottomButton::Main, "Submit")?;
app.show_bottom_button(BottomButton::Main)?;

// Atomic update via setParams:
let params = BottomButtonParams {
    text: Some("Submit"),
    color: Some("#2481cc"),
    text_color: Some("#ffffff"),
    is_active: Some(true),
    is_visible: Some(true),
    icon_custom_emoji_id: Some("123456789"), // Bot API 9.5+
    ..Default::default()
};
app.set_bottom_button_params(BottomButton::Main, &params)?;
Ok(())
}

Convenience aliases exist for the main button (set_main_button_text, show_main_button, enable_main_button, set_main_button_callback, …) and the secondary button (set_secondary_button_text, show_secondary_button, …).

Back and Settings buttons are driven directly:

use telegram_webapp_sdk::webapp::TelegramWebApp;
fn run() -> Result<(), wasm_bindgen::JsValue> {
let app = TelegramWebApp::try_instance()?;
app.show_back_button()?;
let handle = app.set_back_button_callback(|| { /* navigate back */ })?;
app.remove_back_button_callback(handle)?;

app.show_settings_button()?;
let sh = app.set_settings_button_callback(|| { /* open settings */ })?;
app.remove_settings_button_callback(sh)?;
Ok(())
}

Dialogs

use telegram_webapp_sdk::webapp::TelegramWebApp;
async fn run() -> Result<(), wasm_bindgen::JsValue> {
let app = TelegramWebApp::try_instance()?;
app.show_alert("Saved!")?;
let ok: bool = app.show_confirm("Delete this item?").await?;
let button_id: String = app.show_popup(&wasm_bindgen::JsValue::NULL).await?;
let code: String = app.show_scan_qr_popup("Point at a QR code").await?;
app.close_scan_qr_popup()?;
let _ = (ok, button_id, code);
Ok(())
}

open_link (with optional OpenLinkOptions), open_telegram_link, switch_inline_query, share_url, share_message / share_to_story, request_chat, add_to_home_screen, and check_home_screen_status.

Theme and colors

set_header_color, set_background_color, set_bottom_bar_color, color_scheme(), plus the free function telegram_webapp_sdk::api::theme::get_theme_params() returning a parsed palette.

Viewport and safe area

viewport_height(), viewport_width(), viewport_stable_height(), expand_viewport(), and the SafeAreaInset accessors safe_area_inset() / content_safe_area_inset(). Fullscreen and orientation live under request_fullscreen, exit_fullscreen, is_fullscreen, lock_orientation, and unlock_orientation.

Storage

  • CloudStorageapi::cloud_storage::{get_item, set_item, remove_item, get_items, remove_items, get_keys} (each returns a JS Promise).
  • DeviceStorageapi::device_storage::{set, get, remove, clear} (async).
  • SecureStorageapi::secure_storage::{set, get, restore, remove, clear} (async).
use telegram_webapp_sdk::api::device_storage::{get, set};
async fn run() -> Result<(), wasm_bindgen::JsValue> {
set("theme", "dark").await?;
let value: Option<String> = get("theme").await?;
let _ = value;
Ok(())
}

Sensors

Accelerometer, gyroscope, and device orientation each expose start(), stop(), a getter (get_acceleration(), get_angular_velocity(), get_orientation()), and lifecycle callbacks on_started / on_changed / on_stopped / on_failed. LocationManager offers init(), get_location(), open_settings(), on_location_manager_updated, and on_location_requested.

use telegram_webapp_sdk::api::accelerometer::{get_acceleration, start, stop};
start()?;
let reading = get_acceleration();
stop()?;
let _ = reading;
Ok::<(), wasm_bindgen::JsValue>(())

Biometry

api::biometric::{init, is_biometric_available, request_access, authenticate, update_biometric_token, open_settings, is_inited, is_access_granted, device_id, …}.

use telegram_webapp_sdk::api::biometric::{authenticate, init, is_biometric_available, request_access};
fn run() -> Result<(), wasm_bindgen::JsValue> {
init()?;
if is_biometric_available()? {
    request_access("auth-key", Some("Unlock the vault"), None)?;
    authenticate("auth-key", None, None)?;
}
Ok(())
}

Haptics

use telegram_webapp_sdk::api::haptic::{
    impact_occurred, notification_occurred, selection_changed,
    HapticImpactStyle, HapticNotificationType,
};
impact_occurred(HapticImpactStyle::Light)?;
notification_occurred(HapticNotificationType::Success)?;
selection_changed()?;
Ok::<(), wasm_bindgen::JsValue>(())

See also Framework Integration for reactive wrappers over viewport, theme, and safe-area events.

Framework Integration

The crate ships optional integrations for Leptos (feature leptos) and Yew (feature yew). Both expose the same three reactive hooks and the same three system-button components. The hooks seed their state with the current values and re-render when Telegram fires viewportChanged, themeChanged, safeAreaChanged, or contentSafeAreaChanged. Subscriptions are cleaned up automatically on unmount / scope disposal.

Leptos

Provide the context once near the root, then read it with use_context:

use leptos::prelude::*;
use telegram_webapp_sdk::{core::context::TelegramContext, leptos::provide_telegram_context};

#[component]
fn App() -> impl IntoView {
    provide_telegram_context().expect("context");
    let ctx = use_context::<TelegramContext>().expect("context");
    view! { <span>{ ctx.init_data.auth_date }</span> }
}

Reactive hooks

In Leptos the hooks return ReadSignal<T>, so you read them through .get() inside a closure:

use leptos::prelude::*;
use telegram_webapp_sdk::leptos::{use_safe_area, use_theme, use_viewport};

#[component]
fn Status() -> impl IntoView {
    let viewport = use_viewport(); // ReadSignal<ViewportState>
    let theme = use_theme();       // ReadSignal<ThemeState>
    let safe = use_safe_area();    // ReadSignal<SafeAreaState>
    view! {
        <div>
            { move || viewport.get().height }
            { move || theme.get().color_scheme.unwrap_or_default() }
            { move || safe.get().area.map(|i| i.top).unwrap_or(0.0) }
        </div>
    }
}

ViewportState carries height, stable_height, and is_expanded. ThemeState carries color_scheme: Option<String> and a parsed params palette. SafeAreaState carries area and content as Option<SafeAreaInset>.

Components

BottomButton takes a button selector plus reactive text (and optional color / text_color / on_click). BackButton and SettingsButton take a reactive visible signal and an optional on_click closure. All three drive the native Telegram buttons and clean up on unmount.

use leptos::prelude::*;
use telegram_webapp_sdk::{
    leptos::{provide_telegram_context, BackButton, BottomButton, SettingsButton},
    webapp::BottomButton as Btn,
};

#[component]
fn App() -> impl IntoView {
    provide_telegram_context().expect("context");
    let (text, _set_text) = signal("Send".to_owned());
    let visible = RwSignal::new(true);
    view! {
        <BottomButton button=Btn::Main text on_click=move || { /* submit */ } />
        <BackButton visible=visible on_click=move || { /* navigate back */ } />
        <SettingsButton visible=visible on_click=move || { /* open settings */ } />
    }
}

Yew

Read the context with the use_telegram_context hook. It returns Result<TelegramContext, JsValue> and reactively resolves once the context is initialized:

use telegram_webapp_sdk::yew::use_telegram_context;
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    match use_telegram_context() {
        Ok(ctx) => html! { <span>{ ctx.init_data.auth_date }</span> },
        Err(_) => html! { <div>{ "Loading Telegram context..." }</div> },
    }
}

Reactive hooks

In Yew the hooks return the state value directly (not a signal), so you read fields straight off it:

use telegram_webapp_sdk::yew::{use_safe_area, use_theme, use_viewport};
use yew::prelude::*;

#[function_component(Status)]
fn status() -> Html {
    let viewport = use_viewport(); // ViewportState
    let theme = use_theme();       // ThemeState
    let safe = use_safe_area();    // SafeAreaState
    html! {
        <div>
            { viewport.height }
            { theme.color_scheme.clone().unwrap_or_default() }
            { safe.area.map(|i| i.top).unwrap_or(0.0) }
        </div>
    }
}

Components

Yew’s components take plain props: BottomButton uses text / color / text_color / on_click (it always drives the Main button), while BackButton and SettingsButton take a visible: bool prop and an on_click: Callback<()>.

use telegram_webapp_sdk::yew::{BackButton, BottomButton, SettingsButton};
use yew::prelude::*;

#[function_component(App)]
fn app() -> Html {
    let on_main = Callback::from(|_| {});
    let on_back = Callback::from(|_| {});
    let on_settings = Callback::from(|_| {});
    html! {
        <>
            <BottomButton text="Send" color="#000" text_color="#fff" on_click={on_main} />
            <BackButton visible={true} on_click={on_back} />
            <SettingsButton visible={true} on_click={on_settings} />
        </>
    }
}

See also

Mock & Testing

The mock feature installs a configurable fake Telegram.WebApp object on window, so you can develop and test outside the Telegram client. Enable it:

telegram-webapp-sdk = { version = "0.11", features = ["mock"] }

Installing a mock environment

The mock lives under telegram_webapp_sdk::mock. Build a MockTelegramConfig (it implements Default) and call mock_telegram_webapp, which injects window.Telegram.WebApp with mocked initData, themeParams, platform, and version. Use it only in debug builds.

use telegram_webapp_sdk::mock::{config::MockTelegramConfig, init::mock_telegram_webapp};

fn run() -> Result<(), wasm_bindgen::JsValue> {
let config = MockTelegramConfig::default();
mock_telegram_webapp(config)?;
// window.Telegram.WebApp now exists — the SDK behaves as if inside Telegram.
Ok(())
}

Customizing the mocked data

MockTelegramConfig exposes optional fields for the user, auth data, every theme color, and the platform/version. A mocked user is a MockTelegramUser:

use telegram_webapp_sdk::mock::{
    config::MockTelegramConfig, data::MockTelegramUser, init::mock_telegram_webapp,
};

fn run() -> Result<(), wasm_bindgen::JsValue> {
let config = MockTelegramConfig {
    user: Some(MockTelegramUser {
        id: 42,
        first_name: "Alice".into(),
        username: Some("alice".into()),
        language_code: Some("en".into()),
        ..Default::default()
    }),
    platform: Some("android".into()),
    version: Some("9.6".into()),
    ..Default::default()
};
mock_telegram_webapp(config)?;
Ok(())
}

You can also load the config from a TOML file (the same telegram-webapp.toml that telegram_app! reads in debug builds):

use telegram_webapp_sdk::mock::config::MockTelegramConfig;

let config = MockTelegramConfig::from_file("telegram-webapp.toml")?;
Ok::<(), std::io::Error>(())

Testing with wasm_bindgen_test

Because the SDK talks to browser globals, tests run in a headless browser via wasm-bindgen-test. Add it as a dev-dependency:

[dev-dependencies]
wasm-bindgen-test = "0.3"

Configure the tests to run in a browser and install the mock before exercising the SDK:

use telegram_webapp_sdk::{
    core::init::init_sdk,
    mock::{config::MockTelegramConfig, init::mock_telegram_webapp},
    webapp::TelegramWebApp,
};
use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn instance_is_available_after_mock() {
    mock_telegram_webapp(MockTelegramConfig::default()).expect("mock installed");
    init_sdk().expect("sdk initialized");

    let app = TelegramWebApp::instance().expect("instance");
    app.ready().expect("ready");
}

Run the suite against a real browser engine:

# Chrome / Chromium
wasm-pack test --headless --chrome

# or Firefox
wasm-pack test --headless --firefox

Tips

  • Prefer TelegramWebApp::instance() (returning Option) in app code so the same binary degrades gracefully when neither Telegram nor the mock is present.
  • Each wasm_bindgen_test shares one window; install the mock at the start of every test that needs it rather than relying on ordering.
  • Note that TelegramContext::init succeeds only once per thread — design tests so they do not depend on re-initializing the global context.

See also

  • Quick Start — the code you are testing
  • Examples — the examples/vanilla app builds with the mock feature

Examples

The repository ships several runnable examples, wired into the Cargo workspace. Each shows a different integration style — from a full Trunk-built Mini App down to a plain WASM binary and the bot side of a full-stack app.

demo/ — Trunk WASM app

A complete Mini App built with the macros and mock features. It uses telegram_page! / telegram_router! for routing and includes components and pages (demo/src/components, demo/src/pages). Because it enables mock, it runs in an ordinary browser during development.

cd demo
trunk serve        # dev server with live reload at http://localhost:8080
trunk build --release

The demo/index.html loads Telegram’s telegram-web-app.js and the Trunk-built WASM bundle.

examples/vanilla — no framework

A pure-WebAssembly example (examples/vanilla/src/main.rs + ui.rs) that uses the SDK and the DOM helpers directly, with no Yew or Leptos. It depends on the crate with the mock feature, so it runs standalone. Good starting point if you want full control over the DOM.

cd examples/vanilla
trunk serve

examples/bots/rust_bot — Telegram bot

A teloxide-based bot (examples/bots/rust_bot) that opens the demo Mini App via WebApp buttons and receives orders sent from the app through sendData. This is the server side — it is a native binary, not WASM.

cd examples/bots/rust_bot
cp .env.example .env          # add your TELOXIDE_TOKEN
cargo run

Requires a bot token from @BotFather and an HTTPS-served WebApp URL.

examples/integration — full-stack

A two-part example showing the frontend↔backend round trip:

  • examples/integration/frontend — a WASM Mini App that sends a JSON message to the bot via TelegramWebApp::send_data(...). (This member is excluded from the workspace and built on its own with Trunk.)
  • examples/integration/backend — a teloxide bot that receives the update’s web_app_data, parses the JSON, and replies.
# terminal 1 — backend
cd examples/integration/backend
cargo run

# terminal 2 — frontend
cd examples/integration/frontend
trunk serve

The data flow: the user opens the Mini App → interacts with the UI → the app calls send_data → Telegram delivers the payload to the bot as web_app_data → the bot processes it and replies.

Building any WASM example

All the browser-side examples build for wasm32-unknown-unknown and are served with Trunk:

rustup target add wasm32-unknown-unknown
cargo install trunk

See also