Store integrations config — carriers, COD, tracking, lockers

Store-public integration configuration for render/checkout time (couriers-port + tracking-integrations cards). The payload is composed from an ordered block registry — each block owns distinct top-level keys (carriers, cod, tracking today; future blocks append). It is NOT wrapped in an envelope: the blocks ARE the top-level keys.

Security law (the allowlist rule): every block is an explicit allowlist — objects are built field-by-field from capability descriptors and the few named public config values. Credentials are never read into a response object, so secrets can never appear in this payload: carrier API keys, Meta CAPI access_token, GA4 api_secret, Klaviyo private_key. The executable block below asserts their absence on the live payload; tests/store/store-integrations.test.ts asserts it key-by-key.

GET /api/store/integrations — the composed config

  • Purpose: one call at storefront boot/checkout for carrier capabilities, the COD fee row, and the public analytics tag IDs.
  • Auth: anon (x-client-id); x-publishable-api-key is validated when present (unknown/revoked/foreign-tenant key → 400 invalid_publishable_key) and may be omitted by single-channel storefronts.
  • Request: no params.
  • Response:
{
  // Only ENABLED carriers appear — disabled/unconfigured are ABSENT,
  // never `enabled: false`. Keyed by provider slug.
  "carriers": {
    "boxnow": {
      "enabled": true,
      "cod": true,             // carrier collects cash on delivery
      "pickup_points": false,  // office/pickup-point delivery
      "lockers": true,         // locker/APM network
      "lockers_url": "/api/store/integrations/boxnow/lockers"  // only when lockers
    }
  },
  // null unless the cod integration is enabled AND fee_amount > 0 — the
  // exact gate the totals engine applies, so the storefront's optimistic
  // fee row can never disagree with the charged total. EUR only.
  "cod": {
    "enabled": true,
    "fee_amount": 3.5,          // EUR major units
    "fee_currency": "eur",
    "fee_label": "Cash on delivery fee",
    "description": null          // admin's checkout note, or null
  },
  // Public tag config — only ENABLED providers with a public id appear.
  "tracking": {
    "facebookPixel": { "pixelId": "123…" },
    "gtm": { "containerId": "GTM-…" },
    "ga4": { "measurementId": "G-…" },
    "klaviyo": { "publicKey": "…" },
    "googleAds": { "conversionId": "AW-…", "conversionLabel": "…" },
    "consent_required": true     // always present — mirrors the consent CMP's enabled
  }
}
  • Errors: 400 missing_client_id · 400 invalid_publishable_key.
  • SDK: integrations.getIntegrationsConfig(client)
  • Components: carrier/locker pickers, COD fee row in checkout, tracking mounts (<MetaPixel>/<GA4>/<Gtm> behind the consent gate).
  • Settings: admin → Settings → Integrations (per-provider enable + config); consent settings drive tracking.consent_required; Rybbit is deliberately ABSENT (platform analytics, not a tenant integration).

Tracking wiring contract: mount tags only through the consent gate when consent_required (see consent.md); Purchase events MUST use eventID = "purchase_" + order.display_id so Meta dedupes browser Pixel vs server CAPI; write TrackingAttribution keys into cart.metadata (consent-gated) so server events inherit fbp/fbc/anon-id/ga signals.

BODY=$(curl -sf "$BASE/api/store/integrations" -H "x-client-id: $CLIENT_ID")
echo "$BODY" | grep -q '"carriers"'
echo "$BODY" | grep -q '"tracking"'
echo "$BODY" | grep -q '"consent_required"'
# THE ALLOWLIST RULE, asserted negatively: no secret key names, ever.
if echo "$BODY" | grep -qE 'access_token|api_secret|private_key|client_secret|credentials'; then
  echo "SECRET LEAK in /api/store/integrations payload"; exit 1
fi
# A valid publishable key passes validation (B2B channel-scoped dev key).
curl -sf "$BASE/api/store/integrations" -H "x-client-id: $CLIENT_ID" \
  -H "x-publishable-api-key: $PUBLISHABLE_KEY" | grep -q '"tracking"'
# A bogus key → 400 invalid_publishable_key.
STATUS=$(curl -s -o /dev/null -w '%{http_code}' "$BASE/api/store/integrations" \
  -H "x-client-id: $CLIENT_ID" -H "x-publishable-api-key: pk_doc_bogus_$RUN")
test "$STATUS" = 400

GET /api/store/integrations/boxnow/lockers — locker directory

  • Purpose: the BoxNow APM directory for the checkout locker picker. Discover availability via carriers.boxnow.lockers_url on the config above — don't probe for the 503.
  • Auth: anon (x-client-id).
  • Response:
{
  "lockers": [
    {
      "id": "42",
      "title": "BoxNow Sofia Center",
      "addressLine1": "bul. Vitosha 1",
      "addressLine2": "",
      "postalCode": "1000",
      "country": "BG",
      "lat": 42.6977,   // number or null — malformed carrier data → null, never NaN
      "lng": 23.3219,
      "note": ""
    }
  ]
}
  • Errors: 503 {message, lockers: []} (BoxNow not configured/enabled for the store) · 502 {message, lockers: []} (carrier call failed). Every state carries the lockers key — a picker can always map over it.
  • Caching: in-process 10-min TTL per store + Cache-Control: public, max-age=600, stale-while-revalidate=3600.
  • SDK: integrations.listBoxNowLockers(client)
  • Components: checkout locker picker (carrier picker family).
  • Settings: BoxNow credentials + enable in admin → Settings → Integrations.
# The always-true contract: whatever the configuration state (200 with a
# directory, 503 unconfigured, 502 upstream), the payload carries `lockers`.
curl -s "$BASE/api/store/integrations/boxnow/lockers" -H "x-client-id: $CLIENT_ID" \
  | grep -q '"lockers"'