Tenants API

Manage Tenants. Requires BrandOwner on the Brand that owns the Tenants (or platform-superadmin for cross-Brand operations). The Brand context is resolved from the bearer token; you don't pass brandId in the path.

List tenants

GET /api/tenants

Returns every Tenant under the caller's Brand.

200 OK
{
  "items": [
    {
      "id": "ten_01",
      "slug": "acme-customer-1",
      "name": "Acme Customer 1",
      "brandId": "acme-agency",
      "status": "active",
      "createdAt": "2026-05-25T10:30:00.000Z"
    }
  ]
}

Create a tenant

POST /api/tenants

Request body
{
  "slug": "acme-customer-2",
  "name": "Acme Customer 2",
  "preset": "sales-b2b"   // see ADR-006 team presets
}

200 OK
{ "id": "ten_02", "slug": "acme-customer-2", ... }

400 Bad Request
{ "error": "invalid_slug" }
{ "error": "unknown_preset" }

403 Forbidden
{ "error": "requires_brand_owner" }

409 Conflict
{ "error": "slug_exists",
  "detail": "Tenant slug acme-customer-2 already exists for this Brand" }

Behind the scenes the create call seeds the agents for the chosen preset (see ADR-006), opens a new audit chain HMAC-signed by AUDIT_HMAC_SECRET, and initialises an empty encrypted-secrets row scoped to the new Tenant.

Get a tenant

GET /api/tenants/:id

200 OK
{
  "id": "ten_02",
  "slug": "acme-customer-2",
  "name": "Acme Customer 2",
  "brandId": "acme-agency",
  "preset": "sales-b2b",
  "agents": [
    { "slug": "sales", "name": "Sales", "kind": "department" },
    { "slug": "sdr", "name": "SDR", "parentSlug": "sales" },
    ...
  ]
}

Update a tenant

PATCH /api/tenants/:id

Request body
{ "name": "Acme Customer 2 (US)" }

200 OK
{ "id": "ten_02", "name": "Acme Customer 2 (US)", ... }

Provision an agent under a tenant

POST /api/tenants/:id/agents

Request body
{ "slug": "marketing" }

200 OK
{ "slug": "marketing", "name": "Marketing", "kind": "department" }

Sub-agents auto-expand — provisioning marketing will also create growth, brand, and content if they aren't already present.

Provision an MCP server for a tenant

POST /api/tenants/:id/mcp-servers

Request body
{
  "name": "Zapier (Acme Customer 2)",
  "url": "https://mcp.zapier.com/v1/.../sse"
}

200 OK
{ "id": "mcp_01", "name": "Zapier (Acme Customer 2)", "active": true }

400 Bad Request
{ "error": "unsafe_mcp_url", "detail": "Private IP not allowed" }

Invite a Tenant Owner

POST /api/tenants/:id/owner-invite

Request body
{ "email": "ops@acme-customer-2.com" }

200 OK
{ "id": "inv_01", "email": "ops@acme-customer-2.com" }

Tenant health

GET /api/admin/tenants/:id/health — platform-side diagnostic endpoint. Returns counters (audit-chain length, encrypted-secrets count, active MCP servers, last successful agent run).

Delete a tenant

DELETE /api/tenants/:id

200 OK
{ "id": "ten_02", "status": "archived" }

Tenants are soft-deleted (status flipped to archived); audit rows are retained. Hard-delete is operator-driven and not exposed over the API.

Source: src/platform/tenants-api.ts / ADR-003 §Tenant.