API reference
Sub-agent grants
The gate an orchestrator polls before delegating. Create, list, check status, record uses against a budget, revoke. Every refusal names its reason.
Every grant carries an epoch stamp: epoch_index (days since this deployment's epoch origin), quadrant (which quarter of the cycle the day falls in), phase, scheme (the stamp's version), and accumulated_phase (a running phase value under that scheme). The server assigns it when the grant is written so a verifier can check the issue time against the record. Clients do not set it and cannot change it without breaking the signature.
GET/v1/grantsPOST/v1/grantsGET/v1/grants/statusPOST/v1/grants/{grant_id}/revokePOST/v1/grants/{grant_id}/use
GET /v1/grants
List grants
Lists the caller's grants newest first, each with a state computed by the same checks the status route applies.
Parameters
subagentquery, string or null- No description in the schema.
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/grants?subagent=worker-1" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
params = {
"subagent": "worker-1"
}
r = requests.get(f"{API}/v1/grants", headers=headers, params=params, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"count": 1,
"grants": [
{
"action": "granted",
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"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-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"state": "authorized",
"state_reason": "grant g-0c9f31ab7e2d is live",
"subagent": "worker-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 grants; the body names it. |
What would show this is false
Revoke a grant and list again: its state reads revoked. The list and the status route cannot disagree because they run the same check.
POST /v1/grants
Create a sub-agent grant
Writes a grant naming which tools a sub-agent may use, how many calls, and until when; refuses anything the gate could not read back.
The field names are exactly these: subagent, expires_at (an absolute ISO-8601 time), max_tool_calls. Alias keys such as subagent_name or expiry_seconds are refused with 422 naming the correct key, and nothing is written.
If the parent holds a live grant, the child may not exceed it: broader tools, a later expiry or a larger budget is refused as overreach.
status is read back through the same reader the gate uses, so a grant with a past expires_at is reported expired in the same response.
Parameters
None.
Request body
A JSON object with no fixed fields. Follow the example.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/grants" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"expires_at": "2026-09-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"subagent": "worker-1"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"expires_at": "2026-09-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"subagent": "worker-1"
}
r = requests.post(f"{API}/v1/grants", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 201
{
"grant": {
"action": "granted",
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"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-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"subagent": "worker-1"
},
"status": {
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"authorized": true,
"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-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"reason": "grant g-0c9f31ab7e2d is live",
"remaining": 150,
"status": "authorized",
"subagent": "worker-1",
"uses": 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 grants; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | malformed_grant | A field is missing, aliased or unreadable; reason names the correct key. Nothing written. |
| 409 | grant_id_exists | This id already names a live grant; revoke it or choose another id. |
What would show this is false
Write a grant whose expires_at is in the past: 201, and its status reads expired. GET /v1/grants/status then answers expired, not authorized.
GET /v1/grants/status
The delegation gate
Answers whether this sub-agent is authorized right now, for this tool if you name one, and why not if not.
Poll this before delegating. Answers: authorized, no_grant, expired, revoked, budget_exhausted, tool_not_allowed, invalid_expiry, invalid_limit, denied_by_governance. The remedies differ, so the diagnosis is the product.
Matching is exact on the sub-agent name. The most recent live grant wins; a failing recent grant does not shadow an older live one.
Parameters
grant_idquery, string or null- Optional: restrict the check to one grant id
subagentquery, string required- Exact sub-agent name to check
toolquery, string or null- Optional tool name to check against allowed_tools
Request body
None.
Example request
curl
curl -sS -X GET "https://api.afaprotocol.com/v1/grants/status?subagent=worker-1&tool=Bash" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs"
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
params = {
"subagent": "worker-1",
"tool": "Bash"
}
r = requests.get(f"{API}/v1/grants/status", headers=headers, params=params, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"allowed_tools": [
"Read",
"Grep",
"Glob",
"Bash"
],
"authorized": true,
"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-01T20:00:00+00:00",
"grant_id": "g-0c9f31ab7e2d",
"max_tool_calls": 150,
"parent_agent": "orchestrator-1",
"reason": "grant g-0c9f31ab7e2d is live",
"remaining": 147,
"status": "authorized",
"subagent": "worker-1",
"uses": 3
}
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 grants; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | malformed_query | subagent is missing or blank. |
What would show this is false
Ask with tool=Deploy when the grant lists four other tools: tool_not_allowed. The same call without tool: authorized. Nothing was written between the two.
POST /v1/grants/{grant_id}/revoke
Revoke a sub-agent grant
Ends a grant; the gate answers revoked from the next check onward.
A revoke that matches nothing is 404 no_grant, never a bare success. A second revoke of the same grant reports already_revoked and appends nothing.
Revocation is poll-effective at the gate. A registered webhook receives grant.revoked as a push; treat it as a prompt to poll, not as the fact.
Parameters
grant_idpath, string required- No description in the schema.
Request body
A JSON object with no fixed fields. Follow the example.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/grants/g-0c9f31ab7e2d/revoke" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"subagent": "worker-1"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"subagent": "worker-1"
}
r = requests.post(f"{API}/v1/grants/g-0c9f31ab7e2d/revoke", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"grant_id": "g-0c9f31ab7e2d",
"outcome": "revoked",
"status_after": {
"authorized": false,
"reason": "grant g-0c9f31ab7e2d was revoked",
"status": "revoked",
"subagent": "worker-1"
},
"subagent": "worker-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 grants; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 404 | no_grant | Nothing matches this grant id and sub-agent; nothing was revoked. |
| 422 | malformed_revoke | subagent is missing, blank or aliased. |
What would show this is false
status_after comes from the same reader as GET /v1/grants/status. Call that route afterwards and compare; they match.
POST /v1/grants/{grant_id}/use
Record tool calls against the budget
Records n consumed tool calls against a grant; refuses a use that would cross the budget.
The whole n must fit the remaining budget. A use that would cross max_tool_calls is refused entirely rather than partly recorded.
The meter counts what the caller reports. It bounds an honest integration; an integration that never reports is bounded only by expiry and revocation.
Parameters
grant_idpath, string required- No description in the schema.
Request body
A JSON object with no fixed fields. Follow the example.
Example request
curl
curl -sS -X POST "https://api.afaprotocol.com/v1/grants/g-0c9f31ab7e2d/use" \
-H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
-H "Content-Type: application/json" \
-d '{
"n": 3,
"subagent": "worker-1"
}'
Python
import requests
API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
"n": 3,
"subagent": "worker-1"
}
r = requests.post(f"{API}/v1/grants/g-0c9f31ab7e2d/use", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())
Example response
HTTP 200
{
"grant_id": "g-0c9f31ab7e2d",
"outcome": "consumed",
"parent_agent": "orchestrator-1",
"remaining": 147,
"subagent": "worker-1",
"uses": 3
}
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 grants; the body names it. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 409 | budget_exhausted | Not enough budget left for n; the body names uses and remaining. |
| 409 | not_authorized | The gate refuses this grant (no_grant, revoked, expired and so on); outcome names which. |
| 422 | malformed_use | subagent missing or n not a positive integer. |
What would show this is false
Record uses up to the budget, then GET /v1/grants/status: budget_exhausted. The next use is refused with 409 and remaining stays 0.