Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

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.

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. 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.0rc1

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.0rc1
File Size Uploaded
init_data_py-1.0.0rc1.tar.gz 64.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for init-data-py 1.0.0rc1
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

1.0.0

2 release files

This release

1.0.0rc1 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