Frontend development of Telegram Mini Apps (TWA) often starts with choosing between the native window.Telegram.WebApp object and third-party libraries like @twa-dev/sdk. The developer's main task is to properly connect the React component lifecycle with global Telegram events and ensure strict data validation on the backend.
@twa-dev/sdk or window.Telegram.WebApp: Which to Choose
Accessing window.Telegram.WebApp directly works without installing extra packages; however, it complicates typing in TypeScript and requires constant checks for the object's existence during SSR or local runs in the browser. The @twa-dev/sdk package (or modern alternatives from the Telegram Apps ecosystem) provides a wrapper around the global object, guaranteeing type safety and correct initialization.
For React applications, the optimal solution is to create an isolated context or custom hook that encapsulates WebApp API access details and prevents errors during component mounting.
import { useEffect, useState } from 'react';
export function useTelegram() {
const [webApp, setWebApp] = useState(null);
useEffect(() => {
const app = window.Telegram?.WebApp;
if (app) {
app.ready();
setWebApp(app);
}
}, []);
return {
webApp,
user: webApp?.initDataUnsafe?.user ?? null,
initData: webApp?.initData ?? '',
mainButton: webApp?.MainButton ?? null,
close: () => webApp?.close(),
};
}Managing MainButton and Preventing Handler Duplication
The MainButton object is managed by a singleton on the Telegram client side. A common mistake among React developers is attaching an onClick handler inside a component without removing it upon unmounting. This leads to repeated function calls, duplicate order submissions, and memory leaks.
The button's lifecycle must be strictly tied to `useEffect` with cleanup functions via `offClick`:
import { useEffect } from 'react';
import { useTelegram } from './useTelegram';
export function OrderSubmitButton({ onSubmit, isLoading, text = 'ОФОРМИТЬ ЗАКАЗ' }) {
const { mainButton } = useTelegram();
useEffect(() => {
if (!mainButton) return;
mainButton.setText(text);
mainButton.show();
const handleClick = () => {
onSubmit();
};
mainButton.onClick(handleClick);
return () => {
mainButton.offClick(handleClick);
mainButton.hide();
};
}, [mainButton, text, onSubmit]);
useEffect(() => {
if (!mainButton) return;
if (isLoading) {
mainButton.showProgress(false);
mainButton.disable();
} else {
mainButton.hideProgress();
mainButton.enable();
}
}, [mainButton, isLoading]);
return null;
}The initDataUnsafe Vulnerability and Why the Raw initData String Is Needed
The initDataUnsafe object is parsed JSON accessible in JavaScript. Any user can open DevTools or tamper with this object in browser memory, modifying the user id, username, or subscription parameters. Using initDataUnsafe to make business decisions, grant permissions, or charge funds on the backend is strictly prohibited.
For secure authentication, the React application sends the raw initData string to the backend via an HTTP header (for example, Authorization: Bearer <initData>). The backend verifies the authenticity of the entire string using HMAC-SHA-256 and the bot token.
initData Validation on the PHP API Side
The validation algorithm on the backend consists of the following steps:
- Decoding the URL-encoded string and extracting the signature
hash. - Sorting the remaining parameters alphabetically and constructing a string of the form
key=value. - Calculating the secret key:
HMAC-SHA256("WebAppData", bot_token). - Calculating the hash of the constructed string and verifying it using the timing-safe
hash_equalsfunction. - Checking data freshness via the
auth_datefield to protect against replay attacks.
<?php
declare(strict_types=1);
class TelegramAuthService
{
private string $botToken;
public function __construct(?string $botToken = null)
{
$this->botToken = $botToken ?? (string) getenv('TELEGRAM_BOT_TOKEN');
if (empty($this->botToken)) {
throw new RuntimeException('TELEGRAM_BOT_TOKEN не задан');
}
}
public function validateInitData(string $initData, int $maxAgeSeconds = 86400): array
{
parse_str($initData, $data);
if (!isset($data['hash'])) {
throw new InvalidArgumentException('Параметр hash отсутствует');
}
$receivedHash = $data['hash'];
unset($data['hash']);
ksort($data);
$dataCheckArr = [];
foreach ($data as $key => $value) {
$dataCheckArr[] = $key . '=' . $value;
}
$dataCheckString = implode("
", $dataCheckArr);
$secretKey = hash_hmac('sha256', $this->botToken, 'WebAppData', true);
$calculatedHash = hash_hmac('sha256', $dataCheckString, $secretKey);
if (!hash_equals($calculatedHash, $receivedHash)) {
throw new SecurityException('Недействительная подпись initData');
}
if (isset($data['auth_date']) && (time() - (int)$data['auth_date'] > $maxAgeSeconds)) {
throw new SecurityException('Данные устарели (Replay Attack)');
}
if (isset($data['user'])) {
$data['user'] = json_decode($data['user'], true);
}
return $data;
}
}Creating a Controller and Sending a Response to the Telegram Bot API
After successfully validating `initData`, the backend controller can process the target action (for example, creating a request) and notify the user via Telegram Bot API using a cURL request.
<?php
declare(strict_types=1);
class OrderController
{
public function createLead(): void
{
header('Content-Type: application/json');
$headers = getallheaders();
$authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? '';
$initData = trim(str_replace('Bearer ', '', $authHeader));
if (empty($initData)) {
http_response_code(401);
echo json_encode(['ok' => false, 'error' => 'Токен авторизации отсутствует']);
return;
}
try {
$authService = new TelegramAuthService();
$validatedData = $authService->validateInitData($initData);
} catch (Exception $e) {
http_response_code(403);
echo json_encode(['ok' => false, 'error' => 'Ошибка авторизации: ' . $e->getMessage()]);
return;
}
$userId = $validatedData['user']['id'] ?? null;
$leadId = bin2hex(random_bytes(7));
$botToken = getenv('TELEGRAM_BOT_TOKEN');
$ch = curl_init("https://api.telegram.org/bot{$botToken}/sendMessage");
$payload = [
'chat_id' => $userId,
'text' => "<b>Заявка #" . htmlspecialchars($leadId) . " принята!</b>
Скоро мы свяжемся с вами.",
'parse_mode' => 'HTML',
];
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 5,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($response === false || $httpCode !== 200) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'cURL error: ' . $curlError]);
return;
}
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($responseData['ok'])) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Ошибка Telegram API']);
return;
}
echo json_encode(['ok' => true, 'lead_id' => $leadId]);
}
}Common Mistakes in Mini App Development
- Using
file_get_contentsinstead of cURL: leads to thread blocking during network delays and a lack of fine-grained timeout configuration. - Comparing strings with
===: the hash signature must be compared exclusively usinghash_equalsto prevent timing attacks. - Failing to unsubscribe from events: if
offClickis not called in React effects, clicking the button again will trigger the state from previous mounts. - Incorrect key order: if arrays are not sorted using
ksortbefore generating SHA256, the backend will always reject authentic Telegram data.
If you need professional development of complex Mini Apps or integration of CRM systems with Telegram, the specialists at BotCreator will help bring your project of any complexity to life.