API reference

Sign-in and session

Sign in with an emailed code, receive a session cookie, see who you are, sign out. The cookie drives the console and key management; agents use a key instead.

POST /v1/auth/request-otp

Request a sign-in code

Emails a six digit code to the address; the account is created on first successful sign-in.

Auth
none
Capability
none needed (sign-in, code verification and sign-out)
Success
HTTP 200

The code works once and expires after ten minutes. Five wrong attempts lock the pending code.

When the agreement gate is enforced, the body may carry legal_attestations keyed by document id. GET /v1/legal/documents lists what is required and where to read it.

Parameters

None.

Request body

email string required
No description in the schema.
legal_attestations object of LegalAttestation or null
No description in the schema.
LegalAttestation fields
accepted boolean
Default false.
content_hash string
Default "".
version string
Default "".

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/auth/request-otp" \
  -H "Content-Type: application/json" \
  -d '{
  "email": "ops@example.com"
}'

Python

import requests

API = "https://api.afaprotocol.com"
payload = {
    "email": "ops@example.com"
}

r = requests.post(f"{API}/v1/auth/request-otp", json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "expires_in_minutes": 10,
  "status": "sent"
}

Errors

StatusCodeMeaning
422validation errorA required field is missing or a value has the wrong type.
422legal-acceptance-requiredA required agreement is not accepted; the body names the document ids and the text route.
502otp_send_failedThe email provider refused the send. No code was issued.

What would show this is false

Request twice for the same address. The second code replaces the first, and the first then fails with invalid_code.

POST /v1/auth/signout

End the session

Clears the session cookie.

Auth
session cookie only
Capability
none needed (sign-in, code verification and sign-out)
Success
HTTP 200

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/auth/signout" \
  -H "Cookie: afa_jwt=<session cookie>"

Python

import requests

API = "https://api.afaprotocol.com"
cookies = {"afa_jwt": "<session cookie>"}

r = requests.post(f"{API}/v1/auth/signout", cookies=cookies, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "status": "signed_out",
  "user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
}

Errors

StatusCodeMeaning
401missing_token / token_expiredNo session cookie, or one older than eight hours.

What would show this is false

GET /v1/me with the old cookie returns 401 missing_token after this call.

POST /v1/auth/verify-otp

Exchange the code for a session

Checks the code and sets the afa_jwt session cookie on success.

Auth
none
Capability
none needed (sign-in, code verification and sign-out)
Success
HTTP 200

The cookie is HttpOnly, SameSite=Strict, sent only over HTTPS, and valid for eight hours.

Parameters

None.

Request body

code string required
No description in the schema.
email string required
No description in the schema.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/auth/verify-otp" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "482913",
  "email": "ops@example.com"
}'

Python

import requests

API = "https://api.afaprotocol.com"
payload = {
    "code": "482913",
    "email": "ops@example.com"
}

r = requests.post(f"{API}/v1/auth/verify-otp", json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "expires_at": "2026-09-01T22:00:11+00:00",
  "status": "ok",
  "user": {
    "created_at": "2026-08-30T09:14:02+00:00",
    "email": "ops@example.com",
    "id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
    "last_login": "2026-09-01T14:00:11+00:00"
  }
}

Errors

StatusCodeMeaning
422validation errorA required field is missing or a value has the wrong type.
400invalid_code / otp_expired / no_pending_otpWrong code, a code older than ten minutes, or no code pending for this address.
429too_many_attemptsFive wrong codes; request a new one.

What would show this is false

Replay the same code after a success: 400 no_pending_otp. The code was consumed.

GET /v1/me

Who am I

Returns the account the credential resolves to, for a cookie or a key.

Auth
session cookie or API key
Capability
none needed (identity introspection for whoever is signed in)
Success
HTTP 200

A key and a cookie both resolve to the same account. A request carrying both is treated as the key's machine.

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/me" \
  -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/me", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "created_at": "2026-08-30T09:14:02+00:00",
  "email": "ops@example.com",
  "is_admin": false,
  "last_login": "2026-09-01T14:00:11+00:00",
  "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.

What would show this is false

Send a revoked key: 401 invalid_or_expired_api_key. Send nothing: 401 missing_token. The two refusals are distinct on purpose.