Appearance
Events
A Mini App talks to the client over an event bridge. The app emits events named web_app_*; the client answers with events of its own. Everything the native chrome does — buttons, popups, haptics, theme, biometrics, storage — is one of these.
Not implemented
Mini Apps do not run yet — there is no client runtime and no SDK to include. See implementation status.
How the bridge works
The client delivers events into the page by calling, per the TZ:
js
window.Telegram.WebView.receiveEvent(name, params)That namespace is written as Telegram in the specification — presumably for compatibility with existing Mini Apps, though the TZ does not say so, and does not say whether a GROM-branded alias will exist alongside it. Treat the namespace as unsettled.
The client must support every event listed here. An event a platform cannot service is answered with an UNSUPPORTED error rather than silence, which means your page can branch on capability instead of waiting on a timeout. Several events carry an explicit *_failed counterpart for exactly this.
App → client
Names only
The TZ lists these events by name and does not document their parameters. The descriptions below are the plain reading of each name; the payloads are not published, and this page will not invent them.
Where an event also exists in the Telegram Web Apps model its parameters are likely identical, but "likely" is not a schema. Anything you build against an assumed payload today is a guess you will have to re-check.
Interface
| Event | Purpose |
|---|---|
web_app_ready | The page has finished initializing and can be shown |
web_app_expand | Expand the viewport to full height |
web_app_close | Close the Mini App |
web_app_setup_main_button | Configure the primary action button |
web_app_setup_secondary_button | Configure the secondary action button |
web_app_setup_back_button | Show or hide the native back button |
web_app_setup_settings_button | Show or hide the settings entry |
web_app_setup_closing_behavior | Ask for a confirmation prompt before closing |
web_app_set_header_color | Set the header colour |
web_app_set_background_color | Set the background colour |
web_app_set_bottom_bar_color | Set the bottom bar colour |
web_app_request_fullscreen | Enter fullscreen |
web_app_exit_fullscreen | Leave fullscreen |
web_app_toggle_orientation_lock | Lock or unlock screen orientation |
Dialogs and system UI
| Event | Purpose |
|---|---|
web_app_open_popup | Native popup with buttons |
web_app_open_link | Open an external URL |
web_app_open_tg_link | Open an in-platform link |
web_app_open_invoice | Open a payment invoice |
web_app_open_scan_qr_popup | Open the QR scanner |
web_app_close_scan_qr_popup | Close the QR scanner |
web_app_read_text_from_clipboard | Request clipboard contents |
web_app_trigger_haptic_feedback | Fire a haptic |
Information and permissions
| Event | Purpose |
|---|---|
web_app_request_theme | Ask for the current theme |
web_app_request_viewport | Ask for current viewport metrics |
web_app_request_safe_area | Ask for device safe-area insets |
web_app_request_content_safe_area | Ask for insets imposed by client chrome |
web_app_request_write_access | Ask to message the user |
web_app_request_phone | Ask for the user's phone number |
web_app_request_emoji_status_access | Ask for permission to set emoji status |
web_app_set_emoji_status | Set the user's emoji status |
Sensors
| Event | Purpose |
|---|---|
web_app_start_accelerometer / web_app_stop_accelerometer | Start or stop accelerometer updates |
web_app_start_gyroscope / web_app_stop_gyroscope | Start or stop gyroscope updates |
web_app_start_device_orientation / web_app_stop_device_orientation | Start or stop orientation updates |
web_app_request_location | One-shot location request |
web_app_check_location | Check location availability and permission |
web_app_open_location_settings | Open the OS location settings |
Biometrics
| Event | Purpose |
|---|---|
web_app_biometry_get_info | Query availability, type and permission state |
web_app_biometry_request_access | Ask the user to allow biometrics |
web_app_biometry_request_auth | Prompt for a biometric check |
web_app_biometry_update_token | Store or clear the biometry-protected token |
web_app_biometry_open_settings | Open the OS biometry settings |
Storage
| Event | Purpose |
|---|---|
web_app_device_storage_save_key / get_key / clear | Plain local storage |
web_app_secure_storage_save_key / get_key / restore_key / clear | Hardware-backed storage |
device_storage is ordinary local storage — SharedPreferences on Android, localStorage on web — isolated per bot and per user. secure_storage is Keychain on iOS and Keystore with EncryptedSharedPreferences on Android, and is UNSUPPORTED on web. Any flow that requires it needs a path that works without it.
There is no cloud storage event in the TZ. If you are porting a Telegram Mini App that uses CloudStorage, that is a gap to plan around rather than an omission from this page.
Everything else
| Event | Purpose |
|---|---|
web_app_add_to_home_screen | Prompt to install a home-screen shortcut |
web_app_check_home_screen | Check whether the shortcut exists |
web_app_share_to_story | Share media to a story |
web_app_send_prepared_message | Send a message prepared earlier via savePreparedInlineMessage |
web_app_switch_inline_query | Hand a query back to inline mode |
web_app_data_send | Return data to the bot — keyboard-button launches only |
web_app_invoke_custom_method | Proxy a call to your own server |
web_app_request_file_download | Download a file to the device |
oauth_request | Begin an OAuth flow |
oauth_request is the one entry without the web_app_ prefix. The TZ writes it that way; whether that is intentional or a slip is not stated.
web_app_data_send carries the hardest constraints on this page: 4096 bytes, accepted once — later events are ignored and the WebView is force-closed — and available only from a reply-keyboard launch. See opening modes and Limits.
Client → app
These the TZ documents properly, with parameters. Reproduced as specified.
Interface
| Event | Params |
|---|---|
main_button_pressed | null |
secondary_button_pressed | null |
back_button_pressed | null |
settings_button_pressed | null |
popup_closed | button_id? |
visibility_changed | is_visible |
viewport_changed | height, is_state_stable, is_expanded |
theme_changed | theme_params |
fullscreen_changed | is_fullscreen, blur_enabled? |
fullscreen_failed | error: UNSUPPORTED / ALREADY_FULLSCREEN |
safe_area_changed | top, bottom, left, right |
content_safe_area_changed | top, bottom, left, right |
viewport_changed fires repeatedly during an animation. is_state_stable marks the final frame — that is the one to lay out against. See Design.
Payments
| Event | Params |
|---|---|
invoice_closed | slug, status |
status is one of:
| Value | Meaning |
|---|---|
cancelled | Closed before the payment form was submitted |
failed | The attempt ended in an RPC error |
pending | Additional verification (3-D Secure) finished, but the confirming service message has not arrived |
paid | Paid, and the service message was received |
WARNING
Errors inside 3-D Secure land in pending, not failed. A UI that treats "not paid yet" as "failed" will tell users their payment did not go through while it is still in flight. Only paid is success, and only your backend's record of the service message should unlock anything.
Permissions
| Event | Params |
|---|---|
write_access_requested | status: allowed / cancelled |
phone_requested | status: sent / cancelled |
emoji_status_access_requested | status: allowed / cancelled |
emoji_status_set | null |
emoji_status_failed | error: UNSUPPORTED, SUGGESTED_EMOJI_INVALID, DURATION_INVALID, USER_DECLINED, SERVER_ERROR, UNKNOWN_ERROR |
file_download_requested | status: downloading / cancelled |
Biometrics
| Event | Params |
|---|---|
biometry_info_received | available, type: finger / face / unknown, access_requested, access_granted, token_saved, device_id |
biometry_token_updated | status: updated / removed / failed |
biometry_auth_requested | status: authorized / failed, token? |
The TZ calls out one behaviour explicitly: access_requested = true with access_granted = false means the user refused. Asking again does nothing — the OS will not re-prompt. Offer to open settings instead, and send web_app_biometry_open_settings if they accept.
Sensors
| Event | Params |
|---|---|
accelerometer_started / accelerometer_stopped | null |
accelerometer_changed | x, y, z — float, m/s² |
accelerometer_failed | error: UNSUPPORTED |
gyroscope_started / gyroscope_stopped | null |
gyroscope_changed | x, y, z — float, rad/s |
gyroscope_failed | error: UNSUPPORTED |
device_orientation_started / device_orientation_stopped | null |
device_orientation_changed | alpha, beta, gamma, absolute — radians |
device_orientation_failed | error: UNSUPPORTED |
Updates are never emitted faster than the refresh_rate the app asked for, in milliseconds.
Location
| Event | Params |
|---|---|
location_requested | available, latitude, longitude, altitude, course, speed, horizontal_accuracy, vertical_accuracy, course_accuracy, speed_accuracy |
location_checked | available, access_requested?, access_granted? |
Fields the platform cannot supply arrive as null rather than being omitted. Check for null per field; a device that reports a position but no speed_accuracy is normal.
Storage
| Event | Params |
|---|---|
device_storage_key_saved | req_id |
device_storage_key_received | req_id, value or null |
device_storage_cleared | req_id |
device_storage_failed | req_id, error |
secure_storage_key_saved | req_id |
secure_storage_key_received | req_id, value or null, can_restore |
secure_storage_key_restored | req_id, value |
secure_storage_cleared | req_id |
secure_storage_failed | req_id, error — UNSUPPORTED where the platform lacks secure storage |
Every storage event carries req_id. Storage is the one part of the bridge with concurrent in-flight requests, so match responses on req_id rather than assuming the next event answers the last call.
Everything else
| Event | Params |
|---|---|
qr_text_received | data |
scan_qr_popup_closed | null |
clipboard_text_received | req_id, data? |
custom_method_invoked | req_id, result?, error? |
home_screen_added | null |
home_screen_failed | error: UNSUPPORTED |
home_screen_checked | status: unsupported / unknown / added / missed |
prepared_message_sent | null |
prepared_message_failed | error |
oauth_supported | version: 1 |
oauth_result_confirmed | result_url? |
web_app_check_home_screen is always answered with home_screen_checked — no timeout handling needed for that one specifically.
Matching requests to responses
Most pairings are unambiguous by name: web_app_start_gyroscope is answered by gyroscope_started, gyroscope_failed follows a request that could not be serviced. Three are stated outright in the TZ rather than inferred:
| Request | Response | Source |
|---|---|---|
web_app_request_theme | theme_changed | Stated |
web_app_check_home_screen | home_screen_checked | Stated, always answered |
web_app_invoke_custom_method | custom_method_invoked | Stated |
Everything else is matched by name, which is a reasonable inference and not a documented contract. Write your bridge so an unrecognized or unexpected event is logged and ignored rather than throwing.
Custom methods
A Mini App can call your own server through the client, without a direct fetch:
- App emits
web_app_invoke_custom_methodwithreq_id,method,params - Client calls
invokeWebViewCustomMethod(bot, custom_method, params) - The platform forwards it to your bot's webhook or server
- Client answers the app with
custom_method_invokedcarryingreq_idandresultorerror
This is marked P3 in the TZ — the lowest priority in this section, and the most likely to arrive last or differently.
The routing is worth thinking about before adopting it. Your server sees a request relayed by the platform, so you inherit the platform's view of who is calling rather than establishing it yourself. Whether that carries a verifiable identity — and how it relates to initData — is not specified. A plain HTTPS call from your page to your backend, authorized by a session you issued after verifying initData, has a trust model you can actually reason about.