Skip to content
GROWTH DEPT.CRO

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=300

The 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

ParameterWhat it does
?cro_qa=1QA 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=1Logs 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:

MemberWhat it is
CRO.vidThe visitor id
CRO.assignmentsThe 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.qaWhether this is a QA visitor
CRO.originalReferrerThe 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:

MemberWhat 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.campaignsThe 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:

  1. Add a Data Layer Variable named exp_variant_string (and cro_test and cro_variant if you want the ids).
  2. Add a Custom Event trigger for the event name cro_assignment.
  3. Add a GA4 Event tag named experience_impression with the parameter exp_variant_string set to the variable, fired by that trigger.
  4. Publish the container, and register exp_variant_string as 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" } })
});
AppWhat the group sets
TruemedCart attribute _tm_hide_widgets: "true" to hide its widgets
Checkout ReviewsCart attributes _igShowReviewsBlock, _igShowFaqBlock, _igShowTrustSignalBlock, _igShowImageBannerBlock: "true" or "false", with the app's testing option on for each block
ShoptinCart attribute _ig_shoptin_test: "true" or "false", with the app's A/B test mode on
Navidiumwindow._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
SavedBywindow.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 limit

The 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.

ScopeAllows
tests:readFind and read tests, their results, verdicts, segments, time series and exports
tests:writeCreating and editing drafts, pausing tests, and starting, resuming or ending a QA-only test
tests:startStarting, resuming, ending and editing a test shoppers see
messages:writeMessage-test drafts, and the sender's assign and events calls
analytics:readThe sitewide dashboards under /api/v1/analytics
export:readThe 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/mcp
claude 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.

ToolWhat it doesScope
get_shop_info, get_organization, list_organizationsThe brand, its currency and time zone, what it has switched on, and the key's scopestests:read
list_integrationsWhich integrations are ontests:read
search_experiments, search_experiencesTests or personalizations by status, type or name, in the order you choosetests:read
list_experiments, get_experiences_list_fullThe same with full definitionstests:read
get_experience, get_experience_metrics_configOne test's definition and preview links, or how it is judgedtests:read
analyze_experienceResults per group with lift, p-values, the quality gate and the verdict; or by audiencetests:read
get_variation_overview, get_variation_audience, get_variation_productsEach group's numbers, inside one audience's segments, or by producttests:read
get_experience_timeseries, get_variation_timeseriesResults by day, week or hour, or one metric over timetests:read
get_experience_exportThe results, orders, line-items or order reconciliation export as CSV texttests:read
list_custom_events, list_offers, get_offer_productsCustom events, offer tests and the products their offers covertests:read
list_checkout_blocksA checkout test's blocks by grouptests:read
search_products, list_products, get_product, get_products, list_collections, get_collection_productsThe store's catalog, live from Shopifytests:read
get_sitewide_snapshot, get_sitewide_timeseries, get_sales_overviewStore-wide performance, order, conversion and audience numbers, over time, and sales from every orderanalytics:read
get_sitewide_order_distribution, get_sitewide_conversion_funnelThe order-value distribution and the customer journeyanalytics:read
get_event_snapshot, get_event_timeseriesHoliday season against last yearanalytics:read
create_experienceA new draft test or personalizationtests:write
create_custom_eventA new custom event, or a change to onetests:write
update_experienceChange a test (tests:start too for a live test shoppers see)tests:write
add_checkout_block, update_checkout_block, delete_checkout_blockChange one group's checkout blockstests:write
experience_actionStart, 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.

Want help with the install?

Growth Dept's team installs the runtime with you on a call, on a theme, a headless store or a custom site.