> ## 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.

# Set your brand's display identity

> Sets or updates YOUR brand's display-identity block — the display name, the logo references, and the token-set the platform-controlled surfaces render — your brand's own terms of service and privacy policy links, and who holds your brand's consent documents (`consent_holder`), using your own API key. Send any of `display`, `legal_documents` and `consent_holder`. Only those are read here, so the theme, the redirect allowlist, and the charge topology stay operator-controlled and cannot be set through this surface. Before patients arrive your display name must be set, `consent_holder` must be chosen, and every document your served consents link to must be set by the holder you chose. The intake resolve reports `patient_readiness` naming anything still missing. The brand is the one your key owns (resolved from X-Brand-Id); the brand_id in the path must match it (any mismatch, foreign, or unknown brand answers 404, never 403). Validated UNCHANGED by the platform's brand-config validator: a `copy` key, an unknown display key, a non-https and non-root-relative logo reference, or an unknown token slot is refused (422, issues verbatim). The version is server-minted; the write is durable before it activates and audited (which principal, when). If the saved configuration changed since you loaded it, the write is refused with 409 `version-mismatch` and nothing is written: reload it and try again. If the durable store did not answer in time, the answer is 503 with the outcome unknown: the change may or may not have been saved, so read it again before retrying.



## OpenAPI

````yaml /openapi/public-openapi.json put /v1/account/brands/{brand_id}/config
openapi: 3.1.0
info:
  title: Purple API
  version: 0.0.0
servers:
  - url: https://api.staging.purplelabelmd.com
security: []
tags:
  - description: >-
      Browser redirects that begin, complete and end a sign-in on the API host.
      Patients sign in on your member portal instead, at
      https://{MEMBER_HOST}/auth/login?returnTo=/home.
    name: Sessions & authentication
  - description: Run the server-driven intake questionnaire question by question.
    name: Intake
  - description: Address autocomplete for the intake flow.
    name: Addresses
  - description: Read the public status of a patient's enrollment.
    name: Enrollment
  - description: Track a client's onboarding progress.
    name: Onboarding
  - description: Read back the platform configuration stored for your brand.
    name: Brand configuration
  - description: Enable offerings for your brand and set their display copy.
    name: Offering configuration
  - description: Start and track payments for an order.
    name: Payments
  - description: Subscribe to platform events and manage delivery endpoints.
    name: Webhooks
paths:
  /v1/account/brands/{brand_id}/config:
    put:
      tags:
        - Brand configuration
      summary: Set your brand's display identity
      description: >-
        Sets or updates YOUR brand's display-identity block — the display name,
        the logo references, and the token-set the platform-controlled surfaces
        render — your brand's own terms of service and privacy policy links, and
        who holds your brand's consent documents (`consent_holder`), using your
        own API key. Send any of `display`, `legal_documents` and
        `consent_holder`. Only those are read here, so the theme, the redirect
        allowlist, and the charge topology stay operator-controlled and cannot
        be set through this surface. Before patients arrive your display name
        must be set, `consent_holder` must be chosen, and every document your
        served consents link to must be set by the holder you chose. The intake
        resolve reports `patient_readiness` naming anything still missing. The
        brand is the one your key owns (resolved from X-Brand-Id); the brand_id
        in the path must match it (any mismatch, foreign, or unknown brand
        answers 404, never 403). Validated UNCHANGED by the platform's
        brand-config validator: a `copy` key, an unknown display key, a
        non-https and non-root-relative logo reference, or an unknown token slot
        is refused (422, issues verbatim). The version is server-minted; the
        write is durable before it activates and audited (which principal,
        when). If the saved configuration changed since you loaded it, the write
        is refused with 409 `version-mismatch` and nothing is written: reload it
        and try again. If the durable store did not answer in time, the answer
        is 503 with the outcome unknown: the change may or may not have been
        saved, so read it again before retrying.
      operationId: setAccountBrandDisplay
      parameters:
        - $ref: '#/components/parameters/BrandIdHeader'
        - $ref: '#/components/parameters/IdempotencyKeyHeader'
        - description: the brand whose display identity to set (must match X-Brand-Id).
          in: path
          name: brand_id
          required: true
          schema:
            pattern: ^brd_[A-Za-z0-9][A-Za-z0-9_-]*$
            type: string
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccountBrandDisplayWriteRequest'
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                properties:
                  data:
                    $ref: '#/components/schemas/BrandConfigView'
                required:
                  - data
                type: object
          description: the stored configuration (the same projection you read back)
        '400':
          $ref: '#/components/responses/EdgeBadRequest'
        '401':
          $ref: '#/components/responses/EdgeUnauthorized'
        '404':
          $ref: '#/components/responses/EdgeNotFound'
        '409':
          $ref: '#/components/responses/EdgeVersionMismatch'
        '422':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/BrandConfigProblem'
          description: >-
            the display or legal_documents block was refused by the validator
            (issues verbatim)
        '503':
          content:
            application/problem+json:
              schema:
                $ref: '#/components/schemas/EdgeProblem'
          description: >-
            the durable brand-config store did not confirm the write; nothing
            was activated. If the store answered with a failure, nothing was
            saved (`account-brand-config-store-unavailable`; retryable). If the
            store did not answer in time, the outcome is unknown
            (`account-brand-config-outcome-unknown`): the change may or may not
            have been saved, so read it again before retrying.
      security:
        - apiKey: []
