Skip to main content

init-data-py

Package version Downloads Supported Python versions License

Parse, validate and sign Telegram Mini App init data.

Init data is the signed payload a Mini App receives in window.Telegram.WebApp.initData and sends to your backend. Anyone can post a made up query string to that backend, so the signature has to be checked before anything inside it is trusted.

Installation

pip install init-data-py

The core has no dependencies. Validating with Telegram's public key instead of your bot token needs one extra:

pip install "init-data-py[ed25519]"

Requires Python 3.10 or newer.

Quick start

from init_data_py import validate_by_hash

init_data = validate_by_hash(query_string, bot_token)
print(init_data.user.id)

validate_by_hash returns the parsed init data and raises if anything is wrong, so there is never a moment where you are holding data you have not checked yet.

Validating

With your bot token

The usual case, for a backend that owns the bot.

from init_data_py import errors, validate_by_hash

try:
    init_data = validate_by_hash(query_string, bot_token)
except errors.ExpiredError as error:
    ...  # error.issued_at, error.expires_at, error.expired_for
except errors.InvalidInitDataError:
    ...  # anything else wrong with it

Init data expires after one day by default. Pass expires_in to change it, or 0 to turn the check off. Turning it off lets an old query string be replayed forever, so prefer a short lifetime.

from datetime import timedelta

validate_by_hash(query_string, bot_token, expires_in=3600)
validate_by_hash(query_string, bot_token, expires_in=timedelta(hours=1))
validate_by_hash(query_string, bot_token, expires_in=0)  # no expiry check

If you only want a boolean, use is_valid_by_hash. It returns False when the init data is unacceptable, but still raises if your own call was wrong, so a mistake in your code never looks like a rejected user.

from init_data_py import is_valid_by_hash

if not is_valid_by_hash(query_string, bot_token):
    raise PermissionError

Keeping the bot token out of the service

hash_token turns a bot token into the digest that validation actually uses. Store that instead of the token and pass token_hashed=True.

from init_data_py import hash_token, validate_by_hash

digest = hash_token(bot_token)  # do this once, somewhere safe
validate_by_hash(query_string, digest, token_hashed=True)

Without the bot token

A third party that needs to trust init data for a bot it does not own can verify Telegram's Ed25519 signature instead. This needs the ed25519 extra, and only clients from Bot API 8.0 onwards send the signature field.

from init_data_py import validate_by_signature

init_data = validate_by_signature(query_string, bot_id)
init_data = validate_by_signature(query_string, bot_id, environment="test")

is_valid_by_signature is the boolean form. If the extra is not installed, both raise MissingDependencyError rather than reporting a bad signature, so a packaging mistake can never be mistaken for a forged request. Use is_ed25519_available() to check for the extra at startup instead of finding out on the first request. The keys themselves are in TELEGRAM_PUBLIC_KEYS.

Parsing without validating

parse only reads the query string. It proves nothing about where the data came from, so do not trust anything it returns until a validator has passed.

from init_data_py import parse

init_data = parse(query_string)
init_data.user  # a User, or None
init_data.chat  # a Chat, or None
init_data.issued_at  # timezone aware UTC datetime
init_data.to_query_string()  # exactly the string that was parsed

Fields this version does not know about are kept rather than rejected, so a field Telegram adds later cannot break parsing:

init_data.get("some_new_field")  # raw value from the query string
init_data.extra  # every field with no attribute of its own
init_data.user.extra  # the same, inside user and chat

Reading the Authorization header

Mini App backends usually receive init data as Authorization: tma <init data>.

from init_data_py import strip_auth_header, validate_by_hash

raw = strip_auth_header(request.headers["Authorization"])
init_data = validate_by_hash(raw, bot_token)

Signing

Useful for tests and fixtures. Real init data comes from Telegram.

from init_data_py import UnsignedInitData, User, sign

query_string = sign(
    UnsignedInitData(user=User(id=5167898484, first_name="xin")),
    bot_token,
)

