Skip to main content

The branding and theming console model

Branding in Orbit is the set of fields an organization sets to white-label the surfaces its users and customers see — logo, colours, dashboard name, favicon, and the custom domain everything is served from. This page is the model: which read planes consume a branding value, what each field controls, how org-level and subaccount-level branding reconcile, and what renders when nothing is set. Configure it in the dashboard under Settings > Branding or through the branding API.
Branding is presentation only. Regulatory brand registration (10DLC, toll-free, RCS verified sender, branded calling) is a different system — see Brand identity and trust score.

Section 1 — The three planes

A branding value has no meaning until one of three read planes consumes it. Each plane is a distinct read surface, and most “the wrong logo shows” bugs are a plane-level mixup, not a bad save. The dashboard plane resolves from the org’s branding on every authenticated render and on the pre-auth sign-in page. The widget plane carries the same values into embedded surfaces and the public help center’s deliberately restricted shape — plain-text labels and https links only, hex colours only, no raw HTML/CSS. The domain plane is a registration, not a colour; its Section 4 hookup is below.

Section 2 — The field catalog

Branding fields live in one JSONB payload on the organization record (organizations.branding). Every field is nullable; explicit null clears it. The catalog is taken from PATCH validation as shipped: The public help-center subset is deliberately narrower than the dashboard: no raw HTML or CSS, so a help theme cannot inject markup into an unauthenticated public surface. The API enforces it at the validation boundary.

Section 3 — Precedence: org vs subaccount inheritance

Branding composes per organization: each org row carries its own payload, and a subaccount may inherit from its parent.
  • Org-level self (no inheritance). GET /branding returns the org’s own stored values — the values it saved, not the effective resolution. Use it to show “what did we save” on an edit form.
  • Inheritance toggle. PATCH /branding accepts inherit_from_parent: boolean. When true, the effective read walks up the parent chain (recursive CTE, bounded to 8 hops, cycle-safe) and folds ancestor values in nearest-first order.
  • Per-field precedence. An explicitly set child field always wins; a null child field falls through to the nearest ancestor with a non-null value. Inheritance is per field, not all-or-nothing.
  • Provenance reporting. The effective payload reports inherits_from_parent, inherited_from_organization_id, and an inherited_fields map naming which ancestor provided which field — so a parent mixing its own defaults with a couple of child overrides can see exactly which channels inherited.
The org shortcut. PUT /organization/branding is the own-org path so the frontend never special-cases its own org id. It is the same resolver as PUT /subaccounts/:id/branding when :id is the caller’s own organization — validation, cache invalidation, and audit logging stay identical, only the path differs. When a parent wants to set brand defaults its whole family of subaccounts inherits from, it owns those defaults under its own org payload, and each child toggles inherit_from_parent: true. The reseller shortcut is not a shortcut in the data model: both paths write the same organizations.branding JSONB and resolve through the same nearest-first fold. PUT /organization/branding just avoids the caller having to embed its own id.

Section 4 — Custom domain CNAME hookup

custom_domain is the hookup between the branding payload and the white-label domain serving it. Until the domain is registered, verified, and active, the value is a plain string on the payload; once the domain flips to active, any request against your hostname resolves to your org and serves the branded sign-in page, sidebar, and public links. The full DNS/SSL lifecycle — one CNAME, managed certificate, the pending/verifying/active/failed status machine — is covered in Custom domains and managed SSL. The only piece of the domain hookup that lives in branding is the custom_domain field itself; everything else is the domains service.

Section 5 — Safe defaults

When nothing is set, the platform falls back to neutral Devotel chrome:
  • The effective branding resolver degrades to an empty payload (all-null fields) rather than throwing, so a dashboard render never errors because branding is unset.
  • The dashboard shell renders the platform lockup and default blue tokens; white-label colour and logo overrides apply only when explicitly saved or inherited.
  • On the pre-auth sign-in page of a custom domain, the default favicon and branding stay authoritative until the org saves overrides — no wrongly branded page ever renders because a lookup failed.
There is also a POST /branding/reset for org-admins to clear back to defaults (audit-logged as organization.branding.reset). That is the “reset to Devotel chrome” escape hatch, distinct from turning inheritance off.

Section 6 — Worked example: re-skin a subaccount end to end

Suppose a reseller wants its Acme Retail subaccount to have its own logo and colours across the whole tree:
  1. The parent org saves the defaults. PUT /organization/branding on the parent’s own session with the family-wide values (logo, colours, help-center theme). Every child toggles inherit_from_parent: true.
  2. The subaccount overrides selectively. A customer with the subaccount’s own admin role calls PUT /subaccounts/:id/branding with the few fields it wants to pin. The fold picks the child values over the parent’s, and null fields still inherit.
  3. Register the domain. POST /organization/domains (or the subaccount-scoped equivalent) with the bare hostname, publish the cname_target, then POST /organization/domains/:id/verify. Once active, signed-in and sign-out traffic resolves to the branded experience.
  4. Confirm precedence in a read. GET /branding/effective returns the folded values plus inherited_fields and inherits_from_parent, so the subaccount’s portal shows exactly which fields it pinned and which it still pulls from the parent.
The same loop covers the public help center if help fields were set — a customer-only surface with the restricted shape above.

Section 7 — What branding is NOT

Three things are deliberately outside the branding schema:
  • Brand identity and trust score. Regulatory verification — 10DLC, toll-free, WhatsApp, RCS, branded calling, per-number KYC — is a cross-channel rollup read-only hub. Branding declares how your surfaces render; trust score reports how verified your brand is with the carriers. They resolve from different data (registration registries, not your organization’s JSONB) and are not interchangeable. See Brand identity and trust score.
  • Sender identity. Sender IDs, from-email defaults, WABA display names, and RCS agent names are channel-registration concepts configured per-channel. Branding does not cover any of them; the senders surface in the audience module does. Where a legal page or public link is rendered, the channel-level sender still resolves through its own gate.
  • The widget that consumes the widget plane. This page models the fields and precedence; the native-chat-widget configuration surface is a separate guide. Values that drive an embedded widget still resolve through the same org branding payload.

Section 8 — See also