Mini Apps

RU EN

Mini Apps are web apps that open right inside WWChat from a bot button. Conceptually they mirror Telegram Web Apps: you build a normal website (HTML/CSS/JS), host it yourself, and WWChat launches it in an embedded webview, passes signed user data, and exposes a JavaScript SDK for buttons, theming, popups and talking back to the bot.

The WWChat server only signs the launch parameters (initData) with your bot token. All app logic lives on your backend; WWChat never stores your Mini App code.

How it works

  1. The user taps a web_app button (under a message or the bot menu button).
  2. WWChat signs initData with your bot token and opens your URL in a webview.
  3. Inside the webview window.WWChat.WebApp is available. The app calls WebApp.ready().
  4. The app sends WebApp.initData to its own backend.
  5. The backend re-validates the HMAC with the bot token — and trusts the user's identity.

Connecting to a bot

A Mini App is launched from a button carrying a web_app field. The app URL must be https (for local development http://localhost is allowed).

Option 1. Inline button under a message

Add a web_app button to the inline_keyboard when sending a message (see sendMessage):

POST /bot/v1/{token}/sendMessage

{
  "chat_id": "550e8400-e29b-41d4-a716-446655440000",
  "text": "Open our shop:",
  "reply_markup": {
    "inline_keyboard": [
      [ { "text": "🛒 Shop", "web_app": { "url": "https://shop.example.com/app" } } ]
    ]
  }
}

If a button has a web_app field, it takes priority over url and callback_data.

Option 2. Bot menu button

The menu button is a persistent button next to the input field in a chat with the bot. Set it once with setChatMenuButton:

POST /bot/v1/{token}/setChatMenuButton

{
  "menu_button": {
    "type": "web_app",
    "text": "Open app",
    "web_app": { "url": "https://shop.example.com/app" }
  }
}

The current button is returned by getChatMenuButton. To remove the Mini App, set { "type": "default" }. You can also do this without code — via @BotMama (bot settings → Mini App).


JavaScript SDK

The SDK is injected into the page automatically before your scripts run, so window.WWChat.WebApp is already there. For local development you can include it explicitly (a re-load is a no-op):

<script src="https://api.wwchat.org/miniapp/wwchat-web-app.js"></script>

Lifecycle

WWChat.WebApp.ready();    // UI is ready — hide your loading splash
WWChat.WebApp.expand();   // expand the container to full height
WWChat.WebApp.close();    // close the Mini App

Properties

PropertyDescription
initDataSigned query string. Send it to your backend for validation.
initDataUnsafeParsed object (user, auth_date, …). Do not trust without validating the signature.
platformHost platform: ios / android / macos / windows / web.
colorSchemelight or dark.
themeParamsHost theme colors object.
viewportHeight, viewportStableHeightViewport height (px).
isExpandedWhether the container is expanded.
versionSDK version.

Main and secondary buttons

MainButton and SecondaryButton are the container's bottom buttons, controlled by the app. Every method returns the button (chainable).

WWChat.WebApp.MainButton
  .setText('Pay 500 ⭐')
  .show()
  .onClick(function () {
    WWChat.WebApp.MainButton.showProgress();
    // ... your logic, then hideProgress() ...
  });

// or flexibly in one call:
WWChat.WebApp.MainButton.setParams({
  text: 'Done',
  color: '#FF6B35',
  text_color: '#FFFFFF',
  is_active: true,
  is_visible: true
});

Methods: setText, show, hide, enable, disable, showProgress, hideProgress, setParams, onClick, offClick.

Back button

WWChat.WebApp.BackButton.show().onClick(function () {
  WWChat.WebApp.BackButton.hide();
});

Haptic feedback

WWChat.WebApp.HapticFeedback.impactOccurred('medium');        // light | medium | heavy
WWChat.WebApp.HapticFeedback.notificationOccurred('success'); // success | warning | error
WWChat.WebApp.HapticFeedback.selectionChanged();

Popups

WWChat.WebApp.showAlert('Saved');
WWChat.WebApp.showConfirm('Delete?', function (ok) { if (ok) remove(); });
WWChat.WebApp.showPopup({
  title: 'Confirm',
  message: 'Are you sure?',
  buttons: [ { id: 'ok', text: 'OK' }, { id: 'cancel', text: 'Cancel' } ]
}, function (buttonId) { /* the pressed button */ });

Misc

WWChat.WebApp.openLink('https://example.com');   // open in the external browser
WWChat.WebApp.setHeaderColor('#FF6B35');
WWChat.WebApp.setBackgroundColor('#FFFFFF');
WWChat.WebApp.sendData('payload');               // send data back to the bot (keyboard Mini Apps)

Events

WWChat.WebApp.onEvent('themeChanged', function () { applyTheme(); });
WWChat.WebApp.onEvent('viewportChanged', function (e) { /* e.height, e.is_expanded */ });

Available events: themeChanged, viewportChanged, mainButtonClicked, secondaryButtonClicked, backButtonClicked. Unsubscribe with offEvent(type, handler).


Launch parameters

WebApp.initData is the raw signed query string (forward it to your backend as-is). WebApp.initDataUnsafe is the same set parsed into an object for the UI. It's "unsafe" because you may only trust it after validating the signature on the backend.

{
  "auth_date": 1750000000,
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "first_name": "John",
    "last_name": "Doe",
    "username": "john",
    "language_code": "en",
    "photo_url": "https://..."
  },
  "query_id": "...",          // optional
  "chat_type": "private",     // optional: private | group | channel
  "start_param": "promo_42",  // optional: deep-link payload
  "hash": "abc123..."
}
FieldDescription
user.idWWChat user UUID.
user.first_name, last_name, usernameProfile. Phone and email are not passed.
user.language_codeUI language, e.g. en.
auth_dateUnix time of signing. Check freshness (≈1 hour).
chat_typeLaunch chat type: private / group / channel.
start_paramArbitrary deep-link payload (from the button/link).
hashHMAC signature. Validate it on the backend (below).

