Brands API

Manage the Brand catalogue. All routes under /api/admin/brands require platform-superadmin; the public reads under /api/public/brands/by-* are accessible without auth so the sign-in flow can resolve theme + IdP config before the user has a token.

List brands

GET /api/admin/brands

Returns every Brand in the catalogue, including suspended.

200 OK
{
  "items": [
    {
      "id": "aroovo",
      "slug": "aroovo",
      "name": "Aroovo",
      "status": "active",
      "primaryDomain": "agents.aroovo.tech",
      "hostnameVerifiedAt": "2026-05-24T11:02:00.000Z",
      "createdAt": "2026-05-24T10:00:00.000Z"
    }
  ]
}

Get a brand

GET /api/admin/brands/:id

200 OK
{
  "id": "acme-agency",
  "slug": "acme-agency",
  "name": "Acme Agency",
  "status": "active",
  "primaryDomain": null,
  "themeJson": null,
  "authProvider": null,
  "createdAt": "2026-05-25T09:00:00.000Z"
}

404 Not Found
{ "error": "brand_not_found", "detail": "No Brand with id acme-agency" }

Create a brand

POST /api/admin/brands

Request body
{
  "slug": "acme-agency",
  "name": "Acme Agency",
  "primaryDomain": "agents.acme.com"   // optional
}

200 OK
{ "id": "acme-agency", "slug": "acme-agency", ... }

400 Bad Request
{ "error": "invalid_slug", "detail": "slug must match /^[a-z0-9-]+$/" }

409 Conflict
{ "error": "slug_exists", "detail": "Brand slug acme-agency already exists" }

403 Forbidden
{ "error": "requires_platform_owner" }

Update a brand

PATCH /api/admin/brands/:id

Partial patch — only supplied fields are updated.

Request body
{
  "name": "Acme Agency (US)",
  "themeJson": "{ \"--b-pale\": \"#74C3F0\" }"
}

200 OK
{ "id": "acme-agency", "name": "Acme Agency (US)", ... }

409 Conflict
{ "error": "aroovo_brand_locked",
  "detail": "Cannot change status on the platform Brand" }

Lifecycle transitions

POST /api/admin/brands/:id/suspend — flip to suspended.
POST /api/admin/brands/:id/offboard — flip to offboarding (30-day soft delete window).
POST /api/admin/brands/:id/activate — flip back to active.
DELETE /api/admin/brands/:id — soft-delete (same as offboard).

200 OK
{ "id": "acme-agency", "status": "suspended" }

409 Conflict
{ "error": "aroovo_brand_locked" }

Provider config (BYO LLM keys)

GET /api/admin/brands/:id/provider-config — returns status (which keys are set), never plaintext.
POST /api/admin/brands/:id/provider-key — set a provider key. Body: { provider: "anthropic" | "openai" | "openrouter" | "openclaw", value: "<key>" }. The value is encrypted and stored in the Secret vault.
DELETE /api/admin/brands/:id/provider-key — clear a provider key.
PUT /api/admin/brands/:id/openclaw-base-url — set the OpenClaw gateway URL.

Auth provider (BYO identity)

POST /api/admin/brands/:id/auth-provider — pick and configure the Brand's IdP. Body:

{
  "provider": "google",          // entra | google | okta | auth0
  "config": {
    "clientId": "...",
    "hostedDomain": "acme.com"
  }
}

Slack config

POST /api/admin/brands/:id/slack-config — paste Slack bot token + signing secret.
DELETE /api/admin/brands/:id/slack-config — clear.
PUT /api/admin/brands/:id/slack-config/channel-map — map a Slack channel to a Tenant slug.

Hostname verification

POST /api/admin/brands/:id/hostname-verify/start — returns the DNS-TXT challenge token.
POST /api/admin/brands/:id/hostname-verify/check — the platform polls DNS and flips hostnameVerifiedAt on success.
POST /api/admin/brands/:id/hostname-verify/clear — clear the verification (e.g. on domain change).

Brand-Owner invites

GET /api/admin/brands/:id/owners — list Owners.
POST /api/admin/brands/:id/owner-invite — invite a new Brand-Owner by email; emails must validate against the configured IdP.

Tenants under a Brand

GET /api/admin/brands/:id/tenants — list all Tenants for this Brand. Useful when a Brand-Owner's console view needs the full tenant catalogue.

Brand export (offboarding)

GET /api/admin/brands/:id/export — bundle every audit row, tenant config, KB file, and secret reference (not the secret values) into a downloadable archive. Used during the 30-day offboarding window.

Public reads (no auth)

GET /api/public/brands/by-slug/:slug — resolve a slug to { brandId, name, themeJson, authProvider } for the sign-in flow.
GET /api/public/brands/by-id/:id/theme — minimal theme blob for early CSS variable injection.
GET /api/public/brands/by-host/:host — used by the edge middleware to attach x-brand-id.

Source: src/platform/brands-api.ts / ADR-003.