Skip to documentation content

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:

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

bannerId is the banner's internal ID, as returned by GET /api/v1/banners. Every operation is also available as an MCP tool 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.
  • 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.
  • 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.
  • 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.

MethodPathScopeMCP toolWhat it does
GET/design/themesbanners.design:readlist_themesThe banner's themes, newest first, paginated. Each item is a whole theme
POST/design/themesbanners.design:writecreate_themeCreate a theme (HTTP 201, Location header)
GET/design/themes/{themeId}banners.design:readget_themeOne whole theme, with its ETag
PATCH/design/themes/{themeId}banners.design:writeupdate_themeChange 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:admindelete_themeDelete a theme that is not the default and not used by an active A/B test. Needs If-Match
POST/design/themes/{themeId}/duplicatebanners:adminduplicate_themeCopy a theme under newName (the copy is never the default)
POST/design/themes/{themeId}/set-defaultbanners:adminset_default_themeMake this theme the one visitors see after the next publish
GET/design/themes/{themeId}/usagebanners.design:readget_theme_usageA/B test variants that use the theme, and whether it can be deleted

A theme document:

{
  "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:

FieldTypeNotes
namestring1–255 characters. Default Default Theme
isDefaultbooleanDefault true
themeConfigobjectmode (opt-in or opt-out), cookie, guiOptions, categories, language, branding
cssVariablesobjectCSS variable names mapped to values
customCssstringUp to 50,000 characters. Sanitised on save; a non-empty value needs the custom CSS plan feature
customFeaturesobject
templateIdstringUp to 50 characters
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): 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.

SectionPath (GET and write)MCP toolsFields
Layout/design/themes/{themeId}/layout (PATCH)get_design_layout, update_design_layoutthemeConfig.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_colorsthemeConfig.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_typographycssVariables: --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_buttonsconsentModal: 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_logothemeConfig.guiOptions.logo: showLogo, logoUrl (must be a logo stored for this banner, see 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_brandingthemeConfig.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_categoriesthemeConfig.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_csscustomCss, up to 50,000 characters, sanitised on save

Allowed values for the layout section:

FieldValues
consentModal.layoutbox, box inline, box wide, cloud, cloud inline, bar, bar inline
consentModal.positiontop, bottom, middle, top left, top center, top right, middle left, middle center, middle right, bottom left, bottom center, bottom right
preferencesModal.layoutbox, side, side wide, bar
preferencesModal.positionleft, right, center, top, bottom, and the nine top/middle/bottom × left/center/right combinations
preferencesModal.controlStyletoggle, checkbox

The OpenAPI document at /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:

curl -i https://cookiebeam.com/api/v1/banners/3fa85f64-5717-4562-b3fc-2c963f66afa6/design/themes/default/layout \
  -H "Authorization: Bearer $COOKIEBEAM_API_KEY"
HTTP/1.1 200 OK
ETag: "design.layout:q3VZ1m0rXgk8Jw2nT4bYfA"
{
  "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:

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:

{
  "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

MethodPathScopeMCP toolWhat it does
GET/design/presetsbanners.design:readlist_design_presetsThe built-in presets: id, name, type, colorScheme, description, cssVariables, optional customCss
POST/design/themes/{themeId}/apply-presetbanners.design:writeapply_design_presetApply 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.

MethodPathScopeMCP toolWhat it does
POST/design/logo/uploadbanners.design:writeupload_logoUpload dataBase64: PNG, JPEG, WebP or SVG, at most 200 KB. The type is detected from the content and SVG is sanitised
POST/design/logo/importbanners.design:writeimport_logo_from_urlFetch a logo from a public https url (no redirects, size-capped)
POST/design/logo/use-detectedbanners.design:writeuse_detected_logoCopy the logo found by the latest site capture
DELETE/design/logo?url=<cdn url>banners.design:writeremove_logoDelete 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:

{
  "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.

MethodPathScopeMCP toolWhat it does
POST/design/site-capturebanners.design:writestart_site_captureStart 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-capturebanners.design:readget_site_captureThe 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/manualbanners.design:writesave_manual_site_captureEnter 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-matchbanners.design:writematch_websiteRun 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/proposalsbanners.design:readlist_site_match_proposalsThe kept proposals of the latest match, with savedThemeIds for proposals already saved, or null
POST/design/site-match/proposals/{proposalId}/refinebanners.design:writerefine_site_match_proposalAdjust a proposal with an instruction (1–300 characters) and optional logo and locale. Counts as a match
POST/design/site-match/proposals/{proposalId}/applybanners.design:writeapply_site_match_proposalSave 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, these endpoints return:

HTTP statusCodeCause
400invalid_requestValidation failed. details.issues[].path starts with params. or body.
403key_owner_inactiveThe member who created the key is no longer active in the team
403insufficient_permissionThe key's creator cannot make this change on this banner
403test_key_production_forbiddenA test key was used for an operation test keys may not run
403plan_requiredThe plan lacks the feature named in details.feature
404not_foundThe banner, theme, proposal or path does not exist, or the key or its creator cannot see it. details.resource names the resource where available
405method_not_allowedThe path exists, but not for this method
409conflictStale If-Match (details.currentEtag), or the current state does not allow the operation (for example no capture yet)
409resource_lockedThe theme is used by an active A/B test
413payload_too_largeThe body is larger than 64 KB (512 KB for logo uploads)
428precondition_requiredA versioned write was sent without If-Match
429rate_limitedA per-key or per-operation limit was reached