The signature is valid for about an hour (by auth_date) — the client re-requests a fresh one on every launch.


Validating initData

Never trust initDataUnsafe on the client. Send initData to your backend and recompute the HMAC with the bot token — the scheme is compatible with Telegram Web Apps:

secret_key = HMAC_SHA256(key="WebAppData", message=bot_token)
hash       = HEX( HMAC_SHA256(key=secret_key, message=data_check_string) )

where data_check_string is every field except hash, sorted by key and joined as key=value with a newline separator. If the computed hash matches the received one, the data is authentic.

Node.js

const crypto = require('crypto');

function checkInitData(initData, botToken) {
  const p = new URLSearchParams(initData);
  const hash = p.get('hash');
  p.delete('hash');
  const dataCheckString = [...p.entries()]
    .map(function (e) { return e[0] + '=' + e[1]; })
    .sort()
    .join('\n');
  const secret = crypto.createHmac('sha256', 'WebAppData').update(botToken).digest();
  const calc = crypto.createHmac('sha256', secret).update(dataCheckString).digest('hex');
  return calc === hash;   // true → signature is valid
}

Python

import hmac, hashlib
from urllib.parse import parse_qsl

def check_init_data(init_data: str, bot_token: str) -> bool:
    pairs = dict(parse_qsl(init_data))
    received = pairs.pop('hash', '')
    data_check = '\n'.join(k + '=' + pairs[k] for k in sorted(pairs))
    secret = hmac.new(b'WebAppData', bot_token.encode(), hashlib.sha256).digest()
    calc = hmac.new(secret, data_check.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(calc, received)

Keep the bot token on the server only. After validation you can also check auth_date (reject stale launches).


Replying to the chat (answerWebAppQuery)

initData includes a query_id — a signed token carrying the launch context (bot, user, chat). The bot uses it to post a Mini App's result back into the launch chat — with no state kept on the WWChat side.

Flow

  1. The Mini App gets a result from the user (e.g. a placed order).
  2. It sends WebApp.initDataUnsafe.query_id and the result to its backend.
  3. The bot backend calls answerWebAppQuery with that query_id.
  4. WWChat posts a message as the bot into the originating chat.
POST /bot/v1/{token}/answerWebAppQuery

{
  "web_app_query_id": "<query_id from initData>",
  "text": "Order placed ✅",
  "reply_markup": {
    "inline_keyboard": [
      [ { "text": "My orders", "callback_data": "orders" } ]
    ]
  }
}
ParameterTypeDescription
web_app_query_idStringThe query_id value from initData.
textStringThe message the bot will post into the chat.
reply_markupReplyMarkupOptional: an inline keyboard on the result.

query_id has a limited lifetime — answer soon after launch. The reply goes to the chat the Mini App was launched from; if it was launched outside a chat, use a regular sendMessage.


Theming

WWChat tells the app the current theme so it can look native:

var wa = WWChat.WebApp;
document.body.dataset.scheme = wa.colorScheme;     // light | dark
var p = wa.themeParams || {};
document.body.style.background = p.bg_color || '#fff';
document.body.style.color = p.text_color || '#000';

// react to theme changes on the fly:
wa.onEvent('themeChanged', function () { /* re-read wa.themeParams */ });

themeParams is the host colors object (typical keys: bg_color, text_color, hint_color, link_color, button_color, button_text_color, secondary_bg_color). You can also tint the container header and background manually: setHeaderColor / setBackgroundColor.


Full example

A minimal Mini App: greets the user, sends initData for validation, and shows the main button.

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <h1 id="hi">…</h1>
  <script src="https://api.wwchat.org/miniapp/wwchat-web-app.js"></script>
  <script>
    var wa = WWChat.WebApp;
    wa.ready();
    wa.expand();

    var u = wa.initDataUnsafe.user || {};
    document.getElementById('hi').textContent = 'Hi, ' + (u.first_name || 'guest') + '!';

    // IMPORTANT: verify authenticity on the backend, do not trust initDataUnsafe
    fetch('/api/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ initData: wa.initData })
    });

    wa.MainButton.setText('Continue').show().onClick(function () {
      wa.showAlert('Done!');
      wa.close();
    });
  </script>
</body>
</html>

From here on it's plain web development. For bot methods (sending messages, star payments) see the Bot API.