Python SDK
whisper-id on PyPI puts an agent's whole identity chain - verify any agent, provision your own, and source outbound traffic from its /128 - one import away, keyless where it can be and keyed only where it must.
If your agent runs Python - a LangChain tool, a Lambda handler, a cron job that needs to prove it's calling out from a real routable identity - you shouldn't have to hand-roll reverse-DNS, parse TLSA records, and check DANE against a validating resolver to know who you're talking to. The SDK gives you the same two-tier surface as the control plane: no key still verifies any agent against the DNSSEC root; your key unlocks register/connect/policy/logs/revoke and real egress.
Install
pip install whisper-id
Zero runtime dependencies - verify/verify_details/rdap are pure standard-library urllib calls, no requests needed. The control-plane and egress calls (register, identity, policy, logs, revoke, agent, list_agents, egress) drive the whisper CLI binary as a subprocess, so install that too (curl -fsSL https://get.whisper.online | sh, or set WHISPER_BIN to point at it) if you need anything past keyless verification. Source: github.com/whisper-sec/whisper-py. The Node sibling is whisper-id on npm; the dependency-free edge build is whisper-edge.
Tier 1 - keyless: verify without an account
No sign-up, no key. Here is the identity check both ways - first in plain Python so you can see what's happening, then in one SDK call.
Raw Python - the same checks with dnspython and the standard library, no Whisper package:
import dns.resolver, dns.reversename, ssl, hashlib, socket, requests
addr = "2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478" # demo agent "scout"
# 1. PTR: the /128 names itself
fqdn = str(dns.resolver.resolve(dns.reversename.from_address(addr), "PTR")[0]).rstrip(".")
# 2. forward-confirm: the name points back to the same /128
assert addr in {r.address for r in dns.resolver.resolve(fqdn, "AAAA")}
# 3. DANE: the TLS key is pinned in DNS (usage 3, selector 1, matching 1 - DANE-EE / SPKI / SHA-256)
tlsa = dns.resolver.resolve(f"_443._tcp.{fqdn}", "TLSA")[0]
der = ssl.get_server_certificate((fqdn, 443))
spki = hashlib.sha256(ssl.PEM_cert_to_DER_cert(der)).digest() # (SPKI extraction elided)
assert tlsa.cert == spki
That's the skeleton - but getting it right means enforcing the DNSSEC AD bit on every answer, extracting the SubjectPublicKeyInfo (not the whole cert) for the TLSA compare, and verifying the ES256 identity JWS. That fiddly part is what the SDK does for you:
With whisper-id:
from whisper_id import verify, verify_details, rdap
verify("2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478") # -> True
verify_details("2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478")
# {"is_whisper_agent": True, "fqdn": "...", "dane_ok": True, "jws_ok": True,
# "evidence": {"ptr": "...", "forward_aaaa": "...", "dane_tlsa_sha256": "9ec1ef18a1f15e54…", "dane": {...}}}
rdap("2a04:2a01:b69a:6717:e3b0:51ff:3bf7:f478") # the RFC 9083 registry record - keyless, CLI-free
A target that isn't a Whisper agent returns None from verify_details() (and False from verify()) - never a stack trace, never an opaque 500. verify() returns a bool; verify_details() returns the full evidence dict so you can log or re-derive any single step. Under the hood it walks the same chain proof-by-proof (PTR → forward AAAA → TLSA/DANE → DNSSEC AD=1 → signed JWS → transparency-log inclusion); the full walk is in Verify an agent.
Tier 2 - with a key: the control plane
The control plane is one Cypher verb over POST https://graph.whisper.online/api/query. whisper-id doesn't re-implement that HTTP call in Python - each control function is a thin wrapper that drives the whisper CLI as a subprocess (JSON out, parsed back into Python) and decodes its output, so behavior stays identical to the CLI by construction and there's exactly one client to keep in sync with the wire protocol.
Raw Python - what the wrapped call actually sends:
import requests
r = requests.post("https://graph.whisper.online/api/query",
headers={"X-API-Key": "whisper_live_…", "content-type": "application/json"},
json={"query": "CALL whisper.agents({op:'register', args:{label:'shipping-bot'}})"})
print(r.json()) # the procedure-row envelope; result.rows holds address/fqdn/api_key/...
With whisper-id - module-level functions, not a client object; WHISPER_API_KEY (or a whisper login-saved key) is picked up automatically, or pass key= explicitly:
from whisper_id import register, policy, logs, revoke
agent = register("shipping-bot") # op:identity via `whisper create --name`
print(agent.address, agent.name) # Agent has no .fqdn - look it up with agent()/rdap() if you need it
policy(block=["tor-exit", "newly-registered"]) # geography/routing policy isn't a param here yet - see /docs/resolver
for line in logs(agent=agent.address, kind="dns"):
print(line["qname"], line["decision"])
revoke(agent.address) # tears down address, DNS, DANE pin, egress - one call
Pass new_key=True to register() to mint a brand-new agent with its own API key (op:register) instead of claiming the caller's own /128 (op:identity, the default). Each function maps 1:1 to an op in the control-plane reference - if you know the Cypher shape, you know the SDK.
Egress: source Python's own traffic from the agent's /128
egress() is a context manager: it brings up the local proxy (driving whisper connect --ensure, idempotent, shared across calls) and, by default, points the standard HTTP_PROXY/HTTPS_PROXY/ALL_PROXY env vars at it for the life of the block - so requests (and anything else that honors those vars) needs no explicit proxies= argument at all.
from whisper_id import egress
import requests
with egress(agent="shipping-bot", tier="socks5") as e:
requests.get("https://api.example.com/orders") # picks up the env vars automatically
# or pass the proxy explicitly, and confirm the source address is the agent's own /128:
print(requests.get("https://rdap.whisper.online/egress-ip", proxies=e.proxies).json())
# {"ip": "2a04:2a01:…<this agent's /128>…"}
Set set_env=False if you only want the Egress handle (.proxy_url, .socks_url, .proxies) without mutating the process environment. tier="wireguard" brings up the routed Tier-1 tunnel instead, for binding the /128 at the OS level rather than per-process.
Running in AWS Lambda
egress() needs the whisper binary reachable on PATH (or WHISPER_BIN) at call time - Lambda's read-only filesystem means that binary has to arrive as part of a layer or the deployment package, alongside whisper-id itself; there is no separate crypto library to bundle since the SDK has zero pip dependencies of its own.
import os, requests
from whisper_id import egress
def handler(event, context):
with egress(agent=os.environ["WHISPER_AGENT"], tier="socks5") as e:
requests.get("https://api.example.com/orders", proxies=e.proxies)
return {"statusCode": 200}
Region notes and the layer-build walkthrough: AWS Lambda.
Graph & cognition from Python
Beyond identity and control, whisper_id.Graph is a typed wrapper over the same Graph & cognition verbs - identify, assess, explain, variants, walk, origins, history, and more, each keyless as a rate-limited taste (~100/window) and unlimited with a key - plus run_flow() for the multi-step recipe catalog (attack-path, subdomain-takeover, typosquat, and the rest) over server-sent events. Graph().recipes() lists the whole catalog with no network call at all.
from whisper_id import Graph
g = Graph() # keyless: identify/assess/explain/... at the rate-limited taste
g.identify("api.openai.com") # -> [{"host": "...", "canonical_name": "Cloudflare", ...}]
gk = Graph("whisper_live_…") # keyed: lifts the rate limit, unlocks run_flow/query/submit
gk.run_flow("typosquat", {"domain": "paypal.com"})
Errors and rate limits
Keyless calls (verify, verify_details, rdap) need no key and raise nothing on a miss - they just return None/False. Every other failure - a missing key, a bad op, the whisper binary not found on PATH, a control-plane error - raises the single exception type whisper_id.WhisperError with the underlying CLI or gateway message attached, never a bare requests.HTTPError or subprocess.CalledProcessError to unwrap. Fleet size is capped per tenant (5 agents and 5 identities by default, raised on request - see Control plane for the live numbers); list_agents()/logs() return the same shape at any scale, no separate pagination call to learn.
Next
- Control plane - every op the SDK's functions wrap
- Edge SDK - the dependency-free build for Workers, Deno, and Vercel