skillZs
★ LIVE SKILL TAGS ★
>>> LIVE SKILLS INDEX <<<
* OPEN SOURCE *
NO LOGIN, NO TRACKING
※ REAL INSTALL DATA ※
← back to all skills
ssheleg/telegram-dev100 installs

telegram-miniapps

Use when building or auditing a Telegram Mini App — a web page opened inside Telegram — where the whole security model is one signed blob. Covers verifying initData on the server with HMAC-SHA256 and the "WebAppData" key derivation, why initDataUnsafe is named that, the auth_date freshness window, the Ed25519 signature path for a third party with no bot token, session exchange, sending results back to the bot with sendData and answerWebAppQuery, Stars payments inside the app, the viewport and safe-area fields a real device needs, and the SDK package whose name moved. Triggers - "mini app", "miniapp", "telegram web app", "initData", "initDataUnsafe", "WebAppData", "tma.js", "@telegram-apps/sdk", "мини-апп", "телеграм веб-апп", "валидация initData". Not for the bot behind it (telegram-bots) or user accounts (telegram-userbots).

How do I install this agent skill?

npx skills add https://github.com/ssheleg/telegram-dev --skill telegram-miniapps
view source ↗

Is this agent skill safe to install?

  • Gen Agent Trust Hubpass

    The skill provides high-quality educational content and reference implementations for building secure Telegram Mini Apps. It correctly focuses on server-side cryptographic verification of initialization data, implementing HMAC-SHA256 with constant-time comparisons and mandatory freshness checks. The provided code utilizes only standard libraries and follows security best practices for handling external input and session management.

  • Socketpass

    No alerts

  • Snykpass

    Risk: LOW · No issues

What does this agent skill do?

Telegram Mini Apps — one signed blob is the whole model

A Mini App is a web page in a WebView with a query string attached. That query string is the only proof of who the user is, and it is signed with your bot token. Verify it on your server and you have authentication as strong as the bot itself; trust the parsed object the SDK hands you and you have none.

Read against core.telegram.org/bots/webapps on 2026-08-25, Bot API 10.3.

Deep material, loaded on demand:

ReadWhen
references/initdata-verification.mdimplementing or reviewing verification — both algorithms, in three languages, with the failure cases
references/app-to-bot.mdreturning a result to the bot, inline queries, and paying inside the app
references/viewport-and-platform.mdthe app looks wrong on a real phone — viewport, safe areas, fullscreen, themes

Runnable, and shipped beside this file: fixtures/verify_initdata.py — the verifier to copy, plus nine checks that watch it refuse a tampered user id, a stale auth_date, a re-serialised user field and a reversed key derivation. python3 fixtures/verify_initdata.py needs nothing but the standard library.


initDataUnsafe is named that on purpose

// The client hands you both. One is evidence, the other is a convenience.
const raw    = window.Telegram.WebApp.initData;        // the signed string — SEND THIS
const unsafe = window.Telegram.WebApp.initDataUnsafe;  // already parsed — display only

initDataUnsafe.user.id is a number a user can set to anything, because it comes from a page running on their device. Anything it decides — who you are, what you own, what you may see — must be decided again on the server from the raw string. Send initData to your backend, verify it there, and derive the identity from what you verified.

The single defect this skill exists to prevent: an endpoint that takes { userId } from the client. It works, it demos, and it lets anyone read anyone.

Verifying it, exactly

import hashlib, hmac, time
from urllib.parse import parse_qsl

MAX_AGE = 300  # seconds; a decision, not a constant handed down

def verify(init_data: str, bot_token: str) -> dict:
    pairs = dict(parse_qsl(init_data, strict_parsing=True))   # already url-decoded
    received = pairs.pop("hash", None)
    if not received:
        raise ValueError("no hash")
    pairs.pop("signature", None)   # the OTHER protocol's input — see below

    check = "\n".join(f"{k}={pairs[k]}" for k in sorted(pairs))
    secret = hmac.new(b"WebAppData", bot_token.encode(), hashlib.sha256).digest()
    expected = hmac.new(secret, check.encode(), hashlib.sha256).hexdigest()

    if not hmac.compare_digest(expected, received):
        raise ValueError("bad signature")
    if time.time() - int(pairs["auth_date"]) > MAX_AGE:
        raise ValueError("stale")
    return pairs

