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-keyis 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" = 400GET /api/store/integrations/boxnow/lockers — locker directory
- Purpose: the BoxNow APM directory for the checkout locker picker.
Discover availability via
carriers.boxnow.lockers_urlon 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 thelockerskey — 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"'