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/grantGET/v1/delegate/grant/{grant_id}POST/v1/delegate/grant/{grant_id}/revokeGET/v1/delegate/grantsPOST/v1/delegate/{grant_id}/fulfill
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.
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_machinestring required- No description in the schema.
grantor_machinestring or null- No description in the schema.
max_usesinteger- Default
1. parent_idstring or null- No description in the schema.
scopestring or null- No description in the schema.
signaturestring or null- grantor Ed25519 over the canonical authority
signer_idstring or null- No description in the schema.
taskstring required- No description in the schema.
ttl_secondsinteger- 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
| 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 delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 400 | grantor_machine_required | A session caller did not name the grantor. |
| 400 | cannot_grant_to_self | Grantor and grantee are the same machine. |
| 403 | machine_mismatch_for_api_key | A 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.
Parameters
grant_idpath, 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
| 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 delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | grant_not_found | No 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.
Parameters
grant_idpath, 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
| 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 delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | grant_not_found | No such grant on this account. |
| 409 | grant_already_revoked | It 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.
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
| 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 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.
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_idpath, string required- No description in the schema.
Request body
event_idstring or null- No description in the schema.
machine_idstring or null- No description in the schema.
result_hashstring or null- No description in the schema.
scopestring or null- No description in the schema.
signaturestring or null- No description in the schema.
signature_keystring or null- No description in the schema.
taskstring 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
| 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 delegation; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | grant_not_found | No such grant on this account. |
| 403 | grant_revoked / grant_expired | The grant no longer holds. |
| 403 | grantee_mismatch / task_mismatch / scope_mismatch | The caller, task or scope is not what was granted; the body names both values. |
| 403 | grantor_key_unregistered / grant_signature_invalid | The grant is signed and the grantor's key is missing or does not verify. |
| 401 | action_signature_invalid | The grantee has a registered key and the action signature fails. |
| 409 | grant_exhausted | No 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.