October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

Validate Telegram Mini App initData in PHP: HMAC-SHA-256, timing-safe compare, and auth_date expiry

A step-by-step PHP guide to validating Telegram Mini App initData with HMAC-SHA-256, hash_equals, and a deliberate auth_date freshness policy.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To validate Telegram Mini App initData in PHP, rebuild a data-check-string from every received field except hash, compute HMAC-SHA-256 over it with a key derived from your bot token, compare the hex result to the received hash with hash_equals(), and then check that auth_date is recent enough for your application. Telegram’s Mini Apps documentation says you should only use data from initData on the bot’s server and only after it has been validated. initDataUnsafe on the client is not trustworthy.

This guide gives a working PHP function, explains each step, and covers the places where implementations usually go wrong: query-string parsing, argument order in hash_equals(), and the fact that Telegram does not define a mandatory expiry window.

As an Amazon Associate I earn from qualifying purchases.

What the client sends and what the server must do

Inside a Mini App, Telegram.WebApp.initData is a URL-encoded query string containing fields such as query_id, user (a JSON string), auth_date, and hash. Send that raw string unchanged to your backend over HTTPS, for example in a POST body or request header. Do not send the parsed initDataUnsafe object, and never rebuild the string from parsed values on the client.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The server then performs two independent checks, and both must pass before any field is used:

  1. Authenticity: the hash matches the HMAC you compute yourself.
  2. Freshness: auth_date falls inside a window you chose.

The algorithm, step by step

1. Build the data-check-string

  • Split the raw query string into key=value pairs and URL-decode the values.
  • Remove the hash field. For this bot-token HMAC method, every other received field stays in, including signature if present.
  • Sort the remaining pairs alphabetically by key.
  • Write each as key=value and join them with a single line feed ("n", byte 0x0A). No spaces, no trailing newline.

Telegram’s example ordering is auth_date, query_id, user.

2. Derive the secret key

The secret is HMAC-SHA-256 where the message is your bot token and the key is the literal string WebAppData. This is easy to reverse by mistake. In PHP, hash_hmac() takes the data first and the key third, so the call is hash_hmac('sha256', $botToken, 'WebAppData', true). Request raw binary output (true) because this value is used as a key in the next step.

3. Compute the expected hash

Calculate HMAC-SHA-256 over the data-check-string using the derived secret, as a hexadecimal string (the default, non-binary output of hash_hmac()). This should equal the received hash.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

4. Compare in constant time

hash_equals() is PHP’s timing-attack-safe string comparison. The manual describes it as checking whether two strings are equal without leaking information about the contents of known_string via execution time. The argument order matters: the known string (your computed digest) goes first and the user-supplied string (the received hash) goes second. Both arguments must be strings, otherwise PHP raises a TypeError, so type-check the input first.

5. Check auth_date

auth_date is a Unix timestamp marking when the data was produced. Telegram recommends checking it to avoid outdated data, but the official page does not state a maximum age, a clock-skew tolerance, or a replay store. Those are your decisions. See the dedicated section below.

A complete PHP implementation

The function below parses the query string by hand rather than with parse_str(), rejects duplicate keys, and fails closed. It requires PHP 8.0 or later for str_contains()-style conveniences and typed signatures; adapt as needed for older versions.

<?php
declare(strict_types=1);

/**
 * Returns the validated fields (with 'user' decoded) or null on any failure.
 *
 * @param int $maxAgeSeconds  Your application's freshness policy (not set by Telegram).
 * @param int $futureSkew     Tolerated clock drift for auth_date in the future.
 */
function validateTelegramInitData(
    mixed $initData,
    string $botToken,
    int $maxAgeSeconds = 3600,
    int $futureSkew = 30
): ?array {
    if (!is_string($initData) || $initData === '' || $botToken === '') {
        return null;
    }

    // 1. Parse manually so names are never rewritten and duplicates are visible.
    $fields = [];
    foreach (explode('&', $initData) as $pair) {
        if ($pair === '') {
            continue;
        }
        $pos = strpos($pair, '=');
        if ($pos === false) {
            return null;                       // malformed pair
        }
        $key   = urldecode(substr($pair, 0, $pos));
        $value = urldecode(substr($pair, $pos + 1));
        if (array_key_exists($key, $fields)) {
            return null;                       // duplicate key: reject
        }
        $fields[$key] = $value;
    }

    if (!isset($fields['hash'], $fields['auth_date'])) {
        return null;
    }
    $receivedHash = $fields['hash'];
    unset($fields['hash']);

    // 2. Data-check-string: sorted by key, "key=value", joined by "n".
    ksort($fields, SORT_STRING);
    $lines = [];
    foreach ($fields as $k => $v) {
        $lines[] = $k . '=' . $v;
    }
    $dataCheckString = implode("n", $lines);

    // 3. Secret key: HMAC-SHA256(message = bot token, key = "WebAppData"), raw bytes.
    $secretKey = hash_hmac('sha256', $botToken, 'WebAppData', true);

    // 4. Expected hash as lowercase hex, compared in constant time.
    $expected = hash_hmac('sha256', $dataCheckString, $secretKey);
    if (!hash_equals($expected, $receivedHash)) {   // known first, user-supplied second
        return null;
    }

    // 5. Freshness (policy is yours).
    if (!ctype_digit($fields['auth_date'])) {
        return null;
    }
    $authDate = (int) $fields['auth_date'];
    $now = time();
    if ($authDate > $now + $futureSkew || $now - $authDate > $maxAgeSeconds) {
        return null;
    }

    // 6. Only now decode nested JSON.
    if (isset($fields['user'])) {
        $user = json_decode($fields['user'], true);
        if (!is_array($user) || !isset($user['id'])) {
            return null;
        }
        $fields['user'] = $user;
    }

    return $fields;
}

