Multi-Tenancy
The multi_tenant license entitlement is required. All tenant endpoints return 402 Payment Required on deployments without this entitlement.
Tenants provide isolated namespaces with their own capability ceilings. Subdomain-based routing is supported when multi_tenancy.base_domain is configured.
Tenant object
{
"id": "01HXZ...",
"name": "Acme Corp",
"slug": "acme",
"plan": "enterprise",
"disabled": false,
"created_at": "2026-01-10T09:00:00Z",
"egress_fqdns": ["api.acme.com", "app.acme.com"]
}
Slug rules: lowercase alphanumeric + hyphens, 2–64 characters, must start and end with alphanumeric. Slugs are immutable after creation — they appear in subdomain routing and changing them would orphan running sessions.
Reserved slugs: default, api, admin, health, metrics, www, scim, auth
GET /admin/tenants
List all tenants. Requires multi_tenant entitlement.
POST /admin/tenants
Create a tenant. Checks the tenant quota from the license unless the caller is super_admin.
{
"name": "Acme Corp",
"slug": "acme",
"plan": "enterprise"
}
Returns 402 with a quota exceeded body when the license limit is reached:
{
"error": "quota_exceeded",
"resource": "tenants",
"limit": 10,
"current": 10
}
GET /admin/tenants/{id}
PATCH /admin/tenants/{id}
Partial update. The slug field cannot be changed.
{
"name": "Acme Corp (updated)",
"plan": "pro",
"disabled": false,
"ceiling_tools": ["web_search"],
"ceiling_models": ["gpt4o"],
"ceiling_skills": [],
"egress_fqdns": ["api.acme.com"]
}
When any ceiling field is present, all three (ceiling_tools, ceiling_models, ceiling_skills) are applied together.
DELETE /admin/tenants/{id}
Cannot delete the built-in default tenant (returns 403).
Tenant Ceilings
Ceilings define the maximum capabilities any agent within the tenant may use. They are the outermost bound in the permission intersection.
GET /admin/tenants/{id}/ceiling
Returns the current ceiling. Empty lists mean unrestricted.
{
"tenant_id": "01HXZ...",
"ceiling_tools": ["web_search", "calculator"],
"ceiling_models": ["gpt4o"],
"ceiling_skills": [],
"updated_at": "2026-01-15T12:00:00Z"
}
PUT /admin/tenants/{id}/ceiling
Set the ceiling. Empty list for any dimension means unrestricted on that dimension.
{
"ceiling_tools": ["web_search"],
"ceiling_models": [],
"ceiling_skills": []
}
DELETE /admin/tenants/{id}/ceiling
Remove all ceilings for the tenant (fully unrestricted).
Subdomain routing
When multi_tenancy.base_domain is set, requests to <slug>.example.com are automatically associated with the matching tenant before any auth runs. The tenant context is propagated into the JWT at login time and validated by TenantMiddleware.
SCIM requests can specify X-Tenant-Slug header to scope provisioning to a specific tenant.
curl examples
# Create tenant
curl -s -X POST http://localhost:8080/admin/tenants \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"Acme Corp","slug":"acme","plan":"enterprise"}' | jq .
# Set ceiling
TENANT_ID=01HXZ...
curl -s -X PUT "http://localhost:8080/admin/tenants/$TENANT_ID/ceiling" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"ceiling_tools":["web_search"],"ceiling_models":[],"ceiling_skills":[]}' | jq .
# Disable tenant
curl -s -X PATCH "http://localhost:8080/admin/tenants/$TENANT_ID" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"disabled":true}' | jq .