> ## Documentation Index
> Fetch the complete documentation index at: https://docs.purplelabelmd.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Brand configuration

> Set your brand's display identity — name, logo, and theme tokens — with your own key, and see where it renders.

Every brand you operate has a **brand configuration** held on the platform — the record behind the
brand-config readback in your [welcome kit](/pages/integrate/welcome-kit). Part of it is yours to
write directly: the **display identity** — the name, logo, and theme tokens the patient-facing
surfaces wear — is a self-serve write with your own API key. The rest of the configuration (the
questionnaire theme overlay, the redirect allowlist, and your therapy and commercial references)
stays platform-managed; you confirm it through the same readback before you go live.

## Your display identity

The display-identity block is what the platform-rendered surfaces — the patient portal, the hosted
checkout door, and the sign-in screen — read to wear your brand instead of the default. It carries
four things and nothing else:

| Field           | What it is                                                                                                                              |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `display_name`  | **Required.** The brand name patients see (document titles, the sign-in shell). A non-empty string.                                     |
| `logo`          | Logo references per mode: `light` and `dark`. Each is an absolute `https` URL or a root-relative path (`/…`). Nothing else is accepted. |
| `tokens`        | The theme token-set — colors, corner radius, and type families. See [the token reference](#the-token-reference) below.                  |
| `support_email` | Optional. A valid email address for your brand's patient support.                                                                       |

The brand you are writing is **resolved from your key context**, never from the request body. You
present the brand in the `X-Brand-Id` header, and the `brand_id` in the path must match it. A brand
your key does not own — foreign, unknown, or mismatched — answers `404`, never `403`: there is no
way to probe whether another tenant's brand exists.

## Set it with your key

Send the display block to `PUT /v1/account/brands/{brand_id}/config`. The body carries a single
`display` object; that is the only field read here.

```bash theme={null}
curl --request PUT "$BASE_URL/v1/account/brands/brd_acme/config" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme" \
  --header "Content-Type: application/json" \
  --data '{
    "display": {
      "display_name": "Acme Health",
      "logo": {
        "light": "https://cdn.example.com/acme/logo-light.svg",
        "dark": "https://cdn.example.com/acme/logo-dark.svg"
      },
      "tokens": {
        "colors": {
          "light": { "primary": "oklch(0.62 0.19 259)", "primaryForeground": "oklch(0.98 0 0)" },
          "dark":  { "primary": "oklch(0.70 0.17 259)" }
        },
        "radius": "0.625rem",
        "typography": { "fontSans": "Inter, sans-serif" }
      },
      "support_email": "help@example.com"
    }
  }'
```

The response is your stored configuration — the same shape the readback returns:

```json theme={null}
{
  "data": {
    "brand_id": "brd_acme",
    "version": "v4",
    "display": {
      "display_name": "Acme Health",
      "logo": {
        "light": "https://cdn.example.com/acme/logo-light.svg",
        "dark": "https://cdn.example.com/acme/logo-dark.svg"
      },
      "tokens": {
        "colors": {
          "light": { "primary": "oklch(0.62 0.19 259)", "primaryForeground": "oklch(0.98 0 0)" },
          "dark":  { "primary": "oklch(0.70 0.17 259)" }
        },
        "radius": "0.625rem",
        "typography": { "fontSans": "Inter, sans-serif" }
      },
      "support_email": "help@example.com"
    },
    "updated_at": "2026-08-07T14:22:03Z"
  }
}
```

A few things this write guarantees:

* **The version is server-minted.** You never send `version`; the platform mints a new monotone
  version on each write and returns it — so every write, including a repeat, advances the version.
  The stored write is durable before it takes effect, so a restart re-serves exactly what you last
  wrote.
* **The block is merged, not replaced wholesale.** Your display block is merged onto the existing
  configuration, so a display write never disturbs the platform-managed redirect allowlist or theme.

## Read it back

`GET /v1/account/brands/{brand_id}/config` returns the configuration as the platform holds it — the
readback promised in your welcome kit. It is key-scoped the same way: the path `brand_id` must match
your `X-Brand-Id`, and any other brand answers `404`. A brand with no configuration set yet also
answers `404`.

```bash theme={null}
curl "$BASE_URL/v1/account/brands/brd_acme/config" \
  --header "Authorization: Bearer $TEST_API_KEY" \
  --header "X-Brand-Id: brd_acme"
```

## Where it renders

You do not call anything to make the display identity show up. The platform-rendered surfaces
resolve it per request from your brand context and wear it automatically: the patient portal, the
hosted checkout door, and the sign-in screen. A brand that has **no** display block does not error —
those surfaces fall back to the platform's default look, and the fallback is always explicit: the
default is never silently presented as your brand's own identity.

## The token reference

`tokens` is a **partial** theme set. You supply any subset of the slots below; anything you omit
falls back to the platform default when the theme is assembled. Every leaf value is a string. An
unknown group, mode, or slot is refused (see [the wording wall](#the-wording-wall)).

`tokens` accepts three groups:

| Group        | Shape                                                             | Value format                                                       |
| ------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------ |
| `colors`     | `{ light?, dark? }`, each a set of the semantic color slots below | CSS color string (for example `oklch(0.62 0.19 259)` or `#5b3df5`) |
| `radius`     | a single base corner radius                                       | length string, for example `0.625rem`                              |
| `typography` | `{ fontSans?, fontMono? }`                                        | font-family string, for example `Inter, sans-serif`                |

Within `colors.light` and `colors.dark`, the slot names are the semantic color slots of the
platform design-system brand-token contract. They are the exact vocabulary the theme stylesheet is
generated from, so they can never drift from what the surfaces actually render. Grouped by the
surface each one themes:

| Slot family   | Slots                                                                                                                                                    | What it themes                                                 |
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Base surface  | `background`, `foreground`                                                                                                                               | The page canvas and its default text                           |
| Card          | `card`, `cardForeground`                                                                                                                                 | Panels and raised containers                                   |
| Popover       | `popover`, `popoverForeground`                                                                                                                           | Menus, dropdowns, tooltips                                     |
| Primary       | `primary`, `primaryForeground`                                                                                                                           | The main action color (buttons, active states)                 |
| Secondary     | `secondary`, `secondaryForeground`                                                                                                                       | Secondary actions and quieter emphasis                         |
| Muted         | `muted`, `mutedForeground`                                                                                                                               | Subdued backgrounds and helper text                            |
| Accent        | `accent`, `accentForeground`                                                                                                                             | Highlights and hovered surfaces                                |
| Destructive   | `destructive`, `destructiveForeground`                                                                                                                   | Danger actions and error emphasis                              |
| Form controls | `border`, `input`, `ring`                                                                                                                                | Field borders, input fills, and focus rings                    |
| Charts        | `chart1`, `chart2`, `chart3`, `chart4`, `chart5`                                                                                                         | The categorical series colors in data views                    |
| Sidebar       | `sidebar`, `sidebarForeground`, `sidebarPrimary`, `sidebarPrimaryForeground`, `sidebarAccent`, `sidebarAccentForeground`, `sidebarBorder`, `sidebarRing` | The navigation rail and its own accent, border, and focus ring |

**Partial override.** You may set as few as one slot in one mode. The write is merged onto your
existing configuration and, at render time, onto the platform default — so a single-slot write
re-themes only that slot and leaves the rest of your look intact.

**Unknown slots are refused.** A slot name outside the vocabulary above (a typo, or a slot the
platform does not define) fails the whole write with `422` and a slot-level issue — nothing is
stored. A color value must be a non-empty string; an empty or non-string value is refused the same
way.

## The wording wall

The display block is **theming only** — name, logo, and tokens. It is not a copy channel and can
never carry patient-facing wording. A `copy` key inside `display` is refused outright with `422`:

```json theme={null}
{
  "type": "https://purple.md/problems/account-brand-config-validation",
  "title": "brand display rejected",
  "status": 422,
  "issues": [
    {
      "code": "brand.display.copy_forbidden",
      "where": "display.copy",
      "message": "brand display identity may not carry copy — it is name/logo/tokens only (INT-A-06 wording wall)"
    }
  ]
}
```

The wall is structural, not a review step: the refusal is built into the validator, so there is no
configuration in which display copy could slip through. Any display key outside `display_name`,
`logo`, `tokens`, and `support_email` is refused the same way with `brand.display.unknown_key`.

The client write reads **only** the `display` block. Anything else you place in the body — a theme,
a redirect allowlist, a brand id, a charge setting — has no effect here: those stay
platform-managed and cannot be set through your key.

## The operator override

The same display block can also be written from the platform's operator surface, as an override or
as a starting look seeded when your brand is first created. That is a platform-side path, not yours;
it writes the one shared configuration your readback returns, and it runs through the identical
validator and the same wording wall. From your side there is one display identity, whichever path
last wrote it — and the readback always shows you exactly what is stored.

## The rest of your configuration

Everything else in your brand configuration is platform-managed — set through your onboarding
contact and confirmed through the same readback. Each member has a fixed meaning:

* **`theme`** — the presentation overlay for the intake questionnaire: theme tokens the renderer
  applies to each already-sequenced question. It is a **separate** token system from your display
  identity. The display `tokens` above dress the platform-rendered surfaces (portal, checkout,
  sign-in); `theme` dresses the questionnaire your own renderer draws. The two are disjoint — the
  same brand may set both, and neither overrides the other. See
  [how intake serving works](/pages/integrate/how-serving-works#the-rendered-question).
* **`copy`** — brand copy slots for your **non-clinical** surfaces, keyed by slot. A copy slot
  changes wording on those surfaces only; it can never supply or override a question's headline,
  which comes from the reviewed questionnaire. (This is separate from the display block, which
  carries no copy at all.)
* **`enabled_therapies`** — references to the therapies your brand offers. References, never
  definitions: the therapy, its questionnaire, and its clinical rules live on the platform.
* **`commercial`** — identifiers tying the brand to its storefront and pricing. References only.
* **`redirect_allowlist`** — the absolute `https` URL prefixes a patient may be returned to after
  completion. Matching is exact origin plus path prefix; an entry that matches nothing is dropped,
  and an empty list refuses every redirect. Fail-closed by design — an open redirect is
  unrepresentable.

## The clinical wall

The whole brand configuration — display identity included — is **clinically inert by construction**.
Two checks run on every write, before anything is stored:

1. **A closed allowlist.** A configuration, and the display block within it, may carry only their
   permitted keys. Any other key is rejected.
2. **A clinical-shape scan.** The entire configuration is scanned at every depth for any key that
   could carry clinical meaning — facts, options, controls, rules, sequencing, visibility, and
   their relatives. One such key anywhere fails the whole write.

So a brand configuration can change how a surface *looks*, but it can never change what a patient is
asked, the choices offered, what an answer records, or which jurisdiction's requirements apply. The
presentation grammar is themeable, never clinically alterable.
