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": "…" }, "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" "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 not cached.
GET /v1/configcarries no caching headers and nothing between the browser and our API stores it, so a config change (new patterns, a new adapter, a paused store, a kill switch) applies on the shopper's next config fetch: the next page load or route change. A page that has already rendered keeps what it has. Installing site-wide changes nothing here. - Image URLs (
/img/…?v=3) carry the image's version. The bytes behind one URL never change, so images are servedCache-Control: public, max-age=31536000, immutable; a new image gets a new URL. - 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. |
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. Without consent the widget runs in essential mode: it writes nothing until the shopper picks a gift, then only that choice, for at most 7 days, and the markers tied to it. With essential mode switched off it writes nothing at all. 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 erases the session id, the counters and any sealed sign-in email, and keeps only the gift choice, on the 7-day essential lifetime, and its markers. With essential mode switched off it tears the widget 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 before consent is satisfied except, in essential mode, a gift the shopper picks (gwp_sel, for at most 7 days) and the markers tied to it (gwp_attrs, gwp_gsync). This table is the complete list: seven 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 (7 in essential mode) | 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_gsync | localStorage + first-party cookie | Until the empty list is sent (cookie: 30 days, 7 in essential mode) | Order-email gifts only (your server asks us for an order's gifts). Written only once we hold a gift list for the signed-in customer from this device: a short hash of your site key and the customer's number, never the number, meaning "this device owes us the empty list that deletes it". Removed once that list is sent. Written in essential mode too, after a pick. |
__gwp_probe | cookie only | 5 seconds | One-off probe to find your registrable domain, deleted immediately. |
gwp_em | localStorage only | 24 hours | Separate-email stores only (we email the gift link from your domain). Written only in full mode, never in essential mode, once a shopper has signed in: the sign-in email address sealed on the device to our key, which nothing on the device or your page can open, plus its plain SHA-256, which other scripts on your page can read, to check it against the account hash you publish, if you do. Sent with the order number to /v1/redeem on the confirmation page, then removed. |
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, except that in essential mode the gift choice (for at most 7 days) and its markers stay.
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.