API reference

Machine grants

One machine issues a scoped, time-bound, use-capped capability to another. The grantee's action is appended only when a live grant authorizes it.

POST /v1/delegate/grant

Mint a machine-to-machine grant

Issues a scoped, time-bound, use-capped capability from this machine to another and anchors the issuance in the chain.

Auth
session cookie or API key
Capability
delegation
Success
HTTP 201

A key caller is pinned to its key's machine as grantor and may not name another. A session caller must supply grantor_machine.

The issuance is appended to your chain as a delegation_grant event, so a checkpoint covers it and a third party can verify the hand-off in order.

signature, when given, is the grantor's Ed25519 signature over the canonical grant terms; fulfil then checks it against the grantor's registered key.

Parameters

None.

Request body

grantee_machine string required
No description in the schema.
grantor_machine string or null
No description in the schema.
max_uses integer
Default 1.
parent_id string or null
No description in the schema.
scope string or null
No description in the schema.
signature string or null
grantor Ed25519 over the canonical authority
signer_id string or null
No description in the schema.
task string required
No description in the schema.
ttl_seconds integer
Default 3600.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/delegate/grant" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "grantee_machine": "worker-1",
  "max_uses": 3,
  "scope": "staging",
  "task": "deploy",
  "ttl_seconds": 3600
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "grantee_machine": "worker-1",
    "max_uses": 3,
    "scope": "staging",
    "task": "deploy",
    "ttl_seconds": 3600
}

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

Example response

HTTP 201
{
  "epoch": {
    "anchor": "2026-01-01",
    "date_utc": "2026-09-01",
    "epoch_index": 243,
    "phase": {
      "accumulated_phase": "243.000000",
      "cycle_number": 0,
      "epoch_index": 243,
      "scheme": "epoch-accumulation-v1"
    },
    "quadrant": "west",
    "scheme": "afa-epoch-v1"
  },
  "grant": {
    "created_at": "2026-09-01T14:00:00+00:00",
    "expires_at": "2026-09-01T15:00:00+00:00",
    "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
    "grantee_machine": "worker-1",
    "grantor_machine": "orchestrator-1",
    "max_uses": 3,
    "revoked_at": null,
    "scope": "staging",
    "signature": null,
    "signer_id": null,
    "task": "deploy",
    "user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
    "uses": 0
  },
  "status": "granted"
}

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 delegation; the body names it.
422validation errorA required field is missing or a value has the wrong type.
400grantor_machine_requiredA session caller did not name the grantor.
400cannot_grant_to_selfGrantor and grantee are the same machine.
403machine_mismatch_for_api_keyA key caller named a grantor other than its own machine.

What would show this is false

POST /v1/events/search for event_type delegation_grant returns an event whose raw carries these grant terms and this grant_id.

GET /v1/delegate/grant/{grant_id}

One machine grant

Returns one grant with its validity, without consuming a use.

Auth
session cookie or API key
Capability
delegation
Success
HTTP 200

Parameters

grant_id path, string required
No description in the schema.

Request body

None.

Example request

curl

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

Example response

