Skip to main content

Verify MiniApp initData on your backend (HMAC, TTL, error codes)

The backend HMAC-SHA256 flow to verify MPChat MiniApp initData, with a Node.js example, recommended error codes, TTL, and unsupported return flows.

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 initData as a query string; extract hash and remove it from the signed set.

  • Sort remaining fields by key, format as key=value lines, join with newline to build data_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 hash in constant time.

  • Check auth_date freshness (default TTL 300s).

  • If binding a request to one MiniApp, verify the signed miniapp_id matches.

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.

Did this answer your question?