API reference
Keys
Operations under the keys group, generated from the API description.
GET/v1/me/api-keysPOST/v1/me/api-keysDELETE/v1/me/api-keys/{key_id}POST/v1/me/api-keys/{key_id}/rotate
GET /v1/me/api-keys
List keys
Lists the account's keys with prefix, machine, expiry and scopes; never the token.
Parameters
None.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/me/api-keys" \
-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/api-keys", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"active_count": 1,
"expiring_soon": [],
"keys": [
{
"created_at": "2026-09-01T14:02:00+00:00",
"days_until_expiry": 29,
"expires_at": "2026-10-01T14:02:00+00:00",
"id": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
"label": "deploy runner",
"last_used_at": "2026-09-01T14:12:07+00:00",
"machine": "worker-1",
"prefix": "afa-beta-k3ryn7pq",
"revoked_at": null,
"rotate_recommended": false,
"scopes": [
"events:write",
"events:read",
"grants"
]
}
],
"max": 10
}
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 keys; the body names it. |
What would show this is false
Create a key, revoke it, list again: the row stays with revoked_at set and active_count drops by one.
POST /v1/me/api-keys
Create a per-machine key
Issues a token for one machine and returns it once. The token reads afa-beta-<26 characters>-<4-character check>; the check is the first four characters of the base32 encoding of the CRC-32 of the 26, so a mistyped key is refused at the door with 401 malformed_key rather than reported as an unknown one.
scopes is optional. Omitted or null means every capability. Each entry must be one of the fixed capability names; an unknown one is refused by name.
At most ten active keys per account. Only a session can create, rotate or revoke keys; a key cannot mint another key.
Parameters
None.
Request body
labelstring or null- No description in the schema.
machinestring required- Machine identifier: hostname or operator label
org_idstring or null- No description in the schema.
scopesarray of string or null- Capabilities this key may call; null means all of them
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/me/api-keys" \
-H "Cookie: afa_jwt=<session cookie>" \
-H "Content-Type: application/json" \
-d '{
"label": "deploy runner",
"machine": "worker-1",
"scopes": [
"events:write",
"events:read",
"grants"
]
}'
Python
import requests
API = "https://api.afaprotocol.com"
cookies = {"afa_jwt": "<session cookie>"}
payload = {
"label": "deploy runner",
"machine": "worker-1",
"scopes": [
"events:write",
"events:read",
"grants"
]
}
r = requests.post(f"{API}/v1/me/api-keys", cookies=cookies, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 201
{
"key": {
"created_at": "2026-09-01T14:02:00+00:00",
"days_until_expiry": 30,
"expires_at": "2026-10-01T14:02:00+00:00",
"id": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
"label": "deploy runner",
"last_used_at": null,
"machine": "worker-1",
"prefix": "afa-beta-k3ryn7pq",
"revoked_at": null,
"rotate_recommended": false,
"scopes": [
"events:write",
"events:read",
"grants"
]
},
"token": "afa-beta-k3ryn7pq2wsdz4mvbt6xhc5jfa-e4qs",
"warning": "Save this token now. It is never displayed again."
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / token_expired | No session cookie, or one older than eight hours. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 403 | api_key_cannot_issue_keys | The caller is a key. Keys cannot mint keys. |
| 409 | max_keys_reached | Ten keys are active. Revoke one first. |
| 422 | unknown_scope | A scopes entry is not a capability; the body lists the known names. |
What would show this is false
GET /v1/me/api-keys never lists the token, only its prefix. Only the SHA-256 of the token is stored.
DELETE /v1/me/api-keys/{key_id}
Revoke a key
Revokes one key; its token stops working on the next call.
Parameters
key_idpath, string required- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X DELETE "https://api.afaprotocol.com/v1/me/api-keys/7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e" \
-H "Cookie: afa_jwt=<session cookie>"
Python
import requests
API = "https://api.afaprotocol.com"
cookies = {"afa_jwt": "<session cookie>"}
r = requests.delete(f"{API}/v1/me/api-keys/7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e", cookies=cookies, timeout=30)
r.raise_for_status()
print(r.status_code) # 204, no body
Example response
HTTP 204
(no body)
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / token_expired | No session cookie, or one older than eight hours. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 403 | api_key_cannot_revoke_keys | The caller is a key. |
| 404 | key_not_found_or_already_revoked | No active key with this id on this account. |
What would show this is false
The revoked token answers 401 invalid_or_expired_api_key on its next call, and a key.revoked row appears in GET /v1/notifications.
POST /v1/me/api-keys/{key_id}/rotate
Rotate a key
Issues a fresh token for the same machine and revokes the old one in the same call.
The new key keeps the machine, label and scopes of the old one and gets a fresh 30-day expiry. A rotation renews the credential, not its reach.
Parameters
key_idpath, string required- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/me/api-keys/7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e/rotate" \
-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/me/api-keys/7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e/rotate", cookies=cookies, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 201
{
"key": {
"created_at": "2026-09-01T16:40:00+00:00",
"days_until_expiry": 30,
"expires_at": "2026-10-01T16:40:00+00:00",
"id": "9c8e0f1a-2b3c-4d4e-8f5a-6b7c8d9e0f1a",
"label": "deploy runner",
"last_used_at": null,
"machine": "worker-1",
"prefix": "afa-beta-m4tqzs7d",
"revoked_at": null,
"rotate_recommended": false,
"scopes": [
"events:write",
"events:read",
"grants"
]
},
"token": "afa-beta-m4tqzs7dyv2brk6xnhw3jc5pfa-io3v",
"warning": "Save this token now. It is never displayed again. The previous token is revoked."
}
Errors
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / token_expired | No session cookie, or one older than eight hours. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 403 | api_key_cannot_rotate_keys | The caller is a key. |
| 404 | key_not_found_or_already_revoked | No active key with this id on this account. |
What would show this is false
The old token fails with 401 immediately after. The listing shows the old row revoked and a new row with the same machine.