Docs

Publish a new branding revision

POST
/branding

Publishes a new immutable branding revision for the project. Branding revisions cannot be updated or deleted; every edit publishes a new revision, and flow responses resolve the latest revision per project.

The liquid_template is validated lexically on save (size, encoding, banned patterns such as <script> tags, inline event handlers, and the | raw filter). Authoritative LiquidJS validation runs at authoring time via zitadel plan / zitadel apply.

Authorization

oauth2 branding.write
headerAuthorizationBearer <token>

Scope: branding.write

Query Parameters

project_id*string

The unique identifier of the project

Match^[a-zA-Z0-9_-]+$

Request Body

application/json
  1. body

Branding configuration, used in two places with one shape: as the request body of POST /branding, which publishes it as a new immutable revision, and as a read-only projection on flow responses, which carries the project's latest revision. Locally it is the .zitadel/branding/ descriptor that zitadel apply publishes.

Every field is optional. An omitted key uses Zitadel's maintained default, so a revision carrying one colour is as valid as one carrying the whole object.

$schema?string

Editor affordance: path or URL of this file's JSON meta-schema, so an editor validates and autocompletes it. The CLI strips it before upload; the platform ignores it.

layout?string

Layout preset the revision declares, centered or split. It is validated on publish and handed to the template as branding.layout. The bundled default template does not read it, so changing it alone changes nothing on screen, and it is not an appearance control. Not a design catalog: the ejectable designs (centered, minimal) ship as templates and both carry centered; switch designs with zitadel branding eject --design <name> instead of editing this field. split is carried by revisions published from the earlier split designs.

Default"centered"
Value in"centered""split"
liquid_template?string

The LiquidJS template for rendering this step. The orchestrator renders it into its Shadow DOM with the capability dictionaries as context.

Lengthlength <= 131072
logo_url?string

Fallback mark, used only when neither theme.light.logo_url nor theme.dark.logo_url is set. Prefer the per-side marks — one file cannot serve both surfaces.

Match^([hH][tT][tT][pP][sS]://(?:\[[0-9A-Fa-f:.]+\]|[^/:?#@]+)(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?#]|$)|[hH][tT][tT][pP]://([lL][oO][cC][aA][lL][hH][oO][sS][tT]|127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|\[::1\])(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?([/?#]|$))
Formaturi
Lengthlength <= 2048
hero_url?string

Hero image URL, for templates that reference branding.hero_url. The bundled default template does not use it.

Match^([hH][tT][tT][pP][sS]://(?:\[[0-9A-Fa-f:.]+\]|[^/:?#@]+)(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(?:[/?#]|$)|[hH][tT][tT][pP]://([lL][oO][cC][aA][lL][hH][oO][sS][tT]|127\.(?:(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){2}(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])|\[::1\])(?::0*(?:[0-9]|[1-9][0-9]{1,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?([/?#]|$))
Formaturi
Lengthlength <= 2048
theme?

The theme sides this project publishes, and which of them may run.

Publishing one side makes it the only theme that resolves, even under auto or an operating-system preference for the other. Publishing both lets auto follow the operating system, each side using its own logo and palette.

The element-level theme property overrides mode. When one side is published it cannot select the other. Publishing neither side leaves mode, the theme property and the operating system to pick one, painted with the maintained defaults.

typography?

The typeface the login surface renders in. One face covers both body and headings; separate heading and mono faces are not modelled yet.

shape?

Corner rounding, spacing and logo size. Shared across theme sides.

Response Body

The branding revision was created successfully.

application/json
  1. response

A stored branding revision. branding echoes the canonical stored configuration so clients (e.g. the CLI sync engine) can reconcile local files with the server's stored state.

id*string

The unique identifier of this branding revision.

created_at*string

When this revision was published.

Formatdate-time
branding*

Branding configuration, used in two places with one shape: as the request body of POST /branding, which publishes it as a new immutable revision, and as a read-only projection on flow responses, which carries the project's latest revision. Locally it is the .zitadel/branding/ descriptor that zitadel apply publishes.

Every field is optional. An omitted key uses Zitadel's maintained default, so a revision carrying one colour is as valid as one carrying the whole object.

Example Request
import { createZitadelClient } from "@zitadel/api/client";const client = createZitadelClient({  baseUrl: process.env.ZITADEL_URL ?? "http://localhost:8080",  token: process.env.ZITADEL_PROJECT_SECRET!, // server-side only});const branding = await client.createBranding({}, { project_id: "proj_01KWH1P4MYS7F0N9WD3P2E4YM5" });
Example Response
{  "id": "string",  "created_at": "2019-08-24T14:15:22Z",  "branding": {    "$schema": "string",    "layout": "centered",    "liquid_template": "string",    "logo_url": "http://example.com",    "hero_url": "http://example.com",    "theme": {      "mode": "light",      "light": {        "logo_url": "http://example.com",        "palette": {          "primary": "#4F46E5",          "on_primary": "#4F46E5",          "background": "#4F46E5",          "surface": "#4F46E5",          "muted": "#4F46E5",          "border": "#4F46E5",          "text": "#4F46E5",          "text_muted": "#4F46E5",          "link": "#4F46E5",          "success": "#4F46E5",          "warning": "#4F46E5",          "error": "#4F46E5"        }      },      "dark": {        "logo_url": "http://example.com",        "palette": {          "primary": "#4F46E5",          "on_primary": "#4F46E5",          "background": "#4F46E5",          "surface": "#4F46E5",          "muted": "#4F46E5",          "border": "#4F46E5",          "text": "#4F46E5",          "text_muted": "#4F46E5",          "link": "#4F46E5",          "success": "#4F46E5",          "warning": "#4F46E5",          "error": "#4F46E5"        }      }    },    "typography": {      "font_family": "string",      "font_url": "http://example.com",      "scale": 0.75    },    "shape": {      "radius": "none",      "density": "compact",      "logo_scale": 0.5    }  }}

Was this helpful?