Skip to content

JavaScript SDK

This page is shorter than you want it to be, and the reason is the point of it.

Not implemented

Mini Apps do not run yet — there is no client runtime and no SDK to include. See implementation status.

There is no script to include

Not "not yet published" — not specified. The TZ describes the Mini App bridge in terms of events, and never names the JavaScript that would wrap them. Undecided, concretely:

QuestionState
Script URL to includeNot specified
Global namespacePartly specified — see below
npm packageNot mentioned
Version negotiation (isVersionAtLeast or equivalent)Not mentioned
Outbound transport from page to clientNot specified

Any snippet on the internet that shows a GROM Mini App SDK being loaded from a URL is describing something that does not exist. Treat this page as a description of the contract an SDK would sit on top of, not of a library.

What is specified

One direction of the bridge, precisely. The client delivers events into the page by calling:

js
window.Telegram.WebView.receiveEvent(name, params)

The Telegram namespace is written that way in the specification, presumably for compatibility with Mini Apps ported from Telegram. Whether a Grom alias will exist beside it is not stated. Both possibilities are live, so do not build a hard dependency on either spelling.

The outbound half — how your page sends web_app_* events back — is not written down. The TZ says the container is a WebView on mobile (flutter_inappwebview) and a sandboxed iframe on web, which implies a native message handler in the first case and postMessage in the second, but implication is not specification. This is the single largest gap in the Mini App section, because it is the half every SDK function goes through.

Writing against it anyway

The event names in both directions are stable enough to build on — they are enumerated in the TZ and reproduced on the events page. The transport is not. So put a boundary between them:

js
// bridge.js — one file to rewrite when the transport is specified.
const handlers = new Map()

// Inbound: the one half the TZ actually fixes.
window.Telegram = window.Telegram || {}
window.Telegram.WebView = window.Telegram.WebView || {}
window.Telegram.WebView.receiveEvent = (name, params) => {
  for (const fn of handlers.get(name) || []) {
    try {
      fn(params)
    } catch (err) {
      console.error(`handler for ${name} threw`, err)
    }
  }
}

export function on(name, fn) {
  handlers.set(name, [...(handlers.get(name) || []), fn])
  return () => off(name, fn)
}

export function off(name, fn) {
  handlers.set(name, (handlers.get(name) || []).filter((f) => f !== fn))
}

// Outbound: UNSPECIFIED. This is a placeholder, not a working call.
export function send(name, params) {
  throw new Error(`outbound transport not specified: ${name}`)
}

Everything else in your app calls on() and send(). When the transport lands, one file changes.

One handler must not break the others — hence the try/catch in the dispatch loop. A bridge that lets a single listener's exception kill the rest produces failures that look like the platform dropping events.

The surface an SDK would expose

The tables below map the Telegram Web Apps object model onto the events GROM specifies. GROM tracks that model closely, so this is the likely shape — but only the middle column is sourced from the TZ. Nothing here is a GROM API you can call.

State

MemberBacked byIn the TZ
initDataLaunch fragment — verify it server-sideYes
initDataUnsafeSame, parsed client-sideConcept yes, name no
themeParamstheme_changedYes
colorSchemeDerived from theme_paramsImplied
viewportHeight, viewportStableHeight, isExpandedviewport_changedYes
safeAreaInset, contentSafeAreaInsetsafe_area_changed, content_safe_area_changedYes
isFullscreenfullscreen_changedYes
version, platformVersion negotiationNo

initDataUnsafe means what it says

Telegram's name for the client-parsed launch context is a warning label, and it should be read as one. It is fine for rendering a name before your backend answers. It is not an identity, and nothing may be authorized on it.

Authorization uses the server-verified result only. Validating initData explains what an attacker does with an app that gets this wrong: impersonate any user, by editing a URL.

Lifecycle and layout

MemberSendsAnswered by
ready()web_app_ready
expand()web_app_expandviewport_changed
close()web_app_close
requestFullscreen() / exitFullscreen()web_app_request_fullscreen / web_app_exit_fullscreenfullscreen_changed, fullscreen_failed
lockOrientation()web_app_toggle_orientation_lock
enableClosingConfirmation()web_app_setup_closing_behavior
setHeaderColor(), setBackgroundColor(), setBottomBarColor()web_app_set_header_color, web_app_set_background_color, web_app_set_bottom_bar_color

Controls

MemberSendsAnswered by
MainButtonweb_app_setup_main_buttonmain_button_pressed
SecondaryButtonweb_app_setup_secondary_buttonsecondary_button_pressed
BackButtonweb_app_setup_back_buttonback_button_pressed
SettingsButtonweb_app_setup_settings_buttonsettings_button_pressed
HapticFeedbackweb_app_trigger_haptic_feedback
MemberSendsAnswered by
showPopup(), showAlert(), showConfirm()web_app_open_popuppopup_closed
openLink()web_app_open_link
openTelegramLink()web_app_open_tg_link
openInvoice()web_app_open_invoiceinvoice_closed
showScanQrPopup() / closeScanQrPopup()web_app_open_scan_qr_popup / web_app_close_scan_qr_popupqr_text_received, scan_qr_popup_closed
readTextFromClipboard()web_app_read_text_from_clipboardclipboard_text_received

The tg in web_app_open_tg_link is in the TZ verbatim. Whether GROM keeps that spelling or renames it is not stated — another reason to route event names through one module rather than scattering string literals.

Permissions and device

MemberSendsAnswered by
requestWriteAccess()web_app_request_write_accesswrite_access_requested
requestContact()web_app_request_phonephone_requested
setEmojiStatus()web_app_set_emoji_statusemoji_status_set, emoji_status_failed
BiometricManagerweb_app_biometry_*biometry_info_received, biometry_token_updated, biometry_auth_requested
DeviceStorageweb_app_device_storage_*device_storage_*
SecureStorageweb_app_secure_storage_*secure_storage_*
(location)web_app_request_location, web_app_check_locationlocation_requested, location_checked
(sensors)web_app_start_* / web_app_stop_**_started, *_changed, *_failed

Storage and clipboard calls carry a req_id and can overlap. Match responses on req_id; do not assume the next event answers your last call.

Returning data

MemberSendsNotes
sendData()web_app_data_sendReply-keyboard launches only. 4096 bytes, accepted once, then the app is closed
switchInlineQuery()web_app_switch_inline_queryInline-mode launches
shareToStory()web_app_share_to_story
downloadFile()web_app_request_file_downloadfile_download_requested
(prepared message)web_app_send_prepared_messageprepared_message_sent, prepared_message_failed. Pairs with savePreparedInlineMessage
(custom method)web_app_invoke_custom_methodcustom_method_invoked. Marked P3

For inline-button and menu-button launches the way back into the chat is not a bridge event at all — your bot spends the query_id through answerWebAppQuery, server-side.

What to do today

Build the page and the verification. Both are real work that does not depend on the platform: the quickstart has a page shape and a backend endpoint, and validating initData has the part you cannot afford to get wrong.

Keep every bridge call behind an adapter, feature-detect by attempting and handling UNSUPPORTED, and do not ship a build that assumes a global exists. When the runtime lands, the difference between a week of work and an afternoon is whether the event names were scattered through your components or collected in one file.