PyEVP
Python library for the Email Verification Protocol (EVP): verify tokens as a relying party, or issue them for your own email domains. With EVP, the browser obtains a token from the user's email provider proving they control an address, and your server verifies it, with no confirmation email round-trip.
Documentation: https://docs.pyevp.dev/ · Live demo: https://pyevp.dev/demo
Status: alpha. The protocol (draft-hardt-email-verification, WICG Email Verification API) is still changing. PyEVP keeps every moving part in a versioned
Profileso it can follow along.
- Browsers: Chrome, with
chrome://flags/#email-verification-protocolenabled or on a site registered for the origin trial. Tested with Chrome 154. Other browsers send no token. - Email providers: Gmail issues tokens today. Your own domains can too, with
pyevp.issuer. - Python: 3.11 or newer.
Install
pip install "pyevp[dns,httpx2]" # core + the DNS and HTTP adapters
The core depends only on joserfc and idna. [dns,httpx2] adds the adapters
Verifier.default() uses; httpx works too (see DNS, HTTP and caching). [all] adds the Django
integration and the command line.
How it works
-
Render a form with a nonce kept in the user's session:
from pyevp import SessionNonces, token_input nonce = SessionNonces(session).issue() # Flask, Starlette and Django sessions all work
<input type="email" name="email" autocomplete="email"> <input type="hidden" name="evt" autocomplete="email-verification-token" nonce="{{ nonce }}">
(
token_input(nonce)renders the hidden input.) -
When the user picks an address, the browser fills
evtwith<EVT>~<KB-JWT>. -
On submit, verify it:
from pyevp import EVPError, SessionNonces, Verifier verifier = Verifier.default(audience="https://example.com") # your origin try: result = verifier.verify_submission( form.get("evt"), nonces=SessionNonces(session), email=form["email"] ) except EVPError as exc: ... # exc.code says why, e.g. "nonce_mismatch"; fall back to email confirmation else: if result is None: ... # no token: fall back to email confirmation else: result.email, result.issuer # verified
AsyncVerifierhas the same API withawait.
Everything that can be checked offline is checked first, and only hosts found through DNS are
ever contacted, never hosts named in the token. Every rejection raises an EVPError with an
error code.
More
- Quickstart and concepts
- Frameworks: FastAPI, Flask, fastapi-users, AuthX and Django
- Testing your application without network access
- Replay protection, logging and metrics
- Command line:
uvx --from "pyevp[cli]" pyevp discover gmail.comchecks a domain's issuer - Running an issuer for your own mail domains
- Compatibility policy
Examples
Each example is a standalone project with its own tests:
examples/fastapi: FastAPI with session noncesexamples/flask: the same flow with the synchronousVerifierexamples/fastapi_spa: a JSON API for a single-page app, with password recoveryexamples/fastapi_users: fastapi-users registrationexamples/authx: passwordless login with AuthXexamples/django: Django with the template tagexamples/django_allauth: a django-allauth adapterexamples/issuer_fastapi,examples/issuer_django: an issuer for your own domains
Contributing
See CONTRIBUTING.md.
License
Metadata
Release files for PyEVP 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyevp-0.2.0.tar.gz | 71.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyevp-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 166.2 kB
Release files / pyevp-0.2.0.tar.gz
| Download URL | pyevp-0.2.0.tar.gz |
|---|---|
| Size | 71.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f703a269c4cdeab9c5b44818e66a9659b22bdf8bd8fc2dfee27fa4c46d8715e6
|
|
BLAKE2b-256 checksum How to use checksums |
272b502e88e015d44f85cd562474178289066e5c885c0fef78b3db355476b97f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / pyevp-0.2.0-py3-none-any.whl
| Download URL | pyevp-0.2.0-py3-none-any.whl |
|---|---|
| Size | 94.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
769b166adb3750d1ad51cf204ad4e347b0a9b1e2be3bc166e129f5556f35561d
|
|
BLAKE2b-256 checksum How to use checksums |
958267dd76b0b64e7ee7a206e2f005f0213a6edb1b36aa876eed22ac2563e918
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log