Usage:

$raw = $_POST['initData'] ?? null;               // or a request header
$data = validateTelegramInitData($raw, getenv('BOT_TOKEN') ?: '');
if ($data === null) {
    http_response_code(401);
    exit;
}
$telegramUserId = $data['user']['id'];

Return the same generic 401 for every failure so a caller cannot tell whether the signature or the timestamp was the problem. Log the reason server-side if you need it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Why not just use parse_str()?

It is the shortcut most snippets use, and it often works, but PHP’s documented behavior makes it a poor fit for signature reconstruction:

  • It URL-decodes values, which you need, but it also rewrites dots and spaces in parameter names to underscores. A field whose name contains either would be rebuilt under a different key.
  • It is subject to the max_input_vars setting, so unusually large input can be silently truncated.
  • Repeated keys collapse into a single array entry, hiding duplicates that you probably want to reject.

Any of these can make your reconstructed string differ from the one Telegram signed. Today’s standard fields have simple names, so the practical risk is lower than the principle suggests, but a manual split costs a few lines and removes the question. Whatever you choose, test the exact encodings your integration receives, including a user value with non-ASCII characters and escaped slashes.

Do not run json_decode(), htmlspecialchars(), trimming, or re-encoding on values before building the string. The signature covers the decoded text exactly as Telegram sent it.

Choosing an auth_date expiry window

Telegram defines auth_date as a Unix timestamp and advises checking it so outdated data cannot be accepted. The consulted official page gives no number of seconds. Any figure, including the 3600 default in the code above, is an application choice, not a Telegram requirement.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Points to weigh when picking one:

  • Use server time. Compare against time() on your server, never a client-supplied clock.
  • Match the risk. A read-only profile page can tolerate a longer window than a payment, withdrawal, or admin action, which might justify a few minutes.
  • Mind usability. A Mini App left open may keep presenting the same initData it received at launch. A tight window can reject legitimate users who return to an old session. A common pattern is to validate once, then issue your own short-lived session token and let that handle later requests.
  • Handle the future side. An auth_date well ahead of your clock is suspect. A small tolerance for drift is reasonable; the size is again yours to set.

Expiry is not replay protection

A valid signature within the window can be replayed by anyone who captures it, because nothing in the data changes per request. If your threat model needs one-time use, store a unique value (the hash, or query_id where present) until the window closes and refuse repeats. Telegram’s documentation does not require this, so treat it as an optional hardening measure for sensitive actions.

Best Value
Sale
Murach's PHP and MySQL: Training & Reference
  • New
  • Mint Condition
  • Dispatch same day for order received before 12 noon
  • Guaranteed packaging
  • No quibbles returns
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Bot-token HMAC versus third-party Ed25519 validation

Telegram documents two separate verification routes. They use different inputs and must not be mixed.

Bot backend HMAC (this article) Third-party Ed25519
Who verifies The bot owner’s server, which holds the token An external party that should not receive the bot token
Proof field hash signature
Key material Secret derived from the bot token with WebAppData Telegram’s public key plus the bot’s bot_id
Fields excluded from the check string hash only Both hash and signature

If you control the bot, use the HMAC route. Follow Telegram’s Mini Apps documentation for the exact Ed25519 string layout if you are the external verifier.

Common failure causes

  • Swapped HMAC arguments. Using WebAppData as the message and the token as the key yields a different secret and every request fails.
  • Hex versus binary. The secret key must be raw binary (true); the final digest must be hex.
  • Wrong separator. Use "n" in double quotes, not the literal characters backslash-n in single quotes, and no trailing newline.
  • Hash left in the string, or signature wrongly removed in the HMAC route.
  • Wrong bot token. The token must belong to the bot that launched the Mini App; a different bot or a test-environment token gives a mismatch.
  • Altered input. Middleware, frameworks, or JavaScript that decode, re-encode, or trim the string before it reaches PHP.
  • Argument order in hash_equals(). Reversing it still returns the right boolean but defeats the manual’s guidance on which side is the known string.

Operational hygiene

  • Keep the bot token in an environment variable or secret store, never in client code, repositories, or logs. Anyone with the token can forge valid initData.
  • Reject absent, empty, non-string, or oversized input before doing any work.
  • Treat the validated user.id as the identity key and look up your own account records from it; do not trust a user ID sent in a separate request field.
  • Rotate the token through BotFather if it leaks. Doing so invalidates the old derived secret.

Frequently Asked Questions

Is hash_equals timing safe in PHP?

Yes. The PHP manual describes it as comparing two strings without leaking information about the known string through execution time. Pass your computed digest first and the user-supplied hash second, and make sure both are strings.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does Telegram say how long auth_date stays valid?

No. The official Mini Apps page defines auth_date as a Unix timestamp and recommends checking it to avoid outdated data, but it gives no maximum age. You choose the window.

Quick Recap

SaleBestseller No. 4
SaleBestseller No. 5
Murach's PHP and MySQL: Training & Reference
Murach's PHP and MySQL: Training & Reference
New; Mint Condition; Dispatch same day for order received before 12 noon; Guaranteed packaging
$11.49

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.