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/
Status: alpha. The protocol (draft-hardt-email-verification, WICG Email Verification API) and browser support (Chrome origin trial) are still changing. This library isolates every moving part in a versioned
Profileso it can follow along.
Install
pip install "pyevp[dns,httpx2]" # core + the DNS and HTTP adapters
The core depends only on joserfc and idna. DNS and HTTP are pluggable; [dns,httpx2] installs
the default adapters used by Verifier.default(). [all] installs every optional dependency,
including the Django integration and the command line.
The HTTP adapters work with httpx2 (pydantic's maintained fork of httpx) or httpx and prefer
httpx2 when both are installed, so pip install "pyevp[dns,httpx]" works too. A client from either
library can be passed explicitly, e.g. HttpxFetcher(httpx.Client(...)).
How it works
-
Render a form with a fresh nonce stored in the user's session:
<input type="email" name="email" autocomplete="email"> <input type="hidden" name="evt" autocomplete="email-verification-token" nonce="{{ nonce }}">
-
When the user picks an address, the browser fills
evtwith<EVT>~<KB-JWT>. -
On submit, verify it:
from pyevp import Verifier, EVPError, generate_nonce verifier = Verifier.default(audience="https://example.com") # your origin try: result = verifier.verify(form["evt"], nonce=session.pop("evp_nonce"), email=form["email"]) except EVPError as exc: ... # exc.code is an ErrorCode, e.g. "nonce_mismatch"; fall back to email confirmation else: result.email, result.issuer # verified
AsyncVerifierhas the same API withawait verifier.verify(...).
Verification checks the key-binding JWT (audience, nonce, freshness, sd_hash, holder signature)
before doing any I/O. It then discovers the issuer from DNS (_email-verification.<domain>
TXT iss=…), fetches its metadata and JWKS (cached), and verifies the issuer's signature. Only
hosts derived from DNS are ever contacted, never hosts named in the token. Before connecting, the
default fetchers also check that the host resolves only to public addresses; see
private networks for what this check
does not catch.
Every rejected token raises an EVPError with an ErrorCode. The safe default is to fall back to your existing
verification flow; the error table tells which codes
the user can retry and which point at your configuration.
Command line
pyevp[cli] installs a pyevp command for relying-party developers and operators, and for
issuer operators checking their own setup. It runs without installing anything into your project:
uvx --from "pyevp[cli]" pyevp discover gmail.com # DNS record, metadata, keys vs. profile
pbpaste | uvx --from "pyevp[cli]" pyevp inspect # decode a token offline (no signature checks)
uvx --from "pyevp[cli]" pyevp verify "$TOKEN" --audience https://example.com --nonce "$NONCE"
See the CLI guide for every command and option.
More
- Frameworks: FastAPI, Flask, fastapi-users, AuthX and Django
- Testing your application:
FakeIssuerandFakeBrowser, no network needed - Replay protection, logging and metrics
- DNS, HTTP and caching: DNS over HTTPS, a standard-library-only setup, private networks
- Profiles: how PyEVP follows a protocol that is still changing
- 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, without server sessions, with password recoveryexamples/fastapi_users: fastapi-users registration that falls back to the usual verification emailexamples/authx: passwordless login with AuthXexamples/django: plain Django with the template tag andverify_requestexamples/django_allauth: a django-allauth adapterexamples/issuer_fastapi: an issuer for your own domainsexamples/issuer_django: the same issuer on Django, with Django's own users
Contributing
See CONTRIBUTING.md.
License
Metadata
Release files for PyEVP 0.1.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.1.0.tar.gz | 64.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyevp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 150.1 kB
Release files / pyevp-0.1.0.tar.gz
| Download URL | pyevp-0.1.0.tar.gz |
|---|---|
| Size | 64.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
49f354933a6d6d57c34e108ab1615329af60caf37099840d527ad9dc20e9b440
|
|
BLAKE2b-256 checksum How to use checksums |
48edd7cc08fb0bff122a4ee0a39ab99fac0605c014dacdef5a4a53389fca1f55
|
| 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.1.0-py3-none-any.whl
| Download URL | pyevp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 85.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
236774865439ffcd7f76576d30ab3ab89fe429db9eae9ced61ea563ada77b93f
|
|
BLAKE2b-256 checksum How to use checksums |
947c84d5a22b920828ac20fbc13a8fbfad68a93dbde4e1056ac6ad08545a70f2
|
| 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