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.
| Feature | What it enables |
|---|---|
macros | telegram_app!, telegram_page!, and telegram_router! for boilerplate-free apps and routing |
yew | use_telegram_context, reactive use_viewport / use_theme / use_safe_area hooks, and BottomButton / BackButton / SettingsButton components |
leptos | provide_telegram_context, the same reactive use_* hooks, and matching Leptos components |
mock | A configurable mock Telegram.WebApp for local development and testing |
full | Aggregates 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_callbackvsasyncpattern - Framework Integration — Leptos and Yew hooks and components
- Mock & Testing — the
mockfeature andwasm_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"] }
| Feature | Purpose |
|---|---|
macros | telegram_app!, telegram_page!, and telegram_router! macros |
yew | Yew context hook, reactive hooks, and system-button components |
leptos | Leptos context provider, reactive hooks, and system-button components |
mock | Configurable mock Telegram.WebApp for local development |
full | Shortcut 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 — initialize the SDK and show your first button
- Framework Integration — wire up Leptos or Yew
- Mock & Testing — run outside Telegram during development
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()returnsOption<TelegramWebApp>(Nonewhen not running inside Telegram) — handy for graceful degradation.TelegramWebApp::try_instance()returnsResult<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 — the full covered surface, including dialogs, storage, and sensors
- Framework Integration — reactive hooks and button components for Leptos/Yew
- Mock & Testing — run this code outside Telegram
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, ¶ms)?;
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(())
}
Navigation and links
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
- CloudStorage —
api::cloud_storage::{get_item, set_item, remove_item, get_items, remove_items, get_keys}(each returns a JSPromise). - DeviceStorage —
api::device_storage::{set, get, remove, clear}(async). - SecureStorage —
api::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
- Quick Start — the vanilla (no-framework) flow
- Mock & Testing — render components against a mock environment
- Examples — a runnable Trunk demo
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()(returningOption) in app code so the same binary degrades gracefully when neither Telegram nor the mock is present. - Each
wasm_bindgen_testshares onewindow; install the mock at the start of every test that needs it rather than relying on ordering. - Note that
TelegramContext::initsucceeds 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/vanillaapp builds with themockfeature
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 viaTelegramWebApp::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’sweb_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
- Quick Start — the minimal version of the vanilla example
- Framework Integration — Leptos/Yew patterns used in the demo
- Mock & Testing — the
mockfeature that lets these examples run outside Telegram