---
title: "Banner design"
description: "Read and change a banner's themes, layout, colours, typography, buttons, logo, categories and custom CSS, and match the banner to your website, with the CookieBeam REST API."
url: "https://cookiebeam.com/docs/api-reference/banner-design"
lang: "en"
---

# Banner design

Read and change a banner's themes, layout, colours, typography, buttons, logo, categories and custom CSS, and match the banner to your website, with the CookieBeam REST API.

The banner design endpoints expose the same settings as the banner designer in the dashboard. Every path sits under one banner:

```text
https://cookiebeam.com/api/v1/banners/{bannerId}/design/…
```

`bannerId` is the banner's **internal** ID, as returned by [`GET /api/v1/banners`](https://cookiebeam.com/docs/api-reference/banners). Every operation is also available as an [MCP tool](https://cookiebeam.com/docs/api-reference/mcp#banner-design-tools) with the name listed below; both run the same checks.

**Writes change the draft only.** Nothing you change here reaches visitors until the banner is published from the dashboard. There is no publish endpoint yet: the `banners:publish` scope is reserved.

## How these endpoints differ

- **The key acts for its creator.** Each call re-checks that the member who created the key is still active and may read (and, for writes, edit) this part of this banner. See [Who a key acts for](https://cookiebeam.com/docs/api-reference/authentication#who-a-key-acts-for).
- **Scopes.** Reads need `banners.design:read`, writes `banners.design:write`; `banners:read` and `banners:write` also satisfy them. Deleting, duplicating and setting the default theme need `banners:admin`.
- **Test keys** may read and change drafts. They may not delete or duplicate themes, set the default theme, store or remove logo files, or start site captures and style matches (HTTP 403 `test_key_production_forbidden`).
- **Versioned writes.** `PATCH`, `PUT` and `DELETE` on a theme or theme section need `If-Match`. See [Versioned writes](#versioned-writes).
- **Extra rate limits.** Writes are limited to 120 per minute per key; logo import, site capture, style match and refine to 20 per hour per banner. See [Errors and rate limits](https://cookiebeam.com/docs/api-reference/errors-and-limits#extra-limits-on-banner-design-operations).
- **Plan features.** Some settings need a plan feature (advanced design, custom CSS, branding removal, site capture). A change that needs a feature your plan lacks returns HTTP 403 `plan_required` with `details.feature`. Applying a preset is the exception: it applies only the parts your plan includes.

## Themes

`themeId` is a theme UUID, or `default` for the banner's default theme.

| Method | Path | Scope | MCP tool | What it does |
| ------ | ---- | ----- | -------- | ------------ |
| `GET` | `/design/themes` | `banners.design:read` | `list_themes` | The banner's themes, newest first, [paginated](https://cookiebeam.com/docs/api-reference/overview#pagination). Each item is a whole theme |
| `POST` | `/design/themes` | `banners.design:write` | `create_theme` | Create a theme (HTTP 201, `Location` header) |
| `GET` | `/design/themes/{themeId}` | `banners.design:read` | `get_theme` | One whole theme, with its `ETag` |
| `PATCH` | `/design/themes/{themeId}` | `banners.design:write` | `update_theme` | Change a whole theme. `themeConfig` is merged onto the stored one (objects merge, `null` clears a key); every other field replaces the stored value |
| `DELETE` | `/design/themes/{themeId}` | `banners:admin` | `delete_theme` | Delete a theme that is not the default and not used by an active A/B test. Needs `If-Match` |
| `POST` | `/design/themes/{themeId}/duplicate` | `banners:admin` | `duplicate_theme` | Copy a theme under `newName` (the copy is never the default) |
| `POST` | `/design/themes/{themeId}/set-default` | `banners:admin` | `set_default_theme` | Make this theme the one visitors see after the next publish |
| `GET` | `/design/themes/{themeId}/usage` | `banners.design:read` | `get_theme_usage` | A/B test variants that use the theme, and whether it can be deleted |

A theme document:

```json
{
  "id": "0c6c1c3e-2f0a-4b8e-9d55-7e1f2a3b4c5d",
  "bannerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "name": "Default Theme",
  "isDefault": true,
  "themeConfig": {
    "mode": "opt-in",
    "guiOptions": {
      "consentModal": { "layout": "box", "position": "bottom right" },
      "preferencesModal": { "layout": "box", "position": "middle center", "controlStyle": "toggle" },
      "darkMode": "auto"
    },
    "categories": {
      "necessary": { "enabled": true, "readOnly": true },
      "analytics": { "enabled": false, "readOnly": false }
    },
    "branding": { "position": "left", "showBranding": true }
  },
  "cssVariables": { "--cc-btn-primary-bg": "#1d4ed8" },
  "customCss": null,
  "customFeatures": {},
  "createdAt": "2026-09-01T10:00:00.000Z",
  "updatedAt": "2026-10-20T08:30:00.000Z"
}
```

`POST /design/themes` takes the dashboard's theme input. All fields are optional:

| Field | Type | Notes |
| ----- | ---- | ----- |
| `name` | string | 1–255 characters. Default `Default Theme` |
| `isDefault` | boolean | Default `true` |
| `themeConfig` | object | `mode` (`opt-in` or `opt-out`), `cookie`, `guiOptions`, `categories`, `language`, `branding` |
| `cssVariables` | object | CSS variable names mapped to values |
| `customCss` | string | Up to 50,000 characters. Sanitised on save; a non-empty value needs the custom CSS plan feature |
| `customFeatures` | object | |
| `templateId` | string | Up to 50 characters |

```bash
curl -X POST https://cookiebeam.com/api/v1/banners/3fa85f64-5717-4562-b3fc-2c963f66afa6/design/themes \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{ "name": "Autumn campaign", "isDefault": false }'
```

## Theme sections

Each section is a slice of one theme with its own read, its own write and its own `ETag`. Writes to these paths are **JSON merge patches** ([RFC 7396](https://www.rfc-editor.org/rfc/rfc7396)): send only the keys to change, and send `null` to remove a key so its default applies. Custom CSS is the exception: it is replaced as a whole with `PUT`.

| Section | Path (`GET` and write) | MCP tools | Fields |
| ------- | ---------------------- | --------- | ------ |
| Layout | `/design/themes/{themeId}/layout` (`PATCH`) | `get_design_layout`, `update_design_layout` | `themeConfig.disablePageInteraction`; `themeConfig.guiOptions.consentModal.layout` and `.position`; `themeConfig.guiOptions.preferencesModal.layout`, `.position` and `.controlStyle` |
| Colours | `/design/themes/{themeId}/colors` (`PATCH`) | `get_design_colors`, `update_design_colors` | `themeConfig.guiOptions.darkMode` (`auto`, `light`, `dark`); colour CSS variables in `cssVariables`, such as `--cc-bg`, `--cc-primary-color`, `--cc-btn-primary-bg`, `--cc-btn-primary-color`, `--cc-link-color`, `--cc-toggle-on-bg` |
| Typography | `/design/themes/{themeId}/typography` (`PATCH`) | `get_design_typography`, `update_design_typography` | `cssVariables`: `--cc-font-family`, `--cc-font-size`, `--cc-font-weight`, `--cc-line-height`, `--cc-text-align` |
| Buttons | `/design/themes/{themeId}/buttons` (`PATCH`) | `get_design_buttons`, `update_design_buttons` | `consentModal`: `equalWeightButtons`, `flipButtons`, `showCloseButton`, `showAcceptNecessary`, `equalWidthButtons`; `preferencesModal`: `equalWeightButtons`, `flipButtons`, `showAcceptNecessary`; `showFloatingSettings`; `floatingSettingsButton` (`position`, `size` 32–96, `backgroundColor`, `color`, `hoverBackgroundColor`, `offset` 0–100) — all under `themeConfig.guiOptions` |
| Logo settings | `/design/themes/{themeId}/logo` (`PATCH`) | `get_design_logo`, `update_design_logo` | `themeConfig.guiOptions.logo`: `showLogo`, `logoUrl` (must be a logo stored for this banner, see [Logo files](#logo-files)), `logoPosition` (`start`, `center`, `title-end`), `logoSize` (`sm`, `md`, `lg`), `logoAlt` (up to 120 characters), `watermarkOpacity`, `watermarkCorner` |
| Branding footer | `/design/themes/{themeId}/branding` (`PATCH`) | `get_design_branding`, `update_design_branding` | `themeConfig.branding`: `privacyPolicyUrl`, `cookiePolicyUrl`, `position` (`left`, `center`, `right`), `showBranding`. Hiding the CookieBeam branding needs the branding-removal plan feature |
| Categories | `/design/themes/{themeId}/categories` (`PATCH`) | `get_design_categories`, `update_design_categories` | `themeConfig.categories`: a map of category name to `{ enabled, readOnly, autoClear? }`. A category you send replaces the stored record for that category |
| Custom CSS | `/design/themes/{themeId}/custom-css` (`PUT`) | `get_design_custom_css`, `replace_design_custom_css` | `customCss`, up to 50,000 characters, sanitised on save |

Allowed values for the layout section:

| Field | Values |
| ----- | ------ |
| `consentModal.layout` | `box`, `box inline`, `box wide`, `cloud`, `cloud inline`, `bar`, `bar inline` |
| `consentModal.position` | `top`, `bottom`, `middle`, `top left`, `top center`, `top right`, `middle left`, `middle center`, `middle right`, `bottom left`, `bottom center`, `bottom right` |
| `preferencesModal.layout` | `box`, `side`, `side wide`, `bar` |
| `preferencesModal.position` | `left`, `right`, `center`, `top`, `bottom`, and the nine `top/middle/bottom` × `left/center/right` combinations |
| `preferencesModal.controlStyle` | `toggle`, `checkbox` |

The OpenAPI document at [`/api/v1/openapi.json`](https://cookiebeam.com/api/v1/openapi.json) lists the full schema of every section, including every colour variable.

## Versioned writes

Read a section first. The response carries an `ETag`:

```bash
curl -i https://cookiebeam.com/api/v1/banners/3fa85f64-5717-4562-b3fc-2c963f66afa6/design/themes/default/layout \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY"
```

```text
HTTP/1.1 200 OK
ETag: "design.layout:q3VZ1m0rXgk8Jw2nT4bYfA"
```

```json
{
  "data": {
    "themeConfig": {
      "disablePageInteraction": false,
      "guiOptions": {
        "consentModal": { "layout": "box", "position": "bottom right" },
        "preferencesModal": { "layout": "box", "position": "middle center", "controlStyle": "toggle" }
      }
    }
  },
  "version": "v1"
}
```

Send the change with that value as `If-Match`:

```bash
curl -X PATCH https://cookiebeam.com/api/v1/banners/3fa85f64-5717-4562-b3fc-2c963f66afa6/design/themes/default/layout \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY" \
  -H "Content-Type: application/json" \
  -H 'If-Match: "design.layout:q3VZ1m0rXgk8Jw2nT4bYfA"' \
  -d '{ "themeConfig": { "guiOptions": { "consentModal": { "layout": "bar", "position": "bottom" } } } }'
```

The response is the section after the change, with the new `ETag`. If someone changed the section after your read, the write is refused:

```json
{
  "data": {
    "error": "The resource was changed by another writer. Read it again and retry.",
    "code": "conflict",
    "details": { "currentEtag": "\"design.layout:Hk2pW9sQe1VtYc0uRz7mLb\"" },
    "requestId": "9b2e5f0c-8d7a-4e0b-9c1d-2f3a4b5c6d7e"
  },
  "version": "v1"
}
```

Read again, re-apply your change and retry with the new `ETag`. `If-Match: *` skips the check and overwrites whatever is stored. A write without `If-Match` returns HTTP 428 `precondition_required`. A theme used by an active A/B test may be locked: HTTP 409 `resource_locked`, with the test named in `details`.

## Presets

| Method | Path | Scope | MCP tool | What it does |
| ------ | ---- | ----- | -------- | ------------ |
| `GET` | `/design/presets` | `banners.design:read` | `list_design_presets` | The built-in presets: `id`, `name`, `type`, `colorScheme`, `description`, `cssVariables`, optional `customCss` |
| `POST` | `/design/themes/{themeId}/apply-preset` | `banners.design:write` | `apply_design_preset` | Apply a preset by `presetId`. Replaces the theme's CSS variables and custom CSS; plans without advanced design get only the basic palette and font, and keep their custom CSS. `If-Match` is optional here |

## Logo files

Logo files are stored per banner and served from the CDN. Storing one returns its URL; set that URL on a theme with the logo settings section, then publish.

| Method | Path | Scope | MCP tool | What it does |
| ------ | ---- | ----- | -------- | ------------ |
| `POST` | `/design/logo/upload` | `banners.design:write` | `upload_logo` | Upload `dataBase64`: PNG, JPEG, WebP or SVG, at most 200 KB. The type is detected from the content and SVG is sanitised |
| `POST` | `/design/logo/import` | `banners.design:write` | `import_logo_from_url` | Fetch a logo from a public `https` `url` (no redirects, size-capped) |
| `POST` | `/design/logo/use-detected` | `banners.design:write` | `use_detected_logo` | Copy the logo found by the latest [site capture](#match-your-website) |
| `DELETE` | `/design/logo?url=<cdn url>` | `banners.design:write` | `remove_logo` | Delete one logo file. A file still used by a theme or a published version is kept and the response is `{ "deleted": false, "reason": "in_use" }` |

The three `POST` operations return HTTP 201:

```json
{
  "data": {
    "url": "https://cdn.cookiebeam.com/…/logo.svg",
    "contentType": "image/svg+xml",
    "bytes": 4210
  },
  "version": "v1"
}
```

Test keys cannot call these four operations, because the files are written to the live CDN.

## Match your website

These operations capture your homepage, propose banner styles that match it, and save a proposal as a new theme. They need the site capture plan feature, and test keys cannot start captures or matches.

| Method | Path | Scope | MCP tool | What it does |
| ------ | ---- | ----- | -------- | ------------ |
| `POST` | `/design/site-capture` | `banners.design:write` | `start_site_capture` | Start a homepage capture, or join the one running, and return at once (HTTP 202). `url` defaults to the banner domain and must be one of its domains |
| `GET` | `/design/site-capture` | `banners.design:read` | `get_site_capture` | The latest capture: `id`, `status` (`pending`, `ready`, `failed`), `requestedUrl`, and what it found. `null` when there is none. Poll this after starting a capture |
| `POST` | `/design/site-capture/manual` | `banners.design:write` | `save_manual_site_capture` | Enter brand colours by hand instead: `primary`, `background`, `text` (lowercase `#rrggbb`) and `font` (`system-ui`, `Arial, sans-serif`, `Georgia, serif`, `Verdana, sans-serif`, `Times New Roman, serif`) |
| `POST` | `/design/site-match` | `banners.design:write` | `match_website` | Run the AI style match on the latest capture. Optional `baseThemeId` and `locale`. Returns `proposals` (`id`, `name`, …). Counts against the plan's match allowance |
| `GET` | `/design/site-match/proposals` | `banners.design:read` | `list_site_match_proposals` | The kept proposals of the latest match, with `savedThemeIds` for proposals already saved, or `null` |
| `POST` | `/design/site-match/proposals/{proposalId}/refine` | `banners.design:write` | `refine_site_match_proposal` | Adjust a proposal with an `instruction` (1–300 characters) and optional `logo` and `locale`. Counts as a match |
| `POST` | `/design/site-match/proposals/{proposalId}/apply` | `banners.design:write` | `apply_site_match_proposal` | Save a proposal as a new theme on a copy of `baseThemeId`, optionally named `themeName` (1–40 characters). HTTP 201 with `themeId`. Saving the same proposal again returns that theme with `alreadySaved: true` |

A typical flow: start a capture, poll `GET /design/site-capture` until `status` is `ready`, run a match, apply the proposal you like, then make the new theme the default (or edit it further) and publish from the dashboard.

## Errors

Besides the [common errors](https://cookiebeam.com/docs/api-reference/errors-and-limits#error-codes), these endpoints return:

| HTTP status | Code | Cause |
| ----------- | ---- | ----- |
| 400 | `invalid_request` | Validation failed. `details.issues[].path` starts with `params.` or `body.` |
| 403 | `key_owner_inactive` | The member who created the key is no longer active in the team |
| 403 | `insufficient_permission` | The key's creator cannot make this change on this banner |
| 403 | `test_key_production_forbidden` | A test key was used for an operation test keys may not run |
| 403 | `plan_required` | The plan lacks the feature named in `details.feature` |
| 404 | `not_found` | The banner, theme, proposal or path does not exist, or the key or its creator cannot see it. `details.resource` names the resource where available |
| 405 | `method_not_allowed` | The path exists, but not for this method |
| 409 | `conflict` | Stale `If-Match` (`details.currentEtag`), or the current state does not allow the operation (for example no capture yet) |
| 409 | `resource_locked` | The theme is used by an active A/B test |
| 413 | `payload_too_large` | The body is larger than 64 KB (512 KB for logo uploads) |
| 428 | `precondition_required` | A versioned write was sent without `If-Match` |
| 429 | `rate_limited` | A per-key or per-operation limit was reached |
