proof-holdings-sdk
Official Python SDK for the proof.holdings verification API.
Installation
pip install proof-holdings-sdk
For offline JWT verification:
pip install 'proof-holdings-sdk[jwt]'
Quick Start
import asyncio
from proof_sdk import Proof
async def main():
async with Proof("pk_live_...") as proof:
# Create a phone verification
v = await proof.verifications.create(
type="phone",
channel="whatsapp",
identifier="+1234567890",
)
print(f"Verification {v['id']} created: {v['status']}")
# Wait for user to complete verification
result = await proof.verifications.wait_for_completion(v["id"])
print(f"Result: {result['status']}")
asyncio.run(main())
Resources
Verifications
# Create
v = await proof.verifications.create(type="domain", channel="dns", identifier="example.com")
# Retrieve
v = await proof.verifications.retrieve("ver_abc123")
# List with filters
page = await proof.verifications.list(status="verified", type="phone", limit=10)
# Trigger DNS/HTTP check
v = await proof.verifications.verify("ver_abc123")
# Submit OTP code
v = await proof.verifications.submit("ver_abc123", code="ABC123")
# Poll until complete
v = await proof.verifications.wait_for_completion("ver_abc123", interval=2.0, timeout=300.0)
# WABA (WhatsApp Business) verification
v = await proof.verifications.create(type="waba", channel="waba_otp", identifier="+37069199199")
# OTP is sent via WhatsApp — submit with proof.verifications.submit(v["id"], code="123456")
# Telegram Bot verification
v = await proof.verifications.create(
type="telegram_bot", channel="telegram_bot_token", identifier="@mybusinessbot"
)
# Submit bot token with proof.verifications.submit(v["id"], code="bot_token_here")
Verification Requests (Multi-Asset)
# Create multi-asset request
req = await proof.verification_requests.create(
assets=[
{"type": "phone", "required": True},
{"type": "email", "identifier": "user@example.com"},
],
reference_id="user_123",
callback_url="https://yourapp.com/webhook",
expires_in=86400, # 24 hours in seconds
)
print(f"Send user to: {req['verification_url']}")
# Poll until complete
result = await proof.verification_requests.wait_for_completion(req["id"])
Proofs
# Validate online (checks revocation)
result = await proof.proofs.validate("eyJhbGciOi...")
# Revoke
response = await proof.proofs.revoke("ver_abc123", reason="User requested")
# Get revocation list
revoked = await proof.proofs.list_revoked()
# Offline verification (requires proof-holdings-sdk[jwt]) — no API call with your key. Reads two public
# documents: the JWKS for the signature, and the issuer's status list for revocation, which is
# checked by default.
result = await proof.proofs.verify_offline("eyJhbGciOi...")
if not result["valid"]:
# "revoked" | "suspended" | "expired" | "invalid" | "status_unavailable"
# | "key_revoked" | "jwks_stale" | "wrong_key_purpose" | "outside_key_window"
# | "token_lifetime_exceeded" | "unknown_key" — see "Signing-key rotation" below
print(result["reason"])
# "status_unavailable" means the revocation status could NOT be read — an unreachable list, one
# that failed its own checks, or a slot the list does not cover. It is a refusal to guess, not a
# claim that the proof is withdrawn. A token minted without a status-list slot stays valid and
# reports revocation_checked=False, exactly as the issuer's own validate answers for it.
# Signature only, for an air-gapped check against a warm key cache:
signature_only = await proof.proofs.verify_offline("eyJhbGciOi...", check_revocation=False)
assert signature_only["revocation_checked"] is False # never assume it was checked
Signing-key rotation
verify_offline fetches and caches the issuer's key set (JWKS) itself and follows its rotation
rules. The cached set is reused for 10 minutes; a kid it does not know triggers one refetch, at
most once every 30 seconds.
key_revoked— the issuer revoked the key that signed the token. The result carriespull_hint: {"issuer_origin", "handle"}: where to fetch the re-signed token (handleis the proof'sproof_id, or a delegation'ssub). The handle is read from the token itself, whose signature a revoked key no longer vouches for: URL-encode it before building a request from it. A revocation takes effect only after two key-set fetches at least 5 minutes apart both carry it, and is then kept for a year.jwks_stale— the key set could not be refreshed and the cached copy is older thanmax_jwks_age(seconds; default 24 hours, at most 25 days;Proof(key, max_jwks_age=...)). The result carriescache_age_seconds. Callrefresh_jwks()once the issuer is reachable.wrong_key_purpose,outside_key_window,token_lifetime_exceeded— the key belongs to another token family, the token was signed outside the key's validity window, or it lives longer than the key allows.unknown_key— no published key matches the token'skid.
Revoked-key tombstones are kept in memory only — they are not persisted, so a restarted process
learns them again from the issuer. clear_tombstones() forgets them on purpose. refresh_jwks()
makes the next verification refetch the key set (and drops the cached status list); it does not
forget tombstones. A tombstone naming a key last seen live is logged as a warning on the
proof_sdk logger.
Sessions (Phone-First Flow)
session = await proof.sessions.create(channel="telegram")
print(f"Deep link: {session['deep_link']}")
print(f"QR code: {session['qr_code']}")
# Poll until verified
result = await proof.sessions.wait_for_completion(session["id"])
Webhook Deliveries
# List deliveries
deliveries = await proof.webhook_deliveries.list(status="failed")
# Retry a failed delivery
result = await proof.webhook_deliveries.retry("del_abc123")
Error Handling
from proof_sdk import Proof, NotFoundError, RateLimitError, RequestTimeoutError, ProofError
async with Proof("pk_live_...") as proof:
try:
v = await proof.verifications.retrieve("nonexistent")
except NotFoundError as e:
print(f"Not found: {e.code}")
except RateLimitError:
print("Rate limited, try again later")
except ProofError as e:
print(f"API error {e.status_code}: {e.code} - {e}")
Configuration
proof = Proof(
"pk_live_...",
base_url="https://api.proof.holdings", # Default
timeout=30.0, # Seconds, default 30
max_retries=2, # Default 2
max_jwks_age=86_400, # Offline verification stale bound, seconds (default 24h)
)
Credentials
Sign in once with npx @proof-holdings/mcp-server login and construct the client with no key:
proof = Proof() # or Proof(base_url="...")
When the key argument is omitted it is resolved in this order:
PROOF_API_KEY, when set and non-empty.- The entry for this client's base URL in
~/.proof-holdings/credentials, the file the login command writes. The file is keyed by base URL, so a key saved against staging is never sent to production. - Otherwise construction raises
ValueError, naming the three roads: pass the key, setPROOF_API_KEY, or runnpx @proof-holdings/mcp-server login.
The base URL is the base_url argument, else a non-empty PROOF_BASE_URL, else
https://api.proof.holdings. An explicit key is used as given, and an explicit empty key
(Proof("")) is an error — neither reads the environment or the file.
Requirements
- Python >= 3.10
- httpx >= 0.25.0
- cryptography >= 41.0.0 (HITL message encryption)
- PyJWT (optional, the
jwtextra, for offline verification)
Metadata
Release files for proof-holdings-sdk 1.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 | Provenance | Actions |
|---|---|---|---|---|
| proof_holdings_sdk-1.2.0.tar.gz | 110.9 kB | With provenance | Details for proof_holdings_sdk-1.2.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| proof_holdings_sdk-1.2.0-py3-none-any.whl | Python 3 | none | any | Details for proof_holdings_sdk-1.2.0-py3-none-any.whl |
Total release size: 187.5 kB
Release files / proof_holdings_sdk-1.2.0.tar.gz
| Download URL | proof_holdings_sdk-1.2.0.tar.gz |
|---|---|
| Size | 110.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b7896e6f809786a9ca22cab6d21a8f3a93642925906a33e10f42ce6aacfe77f4
|
|
BLAKE2b-256 checksum How to use checksums |
e0811290010404ffd186cdcc5a433a6a7cb7e550b04c587c41b876b6fd0301a2
|
| 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 26, 2026.
Transparency logRelease files / proof_holdings_sdk-1.2.0-py3-none-any.whl
| Download URL | proof_holdings_sdk-1.2.0-py3-none-any.whl |
|---|---|
| Size | 76.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a9cac2f613d122c864818e76bf7f20cbf468ba343bf2d1d50e70b1974938d6cb
|
|
BLAKE2b-256 checksum How to use checksums |
d9b84aab6cdce5ba396ece3568e437b3fbfde8addae008be65127d608349d615
|
| 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 26, 2026.
Transparency logSecurity for proof-holdings-sdk 1.2.0
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.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for proof_holdings_sdk-1.2.0.tar.gz
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 26, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.