API reference
Agreements
The agreement texts with their hashes, and the acceptances recorded for your account. The text route serves the exact bytes the hash covers.
POST/v1/legal/acceptGET/v1/legal/acceptancesGET/v1/legal/documentsGET/v1/legal/documents/{document_id}/text
POST /v1/legal/accept
Record acceptance of a version
Records that this account accepted one specific version of one document, for an account that already exists.
This route needs a signed-in caller, so a new account's first acceptance rides on POST /v1/auth/request-otp instead. Use this one when a document version moves.
Parameters
None.
Request body
acceptedboolean required- No description in the schema.
content_hashstring required- No description in the schema.
document_idstring required- No description in the schema.
versionstring required- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/legal/accept" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"accepted": true,
"content_hash": "1c0cd773e3d89014b809b43966434eac0fab2b701d7f25877f51e165ca320b03",
"document_id": "beta_terms",
"version": "2026-09-08.1"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"accepted": True,
"content_hash": "1c0cd773e3d89014b809b43966434eac0fab2b701d7f25877f51e165ca320b03",
"document_id": "beta_terms",
"version": "2026-09-08.1"
}
r = requests.post(f"{API}/v1/legal/accept", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"acceptance": {
"content_hash": "1c0cd773e3d89014b809b43966434eac0fab2b701d7f25877f51e165ca320b03",
"document_id": "beta_terms",
"email": "ops@example.com",
"recorded_at": "2026-09-01T14:01:30+00:00",
"source": "portal",
"status": "accepted",
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
"version": "2026-09-08.1"
},
"status": "recorded",
"unmet_document_ids": [
"acceptable_use",
"privacy_notice"
]
}
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. |
| 400 | account_has_no_email | The account cannot be tied to a receipt. |
| 422 | legal-attestation-invalid | Wrong version or a hash that does not match the served text; nothing recorded. |
What would show this is false
Send a stale version: 422 naming the current version, nothing is recorded, and GET /v1/legal/acceptances still lists the document as unmet.
GET /v1/legal/acceptances
What this account accepted
Lists the acceptances recorded for the account and which required documents remain unmet.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/legal/acceptances" \
-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/legal/acceptances", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"acceptances": [
{
"content_hash": "1c0cd773e3d89014b809b43966434eac0fab2b701d7f25877f51e165ca320b03",
"current": true,
"document_id": "beta_terms",
"email": "ops@example.com",
"recorded_at": "2026-08-30T09:14:02+00:00",
"source": "signin",
"status": "accepted",
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
"version": "2026-09-08.1"
}
],
"email": "ops@example.com",
"required_document_ids": [
"beta_terms",
"acceptable_use",
"privacy_notice"
],
"unmet_document_ids": [
"acceptable_use",
"privacy_notice"
]
}
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
current is false on a row whose version is no longer the served one, and that document id then appears in unmet_document_ids. In the example one of three agreements is accepted and the other two are listed as unmet.
GET /v1/legal/documents
The agreements and their hashes
Lists the three agreements an account must accept, the beta terms, the acceptable use policy and the privacy notice, each with the version and content hash to echo back.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/legal/documents"
Python
import requests
API = "https://api.afaprotocol.com"
r = requests.get(f"{API}/v1/legal/documents", timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"accept_route": "/v1/legal/accept",
"acceptance_mode": "off",
"documents": [
{
"content_hash": "1c0cd773e3d89014b809b43966434eac0fab2b701d7f25877f51e165ca320b03",
"document_id": "beta_terms",
"media_type": "text/markdown",
"required": true,
"status": "operator_approved",
"summary": "The beta, the thirty-day free period from your first key, what the record can show and what it cannot, and how an account ends.",
"text_url": "/v1/legal/documents/beta_terms/text",
"title": "AFA Protocol beta terms",
"version": "2026-09-08.1"
},
{
"content_hash": "8a1904e90d3e1395ece4639d388636d985849bc80e3a728c276a3b41f2fa9be9",
"document_id": "acceptable_use",
"media_type": "text/markdown",
"required": true,
"status": "operator_approved",
"summary": "What you may not do with the service: nothing unlawful, nothing that attacks the record, and nothing safety-critical.",
"text_url": "/v1/legal/documents/acceptable_use/text",
"title": "AFA Protocol acceptable use policy",
"version": "2026-09-07.1"
},
{
"content_hash": "1b75344ef0dd42603febc7862b1762602b326d2e0702ee3792aefe0c67a2126d",
"document_id": "privacy_notice",
"media_type": "text/markdown",
"required": true,
"status": "operator_approved",
"summary": "Hashes and metadata, never payloads. What we hold, why, for how long, who else sees it, and how to get a copy or delete it.",
"text_url": "/v1/legal/documents/privacy_notice/text",
"title": "AFA Protocol privacy notice",
"version": "2026-09-07.1"
}
],
"gate_enforced": false,
"notice": "Operator-approved. Counsel has not reviewed these documents. Accepting records that you agreed to a specific version of a specific text; it is not advice that the text is adequate.",
"required_document_ids": [
"beta_terms",
"acceptable_use",
"privacy_notice"
]
}
Errors
No error responses beyond transport failures.
What would show this is false
Fetch text_url and hash the body: the SHA-256 equals content_hash. The text route's X-Document-Content-Hash header carries the same value.
GET /v1/legal/documents/{document_id}/text
The exact text a hash covers
Serves the normalised text the content hash was computed over, with the version and hash in headers.
Parameters
document_idpath, string required- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/legal/documents/beta_terms/text"
Python
import requests
API = "https://api.afaprotocol.com"
r = requests.get(f"{API}/v1/legal/documents/beta_terms/text", timeout=30)
r.raise_for_status()
print(r.text)
Example response
HTTP 200
# AFA Protocol beta terms
Version 2026-08-29.1
(the full text follows; headers X-Document-Version and X-Document-Content-Hash accompany it)
Errors
| Status | Code | Meaning |
|---|---|---|
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | unknown_legal_document | No document with this id. |
What would show this is false
sha256 over the response body equals the X-Document-Content-Hash header and the content_hash in GET /v1/legal/documents.