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
- The user taps a
web_appbutton (under a message or the bot menu button). - WWChat signs
initDatawith your bot token and opens your URL in a webview. - Inside the webview
window.WWChat.WebAppis available. The app callsWebApp.ready(). - The app sends
WebApp.initDatato its own backend. - 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
| Property | Description |
|---|---|
initData | Signed query string. Send it to your backend for validation. |
initDataUnsafe | Parsed object (user, auth_date, …). Do not trust without validating the signature. |
platform | Host platform: ios / android / macos / windows / web. |
colorScheme | light or dark. |
themeParams | Host theme colors object. |
viewportHeight, viewportStableHeight | Viewport height (px). |
isExpanded | Whether the container is expanded. |
version | SDK 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..."
} | Field | Description |
|---|---|
user.id | WWChat user UUID. |
user.first_name, last_name, username | Profile. Phone and email are not passed. |
user.language_code | UI language, e.g. en. |
auth_date | Unix time of signing. Check freshness (≈1 hour). |
chat_type | Launch chat type: private / group / channel. |
start_param | Arbitrary deep-link payload (from the button/link). |
hash | HMAC 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
- The Mini App gets a result from the user (e.g. a placed order).
- It sends
WebApp.initDataUnsafe.query_idand the result to its backend. - The bot backend calls
answerWebAppQuerywith thatquery_id. - 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" } ]
]
}
} | Parameter | Type | Description |
|---|---|---|
web_app_query_id | String | The query_id value from initData. |
text | String | The message the bot will post into the chat. |
reply_markup | ReplyMarkup | Optional: 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.