# 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](integrations.md): `tracking.consent_required`
  mirrors `enabled` here — tags mount only through the consent gate when
  true.

## GET /api/store/consent — the CMP config

- **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):

```jsonc
{
  "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).

```bash
# 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
```
