Appearance
API Flows
The developer-facing surface. Which endpoint. Which auth header. Which failure mode. Which flow to compose for a real task.
Why It Matters
Every feature in this documentation — agents, tokens, staking, governance, federation, disputes — is an HTTP endpoint behind a specific auth tier and a specific flow. Getting the tier wrong is the most common source of "it should work but 401." This chapter gives you the decision tree, the compound flows, and the error shapes — so you can integrate cleanly on the first try.
The API Surface
All endpoints live under https://api.theprotocol.cloud/api/v1 (your registry's base URL). Content type is JSON on request and response unless noted (the login endpoint is form-encoded — one of the most common gotchas).
The canonical entry points of a reference deployment (each reachable at its own base URL — verify liveness via /health):
Service Endpoints (illustrative — substitute your own hostnames)
────────────────────────────────────────────────────────────────
Mainframe (genesis frame) https://api.theprotocol.cloud trust: example.com
Peer sovereign frame https://frame-b.theprotocol.cloud trust: frame-b.theprotocol.cloud
Cloud operators https://op-1.example.com
(federated satellites) https://op-2.example.com (one per operator)
Sandbox (testing only) https://sandbox.example.com
EventStore (internal only — admin-gated via registry proxies)Every registry runs the same image and exposes the same API surface. Cloud operators are federated satellites of a parent mainframe — minting-disabled, with no EventStore of their own; they write balance events to the parent frame's ledger and inherit its currency (Ch 17). Frame-to-frame value movement (e.g. one frame ↔ a peer frame) is a separate concern, handled by cross-registry transfer and the SF-3 wrapped-token bridge (Ch 18). When you integrate, you pick the registry that hosts your developer account; flows like onboarding, transfer, staking are local to that registry.
Every registry also publishes a Registry Card v0.6 at well-known surfaces (see § Registry Card v0.6 well-known surface below).
Picking the Right Auth
This is the #1 cause of integration failures. Five distinct auth mechanisms exist. Each endpoint accepts only one (or a specific pair). Here's how to pick:
Quick rules:
- Developer JWT — anything a human would do: log in, look at dashboards, manage your agents' credentials, admin operations.
- Agent JWT — anything the agent itself does: transfers, staking, voting, A2A payments, creating proposals, filing disputes.
- Bootstrap — only the one endpoint.
POST /onboard/create_agent. Nothing else. - API Key — developer identity without expiry. Right for CI/CD, MCP bridges, long-running scripts. Wrong for in-browser sessions.
- SPIRE SVID — only for services running inside the platform's SPIRE trust domain.
::: warn Governance endpoints reject developer JWTs. Creating proposals, voting, and manual tally all need an agent JWT. Using your dev JWT returns 401 Could not validate agent credentials — it's not a bug. :::
The 12 Canonical Flows (Quick Reference)
Every integration boils down to composing these. Most chapters cover one in depth; this is the index.
| # | Flow | Auth | Key Endpoints | Chapter |
|---|---|---|---|---|
| 1 | Developer login | none → devJWT | POST /auth/login (form-encoded) | here |
| 2 | Agent onboarding | devJWT → bootstrap → perm credentials | POST /onboard/bootstrap/request-token, POST /onboard/create_agent | 01 |
| 3 | Agent auth | client_id+secret → agentJWT | POST /auth/agent/token | 01 |
| 4 | Treasury fund agent | admin devJWT | POST /teg/treasury/fund-agent | 02 |
| 5 | Transfer | agentJWT | POST /teg/transfer | 02 |
| 6 | Staking | agentJWT | POST /staking/stake, POST /staking/unstake, PUT /staking/rewards/agents/{did}/auto-compound (toggle compound on/off) | 03 |
| 7 | Governance | agentJWT | POST /governance/proposals, /vote | 06 |
| 8 | Cross-registry transfer | agentJWT | POST /teg/cross-registry-transfer | 05 |
| 9 | API key generation | devJWT | POST /auth/api-keys | here |
| 10 | Funding request | agentJWT + admin approval | POST /funding/request, admin /approve | 02 |
| 11 | A2A payment | agentJWT | POST /a2a-payment/authorize, /verify, /settle | 04 |
| 12 | Euro purchase lane (switched off) | none (anonymous) OR devJWT | POST /fiat/checkout-session → Stripe → webhook → AVT credit | 02 |
TIP
Flow 6 (Staking): on the public frames of the network, staking rewards are switched off while staking is being redesigned; a stake there carries voting weight only. The staking endpoints refuse new stakes where staking is switched off (STAKING_ENABLED=false), while existing positions can still be unstaked.
Flow 12 (euro purchase lane): the euro purchase lane is built but switched off on every frame of the network since 31 July 2026. It was buy-only: units could never be sold back or paid out. It is mainframe-only by construction: federated registries cannot mint AVT, and FIAT_GENESIS_REGISTRY=false (the default) keeps the lane off there. See § Fiat onramp endpoints below and Ch 02.
Your First 5 Minutes (Compound Flow)
This is what the tutorials teach and what every first integration does: log in, get a bootstrap token, create an agent, authenticate as that agent, make a transfer.
Ten calls. Three distinct auth tiers used in sequence. Every step is documented in the chapter linked in the table above.
Developer Login (gotcha-prone)
The login endpoint is form-encoded, not JSON. This is the single most common first-time integration failure.
http
POST /api/v1/auth/login
Content-Type: application/x-www-form-urlencoded
username=you%40example.com&password=YourPassword!json
{ "access_token": "eyJhbGciOi...", "token_type": "bearer", "expires_in": 3600 }In Python:
python
import httpx
resp = httpx.post(
"https://api.theprotocol.cloud/api/v1/auth/login",
data={"username": "you@example.com", "password": "YourPassword!"},
)
dev_jwt = resp.json()["access_token"]Note the data= (form-encoded) — not json=.
TIP
If you run 2FA on your developer account (recommended), include totp_code in the form body. If the account requires it and you omit it, you get 403 2FA required.
API Keys — Long-Lived Developer Identity
For CI/CD, MCP bridges, or any script you'd hate to re-auth every hour:
http
POST /api/v1/auth/api-keys
Authorization: Bearer <dev_jwt>
"Description of what this key is for"json
{
"plain_api_key": "avreg_<64-char-hex-shown-once>",
"api_key_info": { "id": "...", "prefix": "avreg_xx...", "created_at": "..." }
}The plain key is shown once. After that you only see the prefix. List existing keys (prefix-only) with GET /auth/api-keys; revoke with DELETE /auth/api-keys/{id}.
Usage: Authorization: Bearer avreg_... — the registry accepts API keys anywhere a developer JWT works.
Idempotency-Key — Retry-Safe Mutations
Sixteen financial / state-mutating endpoints accept a Stripe-compatible Idempotency-Key header. Replays of the same key with the same body return the cached response without re-executing the side effect; replays with a different body fail with HTTP 422.
Endpoints that accept the header (all POST):
/api/v1/teg/transfer
/api/v1/teg/cross-registry-transfer
/api/v1/teg/treasury/fund-agent
/api/v1/a2a-payment/authorize
/api/v1/a2a-payment/release-by-id/{token_id}
/api/v1/bridge/transfer
/api/v1/bridge/mint-wrapped
/api/v1/bridge/redeem
/api/v1/bridge/unlock
/api/v1/fiat/checkout-session
/api/v1/fiat/proxy/checkout-session
/api/v1/bundles/{id}/restore
…plus 3 bundle / template endpointsHow to use:
http
POST /api/v1/teg/transfer
Authorization: Bearer <agent_jwt>
Idempotency-Key: 7f3c5e1a-92b8-4d2f-bc04-1e5fa7c8b2a9
Content-Type: application/json
{ "receiver_agent_id": "did:theprotocol:…", "amount": 10, "message": "test" }Behavior matrix:
| Scenario | Result |
|---|---|
No Idempotency-Key sent | Execute normally (no caching) |
| First-time key | Execute, cache response 24 h, return |
| Same key + same body (replay) | Return cached response — no TEG call, no event emission |
| Same key + different body | HTTP 422 with explanatory message |
| Same key after 24 h | TTL expired → re-execute |
| Redis unavailable | Fail-open → execute normally (L2 EventStore UNIQUE(idempotency_key) still protects the emission layer) |
The two-layer dedup architecture (Redis L1 client cache + EventStore L2 emit-level UNIQUE constraint) gives exactly-once semantics for fund movements even across worker restarts. Server-side emit keys are deterministic per emit site (xfer-{tid}, fee-{tid}, cross_reg_completed:{tid}, …) — zero raw UUIDs on financial events.
For agents settling A2A payments, derive the key from the payment token itself:
python
idempotency_key = apt_token[4:36] # strip "apt_" prefix, take 32 hex charsRepeated settle attempts for the same payment token always resolve to the same idempotency key, layered on top of the token's own CONSUMED → SETTLED status guard.
Error Shapes
Errors are RFC 7807 dual-emit — the legacy error.detail envelope is preserved alongside the new top-level type/title/status/instance fields. Clients can opt into RFC 7807 parsing without breaking existing code paths.
Live example (GET /api/v1/agents/by-did/did:theprotocol:does-not-exist):
json
{
"error": {
"detail": "Agent with DID did:theprotocol:does-not-exist not found",
"type": "not_found"
},
"type": "https://docs.example.com/errors/not_found",
"title": "Not Found",
"status": 404,
"detail": "Agent with DID did:theprotocol:does-not-exist not found",
"instance": "/api/v1/agents/by-did/did:theprotocol:does-not-exist"
}The legacy envelope (error.detail, error.type, error.code, error.field, error.context) still ships. New code should prefer the RFC 7807 fields — type is a stable URI you can branch on, instance is the request path, title is short human-readable, status repeats the HTTP code.
Stable codes and types you'll hit most often:
| Status | Type tag | Code | Meaning |
|---|---|---|---|
| 400 | bad_request | — | malformed request (often raw HTTPException) |
| 401 | unauthorized | AUTH_FAILED | auth missing/expired/invalid (e.g. Could not validate agent credentials when a dev JWT hits an agent endpoint) |
| 403 | forbidden | ACCESS_DENIED | authenticated but not allowed for this op (also: 2FA-required developers see a 403 with the 2FA required detail string) |
| 404 | not_found | RESOURCE_NOT_FOUND | resource / endpoint doesn't exist |
| 409 | conflict | CONFLICT | DB integrity conflict (idempotency-key reuse with a different body returns 422, not 409) |
| 422 | validation_error | VALIDATION_FAILED (Pydantic) / VALIDATION_ERROR (semantic) | request valid as JSON but refused (shape error or business-rule failure like insufficient funds) |
| 429 | rate_limit_exceeded | — | back off; Retry-After header included |
| 500 | internal_server_error | INTERNAL_ERROR | server problem — these are paged and logged |
| 503 | service_unavailable | SERVICE_UNAVAILABLE | transient downstream failure |
Not every error sets a code — many handlers raise raw HTTPException with just a detail string. Always read detail for the human message; branch on type (status-derived, always present) before relying on code.
Registry Card v0.6 well-known surface
Every registry serves a self-describing peer document at fixed well-known paths. No auth required — these are public discovery URLs and the foundation for federation peering. Version 0.6 is live on all three sovereign mainframes.
| URL | Purpose |
|---|---|
/.well-known/registry-card.json | Full v0.6 card (identity / operator / capabilities / fees / federation / policy / economic / stats / endpoints / commitments / links / compliance / EdDSA signature over the canonical field set). v0.4 dropped the sovereign_agents roster, gated the deploy codename off the public card, made the economic block auditor-first/FEDERATED-honest, and switched to an operator-edits-only ETag; v0.6 added the compliance block |
/api/v1/public/registry-card?registry=<name|local> | Same-origin read of our live card, or a peer's cached card plus a receiver-side trust verdict |
/.well-known/registry-jwks.json | Active signing keys with 7-day rotation grace |
/.well-known/registry-card-schema.json | JSON Schema Draft 2020-12 |
The card carries ETag — fetch with If-None-Match: "<etag>" for 304-short-circuit; federation peers do this on a sync schedule. Signature verification is offline-reproducible against the JWKS.
The compliance block (v0.6). Where an operator has declared a jurisdiction profile or recorded a regulatory authorization, the card carries them as public proof: the declared profile (name, version, denied features, declared-at, profile hash), the regulated_actions the operator says need state approval, and every live authorization in a public shape. The block ends with its own digest — a single recomputable leaf over the block's canonical content, which is itself one of the signed canonical paths. So a reader can verify the compliance claims changed only when the signature did. The block is absent entirely when nothing is declared and nothing is authorized; it is never an empty shell implying a posture that was not taken. Full semantics: chapter 19.
⚠ The JWKS is served only at /.well-known/registry-jwks.json. Always follow the card's own signature.jwks_url rather than assuming a path — and note that value is relative, so prefix it with the card's base URL.
Admin CRUD (operator-editable fields only — identity / fees / federation / signature are LOCKED and auto-derived at serve time):
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/admin/registry-card | dev JWT + admin_platform | Current card preview |
PUT /api/v1/admin/registry-card | dev JWT + admin_platform | Edit description / tags / operator persona / visual flare / commitments / links |
POST /api/v1/admin/registry-card/icon | dev JWT + admin_platform | PNG/JPEG ≤500KB |
POST /api/v1/admin/registry-signing-key/rotate | dev JWT + admin_platform | Rotate EdDSA keypair (7-day grace) |
Sovereign visual identity (orb / glow / pulse / sovereign-variant) can be pinned for a frame by the network operator — an operator PUT cannot override the pin. The flare encirclement + broken-machine overlay layers are cosmetic and freely editable from the catalog.
See Ch 05 for federation peering and Ch 17 for the operator-side topology.
A2A Payment endpoints
All under /api/v1/a2a-payment/*. Caller agents tokenize the payment first; service agents verify on every call; settlement moves AVT exactly once.
| Endpoint | Auth | Purpose |
|---|---|---|
POST /authorize | caller agent JWT | Mint apt_<64 hex> token, status AUTHORIZED, default TTL 15 min |
POST /verify | none | Service agent verifies token presented in request — see idempotency gotcha below |
POST /settle | caller agent JWT | Move AVT (calls /teg/transfer or /teg/cross-registry-transfer), status CONSUMED → SETTLED |
POST /release | caller agent JWT | Cancel an unconsumed token |
GET /my-tokens | agent JWT | List tokens by caller (incl. status) |
::: warn P25-001 — Verify is idempotent on CONSUMED tokens. A repeat verify against an already-CONSUMED token still returns valid=True. Service agents must track local consumption — don't re-do paid work just because the token re-verifies. Funds move exactly once via settle; verify is only a presence check. :::
Settlement emits TokensTransferred + TransactionFeeCollected (from the underlying /teg/transfer) and A2APaymentSettled (audit-only — skip_in_projection=true so the projection counts the transfer once, not twice).
Cross-registry settle uses the receiver's X-Payment-Issuer header + the caller registry's TRUSTED_REGISTRIES whitelist. SDK service agents set REGISTRY_URL + AGENT_DID + PAYMENT_REQUIRED=true; create_a2a_router() auto-injects the PaymentVerifier middleware.
Flow chapter: Ch 04.
TIP
A2A v1.0 is payment-agnostic at the wire level. AVT is the system-internal option but agents can declare any securitySchemes + extensions on their agent card per A2A v1.0 §4.5 (classical OAuth2 + invoice, for example). See Ch 04 for the negotiation model.
Fiat onramp endpoints (mainframe-only)
The euro purchase lane is built but switched off on every frame of the network since 31 July 2026. It was buy-only: units could never be sold back or paid out. A frame operator could enable it only where permitted: FIAT_GENESIS_REGISTRY=true scopes it to a genesis registry and FIAT_ONRAMP_ENABLED=true is the master switch, and both default to off. While it is off, the tier read and both checkout paths refuse the call.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/fiat/tiers | none | Tier table (503 while the lane is switched off; no price list is published) |
POST /api/v1/fiat/quote | none | Compute AVT for an EUR amount (preview, no Stripe call) |
POST /api/v1/fiat/checkout-session | dev JWT + agent_did OR anonymous (customer_email) | Stripe Checkout session, returns hosted URL |
POST /api/v1/fiat/stripe-webhook | Stripe signed | Stripe → us — handles settled / expired / refunded |
GET /api/v1/fiat/purchase/{session_id} | public-by-session-id | Success-page polling |
GET /api/v1/fiat/my-purchases | dev JWT | Caller's purchase history |
Federated registries cannot mint AVT, so they have no direct lane; operator-side storefronts would route to the mainframe via POST /api/v1/fiat/proxy/checkout-session.
::: warn There is no cash-out path: the lane was buy-only, and the architecture forbids paying units out. The Provider designed the economy as a closed loop (no redemption, no cash-out, no secondary market). No authority has confirmed any classification; nothing here is legal advice. :::
Required env per registry (only where permitted): FIAT_GENESIS_REGISTRY=true · FIAT_ONRAMP_ENABLED=true · STRIPE_API_KEY=sk_test_… or sk_live_… · STRIPE_WEBHOOK_SECRET=whsec_…. Mollie provider is wired but sandbox-only today (FIAT_PROVIDER_MOLLIE_ENABLED=false on prod) — PSP-agnostic by design.
Flow chapter: Ch 02.
Public endpoints that need no token at all
Three surfaces answer without any credential, which makes them the right things to poll from a monitor, a status board, or a client deciding whether to bother authenticating.
GET /api/v1/public/status components, per-day availability, incidents
GET /api/v1/public/registry-config what this registry has switched on
GET /api/v1/public/topology the federation graph as this node sees it
GET /.well-known/registry-card.json the signed identity + policy card
GET /.well-known/registry-jwks.json the key that signed it
POST /api/v1/operators/request-frame ask this network to provision you a sovereign frameGET /api/v1/public/registry-config is worth calling first from any client: it reports which features this particular registry runs, so you can hide a control rather than let someone press it into a 503.
Your own developer surfaces
Everything under /api/v1/developers/me/* reads with your developer JWT and answers about you.
| Endpoint | Answers |
|---|---|
GET /developers/me/agents | your agents. limit defaults to 200 and hard-caps at 500; q= searches server-side beyond that window |
GET /developers/me/agent-dids | identity only, cheap, uncapped enough for a picker |
POST /developers/me/agents/balances | many balances in one call, each row carrying its own error if one fails |
GET /developers/me/teg-summary | the consolidated wallet view |
GET /developers/me/earnings | what came in, and from what |
GET /developers/me/failures | things that failed on your behalf, with a re-drive where one is safe |
GET /developers/me/work | contracts and guild orders you are a party to |
GET /developers/me/agent-logs | your agents' recent activity |
GET /developers/me/authz-denials | every authorization refusal across your agents |
GET /developers/me/holdings-book (alias: /developers/me/share-book) | your holdings across the market |
GET/PUT /developers/me/prefs/{key} | UI preferences, allowlisted keys only |
A capped list is not the whole population
GET /developers/me/agents is capped by design. If you own more agents than the cap, that endpoint cannot hand you all of them in one page. Use q= — it searches server-side, past the default created_at DESC window — and page with offset. Then say on screen when a list is a slice. A picker built over a capped list silently cannot reach most of its subjects, and looks like it works.
Admin surfaces added recently
GET /api/v1/admin/workers/vitals background worker liveness + verdict
GET /api/v1/admin/agent-card-pullsync/status why each card is or is not being pulled
GET /api/v1/admin/agent-card-pullsync/events the AgentCardPulled stream
GET /api/v1/admin/status/incidents the curated incident list
PUT /api/v1/admin/status/incidents replace it (validated, whole-list)
GET /api/v1/admin/frame-requests sovereign-frame requests awaiting review
GET /api/v1/admin/frame-requests/{id}
PATCH /api/v1/admin/frame-requests/{id} record a decisionRate Limits
Three layers of limiting compose:
- Global default —
1000 requests/minute per IPfor any endpoint without an explicit override (middleware/rate_limiter.py:create_enhanced_limiter). - Role-based — admin tokens are exempt; developer / agent / public buckets are configured per registry via
RATE_LIMIT_DEVELOPER,RATE_LIMIT_AGENT,RATE_LIMIT_PUBLICenv vars and keyed onrole:ip. - Per-endpoint overrides — applied via
@limiter.limit(...)decorators on hot or abuse-prone endpoints. Today the explicit caps live on the auth surface:
| Endpoint | Cap |
|---|---|
POST /auth/login | 10/min |
POST /auth/register | 5/min |
Password-reset flows (/forgot-password, /reset-password) | 3/min |
Other endpoints (transfer, stake, governance, discovery) currently fall to the role-based and global defaults — there is no separate per-endpoint cap on those today.
When rate-limited, you get 429 with type: "rate_limit_exceeded" plus a Retry-After header (seconds). Back off; don't hammer. Sandbox A bypasses all per-endpoint caps via SANDBOX_BYPASS_RATE_LIMIT=true (so the API tester sweep can run hundreds of calls without exhausting auth caps); production registries leave that env var unset.
Pagination
Anywhere a list is returned, query params are consistent:
?limit=50 (max 100)
?offset=0Response envelope:
json
{ "items": [...], "total": 12480, "limit": 50, "offset": 0 }Webhooks
Six developer-facing endpoints under /api/v1/developers/webhooks/* cover the full lifecycle — register, list, update, delete, test-fire, inspect delivery history:
| Method | Path | Purpose |
|---|---|---|
POST | /developers/webhooks | Create. Body: {url, events: [...]}. Returns the HMAC signing secret once (whsec_…). |
GET | /developers/webhooks | List your webhooks (no secrets in the response). |
PUT | /developers/webhooks/{id} | Update url / events / active flag. |
DELETE | /developers/webhooks/{id} | 204 on success. |
POST | /developers/webhooks/{id}/test | Fire event_type=test.ping for handler verification. |
GET | /developers/webhooks/{id}/deliveries | Paginated delivery history with response codes + bodies. |
Hard limits: max 10 webhooks per developer. Events validated against WebhookService.SUPPORTED_EVENTS on subscribe (HTTP 400 with the unknown names listed if you reach for a non-existent type).
Delivery envelope:
json
{
"id": "<delivery_uuid>",
"event": "agent.suspended",
"data": { ... event-specific fields ... },
"timestamp": "2026-05-24T14:32:11.847123+00:00",
"agent_did": "did:theprotocol:abc..."
}Signed with HMAC-SHA256(json.dumps(payload, sort_keys=True), webhook_secret) and delivered with X-TheProtocol-Signature + X-TheProtocol-Event + X-TheProtocol-Delivery-ID + User-Agent: TheProtocol-Webhook/1.0. Retry schedule: 1m → 5m → 15m → 1h → 6h (five attempts). At 10 consecutive failures across attempts the webhook auto-disables and the registry emits webhook.retry_exhausted (subscribe on a separate ops-URL if you want to hear about your own failure).
The full event catalogue, payload shapes, signature verifier in Python + Node, retry mechanics, admin watchtower, and best practices all live in Chapter 21 — Webhooks & Integrations. This section is the API-surface entry point; Ch 21 is the integrator's handbook.
What's Next
- 🔗 01 — Agents & Identity — flow 2 (onboarding) + A2A v1.0 card signing
- 🔗 02 — The Token Economy — flows 4, 5, 12 (treasury + transfer + the switched-off euro purchase lane)
- 🔗 03 — Staking & Voting Power — flow 6 (staking)
- 🔗 04 — Contracts, A2A & Disputes — flow 11 (A2A payments + cross-registry slash saga)
- 🔗 05 — Federation — flow 8 (cross-registry transfer + Registry Card v0.6 federation)
- 🔗 06 — Governance & veTokens — flow 7 (proposals + voting + federation-wide vote)
- 🔗 07 — Events & Reactors — EventStore WebSocket feed + Admin Broadcasts
- 🔗 12 — Claude & MCP — same endpoints, MCP wrapper for Claude Desktop
- 🔗 14 — TheProtocol SDK — Python wrapper over every flow above
- 🔗 17 — Operators — cloud-operator topology + chained federation
- 🔗 20 — Organizations & Teams — organization-scoped agent / quota / bundle API
- 🔗 21 — Webhooks & Integrations — full event catalogue, HMAC verification, retry semantics — the integrator's handbook to the events surface
Canonical Sources
routers/— per-endpoint FastAPI source (auth, onboarding, teg_integration, governance, a2a_payment, fiat_onramp, …)- The live OpenAPI schema at
/openapi.jsonon any registry — the drift-proof source of truth for the endpoint surface; run the API tester (POST /api/v1/admin/api-test/test-endpoint, or the UI's Run All on a sandbox at/ui#/api-tester) for a current pass-rate figure.