HTTP 200
{
  "epoch": {
    "anchor": "2026-01-01",
    "date_utc": "2026-09-01",
    "epoch_index": 243,
    "phase": {
      "accumulated_phase": "243.000000",
      "cycle_number": 0,
      "epoch_index": 243,
      "scheme": "epoch-accumulation-v1"
    },
    "quadrant": "west",
    "scheme": "afa-epoch-v1"
  },
  "grant": {
    "created_at": "2026-09-01T14:00:00+00:00",
    "expires_at": "2026-09-01T15:00:00+00:00",
    "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
    "grantee_machine": "worker-1",
    "grantor_machine": "orchestrator-1",
    "max_uses": 3,
    "revoked_at": null,
    "scope": "staging",
    "signature": null,
    "signer_id": null,
    "task": "deploy",
    "user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
    "uses": 1
  },
  "status": {
    "expired": false,
    "reason": "ok",
    "remaining_uses": 2,
    "revoked": false,
    "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 delegation; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404grant_not_foundNo such grant on this account.

What would show this is false

remaining_uses drops by one after each fulfil and never below zero. Reading it here consumes nothing.

POST /v1/delegate/grant/{grant_id}/revoke

Revoke a machine grant

Ends a machine grant; a later fulfil is refused.

Auth
session cookie or API key
Capability
delegation
Success
HTTP 200

Parameters

grant_id path, string required
No description in the schema.

Request body

None.

Example request

curl

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

Example response

HTTP 200
{
  "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
  "status": "revoked"
}

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 delegation; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404grant_not_foundNo such grant on this account.
409grant_already_revokedIt was already revoked; nothing changed.

What would show this is false

A fulfil after this returns 403 grant_revoked, and no delegated_action event is appended.

GET /v1/delegate/grants

List machine grants

Lists grants issued and received under this account, each with computed validity.

Auth
session cookie or API key
Capability
delegation
Success
HTTP 200

Parameters

None.

Request body

None.

Example request

curl

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

Example response

HTTP 200
{
  "count": 1,
  "grants": [
    {
      "created_at": "2026-09-01T14:00:00+00:00",
      "epoch": {
        "anchor": "2026-01-01",
        "date_utc": "2026-09-01",
        "epoch_index": 243,
        "phase": {
          "accumulated_phase": "243.000000",
          "cycle_number": 0,
          "epoch_index": 243,
          "scheme": "epoch-accumulation-v1"
        },
        "quadrant": "west",
        "scheme": "afa-epoch-v1"
      },
      "expires_at": "2026-09-01T15:00:00+00:00",
      "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
      "grantee_machine": "worker-1",
      "grantor_machine": "orchestrator-1",
      "max_uses": 3,
      "revoked_at": null,
      "scope": "staging",
      "signature": null,
      "signer_id": null,
      "status": {
        "expired": false,
        "reason": "ok",
        "remaining_uses": 2,
        "revoked": false,
        "valid": true
      },
      "task": "deploy",
      "user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14",
      "uses": 1
    }
  ]
}

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

What would show this is false

After expires_at passes, the same entry shows status.valid false with reason expired. Nothing is deleted.

POST /v1/delegate/{grant_id}/fulfill

Perform a delegated action

Appends a delegated action to the chain only if a live grant authorizes this machine, this task and this scope; otherwise nothing is written.

Auth
session cookie or API key
Capability
delegation
Success
HTTP 200

Fail closed. The grant must exist, be unrevoked and unexpired, name this machine as grantee, match task and scope, have a use left, and, if the grantor signed it, verify against the grantor's registered key. Any failure appends nothing.

The use is consumed after every check passes, so a refused action never burns authority.

Parameters

grant_id path, string required
No description in the schema.

Request body

event_id string or null
No description in the schema.
machine_id string or null
No description in the schema.
result_hash string or null
No description in the schema.
scope string or null
No description in the schema.
signature string or null
No description in the schema.
signature_key string or null
No description in the schema.
task string required
No description in the schema.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/delegate/grant-4b8e2d7c1a9f0e3b5c6d7e8f/fulfill" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "machine_id": "worker-1",
  "result_hash": "sha256:7c805d307ea212e2495af1485c689f18be676122560827921a6168cfa1b10444",
  "scope": "staging",
  "task": "deploy"
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "machine_id": "worker-1",
    "result_hash": "sha256:7c805d307ea212e2495af1485c689f18be676122560827921a6168cfa1b10444",
    "scope": "staging",
    "task": "deploy"
}

r = requests.post(f"{API}/v1/delegate/grant-4b8e2d7c1a9f0e3b5c6d7e8f/fulfill", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "event": {
    "decision": "allow",
    "event_id": "act-3c9d1e7b2a4f60583e1a",
    "event_type": "delegated_action",
    "machine_id": "worker-1",
    "machine_sequence": 3,
    "parent_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
    "payload_hash": "sha256:54f9172d32c54ce3d67c5f80eef15b985ee0aab9290e65ce8350c6fbc0ececa0",
    "prev_hash": "sha256:772e64d4f2db3ca9cedf4cf77d19cc66ba30a2fa8263bb0077a1f391c4ffc0da",
    "raw": {
      "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
      "result_hash": "sha256:7c805d307ea212e2495af1485c689f18be676122560827921a6168cfa1b10444",
      "scope": "staging",
      "tool": "deploy",
      "use_number": 1
    },
    "sequence": 5,
    "signature": null,
    "signature_key": null,
    "tool": "deploy",
    "ts": "2026-09-01T14:20:33+00:00",
    "user_id": "2f6e1a0c-9b4d-4e8a-8c31-5d7f0a2b9c14"
  },
  "grant_id": "grant-4b8e2d7c1a9f0e3b5c6d7e8f",
  "remaining_uses": 2,
  "status": "fulfilled",
  "use_number": 1
}

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 delegation; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404grant_not_foundNo such grant on this account.
403grant_revoked / grant_expiredThe grant no longer holds.
403grantee_mismatch / task_mismatch / scope_mismatchThe caller, task or scope is not what was granted; the body names both values.
403grantor_key_unregistered / grant_signature_invalidThe grant is signed and the grantor's key is missing or does not verify.
401action_signature_invalidThe grantee has a registered key and the action signature fails.
409grant_exhaustedNo use left at the moment of consumption.

What would show this is false

Fulfil from a machine other than the grantee: 403 grantee_mismatch naming both machines, and remaining_uses is unchanged.