API reference

Surfaces

One machine-readable table of which capability is reachable on which surface: REST, hosted MCP, the console, and the operator tools that are not a customer surface.

GET /v1/surfaces

The surface matrix

Which capability is reachable on which surface, generated from the route table and the hosted tool registry so the two cannot disagree.

Auth
none
Capability
surfaces
Success
HTTP 200

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/surfaces"

Python

import requests

API = "https://api.afaprotocol.com"

r = requests.get(f"{API}/v1/surfaces", timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "capabilities": [
    {
      "console": true,
      "hosted_mcp": [
        "afa_grant_create",
        "afa_grant_status",
        "afa_grant_use",
        "afa_grant_revoke",
        "afa_grants_list"
      ],
      "local_operator_tools": true,
      "name": "grants",
      "rest": [
        "GET /v1/grants",
        "GET /v1/grants/status",
        "POST /v1/grants",
        "POST /v1/grants/{grant_id}/revoke",
        "POST /v1/grants/{grant_id}/use"
      ],
      "sdk": true,
      "summary": "Create, check, meter and revoke sub-agent grants."
    }
  ],
  "excluded": [
    {
      "method": "GET",
      "path": "/bundle.authority",
      "reason": "public verification artifact"
    },
    {
      "method": "GET",
      "path": "/static/bundle.pub",
      "reason": "public static assets"
    },
    {
      "method": "GET",
      "path": "/v1/legal/acceptances",
      "reason": "agreement texts and acceptances, consulted before an account exists"
    },
    {
      "method": "GET",
      "path": "/v1/legal/documents",
      "reason": "agreement texts and acceptances, consulted before an account exists"
    },
    {
      "method": "GET",
      "path": "/v1/legal/documents/{document_id}/text",
      "reason": "agreement texts and acceptances, consulted before an account exists"
    },
    {
      "method": "GET",
      "path": "/v1/me",
      "reason": "identity introspection for the signed-in session"
    },
    {
      "method": "POST",
      "path": "/v1/auth/request-otp",
      "reason": "sign-in, code verification and sign-out"
    },
    {
      "method": "POST",
      "path": "/v1/auth/signout",
      "reason": "sign-in, code verification and sign-out"
    },
    {
      "method": "POST",
      "path": "/v1/auth/verify-otp",
      "reason": "sign-in, code verification and sign-out"
    },
    {
      "method": "POST",
      "path": "/v1/internal/cron/anchor",
      "reason": "cron invokers, gated by a shared secret rather than a customer key"
    },
    {
      "method": "POST",
      "path": "/v1/internal/cron/digest",
      "reason": "cron invokers, gated by a shared secret rather than a customer key"
    },
    {
      "method": "POST",
      "path": "/v1/internal/cron/grant-expiry",
      "reason": "cron invokers, gated by a shared secret rather than a customer key"
    },
    {
      "method": "POST",
      "path": "/v1/internal/cron/key-expiry",
      "reason": "cron invokers, gated by a shared secret rather than a customer key"
    },
    {
      "method": "POST",
      "path": "/v1/internal/cron/webhooks",
      "reason": "cron invokers, gated by a shared secret rather than a customer key"
    },
    {
      "method": "POST",
      "path": "/v1/legal/accept",
      "reason": "agreement texts and acceptances, consulted before an account exists"
    },
    {
      "method": "POST",
      "path": "/v1/notifications/digest/run",
      "reason": "administrator only"
    },
    {
      "method": "POST",
      "path": "/v1/notifications/test",
      "reason": "test send; development or administrator only"
    }
  ],
  "generated_from": "openapi+registry",
  "surfaces": {
    "console": {
      "host": "app.afaprotocol.com",
      "note": "The per-capability flag is declared from the console's own calls.",
      "observed": false
    },
    "hosted_mcp": {
      "capability_model": "per_tool",
      "endpoint": "/v1/mcp",
      "endpoint_capability": null,
      "legacy_routes": [
        "GET /v1/mcp/tools",
        "POST /v1/mcp/call"
      ],
      "note": "The endpoint itself needs no capability. Any key of the account may open a session and list the tools; a tool call is admitted only if the key holds that tool's capability, and a refusal names that capability. The per-capability rows above list which tools each capability covers.",
      "observed": true,
      "protocol_versions": [
        "2025-06-18",
        "2025-03-26"
      ],
      "routes": [
        "DELETE /v1/mcp",
        "GET /v1/mcp",
        "GET /v1/mcp/tools",
        "POST /v1/mcp",
        "POST /v1/mcp/call"
      ],
      "tool_count": 33,
      "transport": "streamable-http"
    },
    "local_operator_tools": {
      "customer_surface": false,
      "note": "The operator's own local tooling. It is not offered to customers and is not reachable through this service; the column is declared here so the matrix says where each capability lives, not to advertise it.",
      "observed": false
    },
    "rest": {
      "auth": [
        "X-API-Key",
        "Authorization: Bearer <api key>",
        "session cookie"
      ],
      "base": "/v1",
      "observed": true,
      "openapi": "/openapi.json"
    },
    "sdk": {
      "languages": [
        "python",
        "typescript"
      ],
      "note": "The per-capability flag is declared from the client sources.",
      "observed": false,
      "reason": "Delivery is API-only during the beta; a private client can be discussed on request.",
      "status": "not_offered"
    }
  },
  "unclassified": []
}

Errors

No error responses beyond transport failures.

What would show this is false

Every hosted_mcp name in this response is in tools/list on POST /v1/mcp, and every rest entry is in /openapi.json. A test in the deploy gate holds both equalities. A surface marked not_offered has nothing to fetch.