Five ways this goes wrong, each of which still returns "valid" for somebody:

  • The key derivation is backwards in half the snippets on the internet. The secret is HMAC_SHA256(key="WebAppData", msg=bot_token) — the constant is the key and the token is the message. Swapped, it produces a stable hash that never matches, and the usual fix is to stop checking.
  • Sort by key, join with \n, and use the values as received — url-decoded once, not re-encoded, not re-serialised. user is a JSON string and must stay the exact string that arrived; parsing and re-dumping it changes key order and breaks the hash.
  • Remove hash before building the string, and signature too — but for the right reason. signature is not junk: it is the Ed25519 path's input, a SEPARATE protocol with a SEPARATE canonicalization ({bot_id}:WebAppData prefix, then the sorted pairs without hash or signature). Keep the two canonicalizers as two functions — hmac_check_string and ed25519_check_string in fixtures/verify_initdata.py — because one shared helper serving both is how a canonicalization bug verifies itself, and the fixture's golden vectors (hand-computed, never derived by the oracle) are what catch it: an initData WITH signature passes only the correct path.
  • Duplicates and garbage are refused before any math. parse_qsl hands back a LIST; folding it into a dict keeps the last duplicate silently, and user=innocent&user=admin is an argument about which copy the HMAC covered that no verifier should be having. Refuse duplicate fields and unparseable blobs outright — and give auth_date an upper window too (clock skew, seconds), because a future timestamp only ever gets fresher.
  • auth_date is not optional. Without a freshness window a captured initData is a permanent bearer token. Pick a window, state it, and treat anything older as unauthenticated — not as an error to log and continue past.
  • Compare in constant time. == on a hex digest is a timing oracle; the language's compare_digest equivalent costs nothing.

Do not verify on every request

initData is a login, not a session. Verify it once, mint your own short-lived token, and let the rest of the API use that.

POST /auth/telegram   { initData }   →  verify → your session JWT (short TTL)
GET  /api/...         Authorization: Bearer <your token>

Re-verifying on each call forces MAX_AGE wide enough to cover a long session, which is the same as not checking auth_date at all.

The third-party path, when you have no bot token

An external service cannot hold the bot token, so Telegram signs initData a second way: an Ed25519 signature, verifiable with Telegram's public key. The signed string differs — it is prefixed with <bot_id>:WebAppData and a line feed, then the same sorted key=value pairs with both hash and signature removed. Public keys differ between test and production.

Use it when the verifier is not the bot's owner. Do not use it as a substitute for the HMAC when you do hold the token — the HMAC path is simpler and has fewer moving keys. Both, with code: references/initdata-verification.md.

The SDK's package name moved, and the org page still says the old one

  • Live package: @telegram-apps/sdk (3.11.8 on 2026-08-25), with @telegram-apps/sdk-react beside it.
  • The Telegram-Mini-Apps GitHub organisation still fronts tma.js and @tma.js/sdk, last at 3.3.0 — the earlier name, eight minors behind.
  • The templates in that organisation (reactjs-template, nextjs-template, vuejs-template, solidjs-template) are worth reading; four others there are archived and marked unsupported, and an agent copying the first result gets one of those about half the time.

Neither library is required. The Mini App interface is window.Telegram.WebApp, delivered by telegram-web-app.js; an SDK buys typed access and platform shims, not capability.

The device is not your laptop

A Mini App renders inside a WebView with a keyboard, a header and a home indicator in the way. Read the viewport fields rather than assuming: viewportHeight, viewportStableHeight, safeAreaInset, contentSafeAreaInset, isFullscreen, isActive. 100vh is wrong on both platforms and wrong differently.

Themes come from themeParams and colorScheme and change under the user, not under you — bind to them rather than hardcoding a palette. references/viewport-and-platform.md.

Getting the answer back to the bot

  • sendData(data) — for an app opened from a keyboard button. It closes the app and delivers web_app_data to the bot. The bot must still treat it as input from the user, because it is.
  • answerWebAppQuery — for an app opened from an inline button, using the query_id inside the verified initData. It has a short lifetime; a server that goes and does slow work first will find it expired.
  • Payments run through the bot, in Stars, on the same pre_checkout_query → successful_payment path telegram-bots describes. The Mini App is where the user clicks; it is not where the money is confirmed.

Before you ship

  1. The server derives identity from verified initData, never from a client-supplied id (§ initDataUnsafe is named that on purpose).
  2. auth_date is checked against a stated window, and initData is exchanged for your own session (§ Verifying it, exactly, § Do not verify on every request).
  3. The hash comparison is constant-time, and hash and signature are both removed before the check string is built.
  4. The SDK is @telegram-apps/*, and any template copied from the org is not one of the archived ones (§ The SDK's package name moved).
  5. Layout reads the safe-area and viewport fields rather than 100vh (§ The device is not your laptop).

Add the canonical catalog link to the repository README so users can inspect current installs and available audits. The publishing guide covers the complete discovery path.

<a href="https://skillzs.dev/skills/ssheleg/telegram-dev/telegram-miniapps">View telegram-miniapps on skillZs</a>