Shopify theme: the app embed
Brands Growth Dept works with get the theme app embed and the web pixel inside their own Growth Dept app. Publishing a test writes its configuration to the app's metafield, and Liquid writes it into each page, so the decision needs no network request.
The embed renders just before </head>. Its default CSS shows only each test's control until the runtime adds cro-ready to the <html> element. Switch the embed on in a copy of your theme first; switching it on in the live theme is done with the owner's approval.
Shopify theme: the snippet
Stores without the Growth Dept app connect with a custom-app Admin API token. CRO then writes the gd-cro snippet to the theme and the configuration to the shop metafield gd_cro.config. Render the snippet above </head>:
{% render 'gd-cro' %}
</head>The pixel is a custom pixel pasted into Shopify's Customer events. Runtime files are served from https://cro.growthdept.ai/r/ with versioned names.
Headless: Next.js middleware
The middleware adapter reads the brand's published configuration, decides the visitor's arm on the server and rewrites split URL arms under the original address. It keeps a short in-memory cache with a stale fallback, so a slow request never blocks a page.
GET https://cro.growthdept.ai/c/<public key>/config.json
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=30, stale-while-revalidate=300The public key (pk_…) is on the brand's settings page.
Custom sites: Cloudflare Worker
For a static or custom site in front of a Shopify checkout, the Worker adapter makes the same decision at the edge and serves the arm without changing the address. It adds the visitor's arms to cart permalinks, so the checkout carries them.
When a brand runs a headless or static site beside its Shopify theme, set the cookie domain to the parent domain so both share one visitor and one arm.
URL controls and QA
| Parameter | What it does |
|---|---|
?cro_qa=1 | QA mode on: the visitor also sees QA-only tests. ?cro_qa=0 turns it off. |
?cro=<test>-<arm> | Forces one arm of one test and marks the visitor as QA. ?cro=off clears forcing. |
?cro_debug=1 | Logs each decision to the browser console. |
JavaScript API
On a theme install the runtime exposes window.CRO once the page is parsed. These members are always there:
| Member | What it is |
|---|---|
CRO.vid | The visitor id |
CRO.assignments | The visitor's tests and groups, as test id to group key |
CRO.flag(key) | The value of a feature flag for this visitor |
CRO.track(name, valueMinor) | Sends a custom goal event, with an optional value in minor units |
CRO.price(variantId) | While a price test runs: the visitor's price and compare-at price in cents, or null |
CRO.qa | Whether this is a QA visitor |
CRO.originalReferrer | The external referrer saved before a redirect |
Switch on JavaScript API on the Integrations page for the rest. The store then loads one more small file, cached and loaded after the page is parsed:
| Member | What it does |
|---|---|
CRO.getTests() | The running tests the visitor is in that apply to this page: [{id, name, type, group, groupName, isPreview}] |
CRO.getGroups(testId) | Every group of a test: [{id, key, name, percentage, isControl}] |
CRO.getGroup(testId) | The visitor's group in a running test, or null |
CRO.assign(testId, groupKey) | Puts the visitor in a group from code, such as after a quiz, ahead of targeting and the traffic share, from the next page view. CRO.assign(testId, "", {randomAssignIfNullish: true}) picks the group the visitor's id falls in. Answers the group, or null for a test that is not running |
CRO.cartAttribute() | The _cro value that carries the visitor's groups into an order, for a checkout that does not use the Shopify cart |
CRO.cartAttributes() | Every attribute the runtime puts on the cart (_cro, _cro_ship, _cro_ref, _cro_v), for a cart built outside the theme |
CRO.offers.list() | The visitor's offers: kind, name, thresholds in cents, gift, volume tiers, upsell |
CRO.offers.gifts({achieved, cart}) | The gift offers; with achieved: true, those the cart meets (pass Shopify's /cart.js answer as cart, or get a promise that reads it) |
CRO.user, CRO.campaigns | The same calls under the method names other testing tools use (getExperiments, getTestGroups, getTestGroup, assignTestGroup, getAll, getGWP), so code written for them ports by renaming the global |
To run code once the API is ready, listen for cro:ready on window or document, or set window.onCroReady to a function or an array of functions. Each callback runs in its own try with window.CRO; one set or pushed later runs at once. With the JavaScript API on, cro:ready waits for the whole API.
window.onCroReady = (cro) => {
const tests = cro.getTests();
if (tests.length) console.log(tests[0].name, tests[0].groupName);
};Analytics: GA4, Tag Manager and other tools
Every assignment is pushed to window.dataLayer as a cro_assignment event with cro_test (the test id), cro_variant (the group key) and exp_variant_string (CRO - <test name> - <group name>), before Tag Manager's DOM Ready trigger. To send it to GA4 through Google Tag Manager:
- Add a Data Layer Variable named
exp_variant_string(andcro_testandcro_variantif you want the ids). - Add a Custom Event trigger for the event name
cro_assignment. - Add a GA4 Event tag named
experience_impressionwith the parameterexp_variant_stringset to the variable, fired by that trigger. - Publish the container, and register
exp_variant_stringas an event-scoped custom dimension in GA4.
A store that loads GA4 with gtag.js and no Tag Manager can switch on Google Analytics 4 on the Integrations page instead: the runtime then calls gtag("event", "experience_impression", …) itself on each page view. If GA4 counts fewer visitors than the test does, check that the Google tag loads in the page's head and that window.gtag exists; GA4 also drops some hits that tests count, so compare orders with the test's own results, not GA4's.
Clarity, Hotjar, Amplitude, Heap and Segment get each group through their own browser API once they are switched on there; the runtime never loads a tool. Hotjar gets the event <test> - <group>, Amplitude cro_impression, Heap one CRO Impression event per page view, Segment its standard Experiment Viewed event, and Clarity the tags experiment_name and cro_test_group. To change how often a tool hears of a test, set this in the theme before the app embed:
<script>
window.croSettings = { integrations: {
segment: { trackMode: "assignment" }, // once per visitor and group
heap: { trackMode: "timed", timeSinceAssignment: 3600 }
} };
</script>trackMode is impression (every page view; Heap and Segment by default), assignment (Amplitude's default), timed or session (Hotjar's default). Split URL redirects keep UTM tags and click ids.
Partner apps and custom checkouts
Apps that switch a feature on or off for an A/B test usually read a cart attribute or a window flag. Set it in a content test's group JavaScript, which runs once per page view for that group (at once, after the page loads, or after a delay you choose). The control's JavaScript sets the other value.
fetch("/cart/update.js", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ attributes: { "_tm_hide_widgets": "true" } })
});| App | What the group sets |
|---|---|
| Truemed | Cart attribute _tm_hide_widgets: "true" to hide its widgets |
| Checkout Reviews | Cart attributes _igShowReviewsBlock, _igShowFaqBlock, _igShowTrustSignalBlock, _igShowImageBannerBlock: "true" or "false", with the app's testing option on for each block |
| Shoptin | Cart attribute _ig_shoptin_test: "true" or "false", with the app's A/B test mode on |
| Navidium | window._igNavidiumConfig and the cart attribute of the same name, a JSON object with isOn, autoAddToCart, pctFee and minFee, with the app's app-config option on in its checkout block |
| SavedBy | window.SavedByAB = true or false, read by the app's custom JavaScript |
These attribute names are the ones each app documents for testing tools. We have not checked every app ourselves: start the test QA-only, open a preview link for each group and check the app before shoppers see it. Corso reads another tool's group id and needs its support team; ask us before testing it.
Price, shipping, offer and content tests need nothing extra for express checkout buttons such as JUST, since the discounts and prices are applied in Shopify's checkout. A free gift is the exception: the storefront adds it to the cart when the cart changes, so a button that goes from a product page straight to checkout skips it. Hide those buttons on product pages while a gift offer runs.
A checkout outside Shopify's (Checkout Champ, a custom checkout) must carry the _cro value onto the order as an order attribute, or the order is not counted in the test. With the JavaScript API on, read it from CRO.cartAttribute() when the shopper adds to cart and pass it on; the session must start on the store. Cart permalinks (/cart/<variant>:<qty>) on the store get the cart attributes when clicked; a permalink built in JavaScript should add attributes[_cro] itself. Checkouts that agents create through Shopify's Universal Commerce Protocol run the same discount and price Functions, but carry no _cro attribute, so those buyers get the control and are not counted.
Events
Runtimes send exposures and page views, and the pixel sends add to cart, checkout and purchase events, to one collector:
POST https://cro.growthdept.ai/c/e
Content-Type: text/plain (JSON body, sent with navigator.sendBeacon)
{ "b": "pk_...", ... } batch field b is the brand's public key; 16 KB limitThe collector checks the key and the page's origin and drops malformed batches and bots. A purchase event never counts on its own: the order sync confirms it against Shopify's order.
Public API
Brand API keys are created on the brand's settings page and stored only as hashes. With a key, the public API under https://cro.growthdept.ai/api/v1 creates and edits draft tests, starts, pauses and ends them, reads their results with a verdict, segments, time series and exports, reads the sitewide dashboards, and serves message tests: the version for a recipient, and the events that follow.
| Scope | Allows |
|---|---|
tests:read | Find and read tests, their results, verdicts, segments, time series and exports |
tests:write | Creating and editing drafts, pausing tests, and starting, resuming or ending a QA-only test |
tests:start | Starting, resuming, ending and editing a test shoppers see |
messages:write | Message-test drafts, and the sender's assign and events calls |
analytics:read | The sitewide dashboards under /api/v1/analytics |
export:read | The Data Sharing tables under /api/v1/exports: tests, assignments, sessions, events, orders and line items, page by page |
Each key has rate limits: 50 requests, then 25 more every 30 seconds, with tighter limits for recomputed results (fresh=1), exports and sitewide analytics. Every answer says what is left in X-RateLimit-Remaining; past the limit the API answers 429 with Retry-After.
GET https://cro.growthdept.ai/api/v1/tests/<id>/verdict
Authorization: Bearer cro_...
{"code": "directional_win", "headline": "...", "action": "Keep the test running until ..."}For the full reference, email hello@growthdept.ai.
MCP server for AI assistants
Claude, ChatGPT, Gemini and any other assistant that speaks the Model Context Protocol can connect to https://cro.growthdept.ai/api/mcp (Streamable HTTP). Add it as a connector by that URL and sign in when the assistant asks: on Growth Dept CRO's consent page you choose the brand and what the assistant may do (read tests and results, change and pause tests, start tests shoppers see, read the sitewide dashboards). Viewers can allow reading only, and the assistant can never do more than your role allows. A client that sends headers can use a brand API key in the Authorization header instead. Either way the assistant sees only the tools its scopes cover, and each tool spends from the connection's rate limits like an API call. Disconnect in the assistant, or revoke the connection under Settings, API keys.
claude mcp add --transport http growth-dept-cro https://cro.growthdept.ai/api/mcpclaude mcp add --transport http growth-dept-cro https://cro.growthdept.ai/api/mcp \
--header "Authorization: Bearer cro_..."In Claude (web, desktop and mobile), add a custom connector with the URL under Customize, Connectors. In ChatGPT, add it as a connector in developer mode. Both then sign in.
| Tool | What it does | Scope |
|---|---|---|
get_shop_info, get_organization, list_organizations | The brand, its currency and time zone, what it has switched on, and the key's scopes | tests:read |
list_integrations | Which integrations are on | tests:read |
search_experiments, search_experiences | Tests or personalizations by status, type or name, in the order you choose | tests:read |
list_experiments, get_experiences_list_full | The same with full definitions | tests:read |
get_experience, get_experience_metrics_config | One test's definition and preview links, or how it is judged | tests:read |
analyze_experience | Results per group with lift, p-values, the quality gate and the verdict; or by audience | tests:read |
get_variation_overview, get_variation_audience, get_variation_products | Each group's numbers, inside one audience's segments, or by product | tests:read |
get_experience_timeseries, get_variation_timeseries | Results by day, week or hour, or one metric over time | tests:read |
get_experience_export | The results, orders, line-items or order reconciliation export as CSV text | tests:read |
list_custom_events, list_offers, get_offer_products | Custom events, offer tests and the products their offers cover | tests:read |
list_checkout_blocks | A checkout test's blocks by group | tests:read |
search_products, list_products, get_product, get_products, list_collections, get_collection_products | The store's catalog, live from Shopify | tests:read |
get_sitewide_snapshot, get_sitewide_timeseries, get_sales_overview | Store-wide performance, order, conversion and audience numbers, over time, and sales from every order | analytics:read |
get_sitewide_order_distribution, get_sitewide_conversion_funnel | The order-value distribution and the customer journey | analytics:read |
get_event_snapshot, get_event_timeseries | Holiday season against last year | analytics:read |
create_experience | A new draft test or personalization | tests:write |
create_custom_event | A new custom event, or a change to one | tests:write |
update_experience | Change a test (tests:start too for a live test shoppers see) | tests:write |
add_checkout_block, update_checkout_block, delete_checkout_block | Change one group's checkout blocks | tests:write |
experience_action | Start, pause, resume or end a test (tests:start for one shoppers see) | tests:write |
A key with tests:read alone gives a read-only assistant. Money in the answers is in the store currency's minor units (cents for USD), rates are fractions and lifts are percent.