Config, CSP and device storage
The three things you consult rather than follow: what the server sends the widget, what your Content Security Policy has to allow, and everything the widget writes to a shopper's device.
What a real config response looks like
Section titled “What a real config response looks like”One GET per page, no auth, no cookies (credentials: "omit"). The url parameter is your page's origin + pathname only. The widget strips query and hash before it sends anything, so storefront tokens in query strings never leave the browser.
GET https://cdn.gwpingenuity.com/v1/config?site=YOUR_SITE_KEY&url=https%3A%2F%2Fexample.com%2Fcart{ "config": { "configVersion": 1, "host": { "id": "…", "name": "Example Store", "currency": "GBP", "timezone": "Europe/London" }, "pageType": "basket", // "basket" | "confirmation" | absent "branding": { "primary": "#1668E3", "radius": 12, "font": "…", "theme": "light" }, "copy": { "headline": "…", "subtext": "…", "cta": "…", "terms": "…" }, "placement": { "selector": "[data-gwp-slot]", "fallbackPosition": "after_summary", "basket": { "selector": "[data-gwp-slot]", "fallbackPosition": "after_summary" }, "confirmation": { "selector": "[data-gwp-slot]", "fallbackPosition": "after_summary" } }, "cartAdapter": { "type": "datalayer" }, "eligibility": { "minCartValue": 25, "perSessionCap": 3, "perDayCap": 5 }, "consentMode": "auto", // "auto" | "strict" | "off" "maxGifts": 1, "offers": [ { "offerId": "…", "advertiserId": "…", "advertiser": "Partner Brand", "mono": "PB", "color": "#…", "title": "Free welcome gift", "description": "…", "terms": "…", "type": "Free product", "creative": { "giftImageUrl": "/img/o/…?v=3" }, // origin-relative "redemptionUrl": "https://partner.example/redeem", "minCartValue": 20 } ] }}Things worth knowing:
"config": nullis a legitimate 200 answer: unknown site key, paused store, or a kill switch. The widget renders nothing and does not treat it as an error. A page whose URL matches no pattern still returns a full config, withpageTypesimply absent.- Config answers are edge-cached for up to 60 seconds on every install.
GET /v1/configis the one cached route (nothing else under/v1/*is), keyed on your site key and the page class, and it is cached for basket, confirmation and other pages alike. So a config change (new patterns, a new adapter, a paused store, a kill switch) reaches shoppers within about a minute rather than on their literal next request. This is not a consequence of installing site-wide; a basket-template-only install gets exactly the same window. The response the shopper's browser receives isno-store, so nothing is held in the browser cache on top of that. - A
429or5xxis treated as transient: the widget keeps whatever state it has and retries. A blip never sticks a shopper dark. - The response carries no commercial data — no commission rates, no budgets. Those never reach the browser.
- Fields are additively optional within schema v1. Do not assume a key is present because you saw it once; do not break on a key you do not recognise.
/v1/configis not origin-checked, but/v1/events,/v1/select,/v1/clickoutand/v1/associateare. If your domain is not registered with us, those return403and the widget renders but records nothing. Send us every domain you serve from (subdomains are covered automatically). This is also the first thing you will hit on a dev machine (see local development).
Consent
Section titled “Consent”consentMode is a per-store setting:
| Mode | Behaviour |
|---|---|
auto (default) | Renders and stores unless a CMP explicitly denies, or a banner is present and unanswered. |
strict | Fails closed: renders and stores only on an explicit CMP grant. |
off | Consent is handled entirely upstream by you. The widget reads no CMP at all in this mode. It renders and stores on every shopper. Only use it if your own gate already decides whether GWP may run (typically by not loading the tag until you have permission). |
Under auto and strict, nothing at all is written to the shopper's device before consent resolves: no session id, no counters, no click id. The widget reads IAB TCF v2, Didomi, OneTrust, Cookiebot, Usercentrics, Osano, Shopify Customer Privacy and Google Consent Mode, and re-evaluates when any of them fires a decision event, so an "Accept" renders the widget without a page reload. Under those two modes a withdrawal after the widget has mounted tears it down and erases every gwp_* key.
Content Security Policy
Section titled “Content Security Policy”Everything the widget loads comes from one hostname: cdn.gwpingenuity.com. The bundle resolves its API base from its own <script src> origin, so the script, the /v1/* calls and the images all share that entry.
Add:
script-src https://cdn.gwpingenuity.com;connect-src https://cdn.gwpingenuity.com;img-src https://cdn.gwpingenuity.com;- If your policy sets
script-src-elemseparately, the entry must be there too, becausescript-src-elemdoes not inherit when it is explicitly present. img-srcis optional. Without it, gift images and partner brand icons are blocked and the widget falls back to its gradient + monogram tiles. Nothing breaks; the block just looks plainer.- A wildcard
https://*.gwpingenuity.comin those directives covers this and anything we may serve you in future. - If the Shopify order-email channel is enabled for your store, the tag also calls your own
/cart.jsand/cart/update.js, soconnect-srcneeds'self'as well. That path does not apply to custom platforms.
What you do not need:
- No
'unsafe-eval'. The bundle contains noevaland nonew Function. - No
'unsafe-inline'inscript-src: the tag is an external script, and we inject no inline<script>. - No
frame-src/child-src. There are no iframes. - No
font-src. No web fonts are loaded; the widget uses the font family configured for your brand, from whatever you already serve. - No
worker-src. There are no web workers. - No
form-actionchange. The widget submits no forms cross-origin. - No
integrityattribute. We do not publish an SRI hash for the tag, and adding one yourself has a consequence worth understanding before you do. The full answer is in how it works § versioning, pinning and SRI, including what we offer instead and how to check for yourself which build you are running.
What we store on the shopper's device
Section titled “What we store on the shopper's device”All first-party, on your own domain. Never a third-party cookie, and (under auto and strict consent modes) nothing at all before consent is satisfied. This table is the complete list: five keys, and no other storage mechanism at all. No sessionStorage, no IndexedDB, no Cache Storage, no service worker.
| Key | Where | Lifetime | Purpose |
|---|---|---|---|
gwp_ses | localStorage + first-party cookie | 30 days | Opaque session reference. Not a user id. |
gwp_sel | localStorage + first-party cookie | 30 days | The selected gift(s), with title, brand, click id and redemption URL, so the claim survives basket → confirmation. |
gwp_shows | localStorage + first-party cookie | 2 days | Frequency-cap counters. |
gwp_attrs | localStorage + first-party cookie | 30 days, or until cleared | Shopify order-email path only. Written only if the order-email channel is enabled for your store, and never on a custom platform. A single "1" flag meaning "gwp_* cart attributes may still be live on this shopper's cart", so a later basket view knows it must issue a clearing write rather than let a revoked gift ride into your order email. Carries no gift data. |
__gwp_probe | cookie only | 5 seconds | One-off probe to find your registrable domain, deleted immediately. |
The cookie copies exist so a selection survives a hop to a checkout subdomain where the basket origin's localStorage is invisible. They are set with SameSite=Lax on your registrable domain, and with Secure on HTTPS. If a CMP consent is withdrawn under auto or strict, every one of these keys is erased from both carriers.
Two mechanical notes, because they surprise people during verification. On a hostname with no registrable domain (plain localhost, or a bare single-label intranet name), the cookie half writes nothing at all and only localStorage is used (local development). And each carrier is written independently, so a browser with localStorage blocked or full still keeps the cookie copy, and vice versa.
Where next
Section titled “Where next”- Platform guides: Shopify end to end, custom & headless specifics, and what we would confirm on the platforms we have not shipped yet.
- How the integration works covers the mechanics behind all of the above, plus bundle versioning, pinning and SRI and switching it off.