Whisper · Docs
API reference

Control plane

Every "manage my fleet of agents" feature you'd otherwise build — mint an identity, wire up egress, set a policy, pull the logs, pull the plug — is one Cypher verb over one HTTP endpoint.

whisper.agents({op, args}): no REST surface to memorize, no SDK required, and every op has a keyless read-only sibling, so a stranger can check your work without your key.

The pain this removes

Most "agent management" APIs are a sprawl: one endpoint to create, another to list, a third for policy, a fourth for usage, a fifth to delete — each with its own path, its own verb, its own pagination quirks, and its own way of telling you "no." Multiply that by every automation platform you wire in (n8n, Zapier, a bash script, a Python notebook) and you're maintaining N client integrations against effectively the same five ideas: make one, look at them, govern them, watch them, kill one.

Whisper collapses all five into one call shape. op picks the action, args is a map, the response is always the same envelope whether you asked for a fleet listing or a kill-switch. Learn the shape once — curl, whois -h, a raw dig, a spreadsheet macro, and the whisper CLI all speak it identically — and every future op — the set has only ever grown additively, never a breaking change — costs you nothing new to integrate.

1. Endpoint & auth

Control endpoint POST https://graph.whisper.security/api/query
Auth header X-API-Key: whisper_live_…
Content-Type application/json
Body {"query": "CALL whisper.agents({op:'<op>', args:{…}})"}

There is no key in the body — it travels only in the header, so it never ends up logged in a query string or a Cypher literal. Everything under whisper.agents is scoped to the caller's own tenant; you cannot see, list, or revoke another account's agents regardless of what agent/address you pass.

With stock tools (no Whisper software — just curl):

curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_..." \
  -H "content-type: application/json" \
  -d '{"query":"CALL whisper.agents({op:'"'"'list'"'"', args:{}})"}' | jq .

With Whisper (the CLI does the Cypher-quoting and envelope-unwrapping for you):

whisper list

Building the query string by hand? Map keys are sorted and string values are single-quoted with ' doubled to '' (so Tim O'Reilly becomes 'Tim O''Reilly' — it can never break out of the literal). This is generation, not evaluation: args values are data, never executed as Cypher, so there is no injection surface no matter what a label or hostname contains.

2. The response envelope — read this once

A robust client accepts either shape it may see and always checks ok first:

// shape A — procedure-row table (what /api/query returns today)
{
  "columns": ["op","ok","status","result","error","retry_after"],
  "rows": [[ "list", true, 200,
    { "columns": ["kind","item"], "rows": [["agents", { "label":"my-agent", "...": "..." }]] },
    null, null
  ]]
}

// shape B — flat envelope (same fields, no outer table)
{ "ok": true, "status": 200, "result": {"columns":[...], "rows":[...]}, "error": null }

Unwrap result.rows (positional arrays aligned to result.columns) into records; on ok:false, surface error.detail verbatim — it's written to be actionable and is always secret-free:

{ "type":"about:blank", "title":"Forbidden", "status":403,
  "detail":"scope admin:dns required for op:register", "suggestions":["request the admin:dns scope"] }

A 429/503 may carry retry_after (seconds) — back off exactly that long, no guessing.

3. Every op

Scopes: register, policy, and revoke need admin:dns; the rest need the matching read/write scope. agent=<id> and address=<addr> are interchangeable everywhere below — anything containing : is treated as an address.

register — mint a brand-new agent, with its own key

CALL whisper.agents({op:'register', args:{label:'checkout-bot', contact_email:'ops@example.com'}})

args: label (required), contact_email (optional). returns: agent, address (a real routable /128 off 2a04:2a01::/32), fqdn, ptr, state, and api_key — the new agent's own key, handed back exactly once. Capture it or the agent has to be re-keyed.

With stock tools:

curl -s https://graph.whisper.security/api/query -H "X-API-Key: whisper_live_..." \
  -d '{"query":"CALL whisper.agents({op:'"'"'register'"'"', args:{label:'"'"'checkout-bot'"'"'}})"}'

With Whisper:

whisper agent create --label checkout-bot

identity — allocate a /128 you'll drive yourself

Same allocation machinery as register, minus the second API key — for a caller that already has a key and just wants an address.

CALL whisper.agents({op:'identity', args:{label:'my-agent'}})
CALL whisper.agents({op:'identity', args:{release:true, address:'2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4'}})

args: label (required), contact_email (optional); or release:true + address to hand one back. returns: agent, address, fqdn, ptr, state.

list — the fleet

CALL whisper.agents({op:'list', args:{kind:'agents'}})

args: kind = agents | identities | records (default agents). returns (columns): kind, item — each item is {label, fqdn, address, agent, created, state}.

whisper list                 # human table
whisper list --json | jq .   # same data, scriptable

