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/events

Append one event

Appends one event to the account's chain after checking its continuity with the last stored event.

Auth
session cookie or API key
Capability
events:write
Success
HTTP 200

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-Surface header, 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

decision string or null
No description in the schema.
event_id string required
No description in the schema.
event_type string required
No description in the schema.
machine_id string or null
No description in the schema.
parent_id string or null
No description in the schema.
payload_hash string or null
No description in the schema.
prev_hash string or null
No description in the schema.
raw object or null
No description in the schema.
sequence integer required
No description in the schema.
signature string or null
No description in the schema.
signature_key string or null
No description in the schema.
tool string or null
No description in the schema.
ts string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks events:write; the body names it.
422validation errorA required field is missing or a value has the wrong type.
401event_signature_invalidThe machine has a registered key and the signature does not verify.
409prev_hash_mismatchprev_hash is not the last stored payload_hash; the body carries the expected value.
409non_monotonic_sequencesequence is not above the last stored sequence.
409prev_hash_for_genesisA first event carried a prev_hash; it must be null.
429daily_quota_exceeded10,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.

Auth
session cookie or API key
Capability
events:write
Success
HTTP 200

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_machine string or null
No description in the schema.
events array 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks events:write; the body names it.
422validation errorA required field is missing or a value has the wrong type.
413batch_too_largeMore than 500 events; nothing from the batch is stored.
429rate_limit_exceededOver 100 events this minute; Retry-After says when.
429daily_quota_exceeded10,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.

Auth
session cookie or API key
Capability
events:read
Success
HTTP 200

after and before are ISO-8601 timestamps. cursor is a sequence number to continue from; limit is 1 to 1000.

Parameters

None.

Request body

after string or null
No description in the schema.
before string or null
No description in the schema.
cursor integer or null
No description in the schema.
decision string or null
No description in the schema.
event_types array of string or null
No description in the schema.
limit integer
Default 200.
tool string 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks events:read; the body names it.
422validation errorA required field is missing or a value has the wrong type.
400invalid_after / invalid_beforeA 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.

Auth
session cookie or API key
Capability
events:read
Success
HTTP 200

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

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

Auth
session cookie or API key
Capability
events:read
Success
HTTP 200

Parameters

event_id path, 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

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
403scope_missingA key limited by scopes lacks events:read; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404event_not_foundNo 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.