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