Telegram Mini App in React: SDK, PHP initData Validation, and MainButton Management

Developing Telegram Mini Apps (TWA) using a modern frontend stack requires strict adherence to rules for handling the messenger context and data security. When creating Single Page Applications (SPAs) with React, developers often face choices regarding SDK selection, proper interaction with system UI components (such as MainButton), and backend data validation.

@twa-dev/sdk vs. window.Telegram.WebApp: Which to Choose for React?

When integrating a React application with Telegram Mini Apps, you have two primary ways to interact with the messenger's client API:

  • Direct access to window.Telegram.WebApp: requires linking the script https://telegram.org/js/telegram-web-app.js in index.html. Out of the box, this approach lacks TypeScript typings, requires manual verification of the global object's existence, and necessitates writing your own wrappers.
  • Using the @twa-dev/sdk (or @telegram-apps/sdk) library: an npm package providing ready-to-use TypeScript types, helper methods, and a convenient wrapper around the window.Telegram.WebApp object. This simplifies working within React's component structure and prevents runtime errors during build time.

For production React applications, using npm packages is preferred as they streamline IDE autocomplete and isolate interactions with the global window object.

SDK Initialization and MainButton Management in React

Telegram's main button (MainButton) is located at the bottom of the messenger screen outside the webview container. Managing its state (text, visibility, loader) must be synchronized with the React component lifecycle.

Below is a working example of an order form component that initializes the TWA, expands the application to full screen via expand(), subscribes to clicks on MainButton, and sends the raw initData string to the backend.

import React, { useEffect, useState } from 'react';
import WebApp from '@twa-dev/sdk';

export const ServiceOrderForm: React.FC = () => {
const [service, setService] = useState('audit');
const [errorMessage, setErrorMessage] = useState('');

useEffect(() => {
// Сообщаем Telegram, что Mini App готово к отображению
WebApp.ready();
WebApp.expand();

// Настраиваем MainButton
WebApp.MainButton.setText('ОФОРМИТЬ ЗАЯВКУ');
WebApp.MainButton.show();

const handleSubmit = async () => {
WebApp.MainButton.showProgress();
setErrorMessage('');

try {
const response = await fetch('/api/lead.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
initData: WebApp.initData, // Передаем подлинную строку
service: service
})
});

const result = await response.json();

if (response.ok && result.ok) {
WebApp.close(); // Закрываем Mini App при успехе
} else {
setErrorMessage(result.error || 'Неизвестная ошибка сервера');
}
} catch (err) {
setErrorMessage('Ошибка сети при отправке данных');
} finally {
WebApp.MainButton.hideProgress();
}
};

// Подписываемся на событие нажатия
WebApp.MainButton.onClick(handleSubmit);

// Очищаем обработчики при размонтировании компонента
return () => {
WebApp.MainButton.offClick(handleSubmit);
WebApp.MainButton.hide();
};
}, [service]);

return (
<div style={{ padding: '16px', color: 'var(--tg-theme-text-color)' }}>
<h2>Выбор услуги</h2>
<select
value={service}
onChange={(e) => setService(e.target.value)}
style={{ width: '100%', padding: '8px', margin: '12px 0' }}
>
<option value="audit">Технический аудит</option>
<option value="development">Разработка Telegram бота</option>
</select>
{errorMessage && <p style={{ color: 'red' }}>{errorMessage}</p>}
</div>
);
};

initDataUnsafe vs initData: What Is the Difference and Where Does the Threat Lie?

The WebApp.initDataUnsafe object is an already parsed JavaScript object containing user data (user.id, first_name, username, etc.).

Critically important: Never use initDataUnsafe for user authentication or passing business data to the server! Any user can open DevTools in Telegram Desktop or modify the JS object in memory to impersonate an administrator or another client.

The WebApp.initData string contains a Telegram-signed set of parameters formatted as a query string. It includes a hash parameter generated by Telegram servers using HMAC-SHA-256 and your bot's secret key. Verifying this signature on the PHP backend is the only way to guarantee that the data has not been tampered with.

Validating initData in PHP

