Consent — Consent Mode v2 banner config

The store's CMP configuration for the built-in Consent Mode v2 banner (consent-management card). Defaults are always applied server-side, so the payload is complete and renderable even for an unconfigured store (compliant built-in modal, BG + EN copy shipped).

Storefront wiring (the trap that matters)

  • Mount <ConsentInit> as the first child of <body> — it sets the synchronous Consent Mode v2 DEFAULT and must never wait on this fetch (async default = first-hit consent race). Resolve this config server-side (RSC) and inline it into the document.
  • Render the built-in banner only when enabled && mode === "builtin".
  • mode: "external" = the merchant's CMP owns the UI and must write the same _1c_consent cookie (or call setConsent()) — all Cartbase-side tag gating works off that one seam.
  • Choices persist 12 months in the cookie. Rybbit (platform analytics) stays outside consent by design.
  • Pair with integrations.md: tracking.consent_required mirrors enabled here — tags mount only through the consent gate when true.
  • Purpose: everything the banner needs to render, per locale.
  • Auth: anon (x-client-id).
  • Request: no params.
  • Response — the EXACT allowlist (nothing else will ever appear here):
{
  "consent": {
    "enabled": true,
    "mode": "builtin",              // "builtin" | "external"
    "layout": "modal",              // "modal" (blocking) | "banner-bottom" (non-blocking)
    "privacy_href": "/cookies",
    "reject_on_first_layer": false,
    "copy": {
      "bg": {
        "title": "Преди да продължиш",
        "body": "…",
        "privacy_link_label": "…",
        "accept_label": "…", "settings_label": "…", "reject_label": "…",
        "settings_title": "…",
        "accept_all_label": "…", "save_label": "…", "reject_all_label": "…",
        "necessary_label": "…", "necessary_description": "…",
        "analytics_label": "…", "analytics_description": "…",
        "ads_label": "…", "ads_description": "…"
      },
      "en": { /* same 16 keys — every field always present per locale */ }
    }
  }
}
  • Errors: 400 missing_client_id only — the payload itself always succeeds (a corrupt/missing stored config degrades to defaults, never to a broken banner).
  • SDK: consent.getConsent(client)
  • Components: <ConsentInit> + <ConsentBanner copy={copy[locale]} layout privacyHref rejectOnFirstLayer> (consent family; reference impl src/components/storefront/consent/).
  • Settings: admin → Settings → Consent (enabled/mode/layout/ privacy_href/copy per locale).
# Complete, renderable config — even on an unconfigured store the defaults
# make every documented key present.
BODY=$(curl -sf "$BASE/api/store/consent" -H "x-client-id: $CLIENT_ID")
echo "$BODY" | grep -q '"consent"'
echo "$BODY" | grep -q '"mode"'
echo "$BODY" | grep -q '"layout"'
echo "$BODY" | grep -q '"privacy_href"'
echo "$BODY" | grep -q '"reject_on_first_layer"'
echo "$BODY" | grep -q '"analytics_label"'
echo "$BODY" | grep -q '"ads_description"'
# Missing tenant header → the standard 400.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/consent")
test "$STATUS" = 400