---
title: "Update Webchat Settings"
method: PATCH
path: "/webchat/settings"
tags: ["Web Chat"]
---

# Update Webchat Settings

`PATCH /webchat/settings`

## Request

- Base URL: `https://be.graph8.com/api/v1`
- URL: `https://be.graph8.com/api/v1/webchat/settings`
- Auth: HTTP bearer

Partial edit of the theme. An unset field is left untouched.

THIS IS LIVE. A saved theme is what the next visitor sees, with no publish
step in between.

To REMOVE a design-token override, list its path in
`themeOverridesRemove`. Sending `null` for it does not remove it - under a
partial update that reads as "leave it alone".

## Headers

- `Authorization` string, nullable
- `X-Target-Org-Id` string, nullable

## Request body

- SettingsUpdateRequest: Partial edit of the widget theme. An unset field is left untouched. `themeOverridesRemove` is the only way to DELETE a token override rather than set it to something - sending `null` for a token would be "unset in this request", which the partial-update rule reads as "leave it alone".
  - `chat_bot_id` string, required: Owning chat bot id (UUID).
  - `expected_revision` integer, nullable
  - `surface` 'external' | 'internal', nullable
  - `primaryColor` string, nullable
  - `secondaryColor` string, nullable
  - `fontFamily` string, nullable
  - `fontSize` string, nullable
  - `borderRadius` string, nullable
  - `chatWidth` string, nullable
  - `darkMode` boolean, nullable
  - `userMessageBgColor` string, nullable
  - `userMessageTextColor` string, nullable
  - `assistantMessageBgColor` string, nullable
  - `assistantMessageTextColor` string, nullable
  - `assistantName` string, nullable
  - `showHeader` boolean, nullable
  - `headerTextColor` string, nullable
  - `showSuggestedQuestions` boolean, nullable
  - `showQuickActions` boolean, nullable
  - `suggestedQuestionsBgColor` string, nullable
  - `suggestedQuestionsSectionBgColor` string, nullable
  - `robotIconColor` string, nullable
  - `robotIconType` string, nullable
  - `botIconType` string, nullable
  - `headerImageUrlExternal` string, nullable
  - `headerImageUrlInternal` string, nullable
  - `botAvatarMode` 'agent' | 'preset' | 'custom', nullable
  - `botAvatarPresetUrl` string, nullable
  - `botAvatarCustomUrl` string, nullable
  - `themeSource` 'default' | 'brand_kit' | 'custom', nullable
  - `designTokens` DesignTokens: Extended widget theme tokens stored in chat_widget_settings.designTokens. Shape is the shared FE/BE contract — the TypeScript twin lives in frontend/features/webchat/widget-preview/chat-design-settings.ts.
    - `version` integer
    - `colors` DesignTokenColors: Color slots consumed by the widget's --wcw-* CSS var vocabulary. LIGHT-mode and mode-invariant leaves ONLY. Per-mode dark variants live on the sibling ``DesignTokenColorsDark`` group (``designTokens.colorsDark``), NOT as ``*Dark`` twins here (Phase G). The ``@field_validator("*")`` hex guard below is exactly WHY the dark palette must be a sibling group rather than a nested ``colors.dark`` object — a nested dict would be fed to ``_require_hex_color`` and 422 the save (the same force that made ``DesignTokenHeader`` separate). A legacy stored ``colors.<name>Dark`` leaf is lifted into ``colorsDark`` at parse time by ``DesignTokens._lift_legacy_dark_leaves`` — no data migration.
      - `surface` string, nullable: Widget body bg (light)
      - `textPrimary` string, nullable
      - `textSecondary` string, nullable
      - `border` string, nullable
      - `link` string, nullable
      - `bubbleAssistantBg` string, nullable
      - `bubbleAssistantText` string, nullable
      - `bubbleUserBg` string, nullable
      - `bubbleUserText` string, nullable
      - `launcherBg` string, nullable
      - `launcherIcon` string, nullable
      - `headerGradientFrom` string, nullable
      - `headerGradientTo` string, nullable
      - `heroText` string, nullable: Home hero greeting text
      - `heroTextMuted` string, nullable: Home hero subtitle text
      - `askCardBg` string, nullable: Ask-a-question card bg (light)
      - `askCardBorder` string, nullable: Ask-a-question card border (light)
      - `askCardText` string, nullable: Ask-a-question card text (light)
      - `actionCardBg` string, nullable: Quick-action card bg (light)
      - `actionCardBorder` string, nullable: Quick-action card border (light)
      - `actionCardText` string, nullable: Quick-action card label (light)
      - `actionCardIconTint` string, nullable: Quick-action icon-circle tint (light)
      - `agentBubbleBg` string, nullable: Human-agent bubble bg (light)
      - `agentBubbleBorder` string, nullable: Human-agent bubble border (light)
      - `agentBubbleText` string, nullable: Human-agent bubble text (light)
      - `sendButtonBg` string, nullable: Composer send-button bg
      - `sendButtonIcon` string, nullable: Composer send-button icon
      - `messagesTitleText` string, nullable: Messages screen title text
      - `messagesSearchBg` string, nullable: Messages search bar bg (light)
      - `messagesSearchText` string, nullable: Messages search text (light)
      - `messagesSearchBorder` string, nullable: Messages search bar border (opt-in)
      - `typingDot` string, nullable: Typing-indicator dot (light)
      - `slotChipBg` string, nullable: Slot-picker chip bg (light)
      - `slotChipText` string, nullable: Slot-picker chip text (light)
      - `slotChipHoverBg` string, nullable: Slot-picker chip hover bg (light)
      - `slotChipHoverBorder` string, nullable: Slot-picker chip hover border (light)
    - `colorsDark` DesignTokenColorsDark: Per-mode DARK colour overrides (``designTokens.colorsDark``, Phase G). A sibling of ``DesignTokenColors`` holding the dark variant of every colour leaf that renders differently in dark mode. ABSENT ⇒ the widget renders exactly as it did before this group existed (the dark literal, or — for a leaf an org set in ``colors`` and that historically painted both modes — that ``colors`` value). Only leaves with a genuine dark render path live here; a mode-invariant leaf (launcher, header gradient, send button, hero) has no entry, so no dead tokens are introduced. Field names carry NO ``Dark`` suffix — the group is the mode. The 13 leaves that previously lived as ``colors.<name>Dark`` twins were migrated here; the upconverter on ``DesignTokens`` lifts any legacy value so no row regresses.
      - `surface` string, nullable: Widget body bg (dark)
      - `askCardBg` string, nullable: Ask-a-question card bg (dark)
      - `askCardBorder` string, nullable: Ask-a-question card border (dark)
      - `askCardText` string, nullable: Ask-a-question card text (dark)
      - `actionCardBg` string, nullable: Quick-action card bg (dark)
      - `actionCardBorder` string, nullable: Quick-action card border (dark)
      - `actionCardText` string, nullable: Quick-action card label (dark)
      - `actionCardIconTint` string, nullable: Quick-action icon-circle tint (dark)
      - `agentBubbleBg` string, nullable: Human-agent bubble bg (dark)
      - `agentBubbleBorder` string, nullable: Human-agent bubble border (dark)
      - `agentBubbleText` string, nullable: Human-agent bubble text (dark)
      - `messagesSearchBg` string, nullable: Messages search bar bg (dark)
      - `messagesSearchText` string, nullable: Messages search text (dark)
      - `textPrimary` string, nullable: Message text (dark)
      - `textSecondary` string, nullable: Timestamps & meta text (dark)
      - `border` string, nullable: Borders & dividers (dark)
      - `bubbleAssistantBg` string, nullable: Bot bubble bg (dark)
      - `bubbleAssistantText` string, nullable: Bot bubble text (dark)
      - `typingDot` string, nullable: Typing-indicator dot (dark)
      - `slotChipBg` string, nullable: Slot-picker chip bg (dark)
      - `slotChipText` string, nullable: Slot-picker chip text (dark)
      - `slotChipHoverBg` string, nullable: Slot-picker chip hover bg (dark)
      - `slotChipHoverBorder` string, nullable: Slot-picker chip hover border (dark)
      - `composerPopoverBg` string, nullable: Composer emoji-popover bg (dark)
      - `composerPopoverBorder` string, nullable: Composer emoji-popover border (dark)
    - `typography` DesignTokenTypography: Font family / weights. fontUrl is an optional CSS URL (Google Fonts css2).
      - `fontFamily` string, nullable
      - `fontUrl` string, nullable
      - `headingWeight` integer, nullable
      - `bodyWeight` integer, nullable
      - `fontSize` string, nullable
      - `lineHeight` string, nullable
      - `letterSpacing` string, nullable
    - `radius` DesignTokenRadius: Border-radius slots (CSS dimensions; launcher is typically 50%).
      - `widget` string, nullable
      - `bubble` string, nullable
      - `control` string, nullable
      - `input` string, nullable
      - `launcher` string, nullable
    - `shadows` DesignTokenShadows: box-shadow slots — charset-restricted, see _require_css_shadow.
      - `widget` string, nullable
      - `launcher` string, nullable
      - `bubble` string, nullable
      - `suggested` string, nullable
      - `launcherHover` string, nullable
    - `motion` DesignTokenMotion: Interaction motion — one duration (<time>) + one timing-function.
      - `duration` string, nullable
      - `easing` string, nullable
    - `transform` DesignTokenTransform: RFC Phase E3 — interaction transform SCALES (unitless multipliers). Motion *timing* (duration + easing) already shipped as `DesignTokenMotion`; this is the other half of E3. Every leaf is optional and absence means "the literal `chat-script/src/styles.ts` ships today", so `transform` absent = byte-identical rendering. Each leaf carries its OWN range because the ranges are semantic, not cosmetic: a "grow" interaction above ~1.2 reads as a glitch rather than feedback, and a "press" interaction must stay at or below 1.0 or the element grows when pushed. Rotation is deliberately NOT here. RFC §16 scopes E3 to *scales*, and the launcher's icon swap uses a ±90deg PAIR that must stay symmetric — one leaf would invite an asymmetric swap, two would invite a broken one.
      - `launcherHover` string, nullable
      - `launcherActive` string, nullable
      - `chipHover` string, nullable
      - `chipActive` string, nullable
      - `windowEnter` string, nullable
      - `rippleFrom` string, nullable
      - `rippleTo` string, nullable
    - `input` DesignTokenInput: Composer input surface colors (hex-validated).
      - `background` string, nullable
      - `borderColor` string, nullable
      - `focusBorderColor` string, nullable
    - `badge` DesignTokenBadge: Unread launcher-badge colors (hex-validated).
      - `background` string, nullable
      - `text` string, nullable
    - `spacing` DesignTokenSpacing: Spacing slots — padding shorthand (1-4 dims) and single-dim gaps.
      - `bubblePadding` string, nullable
      - `sectionGap` string, nullable
    - `geometry` DesignTokenGeometry: RFC Phase E4 — launcher / panel / badge / chip geometry, CLAMPED. The most locked-down group in the model, because `chat-script` injects these into the CUSTOMER's page rather than into the widget iframe (RFC §17.1). Unclamped geometry on a host page is a defacement vector, so every leaf is whole-pixel-only and range-bounded — see `_require_px_in_range`. Ranges for `launcherSize` / `launcherOffset*` are INHERITED from the ratified product decision (RFC §19 decision 4: "launcher geometry in scope with clamped ranges — size ~36-64px, offsets ~8-48px"), not invented here. Widget COLLAPSED WIDTH is deliberately absent: it is already plumbed as the flat `chatWidth` column and read at runtime by the deployed bundle, so a token would be a second writer for one property. ── TWO CLAMPS ARE ONE-DIRECTIONAL (max pinned at today's value) ──────────── `widgetWidthExpanded` (max 640) and `launcherOffsetX` (max 20) may only ever SHRINK. This is not conservatism, it is a live constraint: The expanded panel needs a `widgetWidthExpanded + launcherOffsetX` viewport to fit, but the mobile veto that hides the expand control is the hardcoded `MOBILE_BREAKPOINT_PX = 560` (`chat-script/src/components/container.ts`, mirrored as a literal `@media (max-width: 560px)` in `styles.ts`). Today's 640 + 20 = 660 already exceeds it, so viewports 561-659px ALREADY render an overflowing panel — a pre-existing defect (GH #12173). Letting either value GROW would widen that band onto more real devices. A media-query CONDITION cannot read a custom property, so the guard cannot be derived the way the panel's `bottom` is; correcting it means making `MOBILE_BREAKPOINT_PX` config-derived across `container.ts` + `expand.ts` + the emitted media query. That is GH #12173, and it is what lifts these two ceilings. Until it lands, 640 and 20 are load-bearing — do not raise them. THE RULE, so it is applied correctly next time: an axis needs a one-directional clamp exactly when it has NO viewport clamp. The horizontal axis has none. The vertical axis has `calc(100dvh - 140px)`, and the worst case under these ranges is `launcherOffsetY(48) + launcherSize(64) + gap(11) = 123px`, which stays inside that 140px reserve with 17px to spare at every viewport height — which is why `launcherOffsetY`, `launcherSize`, `badgeSize` and `chipGap` are free to grow. Any future range change must preserve `launcherOffsetY + launcherSize + 11 <= 140`.
      - `launcherSize` string, nullable
      - `launcherOffsetX` string, nullable
      - `launcherOffsetY` string, nullable
      - `widgetHeight` string, nullable
      - `widgetHeightExpanded` string, nullable
      - `widgetWidthExpanded` string, nullable
      - `badgeSize` string, nullable
      - `chipGap` string, nullable
    - `header` DesignTokenHeader: How the header surface is painted — mode + gradient blend point. A SEPARATE group, not new `DesignTokenColors` slots, and that is forced: `DesignTokenColors` hex-validates EVERY field (`@field_validator("*")`), so a `"solid"` mode or a numeric stop there would 422 the whole save. Both leaves are ABSENT by default and absence is meaningful — it means "render exactly as this org rendered before this group existed": * ``mode`` absent -> gradient (today's only behaviour). * ``fromStop`` absent -> no interpolation hint is emitted at all, so the CSS is byte-identical to the legacy string (see `buildWidgetCssVars`). The 50/50 default is therefore expressed as ABSENCE, never as a written 50 — existing rows keep rendering without relying on hint-equivalence.
      - `mode` 'solid' | 'gradient', nullable
      - `fromStop` integer, nullable
      - `angle` integer, nullable
  - `themeOverridesRemove` string[], nullable: Token paths to REMOVE. Unsetting a token is not the same as this.
  - `copyOverrides` object, nullable
  - `launcherQuestionLimit` integer, nullable
  - `homeQuestionLimit` integer, nullable
  - `conversationQuestionLimit` integer, nullable
  - `conversationQuestionsAfterFirstMessage` 'hide' | 'remain' | 'collapse', nullable

## Response `200`

Successful Response

- ApiResponseWebchatPayload
  - `data` WebchatPayload, required: An open envelope for one webchat object. Open because the widget's config grows a field per product decision and a pinned schema would DROP it. Same choice the desk surface made.
  - `pagination` PaginationMeta
    - `page` integer, required: Current page number (1-indexed)
    - `limit` integer, required: Items per page
    - `total` integer, required: Total number of items
    - `has_next` boolean, required: Whether there are more pages
    - `next_cursor` string, nullable: Opaque cursor for the next page; null when there are no more items. Pass it back as the `cursor` query param to fetch the following page without computing page numbers.
    - `total_available` integer, nullable: Search endpoints only: how many records really match, when known. Can be larger than `total`, which is capped at the 10,000 results that can be paged. For company search this is a distinct-company estimate. Null when unknown; absent outside search.
    - `total_is_lower_bound` boolean, nullable: Search endpoints only: true when more records match than `total` shows (the result set is larger than the 10,000 results that can be paged). Absent outside search.
    - `notes` string[], nullable: Search endpoints only: plain-language notes about the result, e.g. that more records match than can be paged, or that a reversed `between` range was put in order.

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/graph8/apis/graph8-developer-api.md) · [All operations](https://skmtc.dev/graph8/apis/graph8-developer-api/llms.txt) · [OpenAPI document](https://skmtc.dev/graph8/apis/graph8-developer-api/revisions/2e7cfa0ca921?raw)
