Verify before you trust. Compute an HMAC-SHA256 over the sorted fields, compare in constant time, then check auth_date freshness. Only then read user, chat, or miniapp_id.
Overview
The frontend sends the raw initData string. Your backend must verify its signature with the bot token before using any field. User information comes from verified initData.user; MPChat does not issue platform access tokens in this phase.
Verification steps
Parse
initDataas a query string; extracthashand remove it from the signed set.Sort remaining fields by key, format as
key=valuelines, join with newline to builddata_check_string.Secret key = HMAC-SHA256(key=
"WebAppData", message=bot token).Expected hash = HMAC-SHA256(key=secret, message=
data_check_string).Compare expected and received
hashin constant time.Check
auth_datefreshness (default TTL 300s).If binding a request to one MiniApp, verify the signed
miniapp_idmatches.
Node.js example
import { createHmac, timingSafeEqual } from "node:crypto";function buildCheckString(params) {
return [...params.entries()]
.filter(([k]) => k !== "hash")
.sort(([a], [b]) => a.localeCompare(b))
.map(([k, v]) => `${k}=${v}`)
.join("\n");
}export function verifyInitData(initData, botToken, { ttlSeconds = 300, miniappId } = {}) {
const params = new URLSearchParams(initData);
const hash = params.get("hash");
const authDate = Number(params.get("auth_date"));
if (!hash || !Number.isFinite(authDate)) throw new Error("INIT_DATA_INVALID"); const secret = createHmac("sha256", "WebAppData").update(botToken).digest();
const expected = createHmac("sha256", secret).update(buildCheckString(params)).digest("hex"); const a = Buffer.from(hash, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) throw new Error("INIT_DATA_INVALID"); if (Math.floor(Date.now() / 1000) - authDate > ttlSeconds) throw new Error("INIT_DATA_INVALID");
if (miniappId && params.get("miniapp_id") !== miniappId) throw new Error("MINIAPP_FORBIDDEN"); return JSON.parse(params.get("user"));
}
Recommended error codes
Scenario | Code |
initData missing/malformed/tampered/expired | INIT_DATA_INVALID |
MiniApp does not exist | MINIAPP_NOT_FOUND |
MiniApp disabled | MINIAPP_DISABLED |
miniapp_id not owned by expected bot/context | MINIAPP_FORBIDDEN |
Not supported yet
Full client return flows are not available in the current phase: web_app_data, sendData, answerWebAppQuery, and platform access-token exchange. Your backend does not call a platform Bearer-token endpoint to get user info.
Related
Error responses and logs must never include the bot token, full initData, database IDs, or stack traces.
