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.

Auth
session cookie or API key
Capability
none needed (the account's own free period and its request for continued access)
Success
HTTP 200

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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
401unauthorizedNo 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.

Auth
session cookie or API key
Capability
none needed (the account's own free period and its request for continued access)
Success
HTTP 200

Parameters

None.

Request body

contact_email string or null
No description in the schema.
expected_monthly_calls integer or null
No description in the schema.
notes string
Default "".
organisation string required
No description in the schema.
use_case string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
422validation errorA required field is missing or a value has the wrong type.
422validation_errororganisation and use_case are required and each is capped at 2000 characters.
401unauthorizedNo 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.

Auth
session cookie or API key
Capability
none needed (a message from anybody; no account required)
Success
HTTP 202

Parameters

None.

Request body

email string required
No description in the schema.
message string required
No description in the schema.
name string required
No description in the schema.
organisation string
Default "".
source string
Default "site".
website string
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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
422validation errorA required field is missing or a value has the wrong type.
422invalid_sourcesource must be site or console.
429too_many_inquiriesFive 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.

Auth
session cookie or API key
Capability
none needed (how many accounts the beta will take; public)
Success
HTTP 200

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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo 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.