agent — one agent's detail + live counters

CALL whisper.agents({op:'agent', args:{address:'2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4'}})

args: agent or address. returns (columns): agent, address, fqdn, ptr, label, state, allocated_at, contact, last_seen, dns_queries, dns_blocked, dns_nxdomain, packets, bytes_up, bytes_down, connections_active, connections_total.

The keyless equivalent — anyone, no key, checking the identity half of this same record from stock tools:

dig -x 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 +short
curl -s https://rdap.whisper.online/verify-identity?ip=2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4

(the usage counters are yours only, behind the key; the identity itself is public by design — see Verify an agent).

connect — egress bound to the agent's own /128

CALL whisper.agents({op:'connect', args:{agent:'checkout-bot', tier:'socks5'}})
CALL whisper.agents({op:'connect', args:{agent:'checkout-bot', tier:'wireguard', public_key:'<base64>'}})

args: agent (optional — omit to reuse the most recently connected agent), tier = socks5 (default) | wireguard | anyip. returns: tier, address, fqdn, plus transport fields — socks5/anyip return http_proxy + connection_string (each embeds a bearer) and socks5_endpoint; wireguard returns server_public_key, endpoint, dns, wireguard_config, and (only on the zero-key path) client_private_key.

Bearer hygiene: never persist the SOCKS5 bearer or a WireGuard private key to logs, workflow storage, argv, or a child's environment dump. whisper connect hands it straight to a local proxy and only ever prints a bearer-free socks5h://127.0.0.1:<port>. Full mechanics: Connect & egress.

With Whisper:

whisper connect checkout-bot                      # Tier 1.5, SOCKS5, no root needed
whisper connect checkout-bot --tier wireguard      # Tier 1, routed kernel/wireproxy tunnel

policy — the per-tenant resolver policy

CALL whisper.agents({op:'policy', args:{default:'deny', block:['bad-actor.example'], allow:['api.stripe.com']}})
CALL whisper.agents({op:'policy', args:{}})   # no args -> reads the current policy back

args (set): default = allow | deny; block/allow = arrays of names (max 1000 combined). returns (columns): key, value — e.g. ["default","allow"].

With stock tools, you can read the effect of a deny without a key at all — send the query yourself and watch the resolver refuse:

dig @2a04:2a01:0:53::53 bad-actor.example AAAA +dnssec   # NXDOMAIN if it's on the block list

With Whisper:

whisper policy set --default deny --allow api.stripe.com
whisper policy get

logs — what an agent actually did

CALL whisper.agents({op:'logs', args:{agent:'checkout-bot', kind:'dns', from:'-1h', limit:500}})

args: agent (optional), kind = dns | conn | alloc (omit for all), from/to (epoch-ms, RFC 3339, or relative like -1h), limit (default 1000, cap 10000). returns (columns): ts, kind, qname, qtype, rcode, decision, source, answer, latency_ms, agent, peer, bytes_up, bytes_down, duration_ms, reason, client_src, packets_up, packets_down — empty rows, never an error, when the window has no events.

whisper logs checkout-bot --since 1h --kind dns | jq '.[] | select(.decision=="blocked")'

revoke — the kill-switch, irreversible

CALL whisper.agents({op:'revoke', args:{agent:'checkout-bot'}})

args: agent (id or address). returns: a status field (status/state).

Revocation isn't a database flag you have to trust — it's provable with the exact same stock tools that proved the identity existed:

whisper revoke checkout-bot

# now prove it, with zero Whisper software:
dig -x 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4 +short          # -> nothing
curl -s https://rdap.whisper.online/verify-identity?ip=2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
# -> {"is_whisper_agent": false, ...}

The event also lands in the signed transparency log's revocation status-list (GET https://whisper.online/checkpoint/status-list) — see Transparency log.

4. Trial limits and scaling up

A fresh account can register and run agents immediately — no waitlist, no manual approval — up to a default fleet cap; accounts that need more open a ticket through the console to raise it, and production tenants routinely run up to 1,000 concurrently-registered agents on a single key without any change to the call shape above: list/agent/logs paginate the same way at 5 agents or 1,000. There is no per-op rate limit beyond the shared API gate; a 429 carries retry_after and should be honored exactly, not retried on a fixed backoff.

5. Reference implementation

The whisper CLI (MIT) is the canonical client — internal/client/client.go for transport/auth, cypher.go for the query builder, envelope.go for the two-shape decoder. Match its behavior and any integration you write against whisper.agents is correct by construction; the n8n node, the MCP server, and both SDKs (whisper-py, whisper-node) are all thin wrappers over exactly the calls on this page.


Next: CLI & one-command setup for the zero-config path to every op above, or Graph & cognition for the read-only identify/assess verbs that sit next to whisper.agents on the same endpoint.