The initData verification algorithm in PHP consists of the following steps:

  1. Parsing the query string into an array of parameters.
  2. Extracting and removing the hash parameter from the array.
  3. Sorting the remaining keys alphabetically and formatting a string like key=value key2=value2.
  4. Generating an HMAC-SHA-256 hash of the WebAppData string using the bot token as the key.
  5. Calculating the final HMAC-SHA-256 of the formatted parameter string using the secret key from step 4.
  6. Comparing the calculated hash with the one provided in the hash parameter using hash_equals() (timing-safe comparison).
  7. Checking the freshness of the auth_date field (protection against Replay Attacks).

The initData validation function in PHP:

<?php

function validateTelegramInitData(string $initData, string $botToken): ?array
{
parse_str($initData, $data);

if (!isset($data['hash'])) {
return null;
}

$checkHash = $data['hash'];
unset($data['hash']);

// Сортировка ключей по алфавиту
$dataCheckArray = [];
foreach ($data as $key => $value) {
$dataCheckArray[] = $key . '=' . $value;
}
sort($dataCheckArray);
$dataCheckString = implode("
", $dataCheckArray);

// Вычисление secret_key = HMAC-SHA256("WebAppData", bot_token)
$secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

// Вычисление итогового хеша
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);

// Timing-safe сравнение
if (!hash_equals($calculatedHash, $checkHash)) {
return null;
}

// Проверка срока жизни данных (например, не старше 24 часов)
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > 86400)) {
return null;
}

// Декодируем JSON-объект пользователя, если он присутствует
if (isset($data['user'])) {
$data['user'] = json_decode($data['user'], true);
}

return $data;
}

Receiving a Lead and Making a Callback to the Telegram Bot API via cURL

After successful validation of `initData`, we create a unique application ID, save the data, and send a confirmation to the user via the Telegram Bot API.

<?php
header('Content-Type: application/json');

require_once __DIR__ . '/validate.php';

$rawInput = file_get_contents('php://input');
$input = json_decode($rawInput, true);

if (json_last_error() !== JSON_ERROR_NONE || empty($input['initData'])) {
http_response_code(400);
echo json_encode(['ok' => false, 'error' => 'Bad request']);
exit;
}

$botToken = getenv('TELEGRAM_BOT_TOKEN');
if (!$botToken) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Server config error']);
exit;
}

$validatedData = validateTelegramInitData($input['initData'], $botToken);

if (!$validatedData || empty($validatedData['user']['id'])) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Unauthorized: initData invalid']);
exit;
}

$userId = (int)$validatedData['user']['id'];
$service = filter_var($input['service'] ?? 'default', FILTER_SANITIZE_SPECIAL_CHARS);

// Генерация защищенного ID лида
$leadId = bin2hex(random_bytes(7));

// Отправка текстового сообщения в чат с пользователем через Telegram Bot API
$payload = [
'chat_id' => $userId,
'text' => "Заявка принята!

Номер заявки: {$leadId}
Услуга: {$service}",
'parse_mode' => 'HTML'
];

$ch = curl_init("https://api.telegram.org/bot{$botToken}/sendMessage");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 5
]);

$responseRaw = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

$response = json_decode($responseRaw, true);

if ($httpCode !== 200 || empty($response['ok'])) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Failed to send Telegram notification']);
exit;
}

echo json_encode(['ok' => true, 'lead_id' => $leadId]);

Common Pitfalls in React Mini App Development

  • Forgetting to unsubscribe from MainButton events: if WebApp.MainButton.offClick(handler) is not called in useEffect, duplicate handlers will be attached on every re-render. This leads to multiple parallel API requests on a single click.
  • Missing calls to ready() and expand(): without calling ready(), the messenger might not hide the initial loading indicator, and without expand(), the app window will remain in half-screen mode.
  • Relying on CSS 100vh: the screen height on mobile devices changes when the virtual keyboard appears. Use Telegram CSS variables (such as var(--tg-viewport-height)) or subscribe to WebApp.onEvent('viewportChanged', ...).

Turnkey development of reliable Telegram Mini Apps and complex integrations — available at BotCreator.

New articles on Telegram

We explain what to automate in your business and how it works in practice. No spam.