API reference
Free period and access
Where this account stands in its thirty days, how to ask us for continued access, how many seats are left, and how to write to us with no account at all. No price is served on any of these.
The clock starts at the account's FIRST API KEY, not at the account. Signing in to read the agreements does not start it. Before any key exists the status is not_started and the date is null.
Seven days after the date, a WRITE from an account that has never asked for continued access is refused with 402 and a body naming the route that lifts it. A READ is never refused, at any point. An account that has asked is not refused either, whatever the status of its request.
Every response carries X-AFA-Billing with the account's status, and X-AFA-Free-Period-Ends while the period is running.
GET /v1/billing/status
Where this account is in its free period
Report the end date, which side of it the account is on, how many days are left, and any request for continued access already sent.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/billing/status" \
-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/billing/status", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"billing_status": "free_period",
"days_remaining": 12,
"free_period_days": 30,
"free_period_ends_at": "2026-10-07T09:14:02+00:00",
"request_route": "POST /v1/billing/transition-request",
"transition_request": null,
"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. |
| 401 | unauthorized | No session and no key. The route reports your own account, so it needs to know which one. |
What would show this is false
Issue a key on a brand new account and read this again. Before the first key the status is not_started and the date is null; after it the date is thirty days from that key's creation.
POST /v1/billing/transition-request
Ask us for continued access
Record that this account wants to keep using the service past its free period. Recording it also lifts the write refusal at once, whatever we do about the request afterwards.
Parameters
None.
Request body
contact_emailstring or null- No description in the schema.
expected_monthly_callsinteger or null- No description in the schema.
notesstring- Default
"". organisationstring required- No description in the schema.
use_casestring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/billing/transition-request" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"contact_email": "ops@example.com",
"expected_monthly_calls": 50000,
"notes": "happy to talk any weekday",
"organisation": "Example Ltd",
"use_case": "attestation for a nightly agent run"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"contact_email": "ops@example.com",
"expected_monthly_calls": 50000,
"notes": "happy to talk any weekday",
"organisation": "Example Ltd",
"use_case": "attestation for a nightly agent run"
}
r = requests.post(f"{API}/v1/billing/transition-request", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"status": "recorded",
"transition_request": {
"admin_note": "",
"contact_email": "ops@example.com",
"email": "ops@example.com",
"expected_monthly_calls": 50000,
"notes": "happy to talk any weekday",
"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. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | validation_error | organisation and use_case are required and each is capped at 2000 characters. |
| 401 | unauthorized | No session and no key. |
What would show this is false
Post it twice with different text. The second answer carries the same request_id and the same submitted_at as the first: the date that matters is when you first asked, not when you last edited it.
POST /v1/inquiries
Write to us with no account
Send a message from the site or the console. This is the only unauthenticated write in the service, so it is bounded by a per-address budget, a hidden field and a length cap on every string.
Parameters
None.
Request body
emailstring required- No description in the schema.
messagestring required- No description in the schema.
namestring required- No description in the schema.
organisationstring- Default
"". sourcestring- Default
"site". websitestring- Default
"".
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/inquiries" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"email": "person@example.com",
"message": "We would like to try this on a nightly agent run.",
"name": "A Person",
"organisation": "Example Ltd",
"source": "site"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"email": "person@example.com",
"message": "We would like to try this on a nightly agent run.",
"name": "A Person",
"organisation": "Example Ltd",
"source": "site"
}
r = requests.post(f"{API}/v1/inquiries", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 202
{
"status": "received"
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | invalid_source | source must be site or console. |
| 429 | too_many_inquiries | Five an hour per address. The body carries the limit and a way through. |
What would show this is false
Send six in an hour from one address. The sixth answers 429 and names the address to write to instead, rather than dropping it.
GET /v1/seats
How many accounts this beta will take
Report the seat count and how many are left. Public, because somebody deciding whether to sign up should not have to sign up to find out whether they can.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/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/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. |
What would show this is false
With remaining at zero, POST /v1/auth/request-otp for an address with no account answers 423 and names this route's sibling for getting in touch. An address that already has an account still receives its code.