components:
  parameters:
    BrandIdHeader:
      description: >-
        Your brand id (`brd_...`). It must be one of the brands your API key's
        account owns; with a publishable key it must be the one brand the key
        was issued for. A missing or malformed value is refused with `400`, and
        an unknown or foreign brand with `404`.
      in: header
      name: X-Brand-Id
      required: true
      schema:
        pattern: ^brd_[A-Za-z0-9][A-Za-z0-9_-]*$
        type: string
    IdempotencyKeyHeader:
      description: >-
        Send a unique value on every write. Do not assume that a repeated
        request with the same key is de-duplicated unless the operation says so.
      in: header
      name: Idempotency-Key
      required: false
      schema:
        type: string
  schemas:
    AccountBrandDisplayWriteRequest:
      additionalProperties: false
      description: >-
        The client self-serve write payload. It carries `display`,
        `legal_documents`, `consent_holder`, or any of them together: the client
        sets or updates its own brand display-identity block, its own terms of
        service and privacy policy links, and who holds its consent documents,
        with its own API key. A `legal_documents` write is applied only if the
        configuration has not changed since it was read; otherwise it is refused
        with `409` `version-mismatch` and nothing is written. There is
        deliberately no theme, redirect-allowlist, or charge-topology field —
        those stay platform-controlled; a client cannot set them here (any such
        key is barred by additionalProperties:false and, at runtime, is never
        read). Each block is merged onto the existing configuration and
        validated UNCHANGED by the brand-config validator. A display block
        cannot carry wording: a `copy` key is refused with `422`, and the
        validator's issues are returned as given.
      minProperties: 1
      properties:
        consent_holder:
          description: >-
            Choose who holds your brand's consent documents and identity:
            `purple` (Purple's consent documents apply, and your own legal links
            are not needed) or `client` (your own legal document links apply).
            Any other value is refused with `422` and the issue
            `brand.consent_holder`. Until it is chosen, your brand is not ready
            for patients.
          enum:
            - purple
            - client
          type: string
        display:
          allOf:
            - $ref: '#/components/schemas/BrandDisplay'
          description: >-
            the per-brand display-identity block to set or update (merged onto
            the existing config).
        legal_documents:
          allOf:
            - $ref: '#/components/schemas/AccountBrandLegalDocuments'
          description: >-
            Your brand's own terms of service and privacy policy links, set or
            updated. A slot you send replaces that slot's stored link; a slot
            you omit keeps its stored link. Each link must be an absolute https
            URL to the document as your patients should read it; anything else
            (a relative path, an `http:` URL, an empty string) is refused with
            `422` and the issue `brand.legal_documents.url`, and any other slot
            is refused with `422` and the issue `brand.legal_documents.slot`.
            These links are shown to patients before they consent. You do not
            need them when you chose `consent_holder: purple`: Purple's
            documents apply then.
      type: object
    BrandConfigView:
      additionalProperties: false
      description: >-
        A brand's platform configuration as the platform holds it — clinically
        inert BY CONSTRUCTION (the validator enforces a top-level allowlist + a
        recursive clinical-shaped-key denylist). The your readback and the
        platform staff view return this EXACT shape. Presentation and commercial
        reach only; never a clinical field. additionalProperties:false mirrors
        the validator's top-level allowlist.
      properties:
        brand_id:
          pattern: ^brd_[A-Za-z0-9][A-Za-z0-9_-]*$
          type: string
        commercial:
          additionalProperties:
            type: string
          description: commercial references (storefront/pricing ids) — references only.
          type: object
        consent_holder:
          description: >-
            Who holds this brand's consent documents and identity. `purple`:
            Purple's consent documents apply. `client`: the brand's own legal
            document links apply. Absent means nobody has chosen yet, and the
            brand is not ready for patients until someone does; there is no
            default.
          enum:
            - purple
            - client
          type: string
        copy:
          additionalProperties:
            type: string
          description: brand-overridable copy slots (slot id → display string).
          type: object
        display:
          allOf:
            - $ref: '#/components/schemas/BrandDisplay'
          description: >-
            The per-brand display-identity block (name, logo, token-set), if
            set. Identity only; never a clinical copy channel. When absent, this
            brand has no display identity and the brand-display serve returns
            the explicit purple fallback.
        enabled_therapies:
          description: >-
            therapy references this brand offers (references, never
            definitions). This is also the field the intake surface reads to
            decide which questionnaire a patient is served at a journeyless
            first touch: one enabled therapy serves that therapy's questionnaire
            always; more than one requires the entry link to name an `offering`;
            none serves the platform's shared questionnaire. Values must be
            therapy keys the clinical service currently serves; the vocabulary
            is not enumerated here — it is clinical's committed roster (`GET
            /v1/instrument-bundles/roster`), which grows as new shared intakes
            ship.
          items:
            type: string
          type: array
        legal_documents:
          allOf:
            - $ref: '#/components/schemas/BrandLegalDocuments'
          description: >-
            The brand's REGISTERED LEGAL DOCUMENTS: the absolute https URLs the
            render-time brand-identity token channel substitutes into
            `{brand.legal.telehealth_url}` / `{brand.legal.privacy_url}` /
            `{brand.legal.terms_url}`. IDENTITY, NEVER COPY — the same class as
            `display.display_name` and `redirect_allowlist`, and emphatically
            not a wording channel: a brand still cannot supply one word of
            clinical question or option text. ONE RECORD SERVES CHECKOUT AND
            INTAKE (the checkout-scoped consent-docs registry folds into this
            field), so a brand cannot hold two copies that disagree about which
            privacy policy it publishes. ABSENT, or a slot absent, is VALID and
            means the render serves a NAMED copy-gap refusal on that node —
            never a blank link, never a dead link, never a raw token shown to a
            member.
        redirect_allowlist:
          description: >-
            absolute-https return-URL prefixes permitted as post-completion
            redirect targets.
          items:
            format: uri
            type: string
          type: array
        theme:
          additionalProperties:
            oneOf:
              - type: string
              - type: number
          description: presentation theme tokens — scalar leaves only (string or number).
          type: object
        updated_at:
          description: when this configuration was last written.
          format: date-time
          type: string
        version:
          description: >-
            server-minted monotone version (e.g. v3); the caller never sets it.
            A write may send it back as `expected_version` so that a save made
            from this read is refused if the stored version has moved since.
          type: string
      required:
        - brand_id
        - version
      type: object
    BrandConfigProblem:
      allOf:
        - $ref: '#/components/schemas/EdgeProblem'
        - properties:
            issues:
              description: the validator issues, verbatim (code · where · message).
              items:
                properties:
                  code:
                    type: string
                  message:
                    type: string
                  where:
                    type: string
                required:
                  - code
                  - where
                  - message
                type: object
              type: array
          type: object
      description: >-
        The brand-config validator's refusal (422): an RFC-7807 problem carrying
        the validator's issues VERBATIM (the clinically-inert guarantee's
        refusal — a clinical-shaped field or a non-absolute-https allowlist
        entry). Show the issues as given; they are not rephrased.
    EdgeProblem:
      description: RFC 7807 problem+json error body.
      properties:
        detail:
          type: string
        status:
          type: integer
        title:
          type: string
        type:
          format: uri-reference
          type: string
      required:
        - type
        - title
        - status
      type: object
    BrandDisplay:
      additionalProperties: false
      description: >-
        The per-brand display-identity block: the brand's display name, logo
        references, and token-set that the platform-controlled surfaces (portal,
        checkout door, identity) resolve per request. Identity only — never a
        clinical copy channel: a `copy` key here is refused, and the
        brand-config clinical-shaped-key denylist scans it like everything else.
        It is a sibling of `theme`, not a replacement: `theme` is the
        render-time instrument overlay, while these tokens are the brand-chrome
        identity the platform surfaces render — disjoint consumers, no
        precedence.
      properties:
        display_name:
          description: >-
            the brand's display name shown to patients (document titles, login
            shell).
          minLength: 1
          type: string
        logo:
          additionalProperties: false
          description: >-
            logo references per mode — each an absolute https URL or a
            root-relative path ('/…').
          properties:
            dark:
              description: >-
                dark-mode logo reference (absolute https URL or root-relative
                path).
              type: string
            light:
              description: >-
                light-mode logo reference (absolute https URL or root-relative
                path).
              type: string
          type: object
        support_email:
          description: optional brand support email.
          format: email
          type: string
        support_phone:
          description: >-
            optional brand support phone number, as it should be displayed
            (digits, with an optional leading +, spaces, parentheses, dots and
            hyphens). The member support page offers it as a one-tap call. A
            malformed value is refused with `422`.
          maxLength: 32
          pattern: ^\+?[0-9][0-9 ().-]{6,}$
          type: string
        tokens:
          allOf:
            - $ref: '#/components/schemas/BrandDisplayTokens'
      required:
        - display_name
      type: object
    AccountBrandLegalDocuments:
      additionalProperties: false
      description: >-
        The legal document links a brand sets for itself, each an absolute https
        URL: its terms of service, its privacy policy and, when the brand holds
        its own consent documents (`consent_holder` is `client`), its telehealth
        informed consent. At least one is sent. Sending `telehealth` while
        `consent_holder` is not `client` (as stored, or as set in the same
        write) is refused with `brand.legal_documents.slot`.
      minProperties: 1
      properties:
        privacy:
          description: your privacy policy (absolute https URL).
          format: uri
          pattern: ^https://
          type: string
        telehealth:
          description: >-
            your telehealth informed consent (absolute https URL). Accepted only
            when `consent_holder` is `client`.
          format: uri
          pattern: ^https://
          type: string
        terms:
          description: your terms of service (absolute https URL).
          format: uri
          pattern: ^https://
          type: string
      type: object
    BrandLegalDocuments:
      additionalProperties: false
      description: >-
        A brand's registered legal documents: exactly these three optional
        slots, each an absolute https URL. Any other key is refused. The set is
        closed because each of these is a document a patient is asked to read
        before consenting.
      properties:
        privacy:
          description: the brand's privacy policy (absolute https).
          format: uri
          type: string
        telehealth:
          description: the brand's telehealth informed-consent document (absolute https).
          format: uri
          type: string
        terms:
          description: the brand's terms document (absolute https).
          format: uri
          type: string
      type: object
    BrandDisplayTokens:
      additionalProperties: false
      description: >-
        The brand token-set family (colors per mode, base radius, and type
        families), mirroring the design-system brand-token shape minus name and
        logo (which are hoisted to display_name and logo). A partial override
        set: a brand supplies any subset of the known slots; completeness for
        stylesheet generation is achieved downstream by merging onto the default
        brand fallback. String leaves only; unknown token slots are refused.
      properties:
        colors:
          additionalProperties: false
          description: >-
            semantic color slots per mode — BrandColorSet keys → CSS color
            strings.
          properties:
            dark:
              additionalProperties:
                type: string
              description: dark-mode color slots (BrandColorSet keys → CSS color strings).
              type: object
            light:
              additionalProperties:
                type: string
              description: light-mode color slots (BrandColorSet keys → CSS color strings).
              type: object
          type: object
        radius:
          description: base corner radius, e.g. '0.625rem'.
          type: string
        typography:
          additionalProperties: false
          properties:
            fontMono:
              type: string
            fontSans:
              type: string
          type: object
      type: object
  responses:
    EdgeBadRequest:
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/EdgeProblem'
      description: malformed or absent X-Brand-Id
    EdgeUnauthorized:
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/EdgeProblem'
      description: Missing, unknown or revoked API key. The body is the same in every case.
    EdgeNotFound:
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/EdgeProblem'
      description: >-
        Brand not found for your account. An unknown brand and another account's
        brand get the same `404`, never a `403`.
    EdgeVersionMismatch:
      content:
        application/problem+json:
          schema:
            allOf:
              - $ref: '#/components/schemas/EdgeProblem'
              - properties:
                  current_version:
                    description: >-
                      the version stored now; null when nothing versioned is
                      stored
                    type:
                      - string
                      - 'null'
                  saved_at:
                    description: >-
                      when the stored version was saved; null when that time is
                      not recorded
                    format: date-time
                    type:
                      - string
                      - 'null'
                  type:
                    enum:
                      - https://purple.md/problems/version-mismatch
                    type: string
                required:
                  - current_version
                  - saved_at
                type: object
      description: >-
        Conflict. Nothing was written. Either the write carried
        `expected_version` and the version stored now is not that one (someone
        saved since the caller read), or, on a brand-configuration write, the
        stored configuration was saved by another writer after this server last
        read it. The body carries the version stored now (`current_version`,
        null when nothing versioned is stored) and when it was saved
        (`saved_at`, null when that time is not recorded), and never who saved
        it. Read again, then save from that read. A write without
        `expected_version` gets this answer only in the second case.
  securitySchemes:
    apiKey:
      description: 'Per-client API key (M2M). Presented as `Authorization: Bearer <key>`.'
      scheme: bearer
      type: http

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.