API reference
Events
The record itself. Append one event with chain continuity, append a batch from a local ledger, search, fetch one event with its ancestry, or tail the live stream.
POST/v1/eventsPOST/v1/events/batchPOST/v1/events/searchGET/v1/events/streamGET/v1/events/{event_id}
POST /v1/events
Append one event
Appends one event to the account's chain after checking its continuity with the last stored event.
The first event carries sequence 0 and prev_hash null. Every later event carries the payload_hash of the last stored event as prev_hash and a higher sequence.
The API stores what you send in raw and adds a payload_hash. Keep prompts, tool output and secrets out of raw; the reference clients send hashes and structural fields only.
If the machine registered a signing key, signature must verify over raw or the append is refused.
epoch is response-side context derived from the event time; it is not added to the stored bytes. The response may also carry dedup_advisory when a cacheable tool call repeats an earlier one.
Parameters
X-AFA-Surfaceheader, string or null- Which surface this call arrived on; one of rest, mcp, mcp_stdio, sdk, cli, hook, dashboard. Absent means rest. Stored on the event outside the signed bytes.
Request body
decisionstring or null- No description in the schema.
event_idstring required- No description in the schema.
event_typestring required- No description in the schema.
machine_idstring or null- No description in the schema.
parent_idstring or null- No description in the schema.
payload_hashstring or null- No description in the schema.
prev_hashstring or null- No description in the schema.
rawobject or null- No description in the schema.
sequenceinteger required- No description in the schema.
signaturestring or null- No description in the schema.
signature_keystring or null- No description in the schema.
toolstring or null- No description in the schema.
tsstring or null- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/events" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"decision": "allow",
"event_id": "evt-1a2b3c4d5e6f7081",
"event_type": "tool_call",
"machine_id": "worker-1",
"prev_hash": null,
"raw": {
"target": "staging",
"ticket": "OPS-412"
},
"sequence": 0,
"tool": "deploy"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"decision": "allow",
"event_id": "evt-1a2b3c4d5e6f7081",
"event_type": "tool_call",
"machine_id": "worker-1",
"prev_hash": None,
"raw": {
"target": "staging",
"ticket": "OPS-412"
},
"sequence": 0,
"tool": "deploy"
}
r = requests.post(f"{API}/v1/events", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"epoch": {
"anchor": "2026-01-01",
"date_utc": "2026-08-31",
"epoch_index": 242,
"phase": {
"accumulated_phase": "242.000000",
"cycle_number": 0,
"epoch_index": 242,
"scheme": "epoch-accumulation-v1"
},
"quadrant": "south",
"scheme": "afa-epoch-v1"
},
"event": {
"api_key_id": "7a1c3e5b-2d4f-4a6b-9c8e-0f1a2b3c4d5e",
"decision": "allow",
"event_id": "evt-1a2b3c4d5e6f7081",
"event_type": "tool_call",
"machine_id": "worker-1",
"machine_sequence": 1,
"parent_id": null,
"payload_hash": "sha256:74f696b9d0babf1685dc161c2f83a88c8822f940e54d3876568800d1d3c51ab6",
"prev_hash": null,
"raw": {
"target": "staging",
"ticket": "OPS-412"
},
"sequence": 0,
"signature": null,
"signature_key": null,
"tool": "deploy",
"ts": "2026-08-31T14:12:07+00:00",
"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
},
"quota": {
"limit": 10000,
"remaining": 9999,
"used_today": 1
},
"status": "appended"
}
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 events:write; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 401 | event_signature_invalid | The machine has a registered key and the signature does not verify. |
| 409 | prev_hash_mismatch | prev_hash is not the last stored payload_hash; the body carries the expected value. |
| 409 | non_monotonic_sequence | sequence is not above the last stored sequence. |
| 409 | prev_hash_for_genesis | A first event carried a prev_hash; it must be null. |
| 429 | daily_quota_exceeded | 10,000 events today; writes resume at 00:00 UTC. |
What would show this is false
Post the same body twice. The second answers 409 prev_hash_mismatch and names the hash it expected; the first stored event is unchanged.
POST /v1/events/batch
Append a batch from a local ledger
Appends up to 500 events in one call, for a local ledger syncing its rows.
Limits: 500 events per call, 100 events per minute per key, 10,000 events per account per day. Inserts are idempotent on event_id.
Batch rows are checked for shape only (event_id, sequence, event_type). The single-event route is the one that checks prev_hash continuity and signatures; run the chain verifier after a batch.
Parameters
None.
Request body
client_machinestring or null- No description in the schema.
eventsarray of object- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/events/batch" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"client_machine": "worker-1",
"events": [
{
"decision": "allow",
"event_id": "evt-2b3c4d5e6f708192",
"event_type": "tool_result",
"machine_id": "worker-1",
"payload_hash": "sha256:772e64d4f2db3ca9cedf4cf77d19cc66ba30a2fa8263bb0077a1f391c4ffc0da",
"prev_hash": "sha256:74f696b9d0babf1685dc161c2f83a88c8822f940e54d3876568800d1d3c51ab6",
"sequence": 1,
"tool": "deploy"
}
]
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"client_machine": "worker-1",
"events": [
{
"decision": "allow",
"event_id": "evt-2b3c4d5e6f708192",
"event_type": "tool_result",
"machine_id": "worker-1",
"payload_hash": "sha256:772e64d4f2db3ca9cedf4cf77d19cc66ba30a2fa8263bb0077a1f391c4ffc0da",
"prev_hash": "sha256:74f696b9d0babf1685dc161c2f83a88c8822f940e54d3876568800d1d3c51ab6",
"sequence": 1,
"tool": "deploy"
}
]
}
r = requests.post(f"{API}/v1/events/batch", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"client_machine": "worker-1",
"errors": [],
"ingested": 1,
"quota": {
"limit": 10000,
"remaining": 9998,
"used_today": 2
},
"rate": {
"count": 1,
"limit": 100,
"remaining": 99
},
"skipped": 0
}
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 events:write; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 413 | batch_too_large | More than 500 events; nothing from the batch is stored. |
| 429 | rate_limit_exceeded | Over 100 events this minute; Retry-After says when. |
| 429 | daily_quota_exceeded | 10,000 events today; writes resume at 00:00 UTC. |
What would show this is false
Send 501 events: 413 batch_too_large with the received count, and POST /v1/events/search shows none of them.
POST /v1/events/search
Search events
Returns the account's events, newest first, filtered by type, tool, decision and time window.
after and before are ISO-8601 timestamps. cursor is a sequence number to continue from; limit is 1 to 1000.
Parameters
None.
Request body
afterstring or null- No description in the schema.
beforestring or null- No description in the schema.
cursorinteger or null- No description in the schema.
decisionstring or null- No description in the schema.
event_typesarray of string or null- No description in the schema.
limitinteger- Default
200. toolstring or null- No description in the schema.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/events/search" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"event_types": [
"tool_call"
],
"limit": 50,
"tool": "deploy"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"event_types": [
"tool_call"
],
"limit": 50,
"tool": "deploy"
}
r = requests.post(f"{API}/v1/events/search", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"count": 1,
"results": [
{
"decision": "allow",
"event_id": "evt-1a2b3c4d5e6f7081",
"event_type": "tool_call",
"machine_id": "worker-1",
"machine_sequence": 1,
"parent_id": null,
"payload_hash": "sha256:74f696b9d0babf1685dc161c2f83a88c8822f940e54d3876568800d1d3c51ab6",
"prev_hash": null,
"sequence": 0,
"tool": "deploy",
"ts": "2026-08-31T14:12:07+00:00"
}
],
"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. |
| 403 | scope_missing | A key limited by scopes lacks events:read; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 400 | invalid_after / invalid_before | A timestamp did not parse. |
What would show this is false
count for a window equals the total GET /v1/dashboard/usage reports for the same window; both read the same rows.
GET /v1/events/stream
Live tail
Streams new events for the account as they are appended, over server-sent events.
Content-Type is text/event-stream. A ping arrives every 15 seconds when nothing else does. Nothing from before the connection is replayed.
Parameters
None.
Request body
None.
Example request
curl
curl -sSN -X GET "https://api.afaprotocol.com/v1/events/stream" \
-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/events/stream", headers=headers, stream=True, timeout=(30, None))
r.raise_for_status()
for line in r.iter_lines():
if line:
print(line.decode("utf-8"))
Example response
HTTP 200
event: ready
retry: 3000
data: {"user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"}
event: event
data: {"event_id": "evt-2b3c4d5e6f708192", "event_type": "tool_result", "sequence": 1, "tool": "deploy", "decision": "allow", "machine_id": "worker-1"}
event: ping
data: 1756735927
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 events:read; the body names it. |
What would show this is false
Open the stream, append an event from another terminal: it arrives within a second. Close the terminal that opened the stream and the server drops the listener.
GET /v1/events/{event_id}
One event with its ancestry
Returns one stored event and the prev_hash chain behind it, up to 50 links.
Parameters
event_idpath, string required- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/events/evt-2b3c4d5e6f708192" \
-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/events/evt-2b3c4d5e6f708192", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"ancestry": [
{
"event_id": "evt-1a2b3c4d5e6f7081",
"event_type": "tool_call",
"sequence": 0,
"ts": "2026-08-31T14:12:07+00:00"
}
],
"event": {
"decision": "allow",
"event_id": "evt-2b3c4d5e6f708192",
"event_type": "tool_result",
"machine_id": "worker-1",
"machine_sequence": 2,
"parent_id": null,
"payload_hash": "sha256:772e64d4f2db3ca9cedf4cf77d19cc66ba30a2fa8263bb0077a1f391c4ffc0da",
"prev_hash": "sha256:74f696b9d0babf1685dc161c2f83a88c8822f940e54d3876568800d1d3c51ab6",
"sequence": 1,
"tool": "deploy",
"ts": "2026-08-31T14:12:09+00:00"
},
"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. |
| 403 | scope_missing | A key limited by scopes lacks events:read; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | event_not_found | No such event on this account. Another account's ids are also 404. |
What would show this is false
Ask for an event id that belongs to another account: 404, never another account's data.