Update Webchat Settings
PATCH /webchat/settings
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”.
Authorizations
Parameters
Header Parameters
Request Body required
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”.
object
Owning chat bot id (UUID).
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.
object
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.
object
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.
object
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.
object
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”:
modeabsent -> gradient (today’s only behaviour).fromStopabsent -> no interpolation hint is emitted at all, so the CSS is byte-identical to the legacy string (seebuildWidgetCssVars). The 50/50 default is therefore expressed as ABSENCE, never as a written 50 — existing rows keep rendering without relying on hint-equivalence.
Border-radius slots (CSS dimensions; launcher is typically 50%).
Box-shadow slots — charset-restricted, see _require_css_shadow.
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.
object
Font family / weights. fontUrl is an optional CSS URL (Google Fonts css2).
object
Responses
200
Successful Response
object
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.
object
object
Whether there are more pages
Items per page
Current page number (1-indexed)
Total number of items
422
Validation Error