API reference

Signing identity and settlement chain

Register a machine's Ed25519 public key, then fold events into signed settlement blocks and verify the block chain. This is what makes the record checkable without us.

GET /v1/chain/blocks

List settlement blocks

Lists the account's settlement blocks in order.

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

Parameters

None.

Request body

None.

Example request

curl

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

Example response

HTTP 200
{
  "blocks": [
    {
      "block_hash": "sha256:9263dd51468659ff544714898f6fa402406ae9bdedcb6a18b4a18d3ba30f7818",
      "block_index": 0,
      "created_at": 1756735927.418,
      "event_count": 2,
      "machine_id": null,
      "merkle_root": "sha256:95561b90e5b2564f9c8885d6b272a6bc82ccc056de0ce72e2713c20020eaca54",
      "prev_block_hash": null,
      "range_end": 1,
      "range_start": 0,
      "signature": null,
      "signer_id": null
    }
  ]
}

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

Each block's prev_block_hash equals the previous block's block_hash, and the first block's is null.

POST /v1/chain/checkpoint

Fold events into a settlement block

Folds every event since the last block into a new block whose merkle root covers their payload hashes.

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

With nothing unsettled the response is status noop with settled_through_sequence.

signature, when given, is your Ed25519 signature over merkle_root followed by prev_block_hash. The server stores it and does not verify it; verifying it against your own registered key is what makes the block independent of us.

Parameters

None.

Request body

machine_id string or null
No description in the schema.
signature string or null
client Ed25519 over the block merkle_root||prev_block_hash
signer_id string or null
No description in the schema.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/chain/checkpoint" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "machine_id": null,
  "signature": null,
  "signer_id": null
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "machine_id": None,
    "signature": None,
    "signer_id": None
}

r = requests.post(f"{API}/v1/chain/checkpoint", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "block": {
    "block_hash": "sha256:9263dd51468659ff544714898f6fa402406ae9bdedcb6a18b4a18d3ba30f7818",
    "block_index": 0,
    "created_at": 1756735927.418,
    "event_count": 2,
    "machine_id": null,
    "merkle_root": "sha256:95561b90e5b2564f9c8885d6b272a6bc82ccc056de0ce72e2713c20020eaca54",
    "prev_block_hash": null,
    "range_end": 1,
    "range_start": 0,
    "signature": null,
    "signer_id": null
  },
  "status": "checkpointed"
}

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.

What would show this is false

Recompute merkle_root from the sorted payload hashes of the covered events with the recipe on the Verify page. A different value means the block does not cover the events you think it does.

POST /v1/chain/verify

Verify the block chain

Recomputes every block hash and every link and names the first block that fails.

Auth
session cookie or API key
Capability
verify
Success
HTTP 200

This checks the blocks against themselves. It does not verify the client signatures on blocks; that check is yours to run with your own key.

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/chain/verify" \
  -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.post(f"{API}/v1/chain/verify", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "block_count": 1,
  "broken": [],
  "valid": true
}

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 verify; the body names it.

What would show this is false

A stored block edited below the API shows up as block_hash_mismatch at its index. valid true with block_count 0 means nothing has been checkpointed yet, which is a different fact.

GET /v1/identity

List registered keys

Lists the machines with a registered signing key and each key's fingerprint.

Auth
session cookie or API key
Capability
keys
Success
HTTP 200

Parameters

None.

Request body

None.

Example request

curl

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

Example response

HTTP 200
{
  "identities": [
    {
      "fingerprint": "sha256:b06ac05c8158d444bc27754779144c5b",
      "machine_id": "worker-1",
      "registered_at": "2026-09-01T14:05:00+00:00"
    }
  ]
}

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 keys; the body names it.

What would show this is false

The fingerprint is the SHA-256 of the PEM bytes you registered, first 32 hex characters. Recompute it.

POST /v1/identity/register

Register a machine's signing key

Stores a machine's Ed25519 public key so events from that machine must be signed by it.

Auth
session cookie or API key
Capability
keys
Success
HTTP 200

From this point every POST /v1/events for the machine must carry a signature that verifies over raw, or the append is refused. Registering again rotates the key.

Parameters

None.

Request body

machine_id string required
No description in the schema.
public_key_pem string required
No description in the schema.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/identity/register" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "machine_id": "worker-1",
  "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAExampleKeyBytesOnlyReplaceMe0000000000=\n-----END PUBLIC KEY-----\n"
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "machine_id": "worker-1",
    "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAExampleKeyBytesOnlyReplaceMe0000000000=\n-----END PUBLIC KEY-----\n"
}

r = requests.post(f"{API}/v1/identity/register", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "fingerprint": "sha256:b06ac05c8158d444bc27754779144c5b",
  "machine_id": "worker-1",
  "registered_at": "2026-09-01T14:05:00+00:00",
  "status": "registered"
}

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 keys; the body names it.
422validation errorA required field is missing or a value has the wrong type.
400invalid_ed25519_public_keyThe PEM did not parse as an Ed25519 public key.

What would show this is false

Register a key, then post an unsigned event for that machine: 401 event_signature_invalid with reason signature_mismatch.