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://registry.example.com/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://registry.example.com trust: example.com
Peer sovereign frame https://peer.example.com trust: peer.example.com
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.4 at well-known surfaces (see § Registry Card v0.4 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 | Fiat onramp (backer pledge) | none (anonymous) OR devJWT | POST /fiat/checkout-session → Stripe → webhook → AVT credit | 02 |
TIP
Flow 12 (Fiat onramp) is mainframe-only by construction. Federated registries return 403 Forbidden because FIAT_GENESIS_REGISTRY=false is the default — they cannot mint AVT. See § Fiat onramp endpoints below and the MiCA-safe backer-pledge framing in 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.
POST /api/v1/auth/login
Content-Type: application/x-www-form-urlencoded
username=you%40example.com&password=YourPassword!{ "access_token": "eyJhbGciOi...", "token_type": "bearer", "expires_in": 3600 }In Python:
import httpx
resp = httpx.post(
"https://registry.example.com/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:
POST /api/v1/auth/api-keys
Authorization: Bearer <dev_jwt>
"Description of what this key is for"{
"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:
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:
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):
{
"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 | idempotency-key reuse with different body, or DB integrity conflict |
| 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.4 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.4 on sandbox, prod rollout staged; v0.3 served in parallel during the deprecation window.)
| URL | Purpose |
|---|---|
/.well-known/registry-card.json | Full v0.4 card (identity / operator / capabilities / fees / federation / policy / economic / stats / endpoints / commitments / links / EdDSA signature over 13 canonical fields). 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 |
/api/v1/public/registry-card?registry=<name|local> | New in v0.4 — 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.
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 — Bitcoin Lightning, USDC, classical OAuth2 + invoice — all valid. See Ch 04 for the negotiation model.
Fiat onramp endpoints (mainframe-only)
The genesis registry can accept Stripe Checkout EUR → AVT credit; a peer sovereign frame is wired identically — the flag FIAT_GENESIS_REGISTRY=true enables it per frame, and the master kill switch FIAT_ONRAMP_ENABLED=true lights the surface up.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/v1/fiat/tiers | none | Tier breakpoints + min / max EUR (live: 3 tiers, €1 → €500, 2000 / 2500 / 3000 AVT per EUR) |
POST /api/v1/fiat/quote | none | Compute AVT for an EUR amount (live 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 return 403 Forbidden on every fiat path — they cannot mint AVT and so cannot accept fiat-out. Operator-side storefronts route to the mainframe via POST /api/v1/fiat/proxy/checkout-session (see Ch 02 for the operator-treasury revenue-share roadmap).
::: warn MiCA-safe framing: This surface is a backer pledge, not "buy AVT" in the e-money / MiFID II sense. AVT is a utility token. The frontend at /become-a-backer always frames it as "pledge to support the protocol + receive cosmetic profile flare + AVT credit as consequence." Never describe the surface as fiat-out — there is none, and the architecture forbids it. See Ch 02 § Backer pledges for the legal framing. :::
Required env per registry (only set on registries that should accept fiat): 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.
Rate 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-developer | 5/min |
Account-recovery flows (/recover-account, /set-new-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:
{ "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:
{
"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 + fiat backer pledges)
- 🔗 03 — Staking, APY & 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.3 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.