Appearance
Control Plane API Reference
The Control Plane API is the backend orchestration layer for the Gatez operator and developer portals. It manages tenant lifecycle, API key provisioning, rate limit configuration, analytics, health aggregation, notifications, audit trails, branding, and platform settings.
Base URL: http://localhost:4001
All endpoints require authentication. CORS is configured to allow requests from the operator portal (localhost:3003) and developer portal (localhost:3004), configurable via the ALLOWED_ORIGINS environment variable.
Health
GET /health
Health check with dependency status.
Response: 200 OK
json
{
"status": "healthy",
"service": "control-plane-api",
"version": "0.1.0",
"checks": {
"redis": "connected"
}
}If Redis is unreachable, status returns "degraded" and redis returns "unreachable".
curl Example:
bash
curl http://localhost:4001/healthGET /metrics
Prometheus-format metrics endpoint.
Response: 200 OK (text/plain, Prometheus exposition format)
curl Example:
bash
curl http://localhost:4001/metricsGET /api/health/services
Aggregate health status from all platform layers (APISIX, Redis, AI Gateway, Agent Gateway, ClickHouse, Zitadel).
Response: 200 OK
json
[
{"layer": "L1", "service": "APISIX", "status": "healthy", "latency_ms": 12, "url": "http://gw-apisix:9180"},
{"layer": "L1", "service": "Redis", "status": "healthy", "latency_ms": 1, "url": "redis://gw-redis:6379"},
{"layer": "L2", "service": "AI Gateway", "status": "healthy", "latency_ms": 5, "url": "http://ai-gateway:4000"},
{"layer": "L3", "service": "Agent Gateway", "status": "healthy", "latency_ms": 8, "url": "http://agent-gateway:5001"},
{"layer": "L1", "service": "ClickHouse", "status": "healthy", "latency_ms": 15, "url": "http://gw-clickhouse:8123"},
{"layer": "L1", "service": "Zitadel", "status": "healthy", "latency_ms": 45, "url": "http://gw-zitadel:8085"}
]Each entry includes:
| Field | Type | Description |
|---|---|---|
layer | string | Platform layer (L1, L2, L3) |
service | string | Service name |
status | string | healthy, degraded, or down |
latency_ms | integer | Probe latency in milliseconds |
url | string | Internal service URL |
curl Example:
bash
curl http://localhost:4001/api/health/services \
-H "Authorization: Bearer $TOKEN"GET /api/health/alerts
List active alerts from Prometheus Alertmanager.
Response: 200 OK
json
[]curl Example:
bash
curl http://localhost:4001/api/health/alerts \
-H "Authorization: Bearer $TOKEN"Tenants
GET /api/tenants
List all tenants with usage statistics.
Response: 200 OK
json
[
{
"id": "tenant-alpha",
"name": "Alpha Corp",
"plan": "pro",
"status": "active",
"rate_limit_max": 2500,
"rate_limit_usage": 0,
"token_budget": 2000000,
"token_budget_used": 150000,
"request_count_24h": 0,
"created_at": "2026-03-21"
}
]curl Example:
bash
curl http://localhost:4001/api/tenants \
-H "Authorization: Bearer $TOKEN"POST /api/tenants
Create a new tenant. Atomically provisions across all layers: Redis metadata, rate limit bucket, token budget, APISIX consumer group.
Request Body:
json
{
"name": "Beta Inc",
"plan": "pro",
"admin_email": "admin@beta.com",
"realm_name": "beta",
"claim_path": "tenant_id"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Tenant display name |
plan | string | Yes | Plan tier: free, pro, or enterprise |
id | string | No | Custom tenant ID (auto-generated if omitted) |
admin_email | string | No | Admin contact email |
org_name | string | No | Zitadel organization name |
claim_path | string | No | JWT claim path for tenant extraction |
Plan defaults:
| Plan | Rate Limit (req/min) | Token Budget |
|---|---|---|
free | 100 | 50,000 |
pro | 2,500 | 2,000,000 |
enterprise | 25,000 | 10,000,000 |
Response: 201 Created
json
{
"id": "tenant-beta",
"name": "Beta Inc",
"plan": "pro",
"status": "active",
"rate_limit_max": 2500,
"rate_limit_usage": 0,
"token_budget": 2000000,
"token_budget_used": 0,
"request_count_24h": 0,
"created_at": "2026-03-25"
}Error Response: 409 Conflict
json
{"error": "Tenant already exists"}curl Example:
bash
curl -X POST http://localhost:4001/api/tenants \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "Beta Inc", "plan": "pro", "admin_email": "admin@beta.com"}'GET /api/tenants/:id
Get a specific tenant with current usage.
Path Parameters:
| Parameter | Description |
|---|---|
id | Tenant identifier |
Response: 200 OK
json
{
"id": "tenant-alpha",
"name": "Alpha Corp",
"plan": "pro",
"status": "active",
"rate_limit_max": 2500,
"rate_limit_usage": 0,
"token_budget": 2000000,
"token_budget_used": 150000,
"request_count_24h": 0,
"created_at": "2026-03-21"
}Error Response: 404 Not Found
json
{"error": "Tenant not found"}curl Example:
bash
curl http://localhost:4001/api/tenants/tenant-alpha \
-H "Authorization: Bearer $TOKEN"PATCH /api/tenants/:id
Update tenant properties.
Path Parameters:
| Parameter | Description |
|---|---|
id | Tenant identifier |
Request Body:
json
{
"plan": "enterprise",
"status": "active",
"rate_limit_max": 25000,
"token_budget": 10000000
}All fields are optional. Only provided fields are updated.
| Field | Type | Description |
|---|---|---|
plan | string | Plan tier |
status | string | Tenant status (active, suspended) |
rate_limit_max | integer | Max requests per minute |
token_budget | integer | Token budget (resets remaining to this value) |
Response: 200 OK
json
{"status": "updated"}curl Example:
bash
curl -X PATCH http://localhost:4001/api/tenants/tenant-alpha \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"plan": "enterprise", "rate_limit_max": 25000}'DELETE /api/tenants/:id
Delete a tenant and all associated resources (Redis metadata, rate limit bucket, token budget).
Path Parameters:
| Parameter | Description |
|---|---|
id | Tenant identifier |
Response: 200 OK
json
{"status": "deleted"}curl Example:
bash
curl -X DELETE http://localhost:4001/api/tenants/tenant-alpha \
-H "Authorization: Bearer $TOKEN"Keys
GET /api/keys
List all API keys (masked) across all tenants.
Response: 200 OK
json
[
{
"id": "k-abc12345",
"prefix": "****ab12",
"name": "my-app-key",
"tenant_id": "tenant-alpha",
"routes": ["/v1/chat/completions"],
"status": "active",
"created_at": "2026-03-22",
"expires_at": null,
"request_count": 0
}
]curl Example:
bash
curl http://localhost:4001/api/keys \
-H "Authorization: Bearer $TOKEN"POST /api/keys/request
Submit an API key request (developer workflow).
Request Body:
json
{
"name": "my-app-key",
"tenant_id": "tenant-alpha",
"routes": ["/v1/chat/completions", "/v1/models"],
"reason": "Need API access for my application",
"email": "dev@alpha.com"
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Key name (becomes APISIX consumer name) |
tenant_id | string | Yes | Tenant identifier |
routes | array | Yes | Requested route access |
reason | string | Yes | Justification for the key |
email | string | Yes | Requester email |
Response: 201 Created
json
{
"id": "kr-abc12345",
"email": "dev@alpha.com",
"tenant_id": "tenant-alpha",
"name": "my-app-key",
"routes": ["/v1/chat/completions", "/v1/models"],
"reason": "Need API access for my application",
"requested_at": "2026-03-25T10:00:00Z",
"status": "pending"
}curl Example:
bash
curl -X POST http://localhost:4001/api/keys/request \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app-key",
"tenant_id": "tenant-alpha",
"routes": ["/v1/chat/completions"],
"reason": "API access for my app",
"email": "dev@alpha.com"
}'GET /api/keys/requests
List pending API key requests.
Response: 200 OK
json
[
{
"id": "kr-abc12345",
"email": "dev@alpha.com",
"tenant_id": "tenant-alpha",
"name": "my-app-key",
"routes": ["/v1/chat/completions"],
"reason": "API access for my app",
"requested_at": "2026-03-25T10:00:00Z",
"status": "pending"
}
]curl Example:
bash
curl http://localhost:4001/api/keys/requests \
-H "Authorization: Bearer $TOKEN"POST /api/keys/requests/:id/approve
Approve a key request. Creates an APISIX consumer with key-auth plugin and returns the full API key (shown only once).
Path Parameters:
| Parameter | Description |
|---|---|
id | Key request identifier |
Response: 200 OK
json
{
"status": "approved",
"key_id": "k-abc12345",
"full_key": "gtz_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
"name": "my-app-key"
}DANGER
The full_key is returned only once upon approval. Store it securely. It cannot be retrieved again.
curl Example:
bash
curl -X POST http://localhost:4001/api/keys/requests/kr-abc12345/approve \
-H "Authorization: Bearer $TOKEN"POST /api/keys/requests/:id/deny
Deny a key request.
Path Parameters:
| Parameter | Description |
|---|---|
id | Key request identifier |
Response: 200 OK
json
{"status": "denied"}curl Example:
bash
curl -X POST http://localhost:4001/api/keys/requests/kr-abc12345/deny \
-H "Authorization: Bearer $TOKEN"DELETE /api/keys/:id
Revoke an API key. Removes the APISIX consumer and marks the key as revoked.
Path Parameters:
| Parameter | Description |
|---|---|
id | API key identifier |
Response: 200 OK
json
{"status": "revoked"}curl Example:
bash
curl -X DELETE http://localhost:4001/api/keys/k-abc12345 \
-H "Authorization: Bearer $TOKEN"Rate Limits
GET /api/rate-limits/:tenant_id
Get the rate limit hierarchy for a tenant (global default, tenant override, route overrides).
Path Parameters:
| Parameter | Description |
|---|---|
tenant_id | Tenant identifier |
Response: 200 OK
json
{
"tenant_id": "tenant-alpha",
"global_default": {
"limit": 5000,
"window": "60s",
"current_traffic": 0
},
"tenant_override": {
"limit": 2500,
"window": "60s",
"current_traffic": 0
},
"route_overrides": []
}If no tenant override is set, tenant_override is null.
curl Example:
bash
curl http://localhost:4001/api/rate-limits/tenant-alpha \
-H "Authorization: Bearer $TOKEN"PUT /api/rate-limits/:tenant_id
Update the tenant-level rate limit override.
Path Parameters:
| Parameter | Description |
|---|---|
tenant_id | Tenant identifier |
Request Body:
json
{
"limit": 5000,
"window": "60s"
}| Field | Type | Required | Description |
|---|---|---|---|
limit | integer | Yes | Requests per window |
window | string | No | Time window (default: 60s) |
Response: 200 OK
json
{"status": "updated", "limit": 5000}curl Example:
bash
curl -X PUT http://localhost:4001/api/rate-limits/tenant-alpha \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"limit": 5000}'PUT /api/rate-limits/:tenant_id/routes/:route_id
Update a route-level rate limit override for a tenant.
Path Parameters:
| Parameter | Description |
|---|---|
tenant_id | Tenant identifier |
route_id | APISIX route identifier |
Request Body:
json
{
"limit": 1000,
"window": "60s"
}Response: 200 OK
json
{"status": "updated", "route_id": "route-123", "limit": 1000}curl Example:
bash
curl -X PUT http://localhost:4001/api/rate-limits/tenant-alpha/routes/route-123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"limit": 1000}'Analytics
All analytics endpoints query ClickHouse and accept the following query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | (all tenants) | Filter by tenant |
range | string | 24h | Time range: 1h, 24h, 7d, 30d |
GET /api/analytics/traffic
L1 API Gateway traffic analytics (requests, errors, latency).
Response: 200 OK
json
{
"data": [
{
"hour": "2026-03-25 10:00:00",
"requests": 1520,
"errors": 12,
"avg_latency_ms": 8.5,
"p99_latency_ms": 42.3
}
]
}curl Example:
bash
curl "http://localhost:4001/api/analytics/traffic?tenant_id=tenant-alpha&range=24h" \
-H "Authorization: Bearer $TOKEN"GET /api/analytics/llm
L2 AI Gateway LLM usage analytics (tokens, models, cache hits).
Response: 200 OK
json
{
"data": [
{
"hour": "2026-03-25 10:00:00",
"model": "gpt-4o",
"provider": "openai",
"prompt_tokens": 45000,
"completion_tokens": 12000,
"total_tokens": 57000,
"avg_latency_ms": 350.2,
"cache_hits": 42,
"total_requests": 120
}
]
}curl Example:
bash
curl "http://localhost:4001/api/analytics/llm?tenant_id=tenant-alpha&range=7d" \
-H "Authorization: Bearer $TOKEN"GET /api/analytics/agents
L3 Agent Gateway analytics (tool calls, actions, errors).
Response: 200 OK
json
{
"data": [
{
"hour": "2026-03-25 10:00:00",
"action": "tool_call",
"tool_name": "read_file",
"total": 85,
"errors": 2
}
]
}curl Example:
bash
curl "http://localhost:4001/api/analytics/agents?range=24h" \
-H "Authorization: Bearer $TOKEN"GET /api/analytics/consumers
Per-consumer request analytics (requests, errors, latency by consumer).
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | (all tenants) | Filter by tenant |
consumer_name | string | (all consumers) | Filter by specific consumer |
range | string | 24h | Time range: 1h, 24h, 7d, 30d, 90d |
from | string | - | Custom start date (YYYY-MM-DD) |
to | string | - | Custom end date (YYYY-MM-DD) |
Response: 200 OK
json
{
"data": [
{
"consumer_name": "mobile-app-prod",
"hour": "2026-04-05 10:00:00",
"requests": 2450,
"errors": 8,
"avg_latency_ms": 12.3,
"p99_latency_ms": 48.7
},
{
"consumer_name": "web-portal",
"hour": "2026-04-05 10:00:00",
"requests": 1820,
"errors": 3,
"avg_latency_ms": 9.1,
"p99_latency_ms": 35.2
}
]
}curl Example:
bash
curl "http://localhost:4001/api/analytics/consumers?tenant_id=tenant-alpha&range=7d" \
-H "Authorization: Bearer $TOKEN"curl Example (specific consumer):
bash
curl "http://localhost:4001/api/analytics/consumers?consumer_name=mobile-app-prod&range=24h" \
-H "Authorization: Bearer $TOKEN"Audit
GET /api/audit
Query the audit trail from ClickHouse. Supports filtering and CSV export.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | (all) | Filter by tenant |
action | string | (all) | Filter by action (partial match) |
from | string | (none) | Start timestamp (ISO 8601) |
to | string | (none) | End timestamp (ISO 8601) |
limit | integer | 100 | Max rows (capped at 1000) |
format | string | json | Response format: json or csv |
Response (JSON): 200 OK
json
{
"data": [
{
"timestamp": "2026-03-25T10:05:00Z",
"tenant_id": "tenant-alpha",
"session_id": "sess-xyz789",
"agent_id": "agent-orchestrator",
"tool_name": "read_file",
"action": "tool_call",
"status": "success",
"trace_id": "abc123"
}
],
"rows": 1
}Response (CSV): 200 OK with Content-Type: text/csv and Content-Disposition: attachment; filename=audit_log.csv
curl Example (JSON):
bash
curl "http://localhost:4001/api/audit?tenant_id=tenant-alpha&action=tool_call&limit=50" \
-H "Authorization: Bearer $TOKEN"curl Example (CSV):
bash
curl "http://localhost:4001/api/audit?tenant_id=tenant-alpha&format=csv" \
-H "Authorization: Bearer $TOKEN" \
-o audit_log.csvExport
GET /api/export/:table
Generic CSV export for ClickHouse tables.
Path Parameters:
| Parameter | Description |
|---|---|
table | Table name (allowed: request_log, ai_request_log, agent_audit_log) |
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | (all) | Filter by tenant |
from | string | (none) | Start timestamp |
to | string | (none) | End timestamp |
limit | integer | 1000 | Max rows (capped at 10000) |
Columns by table:
| Table | Columns |
|---|---|
request_log | timestamp, tenant_id, route_id, method, status_code, latency_ms, trace_id |
ai_request_log | timestamp, tenant_id, model, tokens_prompt, tokens_completion, cache_hit, pii_detected, latency_ms, trace_id |
agent_audit_log | timestamp, tenant_id, session_id, agent_id, tool_name, action, status, trace_id |
Response: 200 OK with Content-Type: text/csv and Content-Disposition: attachment; filename={table}.csv
Error Response (invalid table):
json
{"error": "Invalid table name"}curl Example:
bash
curl "http://localhost:4001/api/export/ai_request_log?tenant_id=tenant-alpha&limit=500" \
-H "Authorization: Bearer $TOKEN" \
-o ai_request_log.csvNotifications
GET /api/notifications
List notifications for a tenant.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | all | Tenant identifier |
unread_only | boolean | false | Filter to unread only |
Response: 200 OK
json
{
"notifications": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"tenant_id": "tenant-alpha",
"notification_type": "key_approved",
"title": "API Key Approved",
"message": "Your key request 'my-app-key' has been approved.",
"read": false,
"created_at": "2026-03-25T10:00:00Z"
}
],
"total": 1,
"tenant_id": "tenant-alpha"
}Notification types: key_approved, key_denied, budget_warning, hitl_pending, session_errored, health_degraded.
curl Example:
bash
curl "http://localhost:4001/api/notifications?tenant_id=tenant-alpha&unread_only=true" \
-H "Authorization: Bearer $TOKEN"POST /api/notifications
Create a notification (internal use).
Request Body:
json
{
"tenant_id": "tenant-alpha",
"notification_type": "budget_warning",
"title": "Token Budget Low",
"message": "Your token budget is below 10%. Current remaining: 5,000 tokens."
}| Field | Type | Required | Description |
|---|---|---|---|
tenant_id | string | Yes | Target tenant |
notification_type | string | Yes | Notification category |
title | string | Yes | Short title |
message | string | Yes | Notification body |
Response: 201 Created
json
{"id": "550e8400-e29b-41d4-a716-446655440000", "status": "created"}curl Example:
bash
curl -X POST http://localhost:4001/api/notifications \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "tenant-alpha",
"notification_type": "budget_warning",
"title": "Token Budget Low",
"message": "Your token budget is below 10%."
}'POST /api/notifications/:id/read
Mark a notification as read.
Path Parameters:
| Parameter | Description |
|---|---|
id | Notification identifier |
Response: 200 OK
json
{"id": "550e8400-e29b-41d4-a716-446655440000", "read": true}curl Example:
bash
curl -X POST http://localhost:4001/api/notifications/550e8400-e29b-41d4-a716-446655440000/read \
-H "Authorization: Bearer $TOKEN"GET /api/notifications/preferences
Get notification preferences for a tenant.
Query Parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
tenant_id | string | default | Tenant identifier |
Response: 200 OK
json
{
"tenant_id": "tenant-alpha",
"webhook_url": null,
"slack_url": null,
"email": null,
"enabled_types": ["key_approved", "key_denied", "budget_warning", "hitl_pending"]
}curl Example:
bash
curl "http://localhost:4001/api/notifications/preferences?tenant_id=tenant-alpha" \
-H "Authorization: Bearer $TOKEN"PUT /api/notifications/preferences
Update notification preferences for a tenant.
Request Body:
json
{
"tenant_id": "tenant-alpha",
"webhook_url": "https://hooks.example.com/notify",
"slack_url": "https://hooks.slack.com/services/T00/B00/xxxx",
"email": "alerts@alpha.com",
"enabled_types": ["key_approved", "budget_warning", "hitl_pending", "health_degraded"]
}Response: 200 OK
json
{"status": "updated", "tenant_id": "tenant-alpha"}curl Example:
bash
curl -X PUT http://localhost:4001/api/notifications/preferences \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tenant_id": "tenant-alpha",
"webhook_url": "https://hooks.example.com/notify",
"enabled_types": ["key_approved", "budget_warning"]
}'Branding
GET /api/tenants/:id/branding
Get tenant-specific portal branding.
Path Parameters:
| Parameter | Description |
|---|---|
id | Tenant identifier |
Response: 200 OK
json
{
"tenant_id": "tenant-alpha",
"logo_base64": null,
"portal_title": "Gatez Developer Portal",
"primary_color": null,
"updated_at": null
}curl Example:
bash
curl http://localhost:4001/api/tenants/tenant-alpha/branding \
-H "Authorization: Bearer $TOKEN"POST /api/tenants/:id/branding
Update tenant portal branding. Logo must be under 100KB (base64-encoded).
Path Parameters:
| Parameter | Description |
|---|---|
id | Tenant identifier |
Request Body:
json
{
"portal_title": "Alpha Dev Portal",
"primary_color": "#2563eb",
"logo_base64": "iVBORw0KGgoAAAANSUhEUg..."
}| Field | Type | Required | Description |
|---|---|---|---|
logo_base64 | string | No | Base64-encoded logo image (max 100KB) |
portal_title | string | No | Custom portal title |
primary_color | string | No | Primary color hex code |
Response: 200 OK
json
{
"tenant_id": "tenant-alpha",
"logo_base64": "iVBORw0KGgoAAAANSUhEUg...",
"portal_title": "Alpha Dev Portal",
"primary_color": "#2563eb",
"updated_at": "2026-03-25T12:00:00Z"
}Error Response: 400 Bad Request
json
{"error": "Logo exceeds 100KB limit"}curl Example:
bash
curl -X POST http://localhost:4001/api/tenants/tenant-alpha/branding \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"portal_title": "Alpha Dev Portal", "primary_color": "#2563eb"}'Settings
GET /api/settings
Get platform-wide settings.
Response: 200 OK
json
{
"platform_name": "Gatez Gateway Platform",
"default_rate_limit": 100,
"default_token_budget": 100000,
"data_retention": {
"request_log_ttl_days": 90,
"ai_request_log_ttl_days": 365,
"audit_log_ttl": "forever"
},
"notification_config": {
"slack_webhook_url": null,
"email_smtp": null,
"pagerduty_key": null
}
}curl Example:
bash
curl http://localhost:4001/api/settings \
-H "Authorization: Bearer $TOKEN"PUT /api/settings
Update platform-wide settings.
Request Body:
json
{
"platform_name": "Gatez Gateway Platform",
"default_rate_limit": 200,
"default_token_budget": 200000,
"data_retention": {
"request_log_ttl_days": 90,
"ai_request_log_ttl_days": 365,
"audit_log_ttl": "forever"
},
"notification_config": {
"slack_webhook_url": "https://hooks.slack.com/services/T00/B00/xxxx",
"email_smtp": null,
"pagerduty_key": null
}
}Response: 200 OK
json
{
"status": "updated",
"settings": { ... }
}curl Example:
bash
curl -X PUT http://localhost:4001/api/settings \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"platform_name": "Gatez", "default_rate_limit": 200, "default_token_budget": 200000}'API Lifecycle
PATCH /api/routes/:id/lifecycle
Update the lifecycle state of an APISIX route. Updates the route labels in APISIX and logs the change to the audit trail.
Path Parameters:
| Parameter | Description |
|---|---|
id | APISIX route identifier |
Request Body:
json
{
"lifecycle": "deprecated"
}| Field | Type | Required | Values |
|---|---|---|---|
lifecycle | string | Yes | draft, published, deprecated, retired |
Response: 200 OK
json
{"status": "updated", "route_id": "route-123", "lifecycle": "deprecated"}Error Response: 400 Bad Request
json
{"error": "Invalid lifecycle state"}curl Example:
bash
curl -X PATCH http://localhost:4001/api/routes/route-123/lifecycle \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"lifecycle": "deprecated"}'Agent Identity (Feature-Flagged)
Feature Flag
The Agent Identity feature is disabled by default. Enable via GATEZ_AGENT_IDENTITY=on (server) and VITE_AGENT_IDENTITY=on (frontend). When disabled, all endpoints below return 404 Not Found.
The Agent Identity system provides per-hop agent identity verification via an RFC 8693 Security Token Service (STS). It enables dual-principal authorization (originating human + acting agent) for regulated agent workflows.
See the complete Agent Identity Guide for architecture, use cases, and security model.
POST /api/agents
Register a new agent principal. Only platform admins can register agents.
Headers:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer {jwt-token} |
Content-Type | Yes | application/json |
Request Body:
json
{
"agent_id": "agent:investigation-agent",
"tenant_id": "clinical",
"allowed_audiences": ["agent:oncall-agent", "agent:orders-agent"],
"allowed_tools": ["get_patient", "get_lab_results"],
"credential_ref": "service-user-investigation-client-id",
"description": "Patient investigation agent"
}| Field | Type | Required | Description |
|---|---|---|---|
agent_id | string | Yes | Globally unique agent ID, prefixed form: agent:<name> |
tenant_id | string | Yes | Tenant scope (multi-tenancy invariant) |
allowed_audiences | array | No | Agent IDs this agent may delegate to (A2A delegation policy) |
allowed_tools | array | No | MCP tool names this agent may invoke |
credential_ref | string | No | P0: Zitadel service-user client ID; P1: mTLS cert ref |
description | string | No | Human-readable description |
Response: 200 OK
json
{
"status": "registered",
"agent_id": "agent:investigation-agent"
}Error Response: 400 Bad Request
json
{"error": "agent_id cannot be empty"}curl Example:
bash
curl -X POST http://localhost:4001/api/agents \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent_id": "agent:investigation-agent",
"tenant_id": "clinical",
"allowed_audiences": ["agent:oncall-agent"],
"allowed_tools": ["get_patient"],
"credential_ref": "service-user-investigation-client-id"
}':::note Delegation Policy Projection On registration, the Control Plane API projects allowed_audiences to Redis (gatez:deleg:{agent_id} SET). The STS (issue-time) and L3 (call-time) both read this projection, guaranteeing delegation policy consistency. :::
GET /api/agents
List all registered agent principals.
Headers:
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer {jwt-token} |
Response: 200 OK
json
{
"agents": [
{
"agent_id": "agent:investigation-agent",
"tenant_id": "clinical",
"allowed_audiences": ["agent:oncall-agent"],
"allowed_tools": ["get_patient"],
"credential_ref": "service-user-investigation-client-id",
"description": "Patient investigation agent"
}
]
}curl Example:
bash
curl http://localhost:4001/api/agents \
-H "Authorization: Bearer $TOKEN"GET /api/agents/:agent_id
Fetch a single agent registration.
Path Parameters:
| Parameter | Description |
|---|---|
agent_id | Agent identifier (e.g., agent:investigation-agent) |
Response: 200 OK
json
{
"agent_id": "agent:investigation-agent",
"tenant_id": "clinical",
"allowed_audiences": ["agent:oncall-agent"],
"allowed_tools": ["get_patient"],
"credential_ref": "service-user-investigation-client-id",
"description": "Patient investigation agent"
}Error Response: 404 Not Found
json
{"error": "agent 'agent:unknown' not registered"}curl Example:
bash
curl http://localhost:4001/api/agents/agent:investigation-agent \
-H "Authorization: Bearer $TOKEN"DELETE /api/agents/:agent_id
Deregister an agent principal. Removes the agent from the registry and clears its delegation policy projection.
Path Parameters:
| Parameter | Description |
|---|---|
agent_id | Agent identifier |
Response: 200 OK
json
{
"status": "deleted",
"agent_id": "agent:investigation-agent"
}Error Response: 404 Not Found
json
{"error": "agent 'agent:unknown' not registered"}curl Example:
bash
curl -X DELETE http://localhost:4001/api/agents/agent:investigation-agent \
-H "Authorization: Bearer $TOKEN"GET /api/agent-topology
Live A2A delegation topology for a tenant, aggregated from the STS issuance audit (gateway.sts_token_issuance). Each edge is an observed subject → audience exchange with issued/denied counts and the deepest act_chain seen — this powers the operator A2A topology graph with real token-exchange traffic. Tenant-scoped (mandatory).
Query Parameters:
| Parameter | Required | Description |
|---|---|---|
tenant_id | Yes | Tenant to scope edges to (alphanumeric, -, _) |
range | No | Time window: 1h, 24h (default), 7d, 30d |
Response: 200 OK — task_count/issued/denied are returned as strings (ClickHouse quotes 64-bit ints).
json
{
"edges": [
{
"from_agent": "agent:shopping",
"to_agent": "agent:orders",
"task_count": "3",
"issued": "3",
"denied": "0",
"max_depth": 2,
"last_seen": 1782663081
}
]
}Returns {"edges": []} (not an error) when ClickHouse is unavailable or the tenant has no traffic yet, so the graph degrades gracefully.
curl Example:
bash
curl "http://localhost:4001/api/agent-topology?tenant_id=shopping&range=24h" \
-H "Authorization: Bearer $TOKEN"POST /sts/token
RFC 8693 token exchange endpoint. Mints a short-lived (120s TTL), single-hop JWT carrying an append-only act_chain of [human, agent₁, agent₂…].
Headers:
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | application/json |
Request Body:
json
{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "<Zitadel human JWT (hop 1) or prior STS token (hop N)>",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"audience": "agent:oncall-agent",
"actor_token": "<calling agent's credential JWT>",
"scope": "get_patient"
}| Field | Type | Required | Description |
|---|---|---|---|
grant_type | string | Yes | Must be urn:ietf:params:oauth:grant-type:token-exchange |
subject_token | string | Yes | Hop 1: Zitadel human JWT; Hop N: prior STS token |
subject_token_type | string | No | Token type hint (optional) |
audience | string | Yes | The next hop agent ID (single hop only) |
actor_token | string | Yes | Proves the calling agent's identity (P0: Zitadel service-user JWT) |
scope | string | No | Optional intent → becomes purpose claim in minted token |
Response: 200 OK
json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"issued_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_type": "N_A",
"expires_in": 120
}| Field | Description |
|---|---|
access_token | The STS hop token (single-use, single-hop JWT) |
issued_token_type | Always urn:ietf:params:oauth:token-type:jwt |
expires_in | TTL in seconds (120s) |
Error Responses:
| Status | Error | Description |
|---|---|---|
400 Bad Request | unsupported grant_type | grant_type must be the RFC 8693 token exchange URN |
400 Bad Request | audience not registered | The audience agent ID is not in the Agent Registry |
403 Forbidden | cross-tenant exchange refused | subject, caller, and audience must share the same tenant_id |
403 Forbidden | agent may not delegate | Calling agent's allowed_audiences does not include the requested audience |
403 Forbidden | delegation loop detected | The audience already appears in the act_chain (circular delegation) |
curl Example (Hop 1: Human → Agent):
bash
curl -X POST http://localhost:4001/sts/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "'$HUMAN_JWT'",
"actor_token": "'$AGENT_JWT'",
"audience": "agent:oncall-agent",
"scope": "get_patient"
}'curl Example (Hop N: Agent → Agent):
bash
curl -X POST http://localhost:4001/sts/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token": "'$PRIOR_STS_TOKEN'",
"actor_token": "'$CALLING_AGENT_JWT'",
"audience": "agent:orders-agent"
}':::note STS Processing The STS performs the following checks in order:
- Verify
subject_token: Hop 1 via Zitadel JWKS; Hop N via STS signing key - Resolve calling agent: Lookup
actor_tokenin Agent Registry - Resolve audience agent: Lookup
audiencein Agent Registry - Tenant guard: Verify
subject.tenant_id == caller.tenant_id == audience.tenant_id - Delegation policy: Check
SISMEMBER gatez:deleg:{caller.agent_id} {audience} - Loop detection: Reject if
audiencealready inact_chain - Mint hop token: Append caller to
act_chain, sign with STS key, audit to ClickHouse
See Agent Identity Guide for full details. :::
:::note Signing Algorithm P1 (current): EdDSA (Ed25519) asymmetric signing. The Control Plane publishes its public key at GET /sts/jwks; L3 verifies per-hop tokens against that key (configure STS_JWKS_URL on the agent gateway) — no shared secret crosses the trust boundary. Provide the keypair via STS_ED25519_PRIVATE_DER_B64 (PKCS#8 v2 DER, base64), or let it auto-generate an ephemeral key when GATEZ_ENV=local.
P0 (legacy fallback): HS256 symmetric signing with STS_SIGNING_KEY, used only when STS_JWKS_URL is unset. Internal network only. :::
Endpoint Summary
| Method | Path | Description |
|---|---|---|
| GET | /health | Health check |
| GET | /metrics | Prometheus metrics |
| GET | /api/health/services | Aggregate service health |
| GET | /api/health/alerts | Active alerts |
| GET | /api/tenants | List tenants |
| POST | /api/tenants | Create tenant |
| GET | /api/tenants/:id | Get tenant |
| PATCH | /api/tenants/:id | Update tenant |
| DELETE | /api/tenants/:id | Delete tenant |
| GET | /api/keys | List API keys |
| POST | /api/keys/request | Request API key |
| GET | /api/keys/requests | List pending requests |
| POST | /api/keys/requests/:id/approve | Approve key request |
| POST | /api/keys/requests/:id/deny | Deny key request |
| DELETE | /api/keys/:id | Revoke API key |
| GET | /api/rate-limits/:tenant_id | Get rate limit hierarchy |
| PUT | /api/rate-limits/:tenant_id | Update tenant rate limit |
| PUT | /api/rate-limits/:tenant_id/routes/:route_id | Update route rate limit |
| GET | /api/analytics/traffic | Traffic analytics |
| GET | /api/analytics/llm | LLM usage analytics |
| GET | /api/analytics/agents | Agent analytics |
| GET | /api/analytics/consumers | Per-consumer analytics |
| GET | /api/audit | Query audit trail |
| GET | /api/export/:table | CSV export |
| GET | /api/notifications | List notifications |
| POST | /api/notifications | Create notification |
| POST | /api/notifications/:id/read | Mark as read |
| GET | /api/notifications/preferences | Get notification preferences |
| PUT | /api/notifications/preferences | Update notification preferences |
| GET | /api/tenants/:id/branding | Get branding |
| POST | /api/tenants/:id/branding | Update branding |
| GET | /api/settings | Get platform settings |
| PUT | /api/settings | Update platform settings |
| PATCH | /api/routes/:id/lifecycle | Update route lifecycle |
| PATCH | /api/routes/:id/canary | Set canary traffic split (weight + upstream) |
| GET | /api/license | Get current license tier, validity, expiry |
| GET | /api/tenants/:id/ip-allowlist | Get tenant IP allowlist CIDRs |
| PUT | /api/tenants/:id/ip-allowlist | Set tenant IP allowlist CIDRs |
| GET | /api/webhooks | List webhooks (optionally ?tenant_id=) |
| POST | /api/webhooks | Register webhook (URL + event types) |
| DELETE | /api/webhooks/:id | Delete webhook |
| POST | /api/agents | Register agent principal (feature-flagged) |
| GET | /api/agents | List agent principals (feature-flagged) |
| GET | /api/agents/:agent_id | Get agent registration (feature-flagged) |
| DELETE | /api/agents/:agent_id | Deregister agent principal (feature-flagged) |
| GET | /api/agent-topology | Live A2A delegation edges for a tenant (feature-flagged) |
| POST | /sts/token | RFC 8693 token exchange (feature-flagged) |
| GET | /sts/jwks | STS public verification keys (EdDSA JWKS; public) |