API reference
Operator view
The operator's own read of accounts, requests, messages and seats. Every route needs an administrator; an ordinary account is refused.
These rows are metadata: an address, dates, counts and statuses. No customer payload, no key material and no agreement text passes through them.
GET/v1/admin/accountsGET/v1/admin/digest-previewGET/v1/admin/inquiriesGET/v1/admin/seatsPOST/v1/admin/seatsGET/v1/admin/transition-requestsPOST/v1/admin/transition-requests/{request_id}
GET /v1/admin/accounts
Every account and its clock
One row per account: the address, when it was created, how many keys it holds, when the first key started its free period, when that period ends, and whether it has asked for continued access.
Parameters
limitquery, integer- Default
200.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/admin/accounts" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/admin/accounts", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"accounts": [
{
"billing_status": "free_period",
"created_at": "2026-09-01T08:00:00+00:00",
"days_remaining": 12,
"email": "ops@example.com",
"first_key_at": "2026-09-07T09:14:02+00:00",
"free_period_ends_at": "2026-10-07T09:14:02+00:00",
"keys": 2,
"last_call_at": "2026-09-19T22:41:07+00:00",
"transition_request_status": null,
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}
],
"count": 1,
"truncated": false
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
| 403 | admin_only | The caller authenticated and is not an administrator. |
What would show this is false
truncated says whether the walk stopped at the limit. A count that equals the limit with truncated false is the whole set; with truncated true it is a page, not a total.
GET /v1/admin/digest-preview
The operations digest, unsent
Return the digest content the scheduled send would produce, built by the same collector and renderer, without sending it.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/admin/digest-preview" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/admin/digest-preview", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"counts": {
"accounts_total": 41,
"inquiries_in_window": 3
},
"recipient": "idgafholdingsllc@gmail.com",
"sent": false,
"subject": "AFA Protocol: operations digest, 2026-09-18 to 2026-09-20",
"text": "Window: ...\n\nACCOUNTS AND KEYS\n accounts, total 41\n..."
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
What would show this is false
sent is false. A preview built by a second renderer would be a second place to be wrong, so this one calls the same two functions the scheduled send calls.
GET /v1/admin/inquiries
Messages sent to us
List what came in through POST /v1/inquiries, newest first.
Parameters
limitquery, integer- Default
200.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/admin/inquiries" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/admin/inquiries", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"count": 1,
"inquiries": [
{
"created_at": "2026-09-19T14:22:10+00:00",
"email": "person@example.com",
"id": "inq-3f9a1c74be205d68",
"message": "We would like to try this on a nightly agent run.",
"name": "A Person",
"organisation": "Example Ltd",
"source": "site"
}
]
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
What would show this is false
A submission that filled the hidden field never appears here. Send one and the count does not move.
GET /v1/admin/seats
The seat count, with its source
The same numbers GET /v1/seats serves, plus where the capacity came from.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/admin/seats" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/admin/seats", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"capacity": 40,
"note": "Seats are bounded by review capacity, not by infrastructure.",
"remaining": 29,
"source": "environment",
"used": 11
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
What would show this is false
source reads environment, default, or admin_override, and says which branch produced the number.
POST /v1/admin/seats
Raise or lower the seat count
Set the capacity for the life of this process. Not written to any store: a restart, a deploy or a second replica reads the durable setting again.
Parameters
None.
Request body
capacityinteger required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/admin/seats" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"capacity": 60
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"capacity": 60
}
r = requests.post(f"{API}/v1/admin/seats", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"capacity": 60,
"durable_setting": "AFA_BETA_SEATS",
"note": "Seats are bounded by review capacity, not by infrastructure.",
"persisted": false,
"remaining": 49,
"source": "admin_override",
"status": "set",
"used": 11
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
What would show this is false
persisted is false and durable_setting names the variable to change instead. The operations digest reports the number it read, so a raise is on the record even though the value is not.
GET /v1/admin/transition-requests
Who has asked for continued access
List the requests, newest first, optionally narrowed to one status, with how many are not closed.
Parameters
limitquery, integer- Default
200. statusquery, string or null- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/admin/transition-requests" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
r = requests.get(f"{API}/v1/admin/transition-requests", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"count": 1,
"open_count": 1,
"transition_requests": [
{
"admin_note": "",
"contact_email": "ops@example.com",
"email": "ops@example.com",
"expected_monthly_calls": 50000,
"notes": "",
"organisation": "Example Ltd",
"request_id": "tr-8c1f2a5b9d0e4f37",
"status": "open",
"submitted_at": "2026-09-20T11:02:44+00:00",
"updated_at": "2026-09-20T11:02:44+00:00",
"use_case": "attestation for a nightly agent run",
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}
]
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
| 422 | invalid_status | The status filter must be open, in_progress or closed. |
What would show this is false
Close one and open_count falls. The account's writes are still admitted: the gate reads whether an account asked, never what we did about it.
POST /v1/admin/transition-requests/{request_id}
Move one request along
Set a request to in_progress or closed, with a note.
Parameters
request_idpath, string required- No description in the schema.
Request body
notestring- Default
"". statusstring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/admin/transition-requests/request_id-example" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"note": "call booked for Thursday",
"status": "in_progress"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"note": "call booked for Thursday",
"status": "in_progress"
}
r = requests.post(f"{API}/v1/admin/transition-requests/request_id-example", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"status": "updated",
"transition_request": {
"admin_note": "call booked for Thursday",
"contact_email": "ops@example.com",
"email": "ops@example.com",
"expected_monthly_calls": 50000,
"notes": "",
"organisation": "Example Ltd",
"request_id": "tr-8c1f2a5b9d0e4f37",
"status": "in_progress",
"submitted_at": "2026-09-20T11:02:44+00:00",
"updated_at": "2026-09-21T09:30:00+00:00",
"use_case": "attestation for a nightly agent run",
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 403 | scope_missing | A key limited by scopes lacks admin; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | invalid_status | Only in_progress and closed can be set. open is the state a request is born in. |
| 404 | no_such_request | The id matches no stored request. |
What would show this is false
An id that names nothing answers 404 with no_such_request, never a bare success.