Install the GWP tag
For developers integrating a bespoke storefront, a headless front end (Next.js, Nuxt, Remix, Astro, SvelteKit…), or any platform we do not ship a plugin for. This is the primary integration path, not a fallback. Most of our engineering effort has gone into making the tag platform-agnostic.
What's in this guide
Section titled “What's in this guide”- Basket pages and placement: which URLs count as a basket or confirmation page, where the gift block renders, and slide-out cart drawers.
- Cart adapters sets out the three ways the widget can read your cart total and currency, and how the order id is read on the confirmation page.
- The claim moment: your order-confirmation page, or your own order-confirmation email via the platform-neutral
gwp:selectionhook. - Verify and troubleshoot lists the browser checks to run before you call it done, and every
[GWP debug]line with its cause and fix. - Local and staging development explains what an unregistered dev hostname actually does, and the two ways round it.
- Config, CSP and device storage: a real config response, the consent modes, the CSP entries to add, and the complete list of what we write to the shopper's device.
- Platform guides cover Shopify end to end, custom & headless specifics, and what we would confirm on the platforms we have not shipped yet.
At a glance
Section titled “At a glance”| What you add | One <script> tag. Optionally one <div> mount point per template. |
|---|---|
| What we render | A gift-choice block on your basket page, inside a closed Shadow DOM root. Your CSS cannot leak in, ours cannot leak out. |
| What we read | Your cart total + currency, via one of three adapters you choose. |
| What we write to your page | Only our own container, inside the slot you nominate (or one we inject and remove again). Never your items, prices, cart or markup. |
| What we store | First-party localStorage keys and first-party cookies on your own domain (gwp_ses, gwp_sel, gwp_shows, plus gwp_attrs on the Shopify order-email path, and a 5-second __gwp_probe cookie). Never a third-party cookie. Exhaustive list in device storage. |
| Failure mode | Fail-silent. Every entry point is wrapped; a failure renders nothing and never throws onto your page. |
| Blocking? | No. async, and the widget boots after DOMContentLoaded. |
The widget never changes items or prices. On a custom platform it makes no writes to your cart at all — the only cart-writing code path in the product is a Shopify-specific one, gated behind a per-store setting that does not apply to you.
The tag
Section titled “The tag”<script async src="https://cdn.gwpingenuity.com/w.js" data-site="YOUR_SITE_KEY"></script>Your site key is on your GWP dashboard at dashboard.gwpingenuity.com → Install, which also shows the snippet above with the key already filled in. It is a public identifier, not a secret: it is designed to sit in your page source. It is scoped to your registered domains.
No login yet? Accounts are created by us. There is no self-serve signup, and the key does not exist until the account does. If your company is already on GWP, an owner can invite you from Settings → Users; otherwise email us and allow for that round trip in your plan. The rest of this guide is writable against a placeholder key, but nothing will boot until you have the real one.
| Attribute | Required | Notes |
|---|---|---|
src | Yes | Must be https://cdn.gwpingenuity.com/w.js. The widget derives its API origin from this URL, so script, API calls and images all come from one hostname. |
data-site | Yes | Your site key. Without it the widget does not boot at all. |
async | Recommended | Non-blocking load. |
data-api | No, don't | Overrides the API origin. Only used for GWP-supervised testing. Omit it. |
Where to put it
Section titled “Where to put it”Put it before </body> on your basket template and your order-confirmation template. Those are the two pages the widget can render on.
Two reasons to go site-wide (every page, e.g. in your root layout) instead:
- Your basket is a slide-out mini-cart that opens without a URL change. A tag scoped to a basket route can never be present when the drawer opens.
- Your storefront is an SPA whose route transitions never re-execute per-template scripts. A tag in the root layout is loaded once and follows navigation on its own (see below).
Site-wide is safe: on any page that is not a registered basket or confirmation page, the widget resolves its config, renders nothing, and disarms its document-wide DOM observer. (With a cart drawer enabled it keeps one deliberately cheap watcher, because a drawer can open anywhere: a MutationObserver scoped to your drawer element, plus a few passive cart-event listeners.) The cost is that config requests rise roughly 20–50×, absorbed by our edge cache. That cache is not a site-wide trade-off. It applies to every install, including a basket-template-only one (the config response).
Via a tag manager
Section titled “Via a tag manager”Create a Custom HTML tag containing the snippet. Fire it on basket and confirmation page views, or on all page views if you need the site-wide behaviour above.
Single-page apps
Section titled “Single-page apps”You do not need to re-inject the tag on route changes, and you should not: the loader boots once per document (a duplicate tag is a harmless no-op). It follows client-side navigation on its own by wrapping history.pushState / history.replaceState and listening for popstate, hashchange and load. It re-fetches config when the path changes; a query-string- or hash-only change is deliberately ignored (it cannot change the answer), so variant pickers and facet filters that rewrite the URL do not cause churn.
If your basket hydrates late, or you mutate the cart without a DOM change the widget can see, tell it explicitly:
window.dispatchEvent(new Event("gwp:cartchange"));A dataLayer.push does this implicitly too: the loader wraps dataLayer.push transparently (your own pushes and any GTM consumers are unaffected), provided window.dataLayer already exists when the tag boots. The usual window.dataLayer = window.dataLayer || [] in <head> is enough. If your app creates or replaces dataLayer later, as commonly happens in a hydrated SPA, the wrap is never installed, and you must dispatch gwp:cartchange after each push.
What to send us
Section titled “What to send us”To get from "tag installed" to "live", we need the list below. Send it to support@gwpingenuity.com with [INTEGRATION] in the subject line. The same mailbox answers shoppers asking about a gift they claimed, and the prefix tells us to put yours in front of an engineer instead (more on getting help).
- Every domain you serve the storefront from (subdomains are covered automatically, so
example.comalso admitswww.andcheckout.), plus any staging or local dev hostname you need to work on, unless you are using a subdomain of one you have already registered (local development). - Your basket and confirmation URL patterns, as paths, including localised variants.
- Your cart adapter choice, and its config if
dom(the total selector) or if you want anorderIdSelector. - Your mount points, if they are not
[data-gwp-slot]. - Your drawer selectors (
root,openWhen), if your basket is a slide-out. - Anything unusual about your CSP, especially a restrictive
style-src.
Where next
Section titled “Where next”- Basket pages and placement: the next step. Tell us which URLs are basket and confirmation pages, and where the block should render on them.
- Platform guides: Shopify end to end, custom & headless specifics, and what we would confirm on BigCommerce, Magento, WooCommerce and Salesforce Commerce Cloud.
- How the integration works: the mechanics behind everything above, plus the answers for a security reviewer. See bundle versioning, pinning and SRI and switching it off.
- Ask us something: email
support@gwpingenuity.comwith[INTEGRATION]in the subject. We would rather answer a question than have you guess at a parameter.