Branding
Branding controls how your tenant appears to its members. Every hosted surface a member sees, the recipient claim page, the signing page, and the transactional emails you send, renders from a single tenant theme: a brand name, a logo, a color palette, copy overrides, and a verified from-address. This page covers reading and editing that theme, the logo upload flow, verifying a custom sending domain, and the pre-auth public read that lets a hosted page paint your brand before the member has authenticated.
What Branding Controls
A tenant has one branding theme. It carries the values that render across every member-facing surface: the brand and legal names, the from, reply-to, and support email addresses, a logo, a light and dark color palette, email-specific colors, page copy, and links to your terms and privacy pages. The console Settings and Branding page edits the same theme this API exposes.
Reads return two views of the theme so you can tell what you set apart from what actually renders:
- Resolved. The values that render after layering. Any field you leave unset falls back
to the platform default (the
XKOVAbrand,no-reply@xkova.com, no logo). Every resolved field is present. - Theme. The raw tenant-wide row exactly as you set it. A
nullfield here means you have not set it and it inherits from the platform default.
Resolution runs most-specific first: a surface-specific override, then your tenant-wide theme, then the
platform default. Editing sets your tenant-wide theme; anything you leave null keeps inheriting.
Partner scope is an internal platform scope above tenants that integrators do not use, so you always read and
write your own tenant's theme.
Read and Edit the Theme
GET /v1/branding returns the resolved bundle plus the raw theme. Any tenant-scoped caller can
read it. PUT /v1/branding upserts the tenant-wide theme and requires the
branding.manage permission (an admin or owner). The write is a patch: it updates the fields you
send and leaves every other field untouched, so a partial body is the normal case. The logo is set through
the separate upload flow below, not through this call.
The editable theme fields:
| Field | Shape | Renders as |
|---|---|---|
brand_name, legal_name | Text, up to 120 and 160 chars | The display name and legal entity name across pages and email. |
from_name, from_email, reply_to_email,
support_email | Name text plus valid email addresses | The sender identity and support contact on transactional email. |
primary_color, secondary_color, accent_color | Strict
#RRGGBB hex | The light-mode page palette. |
primary_color_dark, secondary_color_dark,
accent_color_dark | Strict #RRGGBB hex | The dark-mode palette pages
use under prefers-color-scheme: dark; null inherits the light value. |
email_button_color, email_bg_color,
email_text_color | Strict #RRGGBB hex | Email-specific colors. |
app_title, footer_text | Text | The page title and footer copy. |
terms_url, privacy_url | https URLs | Links surfaced on member pages. |
subject_override, intro_override | Text | Override the email subject line and intro copy. |
display_term_preset | One of member, customer,
user, client | The noun used for the end user across member-facing copy. |
copy_overrides | Object with singular, plural,
possessive | Override the end-user noun forms directly instead of picking a preset. |
#RRGGBB hex; named,
three-digit, and eight-digit forms are rejected. Free-text fields reject newlines and angle brackets, and
terms_url and privacy_url must be https. A rejected field returns
validation_failed. The write is idempotent under an Idempotency-Key; see
Idempotency.Upload a Logo
A logo is set in three steps so the image bytes never pass through the API request body.
- Request a target.
POST /v1/branding/logo/upload-urlwith thecontent_type(one ofimage/png,image/jpeg,image/webp,image/gif) and thecontent_lengthin bytes (up to 5 MB). It returns anupload_url, anobject_key, and anexpires_at. - Upload the bytes.
PUTthe image directly to theupload_urlbefore it expires. - Confirm.
POST /v1/branding/logowith theobject_key. The API re-fetches the uploaded object and sniffs its magic bytes; the client-declared content type is not trusted, so a non-image is rejected withvalidation_failed. On success the logo is persisted on the theme and the updated branding is returned.
DELETE /v1/branding/logo clears the logo so surfaces fall back to the default. All three
writes require branding.manage.
Verify a Sending Domain
A branded from_email only delivers reliably from a domain you have verified. Registering a
custom sending domain returns the DNS records to publish, and verification checks that they have propagated.
GET /v1/branding/email-domainslists your registered domains, each with its DNS records and live status. The envelope also carriesprovider_configured, which isfalsewhen the platform's domain-capable email provider is not set up; managing domains then returnsemail_domain_provider_unconfigured.POST /v1/branding/email-domainsregisters a domain (a fully-qualified name such asmail.example.com) and returns the record set to publish. Re-registering an existing domain returnsemail_domain_conflict; a domain the provider rejects returnsemail_domain_invalid.POST /v1/branding/email-domains/{id}/verifyasks the provider to re-check the published records and returns the refreshed status.DELETE /v1/branding/email-domains/{id}removes the domain.
Each domain response carries an id, the domain, its status, the
region, the dns_records to publish (each with a record, name,
value, type, ttl, and priority), a verified_at,
a last_synced_at, and a created_at. The status moves through five states:
| Status | Meaning |
|---|---|
not_started | Registered; the DNS records have not been published or checked yet. |
pending | Verification is in progress while the records propagate. |
verified | The records are live; the domain can send. verified_at
is set. |
failed | Verification did not succeed; re-check the published records. |
temporary_failure | A transient provider error; retry verification. |
The mutations require the branding.email_domain.manage permission. A verify or delete against
an id that does not exist for your tenant returns email_domain_not_found.
Public Pre-Auth Branding Read
Hosted member pages need to paint your brand before the member has authenticated. GET
/v1/public/branding serves that read with no API key: it takes a claim_token query
parameter, and the token itself is the capability, exactly like the recipient
claim endpoints. It resolves the token to a tenant and returns that tenant's public
brand bundle, a subset with the brand name, support email, logo, palette (including the dark-mode colors), app
title, footer text, and terms and privacy links. Internal sender-domain fields are omitted.
An invalid, expired, unknown, or missing token returns the platform-default bundle with a 200,
never a 404, so token validity cannot be fingerprinted by status. The endpoint is rate-limited by
client IP. Use it only to render a hosted page whose claim token the page already holds.
Authorization and Errors
Reads (GET /v1/branding and GET /v1/branding/email-domains) are open to any
tenant-scoped caller. Every mutation requires a permission and is recorded in the
audit log as a configuration change.
| Operation | Permission |
|---|---|
| Upsert theme, logo upload / confirm / delete | branding.manage |
| Add, verify, remove a sending domain | branding.email_domain.manage |
The error codes each surface most commonly returns:
| Surface | Codes |
|---|---|
| Theme upsert, logo | validation_failed, role_denied |
| Sending domains | email_domain_invalid, email_domain_conflict,
email_domain_not_found, email_domain_provider_unconfigured,
email_domain_provider_error |
See Authorization for how permissions resolve from roles and Errors and Correlation IDs for the full catalog.
Related
- Settings and Branding: editing the same theme from the operator console.
- Escrow and Claims: the claim page the public branding read paints.
- Tenants and Workspaces: the tenant a theme belongs to.