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

List grants

Lists the caller's grants newest first, each with a state computed by the same checks the status route applies.

Auth
session cookie or API key
Capability
grants
Success
HTTP 200

Parameters

subagent query, 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

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 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.

Auth
session cookie or API key
Capability
grants
Success
HTTP 201

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

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 grants; the body names it.
422validation errorA required field is missing or a value has the wrong type.
422malformed_grantA field is missing, aliased or unreadable; reason names the correct key. Nothing written.
409grant_id_existsThis 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.

Auth
session cookie or API key
Capability
grants
Success
HTTP 200

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_id query, string or null
Optional: restrict the check to one grant id
subagent query, string required
Exact sub-agent name to check
tool query, 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

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 grants; the body names it.
422validation errorA required field is missing or a value has the wrong type.
422malformed_querysubagent 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.

Auth
session cookie or API key
Capability
grants
Success
HTTP 200

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

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 grants; the body names it.
422validation errorA required field is missing or a value has the wrong type.
404no_grantNothing matches this grant id and sub-agent; nothing was revoked.
422malformed_revokesubagent 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.

Auth
session cookie or API key
Capability
grants
Success
HTTP 200

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

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 grants; the body names it.
422validation errorA required field is missing or a value has the wrong type.
409budget_exhaustedNot enough budget left for n; the body names uses and remaining.
409not_authorizedThe gate refuses this grant (no_grant, revoked, expired and so on); outcome names which.
422malformed_usesubagent 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.