Two credentials, one account
Authentication
A session cookie from the email code flow drives the console and key management. A per-machine key drives agents and hosts. Both resolve to the same account. A key may carry a capability allow-list; a session always holds every capability.
Session cookie
POST /v1/auth/request-otp with your email, then POST /v1/auth/verify-otp with the six digit code. The response sets afa_jwt: HttpOnly, SameSite=Strict, sent only over HTTPS, valid for eight hours. Five wrong codes lock the attempt; a code expires after ten minutes. POST /v1/auth/signout clears it.
Check: GET /v1/me returns 401 missing_token without the cookie and your account with it.
API key
Created in the console or with POST /v1/me/api-keys under the session cookie. The token reads afa-beta-<26 characters>-<4-character check>, is returned once, and only its SHA-256 is stored. The response also carries key_prefix and format, so a client can recognise and validate one without matching on prose. It expires 30 days after issue; POST /v1/me/api-keys/{key_id}/rotate issues a fresh token and revokes the old one. Send it as X-API-Key. Events written under a key carry that key's machine id.
curl -sS "https://api.afaprotocol.com/v1/me" -H "X-API-Key: afa-beta-EXAMPLE-e4qs"Check: the same call with a revoked key returns 401 invalid_or_expired_api_key.
Bearer form
The MCP endpoint accepts the same key as Authorization: Bearer afa-beta-... in addition to X-API-Key. The REST routes read X-API-Key.
Check: call GET /v1/me with each header form. A 401 names the form this deployment does not read.
Capability scopes
A key created with scopes can call only the operations that map to a listed capability. Anywhere else it is refused with 403 and the body {"error": "scope_missing", "required": "<capability>"}. A path nobody has classified refuses a limited key as well, rather than admitting it by omission. Each operation in the reference names the capability it needs.
| Capability | What it covers |
|---|---|
| events:write | Append events, singly or in a batch, and write settlement blocks. |
| events:read | Read, search and stream events; graphs, machines, blocks and exports. |
| grants | Sub-agent grants: create, list, status, use, revoke. |
| delegation | Ordered delegation tokens and machine-to-machine grants. |
| dedup | Dedup checks, membership proofs, cache lookup and counters. |
| verify | Exported record verification, the block chain verifier and the remote tools. |
| notifications | The dispatch log, preferences, test sends and the alert feed. |
| webhooks | Endpoint registration. |
| usage | Usage counts and exports. |
| keys | API keys and machine signing identities. |
| governance | Policy test, chain heal and checkpoint advice. |
| anchors | Anchor receipts and the anchor run. |
| surfaces | The surfaces listing. |
| health | Reserved; the health probe itself needs no key. |
| orgs | The team model, when a build serves it. |
| admin | Reserved for a route this build does not serve. |
Check: create a key with scopes: ["events:read"] and call POST /v1/events: 403 with required events:write. The same call with an unlimited key succeeds.
Precedence and limits
| Rule | What it means for you |
|---|---|
| Key header wins over cookie | A request carrying both is treated as the key's machine. |
| Keys cannot mint keys | Create, rotate and revoke need the cookie; a key gets 403. |
| Ten active keys per account | The eleventh create returns 409 max_keys_reached. |
| Scopes are optional | No scopes means every capability, which every key issued before scopes existed has. |
| Rate ceilings | 100 events per minute per key on batch sync; 10,000 events per account per day. |