DEV Community

Serhii
Serhii

Posted on Originally published at botservice.biz

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))
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)