This release is a pre-release and may not be stable for production use.
init-data-py
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.
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.
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.id
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. Passexpires_in=0for 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.0rc1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| init_data_py-1.0.0rc1.tar.gz | 64.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| init_data_py-1.0.0rc1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.6 kB
Release files / init_data_py-1.0.0rc1.tar.gz
| Download URL | init_data_py-1.0.0rc1.tar.gz |
|---|---|
| Size | 64.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c0d209ff0db76a775fc6faaeb43a1f928739443026b8b24b9dac614c4fe5c87a
|
|
BLAKE2b-256 checksum How to use checksums |
2678b755ad8403e5aac1a8353b963648777badaf6ce3c0b472024e765900b547
|
| 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.0rc1-py3-none-any.whl
| Download URL | init_data_py-1.0.0rc1-py3-none-any.whl |
|---|---|
| Size | 21.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
475932d29512988f8afc51eae026e7801c70e94dd408047e6164720ea2b1da0c
|
|
BLAKE2b-256 checksum How to use checksums |
fe88685c98c44d0734da6a834bdaa7d6fb729d2aac698bcb7d850f68fd4f69c8
|
| 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}
|