Build a Telegram Mini App with React: initData Validation and MainButton

Telegram Mini App with React

This tutorial shows how to connect a React-based Telegram Mini App to a PHP backend. It covers three core areas:

1. initData vs initDataUnsafe – why you should always use HMAC-SHA-256 signed initData. 2. Backend validation – verifying the signature and checking the auth_date expiry. 3. MainButton – sending a button click from React back to your PHP API.

All examples use the official @twa-dev/sdk package (or window.Telegram.WebApp) and follow best practices for security and idempotency.

---

1. Understanding initData

A Telegram Mini App receives an initData parameter when launched. This string can be either:

- initDataUnsafe – raw parameters without any integrity check. Anyone can forge it. - Signed initData – a hash of the parameters plus a secret key, allowing you to verify the caller’s identity.

Always prefer signed initData. If you must support legacy apps, treat them as untrusted.

Signed initData format

signature=sha256(hmac_sha256(secret_key, params))

The params contain all app fields (application_id, add_time, etc.). The backend recomputes the HMAC and compares it to the received value.

---

2. Backend Validation (PHP)

Below is a minimal but complete validation routine using the @twa-dev/sdk library. It checks: - Signature correctness (timing‑safe comparison). - auth_date hasn’t expired (default 24 hours from launch). - Required fields are present.

<?php
require __DIR__ . '/vendor/autoload.php';

use TelegramMiniApp\MiniApp;
use TelegramMiniApp\Validation\InitDataValidator;

// Load configuration from environment
$appId = getenv('TELEGRAM_APP_ID');
$secretKey = getenv('TELEGRAM_SECRET_KEY'); // keep safe!
$launchTime = (new DateTime())->format('Y-m-d H:i:s');

$validator = new InitDataValidator($appId, $secretKey, $launchTime);

// Parse incoming initData (could come from window.Telegram.WebApp or start param)
$rawInitData = $_GET['initData'] ?? null;
if (!$rawInitData) {
http_response_code(400);
exit('Missing initData');
}

// Decode and validate
$decoded = json_decode(base64_decode($rawInitData), true);
if (!is_array($decoded) || !isset($decoded['signature'], $decoded['init_data'])) {
http_response_code(400);
exit('Malformed initData');
}

// Timing‑safe HMAC‑SHA-256 compare
$expectedSignature = hash_hmac('sha256', $decoded['init_data'], $secretKey);
if (!hash_equals($expectedSignature, $decoded['signature'])) {
http_response_code(401);
exit('Invalid initData signature');
}

// Check auth_date (must be within 24 hours of launch)
$now = (new DateTime())->format('Y-m-d H:i:s');
if ($now < $decoded['auth_date'] || $now > $decoded['auth_date'] + '24H') {
http_response_code(401);
exit('Session expired or invalid auth_date');
}

// Continue with your logic (e.g., create a session, show UI)
echo "Validated initData for application ID: $appId\n";

---

3. React Side: MainButton Handling

The React side sends a button click through the same channel that delivered initData. You have two options:

1. Use @twa-dev/sdk (window.Telegram.WebApp) which provides a React-friendly wrapper. 2. Call window.Telegram.WebApp.sendButtonClick() directly.

Both require a unique identifier per button press to avoid collisions.

import { useState } from 'react';
import { TelegramWebApp } from '@twavedev/sdk/react';

function MainButton() {
const [status, setStatus] = useState('waiting');

const handlePress = async () => {
// Generate a random UUID to identify this button press
const btnId = Math.random().toString(36).substring(2, 10);

try {
// Send the button click to the backend (your PHP endpoint)
await fetch('/api/main-button', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ buttonId: btnId, action: 'submit-form' }),
});

if (fetchResponse.ok) {
setStatus('success');
}
} catch (err) {
console.error(err);
setStatus('error');
}
};

return (
<div>
<button onClick={handlePress} disabled={status === 'waiting'}>
Submit
</button>
{status === 'success' && <p>Button submitted!</p>}
{status === 'error' && <p>Something went wrong.</p>}
</div>
);
}

When the backend processes the request, look for the btnId field. Store it in a DB or cache so subsequent requests can match the response to the original click.

---

4. Connecting the Two Worlds

| Step | Action | |------|---------| | 1 | Launch the Mini App from t.me/Bot?start=... (deep link). | | 2 | Server returns initData containing init_data and signature. | | 3 | Validate with the HMAC-SHA-256 routine above. | | 4 | On success, forward the btnId to your PHP handler. | | 5 | Return a JSON response { success: true } to the frontend. |

Security notes: - Never trust initDataUnsafe; always verify the signature. - Keep TELEGRAM_SECRET_KEY out of source control (use .env). - Set a short auth_date expiry (e.g., 24 h) to prevent replay attacks. - Use a unique btnId per press to avoid race conditions.

---

Further Reading

For deeper dives, see the Telegram Bot API documentation. The official SDK also has a React integration guide: @twa-dev/sdk.

Botservice — studio that ships Telegram bots / Mini Apps.

New articles on Telegram

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