sign returns a new query string and changes nothing in place. It produces auth_date and hash itself, so passing either one in is an error. A mapping or a list of pairs works too, when you want to control the exact strings.

Errors

Everything raised by this library inherits from InitDataError. Everything caused by bad init data inherits from InvalidInitDataError, which is exactly what the is_valid_* functions turn into False.

InitDataError
├── InvalidInitDataError
│   ├── ParseError
│   │   ├── MalformedQueryStringError
│   │   ├── MalformedAuthHeaderError
│   │   ├── DuplicateFieldError
│   │   ├── MissingFieldError
│   │   │   ├── AuthDateMissingError
│   │   │   └── HashMissingError
│   │   └── MalformedFieldError
│   ├── AuthenticityError
│   │   ├── HashInvalidError
│   │   ├── SignatureMissingError
│   │   └── SignatureInvalidError
│   └── ExpiredError
└── MissingDependencyError

MissingDependencyError sits outside InvalidInitDataError on purpose. A missing optional dependency raises instead of quietly reporting invalid data.

Migrating from 0.2.x

Version 1.0 replaces the InitData class with functions. The old class rebuilt the signed string from parsed fields, which produced hashes Telegram would reject whenever a value held a slash or the JSON key order differed.

0.2.x 1.0
InitData.parse(qs) parse(qs)
InitData.from_query_string(qs) parse(qs)
init_data.validate(token) validate_by_hash(qs, token)
init_data.validate(token, raise_error=False) is_valid_by_hash(qs, token)
init_data.validate(token, lifetime=3600) validate_by_hash(qs, token, expires_in=3600)
InitData(user=user).sign(token) sign(UnsignedInitData(user=user), token)
init_data.calculate_hash(token) not public, use validate_by_hash
from init_data_py.types import User from init_data_py import User
errors.InitDataPyError errors.InitDataError
errors.UnexpectedFormatError errors.ParseError
errors.SignMissingError errors.HashMissingError
errors.SignInvalidError errors.HashInvalidError

The old error names are kept as aliases of the new classes, so existing except clauses keep working.

Two behaviour changes worth knowing:

  • Init data now expires by default. 0.2.x accepted init data of any age unless you passed lifetime. Pass expires_in=0 for the old behaviour.
  • Unknown fields no longer raise. 0.2.x rejected any field it did not recognise, which broke every time Telegram added one.

Development

This project uses uv.

uv sync --extra ed25519                        # set up the environment
uv run python -m unittest discover --verbose   # run the tests
uv run ruff check .                            # lint
uv run ruff format .                           # format
uv run mypy                                    # type check
uv build                                       # build the distributions

The test suite must also pass without the extra, since the core is meant to have no dependencies:

uv sync && uv run python -m unittest discover

License

This library is licensed under the MIT License.

Metadata

Release files for init-data-py 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for init-data-py 1.0.0
File Size Uploaded
init_data_py-1.0.0.tar.gz 64.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for init-data-py 1.0.0
File Interpreter ABI Platform
init_data_py-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 85.7 kB

Release files / init_data_py-1.0.0.tar.gz

Download URL init_data_py-1.0.0.tar.gz
Size 64.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d6b372db700126d8fb4e86db733b6b35897c1357ca698f3d65c8e8d94ae630e3
BLAKE2b-256 checksum
How to use checksums
ef9d20908944654f10884e4133ff87f8e04d3bdb281b0c0deed2ad7141465058
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / init_data_py-1.0.0-py3-none-any.whl

Download URL init_data_py-1.0.0-py3-none-any.whl
Size 21.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8e9789010d8a926194ff95714ea3dda7b84450daa7a113a54be1da45fa6c3185
BLAKE2b-256 checksum
How to use checksums
8404e01f1576e30807db86adc82913512ed65c2f439eb1522b0efa6a817c7097
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.10 {"installer":{"name":"uv","version":"0.12.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

0.2.7

2 release files

0.2.6

2 release files

0.2.5

2 release files

0.2.4

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page