# Apex by Drip documentation Official documentation for installing, configuring, operating, and integrating Apex by Drip. ## Getting started - [What is Apex?](https://docs.drip-apex.com/docs/getting-started/what-is-apex): A plain-language overview of Apex, the core A/B testing concepts, and how the storefront SDK, Worker, and dashboard fit together. - [Glossary](https://docs.drip-apex.com/docs/getting-started/glossary): Plain-language definitions for common Apex terms such as experiments, variations, goals, assignments, guardrails, SDK, Worker, and consent mode. - [Launchpad](https://docs.drip-apex.com/docs/getting-started/launchpad): The daily command center for a shop. It shows readiness, active tests, draft blockers, recent learning, and the next best action. - [Access, authentication, and workspace selection](https://docs.drip-apex.com/docs/getting-started/access-authentication-workspace-selection): Controls who can enter Apex, which workspace they operate in, and which shop becomes active for every dashboard and API action. - [How Apex detects your store](https://docs.drip-apex.com/docs/getting-started/store-scan): What the onboarding store scan reads, how platform detection works, and what the detected storefront tools mean. - [Install Apex on Shopify](https://docs.drip-apex.com/docs/getting-started/shopify): Connect the Apex Shopify app, enable the storefront app embed, and understand what Shopify data Apex receives. - [Install Apex on WooCommerce](https://docs.drip-apex.com/docs/getting-started/woocommerce): Connect the Apex WooCommerce plugin with an install token, upload the plugin zip, and understand what WooCommerce data Apex receives. - [Verify your Shopify tracking](https://docs.drip-apex.com/docs/getting-started/shopify-verify): Confirm the Shopify app connection, storefront app embed, and first Apex telemetry signal during onboarding. - [Install Apex on Shopware](https://docs.drip-apex.com/docs/getting-started/shopware): Install the private Apex Shopware app package, confirm registration, and understand what Shopware data Apex receives. - [Install Apex on a custom storefront](https://docs.drip-apex.com/docs/getting-started/custom-js): Add the Apex script snippet to a custom, Webflow, headless, or other non-native storefront. - [Coexistence with other testing tools](https://docs.drip-apex.com/docs/getting-started/coexistence-with-other-testing-tools): Move page areas to Apex safely while existing experimentation and analytics tools remain installed. - [Install Apex on a headless React storefront](https://docs.drip-apex.com/docs/getting-started/headless-react): Add Apex to a Next.js or Hydrogen storefront, render experiment branches with React hooks, and preserve Shopify cart attribution. - [Install Apex on a headless Nuxt storefront](https://docs.drip-apex.com/docs/getting-started/headless-nuxt): Add Apex to a Nuxt 3 Shopify storefront, configure OneTrust consent, handle SPA navigation, and preserve cart attribution. ## Experiments - [Experiment creation and lifecycle](https://docs.drip-apex.com/docs/experiments/overview): The core A/B testing workflow: create a draft, configure targeting and metrics, build variants, QA, launch, monitor, pause, complete, archive, or restore. - [Ideas and backlog](https://docs.drip-apex.com/docs/experiments/ideas-backlog): The planning layer for experiment concepts before they become tests. It stores hypotheses, evidence, source context, comments, priority, and handoff notes. - [Variant builder, visual mutations, and code mode](https://docs.drip-apex.com/docs/experiments/builder-visual-code): The builder authors the actual storefront change for a variation. It supports visual mutations, custom CSS/JS, file-based code workspaces, previews, revisions, and generated changes. - [Migrating a test from Kameleoon](https://docs.drip-apex.com/docs/experiments/migrating-from-kameleoon): Move a Kameleoon JavaScript and CSS test into Apex, preview it safely, and finish its targeting, goals, and QA. - [Template tests on Shopify](https://docs.drip-apex.com/docs/experiments/template-tests-shopify): Test Shopify alternate templates with a same-page redirect and the view query parameter. - [Apex Operator and tool-led building](https://docs.drip-apex.com/docs/experiments/apex-operator): Apex Operator is the model-led experiment builder. It gives the model compact tools for inspecting pages, network traffic, state, selectors, artifacts, QA evidence, and persistence. - [Edit a draft on the live store](https://docs.drip-apex.com/docs/experiments/on-site-editor): Use the on-site editor to inspect, change, and save a draft against the merchant's real page while keeping Quick preview as the zero-install fallback. - [QA workspace, previews, screenshots, and launch gating](https://docs.drip-apex.com/docs/experiments/qa-and-previews): QA proves that a variant renders correctly, tracking fires, screenshots are captured, and launch blockers are resolved before visitors see a test. - [Targeting, pages, audiences, and segments](https://docs.drip-apex.com/docs/experiments/targeting-audiences): Defines who and what page qualifies for experiments, feature flags, and personalization rules. - [Metrics, goals, and conversion tracking](https://docs.drip-apex.com/docs/experiments/metrics-goals): Creates reusable success events for experiments: click, pageview, custom, revenue, and Shopify-connected conversion goals. - [Metric definitions](https://docs.drip-apex.com/docs/experiments/metric-definitions): Defines the populations, counting rules, and quality checks behind experiment results, so raw events can be reconciled with reported numbers. - [Results, imported outcomes, and readback](https://docs.drip-apex.com/docs/experiments/results): Turns live and completed experiment data into decisions, learnings, and future backlog evidence. - [Guardrails, planned tests, and auto-pause](https://docs.drip-apex.com/docs/experiments/guardrails-auto-pause): Defines the statistical and operational rules that determine when tests can launch, pause, and be interpreted. - [Checkout-block experiments](https://docs.drip-apex.com/docs/experiments/checkout-blocks): Launch checkout message tests on Shopify Plus after setup and a QA checkout. - [Feature flags](https://docs.drip-apex.com/docs/experiments/feature-flags): Runtime toggles for off/on variants, typed payloads, rollout percentages, targeting, and source-experiment promotion. - [Personalization](https://docs.drip-apex.com/docs/experiments/personalization): Signal-driven storefront experiences with ordered rules, payloads, targeting, rollout controls, and decision debugging. ## Analytics - [Analytics suite](https://docs.drip-apex.com/docs/analytics/overview): Store-wide and experiment-aware reporting across visitors, conversions, revenue, ecommerce, traffic, funnels, journey flow, realtime events, and measurement quality. - [Dashboard and realtime analytics](https://docs.drip-apex.com/docs/analytics/dashboard-realtime): Existing analytics surfaces for overview reporting and realtime install debugging. - [Heatmaps, behavior screenshots, and visual evidence](https://docs.drip-apex.com/docs/analytics/heatmaps-evidence): Adds visual and behavioral proof to analytics so teams can understand not only whether a test moved a metric but how visitors interacted. - [Funnels and ecommerce analytics](https://docs.drip-apex.com/docs/analytics/funnels-ecommerce): Existing funnel and ecommerce analytics surfaces for page detection, path analysis, revenue, and reconciliation. - [Profit analysis and Shopify reconciliation](https://docs.drip-apex.com/docs/analytics/profit-reconciliation): Adds product cost, inventory, order, and contribution-margin context so experiment results can be read against profit, not only conversion rate. - [Store profile, audits, signals, and learning runs](https://docs.drip-apex.com/docs/analytics/store-profile-signals): The persistent memory of a shop: brand facts, technical findings, product-data depth, storefront evidence, customer signals, audit findings, and operator learnings. ## Statistics - [Statistics](https://docs.drip-apex.com/docs/statistics): How Apex measures experiments, protects decisions, and verifies its statistical guarantees with deterministic simulation. - [How Apex statistics work](https://docs.drip-apex.com/docs/statistics/methodology-and-certification): The business logic, statistical safeguards, and three-million-experiment certification behind Apex decisions. - [Audit of Bayesian stopping rules](https://docs.drip-apex.com/docs/statistics/bayesian-stopping-audit): Deterministic A/A simulations of common chance-to-beat and expected-loss decision rules under daily peeking. - [Audit of bandit campaign-mode allocation](https://docs.drip-apex.com/docs/statistics/bandit-campaign-audit): 1.15 million simulated merchant campaigns measuring what adaptive traffic allocation earns, and what it does to the reported result. ## Landing pages - [Landing pages and landing-page QA](https://docs.drip-apex.com/docs/landing-pages/overview): Keeps the versions, previews, QA evidence, publish state, and experiments of landing pages that are built outside Apex and deployed with the CLI. - [Build landing pages with your own tools](https://docs.drip-apex.com/docs/landing-pages/build-and-deploy): Build a landing page in a folder with Claude Code, Codex or any static-site build, then apply, preview, deploy, roll back and test it with Apex. ## Settings and account - [Store connection, Shopify install, and Web Pixel](https://docs.drip-apex.com/docs/settings-account/installation): Connects Apex to a storefront so experiments, analytics, Shopify events, product data, and revenue analysis can work. - [Runtime controls, SDK, anti-flicker, and consent](https://docs.drip-apex.com/docs/settings-account/runtime-sdk-controls): The browser runtime fetches experiment config, assigns visitors, applies variants, tracks goals, respects runtime settings, and batches events. - [Edge routing and custom domains](https://docs.drip-apex.com/docs/settings-account/edge-routing-domains): Routes customer domains through the Apex Worker path for edge-managed experiences and SSL-for-SaaS style storefront handling. - [API keys and public API v1](https://docs.drip-apex.com/docs/settings-account/api-keys): Lets external systems create and manage experiments, variations, goals, screenshots, webhooks, privacy requests, and harness sessions. - [Team, clients, organization, account, and admin settings](https://docs.drip-apex.com/docs/settings-account/team-organization): Manages who can access work, which clients belong to an agency, account-level preferences, integrations, and organization-level configuration. - [Privacy, export, deletion, and auditability](https://docs.drip-apex.com/docs/settings-account/privacy-data): Supports compliance workflows for exporting or deleting visitor/user data and auditing sensitive operational actions. - [Assets and references](https://docs.drip-apex.com/docs/settings-account/assets-references): Stores images, PDFs, creative references, and prompt material used for experiments, landing pages, and Operator instructions. - [Allowlist Apex in your firewall](https://docs.drip-apex.com/docs/settings-account/firewall-allowlisting): Let Apex previews pass your storefront WAF without weakening other protections. ## Data and privacy - [GDPR export and deletion](https://docs.drip-apex.com/docs/data-privacy/gdpr-export-deletion): Use Apex privacy endpoints to export or delete visitor data for a shop-scoped privacy request. - [Consent integration for privacy teams](https://docs.drip-apex.com/docs/data-privacy/consent-integration): Plain-language guidance for how Apex renders tests before consent while blocking persistent identity and tracking until permission is available. - [Consent and regional compliance](https://docs.drip-apex.com/docs/data-privacy/consent-regional-compliance): Configure Apex so storefront testing and tracking respect consent requirements, Do Not Track, and regional privacy expectations. - [Developer access and data](https://docs.drip-apex.com/docs/data-privacy/developer-access-data): A public overview of the Apex data stores, developer access paths, and safety rules for technical teams integrating with Apex. ## Developers - [Developer overview](https://docs.drip-apex.com/docs/developers/overview): Developer-oriented Apex documentation for SDK runtime behavior, API keys, public API v1, webhooks, privacy exports, and generated references. - [Public API surface map](https://docs.drip-apex.com/docs/developers/api/overview): Routes in the Apex public API v1 contract, grouped by host and authentication. - [Webhook payload reference](https://docs.drip-apex.com/docs/developers/webhooks): Configure Apex webhooks, verify signatures, and handle the event payloads Apex delivers today. - [Public event schema](https://docs.drip-apex.com/docs/developers/event-schema): The Apex analytics event wire format accepted by the Worker and written to Tinybird. - [Worker public endpoints](https://docs.drip-apex.com/docs/developers/worker-endpoints): Public Apex Worker routes for event ingest, snippets, runtime config, landing pages, previews, and health checks. - [Shopify Pixel integration](https://docs.drip-apex.com/docs/developers/shopify-pixel): How the Apex Shopify Web Pixel maps Shopify customer events, dedupes event IDs, and buffers attribution. - [External checkout pixels](https://docs.drip-apex.com/docs/developers/external-checkout-pixels): Attribute purchases from third-party checkout or booking platforms with Apex handover and a confirmation-page pixel. - [SDK examples](https://docs.drip-apex.com/docs/developers/sdk): A thin pointer to the full Apex SDK reference for install, runtime API, React bindings, and InitConfig. - [Price basis for value-bearing treatments](https://docs.drip-apex.com/docs/developers/price-basis-value-treatments): Choose and document the price source for treatments built against staging and production storefronts. ## API reference - [Generated OpenAPI reference](https://docs.drip-apex.com/docs/api/reference): Generated endpoint reference built from the local Apex OpenAPI spec. ## CLI reference - [Install and authenticate the Apex CLI](https://docs.drip-apex.com/docs/cli/install-auth): Install apex, sign in with the default browser OAuth flow, and use API-key fallbacks for headless environments. - [CLI commands, JSON, and exit codes](https://docs.drip-apex.com/docs/cli/commands-output): Every registered Apex CLI command, the stable JSON envelope, exit-code meanings, and headless agent usage rules. - [Experiment lifecycle commands](https://docs.drip-apex.com/docs/cli/experiments-lifecycle): Create, list, inspect, read results, pause, complete, and delete Apex experiments from the CLI. - [Experiment filesystem and QA commands](https://docs.drip-apex.com/docs/cli/experiments-filesystem-qa): Pull experiment files, plan and apply safe changes, and record QA evidence from the Apex CLI. - [Workspace, Operator, and roadmap commands](https://docs.drip-apex.com/docs/cli/workspace-operator-roadmap): Check out draft variation workspaces, run live previews, publish workspace files, and pull approved roadmap handoffs. - [Push and GitOps with `.drip.config.ts`](https://docs.drip-apex.com/docs/cli/push-gitops): Use apex push to sync local experiment definitions to Apex with slug-based idempotent upserts. - [Tracking, goals, targeting, preview, and harness](https://docs.drip-apex.com/docs/cli/tracking-goals-targeting-preview-harness): Generate tracking manifests, manage goals, test targeting, open QA previews, and publish AB Test Harness artifacts. - [Agent tools: SOS, brand, schema, and skills](https://docs.drip-apex.com/docs/cli/agent-tools-sos-brand-schema-skills): Use Apex CLI agent-oriented commands for brand context, SOS handoffs, command schema introspection, and local skill installation. - [Commerce definition](https://docs.drip-apex.com/docs/cli/commerce-definition): Read and save price, shipping, offer and post-purchase setup as commerce.json. - [Commerce prices](https://docs.drip-apex.com/docs/cli/commerce-prices): Export selection prices and import bulk overrides with revision checks. ## Reference - [Complete feature catalog](https://docs.drip-apex.com/docs/reference/feature-catalog): Route-level map of Apex app surfaces from the original docs catalog. - [Data model](https://docs.drip-apex.com/docs/reference/data-model): Apex relational model families and enum groups from the original docs data model map. - [Help, docs, brand surfaces, and generated references](https://docs.drip-apex.com/docs/reference/help-docs-brand-generated-references): The support layer for understanding Apex, finding help, sharing brand context, and inspecting machine-readable API documentation. ## MCP server - [Install and configure the Apex MCP server](https://docs.drip-apex.com/docs/mcp/install-configure): Connect ChatGPT, Codex, Claude Desktop, Claude Code, or Cursor to Apex experiments with the Drip MCP server. - [Apex MCP tool reference](https://docs.drip-apex.com/docs/mcp/tool-reference): Reference for the tools exposed by the Apex MCP server. ## SDK reference - [Install and quickstart the Apex SDK](https://docs.drip-apex.com/docs/sdk/install-quickstart): Choose the hosted script snippet or npm package, initialize Apex, and send your first SDK events. - [`init()` and InitConfig reference](https://docs.drip-apex.com/docs/sdk/init-config): Reference every supported Apex SDK InitConfig field with its type, default, and intended use. - [SSR framework coexistence](https://docs.drip-apex.com/docs/sdk/framework-coexistence): Integrate Apex with hydrating Nuxt and Next.js storefronts using mutation re-assertion, hydration timing, and explicit lifecycle hooks. - [Consent modes](https://docs.drip-apex.com/docs/sdk/consent-modes): Configure Apex SDK consent gating with custom consent, OneTrust, Usercentrics, Cookiebot, CCM19, Shopify Customer Privacy, Pandectes, or CookieScript. - [SPA and router modes](https://docs.drip-apex.com/docs/sdk/spa-router): Configure Apex SDK route observation, DOM re-evaluation, and manual activation for dynamic storefronts. - [Anti-flicker modes](https://docs.drip-apex.com/docs/sdk/anti-flicker): Use scoped, full-page, custom, or disabled anti-flicker behavior in the Apex SDK. - [Runtime API](https://docs.drip-apex.com/docs/sdk/runtime-api): Reference the Apex SDK runtime functions for initialization, tracking, consent, flags, diagnostics, and assignment state. - [Code-owned definitions and `.drip.config.ts`](https://docs.drip-apex.com/docs/sdk/config-definitions): Define experiments and feature flags in TypeScript, then pair them with Apex CLI GitOps push. - [React bindings](https://docs.drip-apex.com/docs/sdk/react-bindings): Use DripProvider and all Apex SDK React hooks for assignments, variations, flags, tracking, revenue, cart metafields, and loading state. - [Vue and Nuxt bindings](https://docs.drip-apex.com/docs/sdk/vue-bindings): Reactive Apex assignments, feature flags, tracking, and server-rendered Nuxt variants. - [Feature flags in code](https://docs.drip-apex.com/docs/sdk/feature-flags): Define, evaluate, and read typed Apex feature flag values from the SDK. - [Migration notes](https://docs.drip-apex.com/docs/sdk/migration-notes): Move removed Apex SDK init aliases to the current InitConfig fields. ## App SDK - [App SDK overview and install](https://docs.drip-apex.com/docs/app-sdk/overview): Run Apex flags and tests in hybrid apps built with Ionic, Angular, Capacitor or Cordova. Install the npm package and configure it with a publishable key. - [Angular setup](https://docs.drip-apex.com/docs/app-sdk/angular-setup): Configure the App SDK in an Angular or Ionic app with an app initializer, a root service, optional NGXS state, an RxJS helper and Capacitor Preferences storage. - [Flags and typed values](https://docs.drip-apex.com/docs/app-sdk/flags-typed-values): Read boolean, string, number and JSON object flag values in a hybrid app, validate typed objects, and control when Apex records an exposure. - [Goals and purchases](https://docs.drip-apex.com/docs/app-sdk/goals-purchases): Track goal events and order revenue from a hybrid app. Send each purchase once per order with a stable order ID, and understand how Apex attributes revenue to tests. - [Users and identity](https://docs.drip-apex.com/docs/app-sdk/users-identity): Identify signed-in users in a hybrid app, wait for their assignments before reading flags, and return to the anonymous device identity on logout. - [Test your build](https://docs.drip-apex.com/docs/app-sdk/testing-your-build): Mark test-build traffic as QA, pin a test phone or test account to a variation with QA devices, and check a device's events with the device lookup. - [App lifecycle and offline behaviour](https://docs.drip-apex.com/docs/app-sdk/lifecycle-offline): How the App SDK queues events offline, sends them when the app goes to the background, refreshes assignments in the foreground, and handles sessions. - [Troubleshooting](https://docs.drip-apex.com/docs/app-sdk/troubleshooting): Turn on App SDK diagnostics with a logger, read each log line, and fix common setup problems such as a blocked events host or an invalid publishable key. - [Supported versions](https://docs.drip-apex.com/docs/app-sdk/supported-versions): Angular, Ionic, Capacitor, Cordova, TypeScript and Node versions tested with the App SDK, plus build requirements for current iOS and Xcode. ## Dashboard and realtime analytics ## Overview Core dashboard analytics across visitors, conversions, revenue, ecommerce, geo, sessions, entry and exit, and measurement quality. ## Realtime Live event console for verifying install health, pageview flow, custom events, and QA traffic. ## Funnels and ecommerce analytics ## Funnels Build and run funnels from detected pages, then analyze breakdowns. ## Ecommerce Core dashboard analytics across visitors, conversions, revenue, ecommerce, geo, sessions, entry and exit, and measurement quality. ## Heatmaps, behavior screenshots, and visual evidence > **Simple terms** > > Visual evidence helps explain what visitors saw or did, not just whether the metric moved. Adds visual and behavioral proof to analytics so teams can understand not only whether a test moved a metric but how visitors interacted. ## Routes - `/experiments/[id]/results?tab=behavior` - `/analytics` - `/api/analytics/experiment/[id]/heatmap` - `/api/analytics/experiment/[id]/heatmap/screenshots` - `/api/analytics/experiment/[id]/behavior` - `/api/analytics/experiment/[id]/behavior-summary` ## Implementation behavior ## Recent release notes - The latest release normalises tap coordinates into the capture's frame and splits overlay and pinned (fixed) taps into separate fields; heatmap rollups now report category and element-level aggregations driven by the page model. {/* source: changelog + spec-6384 */} - **Heatmaps:** Show both versions at phone size on the real page picture, scroll them together, and list where sessions tapped by area. Harden capture recovery, partial session tables, and narrow-screen export availability. {/* source: changelog */} - Cohort session counts used for arm denominators are now read from a single arm-sessions reader so every arm uses a consistent session denominator; when that read fails the arm is marked unavailable instead of returning zero. {/* source: changelog + spec-6384 */} - Field truncation and cells totals are tracked per row type (page, pinned, overlay) rather than being overwritten across types. {/* source: changelog + spec-6384 */} - Behavior screenshots and heatmap endpoints store or query visual evidence tied to shop, page, experiment, variation, and time window. - The results Behavior tab combines heatmap data, behavior data, and conversion metrics in one readback. - Note: retired "behavior explorer" landing links now redirect straight to an experiment's Results page and open the Behavior tab (previously they served an intermediate landing page). {/* source: release notes */} - Operator QA evidence can also include screenshots and observations gathered during preview. - **Heatmaps:** Raised the heat ramp transparency floor to keep page backgrounds clear between tap clusters. {/* source: changelog + spec-6475 */} ## How to use it - Use visual evidence when conversion movement needs explanation. - Compare control and treatment screenshots for obvious rendering differences. - Use heatmap or behavior data alongside visitor trails rather than as the only decision source. - Capture fresh evidence after variant changes. ## Session replays Replay-enabled experiments add a **Session replays** section to the results Behavior tab. The list excludes QA sessions by default and supports variation, device, conversion, and friction filters. Open a row to watch the masked recording without leaving the result. {/* Note: QA sessions are excluded by default. A recent release changed the QA session marker so that a QA session with no pinned variation stores a small cookie marker; this lets the on-store QA panel survive plain navigation after "Reset to real assignment". See the release spec and worker/SDK parity tests for implementation details. */} Enable replay capture before launch. New recordings use the experiment's replay sample rate, so an enabled test can remain empty until a sampled visitor arrives. Input masking is applied before storage and cannot be removed in the player. Production recordings expire after 30 days. Use rage clicks, dead clicks, error clicks, quick backs, form abandonment, and scroll depth to choose useful sessions. Replays explain a result. They do not select the winning variation. ## When not to use it - Do not use heatmaps as the sole decision source for an experiment winner. - Do not compare screenshots captured before and after unrelated page/theme changes as if they are equivalent. - Do not overread sparse heatmaps with low traffic. ## Implementation source - Experiment detail and analytics pages consume visual evidence APIs. - Handlers include `/api/analytics/experiment/[id]/heatmap`, `/api/analytics/experiment/[id]/heatmap/screenshots`, `/api/analytics/experiment/[id]/behavior`, and `/api/analytics/experiment/[id]/behavior-summary`. - Evidence is keyed by shop, page, experiment, variation, device, and time window where supported. ## Data and API - APIs under `/api/analytics/experiment/[id]/heatmap`, `/api/analytics/experiment/[id]/heatmap/screenshots`, `/api/analytics/experiment/[id]/behavior`, `/api/analytics/experiment/[id]/behavior-summary`, screenshot jobs, and operator evidence endpoints. - Tinybird event streams plus persisted screenshot/evidence records. - Experiment and variation IDs connect visual evidence to results. ## Failure modes - Missing screenshot: target page inaccessible, job failed, or evidence was captured before the variation rendered. - Heatmap sparse: not enough traffic or events in the selected time window. - Replay list empty: replay capture was off, no sampled visitor has arrived, or the retained recording has expired. - Visual proof can mislead if traffic/device mix differs between variants. ## Analytics suite > **Simple terms** > > Analytics shows what visitors and tests are doing across the store: realtime events, traffic, funnels, flow, visitors, revenue, and measurement quality. Store-wide and experiment-aware reporting across visitors, conversions, revenue, ecommerce, traffic, funnels, journey flow, realtime events, and measurement quality. ## Routes - `/analytics` - `/analytics/audience` - `/analytics/traffic` - `/analytics/visitors` - `/analytics/realtime` - `/analytics/funnels` - `/analytics/funnels/new` - `/analytics/flow` ## Notes - The app previously had a separate "behavior explorer" landing URL; retired explorer links now answer a redirect and take users directly to an experiment's Results page with the Behavior tab selected. {/* source: release notes */} ## Implementation behavior - The SDK and Worker send normalized events into the Tinybird `events` datasource. - Dashboard APIs proxy Tinybird pipes and combine them with Prisma context for shop, experiment, and goal names. - Realtime views query recent events to verify install, pageview, custom event, and QA traffic. - Funnels use detected pages or saved funnel steps to compute conversion and drop-off. - Visitor explorer reads sessions and event trails for individual debugging. ## How to use it - Use Overview for business-level health and experiment summaries. - Use Realtime immediately after installation or QA. - Use Traffic and Audience to understand quality and targeting context. - Use Funnels and Flow when the question is journey movement rather than one metric. - Use Visitors to debug one visitor, session, or identifier. ## When not to use it - Do not use store-wide analytics to make a variant decision without opening the experiment-level report. - Do not use Realtime as long-term reporting; it is mainly for debugging fresh events. - Do not trust funnel results until page detection and URL normalization are checked. ## Implementation source - Pages live under `src/app/(dashboard)/analytics` and its child routes. - Handlers live under `/api/analytics/*`, including `/api/analytics/realtime` and `/api/analytics/semantic/*`; detected pages are read from the Tinybird `detected_pages` pipe. - Tinybird pipes and datasource definitions live under `packages/tinybird`. ## Data and API - Tinybird datasource: `events` with shop, visitor, session, user, event type, experiment, variation, URL, properties, device, country, timestamp. - Pipes include `dashboard_stats`, `experiment_visitors`, `experiment_conversions`, `experiment_timeseries`, and `detected_pages`. - APIs under `/api/analytics/*`, including `/api/analytics/realtime` and `/api/analytics/semantic/*`. ## Failure modes - Realtime empty: SDK not installed, wrong shop ID, Worker error, or blocked network call. - Funnels incomplete: detected pages have not accumulated enough events or URL normalization differs. - Experiment report blank: experiment IDs or variation IDs are missing from tracked events. ## Profit analysis and Shopify reconciliation > **Simple terms** > > Profit analysis adds cost and order context so a winning test can be judged by margin, not only conversion rate. Adds product cost, inventory, order, and contribution-margin context so experiment results can be read against profit, not only conversion rate. ## Routes - `/settings/integrations` - `/api/analytics/shopify-reconciliation` - `/api/analytics/commerce` ## Implementation behavior - Shopify sync jobs fetch product, inventory, order, and cost data when required scopes are connected. - Reconciliation compares Shopify events and Apex events to detect missing or mismatched revenue records. - Variant cost and margin data can be joined to experiment outcomes to judge business impact. - Analytics endpoints expose reconciliation state, commerce metrics, sync health, and data quality state. ## How to use it - Reconnect Shopify with the needed scopes before relying on margin reporting. - Run or monitor sync jobs before reading profit-sensitive experiments. - Check reconciliation when revenue differs between Shopify and Apex. - Use contribution margin as a secondary decision metric when discounts, bundles, or upsells are involved. ## When not to use it - Do not use margin reporting before Shopify scopes and cost data are confirmed. - Do not compare profit across variants if revenue attribution is failing. - Do not treat delayed Shopify sync data as real-time truth. ## Implementation source - Integration UI lives under `src/app/(dashboard)/settings/integrations`. - Handlers include `/api/analytics/shopify-reconciliation`, `/api/analytics/commerce`, commerce sync, and the Shopify webhook reconciliation cron. - Prisma product/order/cost records are joined with Tinybird revenue and experiment context for reporting. ## Data and API - Shopify product/order/inventory/cost records, variant costs, reconciliation records, and experiment revenue events. - APIs under `/api/analytics/shopify-reconciliation`, `/api/analytics/commerce`, `/api/analytics/commerce/sync`, `/api/cron/shopify-webhook-reconcile`, and integration settings. - Tinybird revenue events plus Prisma product/order cost tables. ## Failure modes - Margin unavailable: missing Shopify scopes or no cost data. - Revenue mismatch: Web Pixel settings, order sync delay, currency, or duplicate events. - Sync stale: rerun sync and inspect job logs before trusting profit readback. ## Store profile, audits, signals, and learning runs > **Simple terms** > > Store profile is Apex's memory for the brand, technical setup, evidence, and learnings for a shop. The persistent memory of a shop: brand facts, technical findings, product-data depth, storefront evidence, customer signals, audit findings, and operator learnings. ## Routes - `/store-profile` - `/signals` ## Implementation behavior - Audit runs collect evidence and findings about storefront quality, tracking setup, product data, and technical readiness. - Brand and technical profiles store normalized facts that Operator can reuse across ideas and builds. - Signals capture behavior, reviewed findings, semantic sequences, and store facts that help audiences and personalization. - Learning runs summarize completed work into reusable context for future experiment planning. ## How to use it - Open Store Profile before asking Operator to build major variants. - Review and approve findings so only trusted evidence influences later work. - Use Signals for reusable behavioral or store facts. - Refresh audits after major store, theme, product catalog, or tracking changes. ## When not to use it - Do not use stale profile facts after a major theme, app, product, or tracking change. - Do not let unreviewed audit findings drive important decisions. - Do not use signal-based personalization until the underlying signals are trustworthy. ## Implementation source - Pages live under `src/app/(dashboard)/store-profile` and `src/app/(dashboard)/signals`. - Handlers live under `/api/store-profile`, `/api/shop-audits`, `/api/signals`, and semantic analytics endpoints. - Profile, evidence, finding, fact, and learning-run models are defined in `prisma/schema.prisma`. ## Data and API - Models include `ShopAuditRun`, `ShopAuditEvidence`, `ShopAuditFinding`, `ShopBrandProfile`, `ShopTechnicalProfile`, `ShopLearningRun`, `ShopStoreFact`, and signal records. - APIs under `/api/store-profile/*`, `/api/signals`, `/api/analytics/semantic/*`, and audit review endpoints. - Evidence can include screenshots, detected pages, product data, and runtime checks. ## Failure modes - Operator lacks context: profile is empty or findings are unreviewed. - Old technical advice: rerun audit after a theme, app, or tracking change. - Signal-based targeting is weak until enough events and facts exist. ## Generated OpenAPI reference ## Angular setup Configure the SDK once in an Angular app initializer. Then inject one root service wherever the app reads flags or tracks events. ## Root service ```ts // apex.service.ts import { Injectable, NgZone, inject } from '@angular/core'; import { Router } from '@angular/router'; import { Apex } from '@drip-apex/app-sdk'; @Injectable({ providedIn: 'root' }) export class ApexService { private readonly zone = inject(NgZone); private readonly router = inject(Router); init(): Promise { return this.zone.runOutsideAngular(() => Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_ID', applicationId: 'com.example.shop', appVersion: '1.0.0', // The current route, without query or fragment. screen: () => this.router.url.split(/[?#]/)[0], }), ); } welcome(): boolean { return Apex.flag('welcome', false); } subscribe(update: () => void) { return Apex.onChange((change) => { if (change.type === 'changed') this.zone.run(update); }); } } ``` Start `configure()` outside the Angular zone. SDK timers and network responses then do not trigger change detection. Re-enter the zone with `zone.run()` only to update the UI. ## Run it at startup Add the initializer to `ApplicationConfig.providers` (Angular 19 and later): ```ts // app.config.ts import { ApplicationConfig, inject, provideAppInitializer } from '@angular/core'; import { ApexService } from './apex.service'; export const appConfig: ApplicationConfig = { providers: [ provideAppInitializer(() => inject(ApexService).init()), ], }; ``` Angular versions without `provideAppInitializer` use `APP_INITIALIZER`. Capture the service synchronously in the factory: ```ts import { APP_INITIALIZER, inject } from '@angular/core'; import { ApexService } from './apex.service'; export const apexInitializer = { provide: APP_INITIALIZER, multi: true, useFactory: () => { const service = inject(ApexService); return () => service.init(); }, }; ``` The initializer waits for `configure()`. The first render then sees cached or fresh flag values. The wait is at most `initTimeoutMs` (default 3,000 ms). ## Use it in a component ```ts import { Component, OnDestroy, inject } from '@angular/core'; import { ApexService } from './apex.service'; @Component({ selector: 'app-home', template: `

{{ enabled ? 'Welcome back' : 'Welcome' }}

`, }) export class HomeComponent implements OnDestroy { private readonly experiments = inject(ApexService); enabled = this.experiments.welcome(); private readonly subscription = this.experiments.subscribe(() => { this.enabled = this.experiments.welcome(); }); ngOnDestroy(): void { this.subscription.cancel(); } } ``` `onChange` callbacks run asynchronously. Update Angular state through change detection or signals, as you do for other async data. ## Optional: NGXS state Apps that use NGXS can mirror flag values into the store. Selectors and components then read flags like other state. The mirror reads with manual exposure, so filling the store never counts an exposure. Call `logExposure()` where the app shows the variant. ```ts // apex-flags.state.ts import { Injectable } from '@angular/core'; import { Action, State, StateContext, createSelector } from '@ngxs/store'; export interface ApexFlagsStateModel { /** Typed flag values by key (the default when unassigned or invalid). */ values: Record; /** Set when the SDK stopped networking, for example after an invalid key. */ stopped: string | null; updatedAt: number | null; } export class ApexFlagsChanged { static readonly type = '[Apex] Flags changed'; constructor(public readonly values: Record) {} } export class ApexStopped { static readonly type = '[Apex] Stopped'; constructor(public readonly code: string) {} } @State({ name: 'apexFlags', defaults: { values: {}, stopped: null, updatedAt: null }, }) @Injectable() export class ApexFlagsState { /** store.select(ApexFlagsState.flag('banner')) */ static flag(key: string) { return createSelector([ApexFlagsState], (state: ApexFlagsStateModel) => state.values[key] as T); } @Action(ApexFlagsChanged) changed(ctx: StateContext, action: ApexFlagsChanged): void { ctx.patchState({ values: action.values, updatedAt: Date.now() }); } @Action(ApexStopped) stopped(ctx: StateContext, action: ApexStopped): void { ctx.patchState({ stopped: action.code }); } } ``` This service version replaces the root service above. It registers the flags the app reads, publishes them to the store at startup and after every change, and adds an RxJS helper. ```ts // apex.service.ts (NGXS version) import { Injectable, InjectionToken, NgZone, inject } from '@angular/core'; import { Store } from '@ngxs/store'; import { Observable } from 'rxjs'; import { Apex, type ApexChange, type ApexOptions } from '@drip-apex/app-sdk'; import { ApexFlagsChanged, ApexStopped } from './apex-flags.state'; export interface ApexFlagDefinition { key: string; defaultValue: T; validate?: (value: unknown) => value is T; } /** Pass userId when the app knows the signed-in user at startup. */ export type ApexInitOptions = ApexOptions & { userId?: string | null }; export type ApexOptionsFactory = () => ApexInitOptions | Promise; export const APEX_OPTIONS = new InjectionToken('APEX_OPTIONS'); export const APEX_FLAGS = new InjectionToken('APEX_FLAGS'); @Injectable({ providedIn: 'root' }) export class ApexService { private readonly zone = inject(NgZone); private readonly store = inject(Store); private readonly optionsFactory = inject(APEX_OPTIONS); private readonly flags = inject(APEX_FLAGS, { optional: true }) ?? []; async init(): Promise { const { userId, ...options } = await this.optionsFactory(); // Subscribe before configure: onChange does not replay, so a fatal startup stop (for example an invalid key) // would otherwise never reach the store. Apex.onChange((change: ApexChange) => this.zone.run(() => { if (change.type === 'stopped') this.store.dispatch(new ApexStopped(change.code)); else this.publish(); }), ); await Apex.configure(options); if (userId !== undefined) await Apex.identify(userId); this.publish(); } /** Emits now and after every change. Reading it records the exposure. */ flag$(key: string, defaultValue: T, validate?: (value: unknown) => value is T): Observable { return new Observable((subscriber) => { const read = () => subscriber.next(Apex.flagObject(key, defaultValue, { validate })); read(); const sub = Apex.onChange((change) => { if (change.type === 'changed' && change.keys.includes(key)) this.zone.run(read); }); return () => sub.cancel(); }); } /** Records the exposure for a flag whose value came from the store. */ logExposure(key: string): void { Apex.logExposure(key); } identify(userId: string | null): Promise { return Apex.identify(userId); } track(event: string, properties?: Record): Promise { return Apex.track(event, properties); } trackPurchase(orderId: string, revenue: number, currency: string): Promise { return Apex.trackPurchase(orderId, revenue, currency); } private publish(): void { const values: Record = {}; for (const f of this.flags) { values[f.key] = Apex.flagObject(f.key, f.defaultValue, { validate: f.validate, exposure: 'manual' }); } this.store.dispatch(new ApexFlagsChanged(values)); } } ``` A provider function registers the options, the flags, the state and the initializer. It needs `provideStore()` at the root. ```ts // provide-apex.ts import { EnvironmentProviders, inject, makeEnvironmentProviders, provideAppInitializer } from '@angular/core'; import { provideStates } from '@ngxs/store'; import { ApexFlagsState } from './apex-flags.state'; import { APEX_FLAGS, APEX_OPTIONS, ApexFlagDefinition, ApexOptionsFactory, ApexService } from './apex.service'; export function provideApex(options: ApexOptionsFactory, flags: ApexFlagDefinition[] = []): EnvironmentProviders { return makeEnvironmentProviders([ { provide: APEX_OPTIONS, useValue: options }, { provide: APEX_FLAGS, useValue: flags }, provideStates([ApexFlagsState]), provideAppInitializer(() => inject(ApexService).init()), ]); } ``` ```ts // app.config.ts import { ApplicationConfig } from '@angular/core'; import { provideStore } from '@ngxs/store'; import { provideApex } from './apex/provide-apex'; import { capacitorPreferencesStorage } from './apex/apex-preferences.storage'; export const appConfig: ApplicationConfig = { providers: [ provideStore([]), provideApex( () => ({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_ID', applicationId: 'com.example.shop', storage: capacitorPreferencesStorage(), userId: null, // Or the signed-in user ID, if the app knows it at startup. }), [{ key: 'welcome', defaultValue: false }], ), ], }; ``` Read a flag from the store and record the exposure where the variant is visible: ```ts readonly welcome$ = this.store.select(ApexFlagsState.flag('welcome')); ngOnInit(): void { this.apex.logExposure('welcome'); } ``` Or use the RxJS helper. It reads with automatic exposure, so subscribe only where the app shows the variant: ```ts readonly welcome$ = this.apex.flag$('welcome', false); ``` ## Capacitor Preferences storage The SDK stores its state in `localStorage` by default. To use Capacitor Preferences (UserDefaults on iOS, SharedPreferences on Android), pass a storage adapter. Capacitor stays an app dependency, not an SDK dependency. ```ts // apex-preferences.storage.ts import { Preferences } from '@capacitor/preferences'; import type { ApexStorage } from '@drip-apex/app-sdk'; /** Prefixes keys so Apex data stays separate from the app's own preferences. */ export function capacitorPreferencesStorage(prefix = 'apex.'): ApexStorage { return { async get(key: string): Promise { return (await Preferences.get({ key: prefix + key })).value; }, async set(key: string, value: string): Promise { await Preferences.set({ key: prefix + key, value }); }, }; } ``` A storage adapter must replace a value in one step, or reject without changing it. `Preferences.set` meets this rule. ## Cordova content security policy Cordova apps must allow the events host in their content security policy before you test. Without `connect-src https://events.drip-apex.com`, every SDK request fails with a network error. Keep the other origins your app needs: ```html ``` Cordova iOS apps need `cordova-ios` 8 or later to build with current Xcode. See [Supported versions](/docs/app-sdk/supported-versions). ## Related pages - [Flags and typed values](/docs/app-sdk/flags-typed-values) - [Users and identity](/docs/app-sdk/users-identity) - [App lifecycle and offline behaviour](/docs/app-sdk/lifecycle-offline) ## Flags and typed values Read a flag at the point where the app selects its content or code path. Every read returns a value: the assigned value, a cached value, or your default. ## Read a primitive flag ```ts import { Apex } from '@drip-apex/app-sdk'; const showBanner = Apex.flag('checkout_banner', false); const ctaLabel = Apex.flag('cta_label', 'Buy now'); const freeShippingFrom = Apex.flag('free_shipping_from', 50); ``` The default sets the type. The assigned value must have the same JSON type: boolean, string or number. The SDK does not convert types. For example, the string `"true"` does not match a boolean default. Numbers must be finite, and integers must be safe JavaScript integers. A missing or incompatible value returns the default. The SDK logs a warning once per key per session, but only after it has loaded an assignment set from cache or the network. Reads before that first load return defaults without warnings. ## Read a typed object Create the flag with object values in Apex. Then read it with `flagObject` and a validator: ```ts type Welcome = { title: string; button: { label: string; enabled: boolean } }; const fallback: Welcome = { title: 'Welcome', button: { label: 'Start', enabled: false } }; const isWelcome = (v: unknown): v is Welcome => { if (!v || typeof v !== 'object') return false; const x = v as Partial; return typeof x.title === 'string' && typeof x.button?.label === 'string' && typeof x.button?.enabled === 'boolean'; }; const content = Apex.flagObject('welcome', fallback, { validate: isWelcome }); ``` TypeScript types do not check nested fields at runtime. Without a validator, `flagObject` checks only the outer JSON type. Use a validator for every object flag. - A value that passes validation is returned as a copy. Unknown keys stay in the copy. - A value that fails validation, or a validator that throws, returns the default. The SDK logs a warning and records no exposure. - The SDK never throws into app code. ## Change values without an app release Each variation value must be JSON with finite numbers, at most 16 KB after serialization. You can edit a value later in Apex or with `PATCH /api/v1/flags/{id}`. The app gets the new value on its next successful assignment refresh, without an app release. Typed values replace code variants for content and configuration tests. Layout and flow tests still need both versions in your app code behind the flag. Apex cannot remove that code from the app for you. ## Exposures An exposure tells Apex that a user saw a variation. By default, a read records the exposure automatically. This applies to `flag`, `flagObject` and `variation`. The SDK records one exposure per test and variation per session, so repeated reads are safe. - A read of a missing assignment records no exposure. - A read of an incompatible or invalid value records no exposure. - A read while `identify()` is pending records no exposure. See [Users and identity](/docs/app-sdk/users-identity). ## Manual exposure Read the value early and record the exposure only when the user can see the result. Use `exposure: 'manual'` and call `logExposure` later: ```ts const content = Apex.flagObject('welcome', fallback, { validate: isWelcome, exposure: 'manual' }); // Later, when the welcome content is on screen: Apex.logExposure('welcome'); ``` `flag` accepts the same option: `Apex.flag('checkout_banner', false, { exposure: 'manual' })`. Manual exposures use the same per-session dedupe, so a repeated `logExposure` call is safe. A `logExposure` call while `identify()` is pending is ignored and is not replayed later. Call it again after the new assignments load. ## Read the variation `variation(key)` returns details about the assigned variation. It also records the exposure by default. ```ts const v = Apex.variation('welcome'); // { id, index, isControl, kind, value, reason } // Read early without counting an exposure: const preview = Apex.variation('welcome', { exposure: 'manual' }); // Later, when visible: Apex.logExposure('welcome'); ``` | `reason` | Meaning | |---|---| | `fresh` | The value came from the network less than 60 seconds ago. | | `cached` | The value came from the device cache. | | `stale` | The value is more than 60 seconds old. | | `default` | The user has no assignment for this flag. | For a flag the user is not assigned to, `variation` returns an object with an empty `id`, `value: null` and `reason: 'default'`; it records no exposure then. It returns `null` only before `configure()`, for an invalid key or options, and when the stored value is not valid JSON. Use `variation()`, `flag()` or `flagObject()` with `exposure: 'manual'` when you need a value without counting an exposure. ## React to changes Values can change after a refresh. Subscribe to update the UI: ```ts const subscription = Apex.onChange((change) => { if (change.type === 'changed' && change.keys.includes('welcome')) { // Read the flag again and update the UI. } if (change.type === 'stopped') { // The SDK stopped networking, for example after an invalid key. } }); // When the view closes: subscription.cancel(); ``` ## End a test When you conclude a flag in Apex, every user gets the winning variation. Apps receive the winner on their next successful assignment request: at startup, when the app returns to the foreground, or after `Apex.refresh()`. A session that stays in the foreground keeps its earlier value until that request succeeds. Users in a global holdout get no assignment and keep your default value. Then remove the flag read from the app and release a new version. Archive the flag when Apex reports that the app no longer calls it. The **Removal checklist** on the flag page shows this status. ## Related pages - [Goals and purchases](/docs/app-sdk/goals-purchases) - [Test your build](/docs/app-sdk/testing-your-build) - [Feature flags](/docs/experiments/feature-flags) ## Goals and purchases Call `track()` once at each goal action. Call `trackPurchase()` once per order at order confirmation. ## Track a goal ```ts await Apex.track('signup_completed'); await Apex.track('add_to_cart', { product_id: 'sku-123', screen: 'product' }); ``` - Use the event name of the goal you measure in Apex. - Event names are non-empty strings of up to 256 characters. - Properties are optional. They must be a JSON object with finite numbers. - A string `screen` property sets the screen for this event. Otherwise the SDK uses your `screen` option. A `track` event carries no test or variation fields. Only purchases carry per-test copies. See [Attribution](#attribution). ## Track a purchase ```ts await Apex.trackPurchase(order.id, order.total, order.currency, { screen: 'checkout' }); ``` | Argument | Rule | |---|---| | `orderId` | A stable, non-empty string of up to 1,024 characters. Use your own order ID. | | `revenue` | A finite, non-negative number in currency units, for example `42.5`. | | `currency` | A three-letter ISO 4217 code, for example `EUR`. The SDK trims and uppercases it. | | `properties` | Optional. The same rules as `track`. | Call `trackPurchase()` once per order. If the same order ID reaches Apex more than once, results still count the order once. Always send the same order ID for the same order. The SDK removes order, revenue and currency fields from your properties before it adds the explicit arguments. Your properties cannot override them. Other properties stay. ## Attribution Apex attributes purchase revenue to every test the user is assigned to. `trackPurchase()` sends one base row without test fields. It also sends one copy for each current assignment, with that test's experiment and variation IDs. All rows share one event ID, so retries cannot double count them. The SDK keeps all rows of one purchase together. It queues them together and sends them in one request. ## Blocked properties The SDK drops an event that contains any of these property keys: `email`, `phone`, `password`, `ssn`, `credit_card` or `card_number`. The check ignores case and accents. Do not send personal data in event properties. The SDK also drops an event with an invalid `revenue` property. A valid value is a finite number, or a decimal string such as `"42"` or `"42,5"`. ## What the promise means `track()` and `trackPurchase()` resolve when the event is in the device queue. They do not wait for delivery to Apex. They never reject. Invalid input is logged and dropped. See [Troubleshooting](/docs/app-sdk/troubleshooting) for the log lines. ## Related pages - [Users and identity](/docs/app-sdk/users-identity) - [App lifecycle and offline behaviour](/docs/app-sdk/lifecycle-offline) - [Metrics and goals](/docs/experiments/metrics-goals) ## App lifecycle and offline behaviour The SDK stores events on the device and sends them in batches. It sends the queue when the app goes to the background and refreshes assignments when the app returns to the foreground. ## Offline queue Every `track()`, `trackPurchase()` and exposure goes into a queue in device storage. The SDK sends the queue every `flushIntervalSeconds` (default 5 seconds), in batches of up to 20 events. - When `track()` resolves, the event is in device storage. It stays there until Apex confirms it, so a crash or a closed app does not lose it. - When the app is offline, the SDK retries with a growing delay: 5, 10, 20, 40, then 60 seconds. - The queue holds up to 2,000 events. When it is full, the SDK removes the oldest events first. - Unsent events expire after 47 hours. - Each event keeps the same event ID on every retry, so Apex counts it once. To send the queue now, for example before a planned app close: ```ts await Apex.flush(); ``` `flush()` resolves when the SDK finishes its local work. It does not wait for delivery. ## Background and foreground The SDK listens for app state changes from these sources: - Browser `visibilitychange` events. - Cordova `pause` and `resume` events. - Capacitor App `appStateChange` events, when the App plugin is available on `window.Capacitor.Plugins`. You do not need to install a plugin for this. When the app goes to the background, the SDK first finishes any pending storage write. Then it sends the queue. When the app returns to the foreground, the SDK refreshes assignments after a short random delay (0 to 10 seconds by default). It also sends any queued events. ## Forward lifecycle events yourself Some Capacitor versions expose App only as an imported module. Forward its state to the SDK: ```ts import { App } from '@capacitor/app'; import { Apex } from '@drip-apex/app-sdk'; App.addListener('appStateChange', ({ isActive }) => { Apex.setAppState(isActive ? 'foreground' : 'background'); }); ``` You can also call `Apex.setAppState('background')` and `Apex.setAppState('foreground')` from your own shell hooks. Repeated signals within one second count as one change. ## Assignment refresh The SDK requests assignments at these times only: - On `configure()`. - On `identify()`. - When the app returns to the foreground. - When you call `Apex.refresh()`. There is no periodic polling. A value older than 60 seconds reports `reason: 'stale'`, but the SDK does not refresh it on a timer. To refresh in a long foreground session, call `Apex.refresh()`. ## Sessions Each cold launch starts a new session. After 30 minutes in the background, the SDK also starts a new session when the app returns. A new session allows a new exposure for each assignment. ## Shut down Call `Apex.shutdown()` when the app no longer needs the SDK. It stops networking and waits for pending storage writes. A second `configure()` call shuts down the previous context for you. To run a separate context, create `new ApexClient(options)` and await its `ready` promise. Do not run two clients with the same storage, shop and environment at the same time. ## Related pages - [Goals and purchases](/docs/app-sdk/goals-purchases) - [Troubleshooting](/docs/app-sdk/troubleshooting) ## App SDK overview and install The App SDK runs Apex flags and tests inside hybrid apps built with Ionic, Angular, Capacitor or Cordova. It reads assigned flag values, records exposures, and sends goals and purchases to Apex. The package is `@drip-apex/app-sdk`. It has no runtime dependencies and needs no native plugin. It uses only WebView APIs: `fetch`, timers, Web Crypto, `localStorage` and visibility events. It does not change the DOM and does not load the storefront SDK. ## How it works 1. The app calls `Apex.configure()` once at startup. 2. The SDK gets the assignments for the device, or for the signed-in user after `identify()`. 3. Your code reads a flag where it selects content or a code path. The read records the exposure. 4. Your code calls `track()` for goals and `trackPurchase()` for orders. 5. The SDK queues events on the device and sends them in batches to `https://events.drip-apex.com`. Flag reads are synchronous. Before the first response, reads return your default value or the cached assignment. ## Install the package ```sh npm install @drip-apex/app-sdk ``` The same package works in Capacitor and Cordova apps. Capacitor plugins such as Preferences and App are optional. You add them only if you want the integrations described in [Angular setup](/docs/app-sdk/angular-setup) and [App lifecycle and offline behaviour](/docs/app-sdk/lifecycle-offline). Cordova apps must allow the events host in their content security policy. See [Cordova content security policy](/docs/app-sdk/angular-setup#cordova-content-security-policy). ## Get your publishable key and shop ID 1. In Apex, open **Settings → Developers**. 2. On **Keys and webhooks**, select **Create key** and choose the **Publishable** type. 3. Copy the key. It starts with `apx_pk_`. 4. On **IDs and references**, copy the **Shop id**. A publishable key is safe to ship in an app binary. It can only read assignments and send events. Never put a secret API key or a shop secret in the app. ## Configure the SDK ```ts import { Apex } from '@drip-apex/app-sdk'; await Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_ID', applicationId: 'com.example.shop', appVersion: '1.0.0', screen: () => 'home', qaMode: false, }); const enabled = Apex.flag('welcome', false); await Apex.track('signup_completed'); await Apex.trackPurchase('order-42', 42.5, 'EUR'); ``` `configure()` never rejects. It resolves when the SDK has loaded its stored state and the first assignment request has finished. It waits at most `initTimeoutMs` (default 3,000 ms) for that request. A late response still updates flag values. Call `configure()` once, in app initialization. Angular apps use an app initializer, as shown in [Angular setup](/docs/app-sdk/angular-setup). ## Create a flag for the app In Apex, create a feature flag and set **Delivery** to **API**. The flag key is the key your code reads, for example `welcome`. API flags can carry typed values: booleans, strings, numbers or JSON objects. See [Flags and typed values](/docs/app-sdk/flags-typed-values). ## Options | Option | Default | Use | |---|---|---| | `publishableKey` | Required | The `apx_pk_` key from **Settings → Developers**. | | `shopId` | Required | The shop ID from **IDs and references**. | | `environment` | `production` | Any other value marks all events as QA. | | `qaMode` | `false` | Set `true` in test builds. See [Test your build](/docs/app-sdk/testing-your-build). | | `applicationId` | `unknown` | Your bundle or package ID. Apex uses it in screen URLs. | | `appVersion` | Empty string | Your app version. Apex shows it in the device lookup. | | `screen` | Returns `home` | A function that returns the current route, without query or fragment. | | `storage` | `localStorage` | An adapter for durable storage, for example Capacitor Preferences. | | `initTimeoutMs` | `3000` | How long `configure()` and `identify()` wait for assignments. `0` skips the wait. | | `flushIntervalSeconds` | `5` | Time between event sends. Clamped to 0.1 to 86,400. | | `batchSize` | `20` | Events per request. Clamped to 1 to 20. | | `queueCapacity` | `2000` | Maximum queued events. Clamped to 1 to 2,000. | | `queueRetentionHours` | `47` | How long unsent events stay in the queue. Clamped to 0 to 47. | | `refreshJitterSeconds` | `[0, 10]` | Random delay before a foreground assignment refresh. | | `platform` | Detected | `ios`, `android` or `web`. Set it only to override detection. | | `logger` | None | A function that receives diagnostic lines. See [Troubleshooting](/docs/app-sdk/troubleshooting). | | `fetch` | WebView `fetch` | A custom fetch function. | | `baseUrl` | `https://events.drip-apex.com` | The Apex events host. | ## Platform detection The SDK sets the event platform to `ios`, `android` or `web`. It checks Capacitor `getPlatform()` first, then Cordova `device.platform`, then the user agent. If none of these match, it uses `web`. An explicit `platform` option takes precedence. ## Server-side rendering You can import the SDK in code that also runs on the server. During server-side rendering, `configure()` logs once, does no storage or network work, and flags return their defaults. Configure again in the browser to start the SDK. ## Related pages - [Angular setup](/docs/app-sdk/angular-setup) - [Flags and typed values](/docs/app-sdk/flags-typed-values) - [Goals and purchases](/docs/app-sdk/goals-purchases) - [Supported versions](/docs/app-sdk/supported-versions) - [API keys](/docs/settings-account/api-keys) - [Feature flags](/docs/experiments/feature-flags) ## Supported versions The App SDK is tested with the versions below. It ships ESM and CommonJS builds that target ES2019, with type declarations for both. It uses no Node APIs at runtime. ## Tested versions | Tool | Tested versions | |---|---| | Angular | 15 to 22 | | Ionic | 6 to 8 | | Capacitor | 8 | | Cordova iOS (`cordova-ios`) | 8 and later | | TypeScript | 4.9 to 6 | The SDK works with the Angular webpack and esbuild builders. ## Build requirements - **iOS 27 with Xcode 27:** Capacitor apps need Capacitor 8.5 or later. - **Cordova on iOS:** use `cordova-ios` 8 or later to build with current Xcode. Older Cordova iOS shells lack UIScene support and can crash at launch on current iOS. This is not related to Apex. - **Angular 20:** needs Node 20.19 or later. - **Angular before 19:** use `APP_INITIALIZER` instead of `provideAppInitializer`. See [Run it at startup](/docs/app-sdk/angular-setup#run-it-at-startup). ## Platforms Events report the platform as `ios`, `android` or `web`. Apex detects it from Capacitor, then Cordova, then the user agent. Set the `platform` option to override it. ## Related pages - [App SDK overview and install](/docs/app-sdk/overview) - [Angular setup](/docs/app-sdk/angular-setup) ## Test your build Set `qaMode: true` in test builds. Pin your test phone to a variation with a QA device. Then check its events in the device lookup. ## Mark test builds as QA ```ts await Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_ID', applicationId: 'com.example.shop', qaMode: !environment.production, }); ``` With `qaMode: true`, every event from the build is QA traffic: exposures, goals, purchases and custom events. Any `environment` other than `production` also marks events as QA. Results hide QA traffic unless you turn on the QA toggle. Store builds use `environment: 'production'` and `qaMode: false`. Queued events keep the QA mark they had when the app queued them, even if the option changes later. ## Find the identity to register QA devices and the device lookup use the identity the app sends to Apex: - Before `identify()`: the device ID, `Apex.deviceId`. - After `identify()`: the app user ID, `Apex.userId`. Show the value on a debug screen in your test build, or log it: ```ts console.log('Apex identity', Apex.userId); ``` ## Pin a test phone to a variation QA devices are available on flags with **API** delivery. 1. In Apex, open **Feature Flags** and select the flag. 2. In the **QA devices** panel, enter the **Identity**: the device ID, or the app user ID after login. 3. Choose the **Variation** to show on that device. 4. Optional: add a **Label**, for example `QA iPhone 15`. 5. Set **Expires after (days)**. The default is 14 days, the maximum is 90. 6. Select **Add QA device**. The device now gets the selected variation, and all its events count as QA traffic. Audience targeting still applies to a pinned identity. A flag can have up to 50 active QA devices. The app picks up the pinned variation on its next assignment refresh. Bring the app back to the foreground, or call `Apex.refresh()`. When you conclude a flag, its QA devices no longer apply. Every device, QA devices included, gets the winner. ## Check a device's events 1. In Apex, open **Analytics → Realtime**. 2. In **Device lookup**, enter the device ID or app user ID. 3. Choose a time window: 15 minutes, 1 hour, 6 hours or 24 hours. The list shows the newest events first and refreshes every 5 seconds. Each row shows the event, the flag or test, the variation, the goal, the revenue, the platform and the app version. QA events are included. New events can take a few seconds to arrive. During a login change, exposures can still use the previous identity. Check both identities when you test a login flow. ## Check events from a terminal You can also read the same data through the API. Use a secret API key with the `experiments:read` permission, from a trusted terminal. Never put that key in the app. ```sh curl --get 'https://app.drip-apex.com/api/v1/tracking/verify' \ --header "Authorization: Bearer $APEX_API_KEY" \ --data-urlencode 'identity=' ``` The default window is 60 minutes. Add `minutes` (up to 1,440) and `limit` (1 to 200 rows) to change it. Each event includes `platform`, `app_version`, `sdk_version` and `environment`. ## Related pages - [Users and identity](/docs/app-sdk/users-identity) - [Troubleshooting](/docs/app-sdk/troubleshooting) - [Realtime dashboard](/docs/analytics/dashboard-realtime) ## Troubleshooting Pass a `logger` to see what the SDK does. Without a logger the SDK writes nothing, not even to the console. ## Turn on diagnostics ```ts await Apex.configure({ publishableKey: 'apx_pk_live_…', shopId: 'YOUR_SHOP_ID', logger: (line) => console.debug(line), }); ``` In the tables below, `…` and `4xx` stand for the values in the real line, such as a delay or an HTTP status. Log lines never contain the key, the `Authorization` header, response bodies or exception details. Each network failure type logs at most once per 60 seconds, so a long outage does not flood the log. An error in your logger function does not affect the SDK. ## Setup and network | Log line | Meaning and fix | |---|---| | `Apex configure: invalid options; SDK stopped` | An option is invalid, for example a key that does not start with `apx_pk_` or a missing shop ID. Fix the options. The SDK does no storage or network work. | | `Apex SDK stopped: invalid_publishable_key` | Apex does not know the key, or it was revoked or expired. Create a new publishable key. Flag reads keep their last values. | | `Apex SDK stopped: publishable_key_not_allowed` | The request used a route that does not accept publishable keys. Check that `baseUrl` is the Apex events host. | | `Apex assignment fetch failed: network error; retry scheduled in …` | The app could not reach Apex. On Cordova, check the [content security policy](/docs/app-sdk/angular-setup#cordova-content-security-policy). Also check the device connection. | | `Apex event delivery failed: network error; retry scheduled in …` | Events could not be sent. They stay in the queue and the SDK retries. | | `Apex assignment fetch failed: HTTP 503; retry scheduled in …` | Apex is temporarily unavailable. The SDK waits and retries. | | `Apex assignment fetch failed: HTTP 4xx; no retry scheduled` | Apex rejected the request. Check `shopId` and `environment`. | | `Apex event delivery failed: HTTP 4xx; batch dropped` | Apex rejected a batch. The SDK removes it from the queue. | | `Apex requires a browser or WebView; initialization skipped` | The SDK ran without a DOM, for example during server-side rendering. Configure it again in the browser. | | `Apex initialization failed` | The SDK could not start, for example because device storage failed. | The SDK sends the publishable key only as an `Authorization: Bearer` header and sends no cookies. Cookie authentication does not work from `capacitor://localhost`. ## Storage | Log line | Meaning and fix | |---|---| | `Apex identity storage unavailable; networking stopped` | The SDK could not read or write its device ID. Check your storage adapter. | | `Apex identity could not be saved; the SDK stopped networking` | An `identify()` write failed. The SDK keeps the previous identity and stops networking until a later `identify()` saves. | | `Apex could not persist events` | A queue write failed. The SDK keeps the previous queue. | | `Apex dropped purchase: queue write failed` | A purchase could not be saved. Call `trackPurchase()` again with the same order ID. | | `Apex could not load …` | A stored value could not be read. The SDK uses defaults. | | `Apex dropped event rows: queue capacity` | The queue was full, so the SDK removed the oldest events. | | `Apex dropped expired or oversized event rows` | Events were older than the retention window, or a purchase was too large. | ## Flags | Log line | Meaning and fix | |---|---| | `Apex typed flag 'key' unavailable or incompatible; using default` | The flag has no assignment, or its value does not match your default type or validator. Check the flag key, its **API** delivery, and the value type. | | `Apex dropped exposure: session dedupe capacity` | One session recorded more than 4,096 different exposures. New exposures are dropped until the next session. | ## Events | Log line | Meaning and fix | |---|---| | `Apex dropped event: invalid event name` | The name is empty or longer than 256 characters. | | `Apex dropped event: property email is not allowed` | A property key is blocked as personal data. Remove it. | | `Apex dropped event: revenue must be a finite number` | The `revenue` property is not a valid number. | | `Apex ignored non-JSON event properties` | Properties must be a plain JSON object. | | `Apex dropped purchase: order_id must be a string` | The order ID is not a string, or it is longer than 1,024 characters. | | `Apex dropped purchase: order_id is required` | The order ID is empty. | | `Apex dropped purchase: revenue must be a finite non-negative number` | Revenue is negative, infinite or not a number. | | `Apex dropped purchase: currency must be a 3-letter ISO 4217 code` | Use a code such as `EUR` or `USD`. | | `Apex dropped purchase: group exceeds queue capacity or the 200-event request limit` | The purchase has more rows than the queue or one request can hold. | ## Other lines | Log line | Meaning and fix | |---|---| | `Apex ignored invalid identity or attributes` | The user ID is empty, too long or not a string. Use a non-empty string of up to 1,024 characters, or `null`. | | `Apex ignored invalid app state` | `setAppState()` accepts only `foreground` or `background`. | | `Apex ignored invalid change handler` | `onChange()` needs a function. | | `Apex listener failed` | Your `onChange` handler threw an error. Other handlers still run. | | `Apex shell lifecycle listener unavailable` | The Capacitor App listener could not be added. [Forward lifecycle events yourself](/docs/app-sdk/lifecycle-offline#forward-lifecycle-events-yourself). | ## Common problems **Every request fails with a network error on Cordova.** Add `connect-src https://events.drip-apex.com` to the content security policy. **Flags always return the default.** Check that the flag is active and uses **API** delivery, that the key matches, and that the user matches the flag's targeting. Use the [device lookup](/docs/app-sdk/testing-your-build#check-a-device-s-events) to see the device's exposures and events. **Screen URLs show `app://unknown/home`.** The SDK lowercases the `applicationId` and replaces underscores with hyphens before validating the host. For example, `com.example.my_app` reports `app://com.example.my-app/`. The normalized host must contain only letters, digits, dots and hyphens, and must start and end with a letter or digit; invalid hosts still fall back to `app://unknown/home`. **The app still runs the old SDK after you reinstall the same version.** Clear Angular's `.angular/cache` folder and rebuild. **Results do not show test-build events.** Test builds send QA traffic. Turn on the QA toggle in results. ## Related pages - [Test your build](/docs/app-sdk/testing-your-build) - [App lifecycle and offline behaviour](/docs/app-sdk/lifecycle-offline) ## Users and identity The SDK starts with an anonymous device ID. Call `identify(userId)` on login and `identify(null)` on logout. Await each call before you read flags for the new identity. ## Device ID and user ID On first launch the SDK creates a random device ID and stores it. Apex assigns tests to this ID until the app calls `identify()`. | Property | Value | |---|---| | `Apex.deviceId` | The anonymous device ID. `undefined` before `configure()`. | | `Apex.userId` | The identity the SDK currently uses: the user ID after `identify()`, otherwise the device ID. | Use a stable user ID from your own system. Do not use an email address or a phone number. ## Log in ```ts async function onLogin(user: { id: string }) { await Apex.identify(user.id); // Read flags here. They now return this user's values. const enabled = Apex.flag('welcome', false); } ``` `identify()` never rejects. It resolves when the new user's assignments load, or after `initTimeoutMs` (default 3,000 ms). User IDs are non-empty strings of up to 1,024 characters. The second argument, `attributes`, is accepted but not sent to Apex. ## Why you await identify Until the new user's assignments load, the SDK keeps serving the previous identity's values. During this time: - Flag reads return the previous values and record **no** exposure. - `logExposure()` calls are ignored and are not replayed later. After the new assignments load, read the flag again or call `logExposure()` again. The SDK then counts the exposure for the new user. If the wait times out, reads stay uncounted until a matching response arrives. If the app calls `identify()` again before the first call finishes, the later call wins. ## Identify at startup If the app already knows the signed-in user at startup, identify before the first render. This prevents an exposure for the anonymous device of a user who is in fact signed in: ```ts await Apex.configure(options); if (session.userId) await Apex.identify(session.userId); ``` The [NGXS service](/docs/app-sdk/angular-setup#optional-ngxs-state) accepts a `userId` option for this. ## Log out ```ts async function onLogout() { await Apex.identify(null); } ``` `identify(null)` returns to the device ID and clears the saved user for the next launch. ## Cold launch The SDK saves the identified user per shop and environment. On the next cold launch, it restores that user's cached assignments at once, before it waits for the network. Without a saved user, it restores the anonymous device cache. ## Purchases during a login change While `identify()` is pending, `trackPurchase()` still works. The base purchase row uses the new identity. The per-test copies use the previous identity and its assignments. After the new assignments load, all rows use the new identity. ## Related pages - [Flags and typed values](/docs/app-sdk/flags-typed-values) - [Test your build](/docs/app-sdk/testing-your-build) ## Agent tools: SOS, brand, schema, and skills These commands are designed for automation and external agent workflows. They expose public CLI behavior only; internal dashboards, internal API routes, and operations runbooks are not part of this reference. ## Documentation for AI tools AI tools can discover the concise documentation index at [`/llms.txt`](https://docs.drip-apex.com/llms.txt) or read the complete documentation corpus at [`/llms-full.txt`](https://docs.drip-apex.com/llms-full.txt). Both files are generated from the same manifest as this site, so their page lists stay current. ## Brand context ```bash apex brand apex brand --json ``` `apex brand` reads the current shop brand context. Human output prints the prompt-ready brand block when one exists. JSON output returns `brandContext`, or `null` with a message when no profile exists yet. Use this before generating customer-facing experiment copy so external agents inherit the same brand grounding as Apex-managed workflows. ## SOS tickets ```bash apex sos create \ --type qa \ --experiment 00000000-0000-4000-8000-000000000040 \ --sub-class "tracking evidence missing" \ --loom https://www.loom.com/share/example \ --description @sos-description.md apex sos mine --json ``` | Command | Flags | Behavior | |---|---|---| | `apex sos create` | `--type `, `--loom `, `--experiment `, `--sub-class `, `--description `, optional `[description]` argument | Creates an Apex SOS ticket through the API; with `--experiment` it appends to that experiment's open ticket of the same type when one exists. Without `--experiment`, the CLI now always opens a new Jira ticket instead of appending to the shop's oldest open ticket of the same type. | | `apex sos mine` | None beyond global flags | Lists tickets created by the current API key. | SOS type values are `full_handover`, `design`, `development`, `qa`, `config`, `bug`, and `help`. The default type is `qa`. Bug reports and help requests work with shop API keys; the internal handover, design, development, QA, and config types require a DRIP user account with platform access. {/* user-note: "Your SOS tickets now show the customer, the parent experiment, and the Backlog source with a direct link into Apex; when no link exists the UI shows 'Not linked' instead of 'General'. Filters are available for long ticket lists." */} ## Schema introspection ```bash apex schema --json ``` `apex schema` prints the current command tree, aliases, argument metadata, options, descriptions, default values, nested commands, and a `body` JSON Schema for commands with a documented request document. Agents should call this before scripting unfamiliar commands. ### Field semantics | Field | Meaning | |---|---| | `required` | For options, the flag takes a required value; for arguments, the positional itself is mandatory. | | `optional` | The option takes an optional value when its flag is present. | | `mandatory` | The option flag itself must be passed. This is always a boolean. | | `defaultValue` | The portable default exposed to callers. Path defaults are environment-independent and resolved against the current directory only when the command runs. | | `body` | The JSON Schema of the JSON document sent by that command. | | `schemaVersion` | The top-level schema contract version. Version `2` adds explicit mandatory-option semantics. | ## Skill files ```bash apex skills apex skills --install apex skills --install --target ./.claude/skills --json ``` `apex skills` prints the package paths for the CLI `AGENTS.md` contract and the bundled `apex-cli` skill. With `--install`, it copies both files into `/apex-cli`. The default target is `.claude/skills`. ## Agent contract reminders - Use `apex`, not inferred fuzzy command names. - Prefer `--json` and parse the envelope, not human tables. - Treat `.apex/` folders as CLI-owned metadata. - Treat preview tokens and API keys as secrets. - Do not edit generated QA or launch state directly; use `apex experiments qa`. ## Related pages - [CLI commands, JSON, and exit codes](/docs/cli/commands-output) - [Install and authenticate the Apex CLI](/docs/cli/install-auth) - [Workspace, Operator, and roadmap commands](/docs/cli/workspace-operator-roadmap) ## CLI commands, JSON, and exit codes Use this page as the command map before scripting the CLI. The source of truth is the live `apex schema --json` command. ## Global behavior - `apex` is the canonical binary. `drip` is a compatibility alias. - `--json` is available globally and on every registered command. - Piped stdout automatically uses JSON mode even when `--json` is omitted. - Human progress, prompts, diagnostics, and update notices go to stderr. ## CLI version floor Every v1 API response includes `X-Apex-Cli-Minimum` and, when known at build time, `X-Apex-Cli-Latest`. The minimum is advisory until server-side enforcement is enabled. When enforcement is enabled, a write from an older Apex CLI returns HTTP 426 with code `cli_upgrade_required` and the exact upgrade command. Read commands continue to work so agents can inspect state and recover safely. - `apex push` and `apex experiments apply` write one hosted asset upload line to stderr for each inline image above 4,096 characters and each referenced local image file. Pass `--no-asset-upload` to keep inline and local image references unchanged. - Do not parse human tables. Use `apex schema --json` to discover commands and flags. - `apex --version --json` returns the standard envelope with the package name and version in `data`; `apex --version` remains a bare version string for compatibility, including in pipes. JSON output uses this stable top-level envelope: ```json { "ok": true, "version": "0.1.1", "command": "apex experiments list", "data": {} } ``` Errors use the same envelope shape with `ok: false` and an `error` object: ```json { "ok": false, "version": "0.1.1", "command": "apex experiments apply", "error": { "message": "Applying these changes revokes the QA approval from 2026-10-01T12:00:00.000Z; pass --accept-qa-invalidation to confirm.", "code": 4, "details": { "reason": "qa_approval_present", "qa_approved_at": "2026-10-01T12:00:00.000Z", "required_acknowledgment": "accept-qa-invalidation", "retry": "apex experiments apply ./my-test --accept-qa-invalidation" } } } ``` When filesystem apply would replace authored URL rules with a Page attachment, it returns exit `4`; review the change and retry with `--confirm-targeting-replace`. With `--json`, stdout carries exactly one JSON document and nothing else. {/* user-note: "apex --json now prints a single JSON document to stdout; scripts should read the API body directly. Update jq paths, e.g. from `.data.experiments[]` to `.data.data[]`, and from `.data.experiment.id` to `.data.id`. Ticket: #3458" */} For commands backed by one API request, `data` is the API response body exactly as the API returned it: lists keep `{ "data": [...], "pagination": {...} }`, so rows are at `.data.data[]`, and single resources are at `.data`. `apex experiments get` adds only `dashboard_url`. Commands that combine several requests or local files, such as `experiments create`, `pull`, `plan` and `apply`, print a documented CLI-composed object. When an API response caused a failure, `error.api` carries `{ "status", "body" }` with the API error body verbatim; `error.code` stays the CLI exit code. CLI 0.13.0 introduced this mirroring; earlier releases nested bodies under keys such as `.data.experiment`. ## Headless and agent usage ```bash DRIP_API_KEY=apx_your_key_here apex schema --json apex experiments plan ./my-test --json apex experiments apply ./my-test --json ``` In headless mode: - Do not wait for prompts. - Prefer explicit `--json`. - If a command returns exit code `4`, inspect `error.details.retry` and rerun with the exact flag only after checking the planned destructive or QA-invalidating action. - Never commit `~/.drip/credentials.json`, API keys, or preview tokens. - Note: CLI experiment readback for the v1 experiment format now guarantees the presence of lifecycle timestamp fields (`started_at`, `scheduled_start_at`, and `qa_approved_at`) in `apex experiments get` / `apex experiments list --full --json` outputs when those fields are applicable. Integrations should read those fields from the JSON envelope to determine scheduling and QA state. ## Exit codes | Code | Meaning | Typical recovery | | ---: | ---------------------------------------------------------- | ---------------------------------------------------------------------------- | | `0` | Success. | Continue. | | `1` | General failure. | Read `error.message` and command-specific details. | | `2` | Auth or permission failure. | Run `apex login`, set `DRIP_API_KEY`, or check API-key permissions. | | `3` | Validation failure. | Fix arguments, JSON, config, or unsupported file content. | | `4` | Confirmation required. | Inspect `error.details.retry`; rerun with the named flag only when intended. | | `5` | Conflict, including stale filesystem state where detected. | Pull fresh state, reapply edits, and plan again. | | `6` | A check command reported a failing verdict (`apex install check`, `apex install steps`, `apex shops doctor`). | Read `error.details.failed_checks` (install check), `error.details.steps` (install steps) or the FAIL checks; fix the storefront or configuration, then rerun. | ## Login and context `apex login` authenticates your account once: the browser flow mints an expiring, account-scoped user key that covers every organization and shop your account can access, then lets you pick an org/shop context. Inspect the credential and everything it can reach with `apex whoami`; switch the stored context with `apex use [/]` or `apex use --org --shop `; override per invocation with `--org`/`--shop` or the `DRIP_ORG`/`DRIP_SHOP` environment variables. An environment-key run cannot persist `apex use`; it returns `context_not_persistable` with the exact `DRIP_ORG`/`DRIP_SHOP` values to export. Every JSON envelope echoes the effective `context` (org, shop, credential kind). Automation keeps injecting a narrow shop key per process with `DRIP_API_KEY`; shop and user credentials can coexist. `apex login --user` is a deprecated alias. Use `apex logout --user` to remove only the user credential; plain `apex logout` removes both. ## Command index The command names in this index are checked against `apex schema --json` by the CLI CI lane. | Command | Purpose | | --- | --- | | `apex login` | Authenticate your account and pick an org/shop context. | | `apex logout` | Remove stored credentials. | | `apex whoami` | Show your identity, credential, and available org/shop context. | | `apex use` | Set the current Apex organization and shop context. | | `apex account` | Manage your Apex account. | | `apex account profile` | Show the current account profile. | | `apex account profile set` | Update the current account profile. | | `apex keys` | Manage shop API keys with a user credential. | | `apex keys list` | List API-key metadata for a shop. | | `apex keys create` | Create a shop API key and print its secret once. | | `apex keys revoke` | Revoke a shop API key. | | `apex webhooks` | Manage shop webhooks. | | `apex webhooks list` | List webhooks. | | `apex webhooks create` | Create a webhook. | | `apex webhooks update` | Update a webhook URL, events, or active state. | | `apex webhooks delete` | Delete a webhook. | | `apex webhooks deliveries` | List webhook deliveries including failure and disabled state. | | `apex webhooks test` | Send a signed test event to a webhook. | | `apex simulate` | Simulate data-layer triggers without emitting events. | | `apex forms` | Form and checkout analytics: tracked forms, field drop-off, errors and sessions. Needs the shop's form analytics switch. | | `apex forms list` | List tracked forms with status and coverage; the Shopify checkout is listed first and tracked automatically. | | `apex forms add` | Track a storefront form: ` --url --selector ` and exactly one of `--complete-on-goal`, `--complete-on-page` or `--complete-on-call`. | | `apex forms remove` | Stop tracking a storefront form; its history stays readable for 30 days. The checkout cannot be removed. | | `apex forms summary` | Views, starters, completions, abandons, checkout steps and lost revenue with its basis; filter with `--range`, `--split`, `--device`, `--country`. | | `apex forms fields` | Per-field time, returns, error share and drop-off; `--format` takes csv, json or table. | | `apex forms errors` | Validation errors by field and type with sessions and the share abandoned after; text shows only once seen in 20 sessions. | | `apex forms sessions` | Recent form sessions as field paths (never typed values); filter with `--outcome`, `--with-replay`, page with `--cursor`. | | `apex forms field` | One field's daily trend, time histogram, devices and errors. | | `apex forms label` | Give a field identity a readable label, or `--clear` it. | | `apex forms merge` | Report a retired field identity under the field that replaced it. | | `apex forms unmerge` | Undo a field merge. | | `apex forms detect` | Find the forms on one of the shop's pages (field metadata only) to pick a selector for `apex forms add`. | | `apex surveys` | Manage surveys. | | `apex surveys list` | List surveys; filter with `--status `. | | `apex surveys get` | Get a survey by ID. | | `apex surveys create` | Create a survey from inline JSON, `@file`, a file path, or `--file `; use one input form. See the survey body example below. | | `apex surveys update` | Update a survey by ID from inline JSON, `@file`, a file path, or `--file `; use one input form. See the survey body example below. | | `apex surveys start` | Start a survey; publication warnings print to stderr. | | `apex surveys pause` | Pause a survey. | | `apex surveys complete` | Complete a survey. | | `apex surveys duplicate` | Duplicate a survey. | | `apex surveys delete` | Delete a draft or completed survey; live and paused surveys return conflict exit code 5. | | `apex experiments` | Manage experiments. | | `apex experiments list` | List code-free summaries by default; filter effective targeting server-side with `--url `, use `--fields ` for selected columns, or `--full` for full details. | | `apex experiments get` | Get experiment details by ID or slug. JSON readback always carries started_at, scheduled_start_at and qa_approved_at as ISO-8601 UTC timestamps or null (all null on a fresh draft). | | `apex experiments results` | Read experiment results and timeseries by ID or slug. | | `apex experiments update` | Update an experiment | | `apex experiments create` | Create a complete experiment with page targeting, audiences, goals, and an optional QA preview URL. JSON readback always carries started_at, scheduled_start_at and qa_approved_at as ISO-8601 UTC timestamps or null (all null on a fresh draft). | | `apex experiments attach-idea` | Attach an experiment to an idea. | | `apex experiments detach-idea` | Detach an experiment from its idea. | | `apex experiments pause` | Pause a running experiment. | | `apex experiments complete` | Complete or stop an experiment. | | `apex experiments delete` | Delete an experiment. | | `apex experiments pull` | Pull the complete versioned experiment filesystem and its local receipt. | | `apex experiments status` | Read context, versions, QA, and handover next steps without writing. | | `apex experiments plan` | Plan experiment filesystem changes. | | `apex experiments apply` | Apply experiment filesystem changes to Apex; receipt files include submitted and stored digests and the named normalisation list. | | `apex experiments screenshots` | Force-assign every capture, including control, to its variation server-side, attach before/after QA evidence, and download files with `before-` / `after-` prefixes. Use `--no-label-pairs` for legacy filenames. | | `apex experiments qa` | Manage experiment QA runs from the CLI. | | `apex experiments qa status` | Show the latest experiment QA run (creates one only when none exists; reads never change updatedAt). | | `apex experiments qa probe` | Run saved commerce QA probes, or read evidence with `--saved`. | | `apex experiments qa events` | List QA-tagged goal and custom events for a QA session. | | `apex experiments qa check` | Update one QA checklist item with status, notes, and evidence. | | `apex experiments qa notes` | Update QA run notes or acceptance criteria summary. | | `apex experiments qa approve` | Approve QA and move to ready_to_launch; qa reopen returns it to QA. | | `apex experiments qa reopen` | Reopen QA and clear scheduled launch state. | | `apex experiments qa evidence` | Attach screenshot jobs or upload typed QA evidence. | | `apex results` | Import and read past test results. | | `apex results import` | Inspect, preview, and import CSV or JSON result files. | | `apex results list` | List imported and native past results, with source filters and pagination. | | `apex results get` | Read one past result by legacy ID or UUID. | | `apex results delete` | Permanently remove one imported past result after confirmation. | | `apex commerce` | Commerce catalog and price tools. | | `apex commerce catalog` | Look up products for commerce drafts. | | `apex commerce catalog search` | Search the commerce product catalog. | | `apex commerce catalog get` | Get one product and its variants. | | `apex commerce price` | Read and bulk-edit selection prices. | | `apex commerce price export` | Export effective prices as CSV. | | `apex commerce price import` | Import price overrides with revision checks. | | `apex commerce definition` | Read and save the canonical commerce definition. | | `apex commerce definition get` | Read a commerce.json definition envelope. | | `apex commerce definition put` | Save a commerce.json definition with revision checking. | | `apex push` | Push experiment definitions from config to the API. | | `apex preview` | Open a QA preview URL for an experiment variation; proxy previews return a QA run receipt. | | `apex roadmap` | Work with approved roadmap ideas and build handoffs. | | `apex roadmap approved` | List approved roadmap ideas ready to build in Apex. | | `apex roadmap handoff` | Get apex.roadmap_handoff.v1 JSON: contract, schema_version, item, experiment, instructions, goals, mockups, acceptance_criteria, harness, scoring, source, metadata. | | `apex harness` | Develop AB Test Harness projects through Apex live QA sessions. | | `apex harness init` | Create `apex.harness.json` for a harness folder. | | `apex harness dev` | Build, upload, preview, and watch a harness folder. | | `apex harness publish` | Verify, build, upload, and publish a harness artifact. | | `apex workspace` | Check out, preview, and publish Apex draft workspace files. | | `apex workspace checkout` | Check out an Operator workspace for one variation. | | `apex workspace status` | Show changed local workspace files. | | `apex workspace dev` | Upload one preview version or watch workspace files. | | `apex workspace publish` | Build and publish changed workspace files back to Apex. | | `apex tracking` | Create tracking ingestion manifests and QA handoffs. | | `apex tracking health` | Read the shop's tracking-health checks (install, events, orders linked, currency, exposure, SRM by device, selectors, snippet size, settings changes); exits 0 for pass, 1 for warn or fail, 3 for unknown. | | `apex tracking inspect` | Create a local tracking manifest from funnel URLs; its output path is relative to the current directory. | | `apex tracking plan` | Print the implementation plan from a tracking manifest path relative to the current directory. | | `apex tracking qa` | Print the QA checklist for a tracking manifest path relative to the current directory. | | `apex tracking verify` | Verify live tracking events, filter by `--event-type`, and preview event properties. | | `apex tracking latency` | Compare forced-preview and natural page-load decision latency before launch (init events only, excluding route changes); accepts an experiment ID or key, `--days` (1–30), and `--qa-session`. | | `apex tracking manifest` | Print the shop's tracking manifest, including its tracked forms (key, type, selector, completion rule). | | `apex events` | Export raw tracking events. | | `apex events export` | Export every raw event in a window of at most 31 days that does not end in the future, with automatic pagination. | | `apex goals` | Manage conversion and revenue goals. | | `apex goals list` | List goals. | | `apex goals create` | Create a goal. | | `apex goals update` | Update a goal. | | `apex goals test` | Dry run a goal URL pattern against traffic. | | `apex goals suggest` | Suggest goals and funnels from analytics. | | `apex funnels` | Manage analytics funnels. | | `apex funnels list` | List funnels. | | `apex funnels create` | Create a funnel. | | `apex funnels update` | Replace a funnel's steps. | | `apex funnels delete` | Delete a funnel. | | `apex funnels run` | Run funnel analysis. | | `apex funnels validate` | Validate funnel steps without writing. | | `apex environments` | Manage runtime environments and domain mappings. | | `apex environments list` | List runtime environments and mapped domains. | | `apex environments set-domain` | Map a domain to a runtime environment. | | `apex environments remove-domain` | Remove a domain mapping. | | `apex consent` | Manage consent gating. | | `apex consent get` | Show consent settings. | | `apex consent set` | Enable and configure consent gating. | | `apex consent off` | Disable consent gating. | | `apex triggers` | Manage data-layer triggers and temporary setup capture. | | `apex triggers get` | Read the shop's runtime triggers. | | `apex triggers set` | Replace the shop's runtime triggers from a JSON file. | | `apex triggers propose` | Propose candidate triggers from a discovery report. | | `apex triggers apply-recipe` | Apply a server-owned trigger recipe. | | `apex triggers enable-capture` | Enable temporary setup capture. | | `apex triggers disable-capture` | Disable setup capture and clear its expiry. | | `apex project-code` | Inspect and safely change shop project code. | | `apex project-code get` | Read complete code and its opaque version. | | `apex project-code diff` | Preview a partial JSON change without writing. | | `apex project-code set` | Create a backup before a version-checked write. | | `apex domains` | Manage install domains and landing-page domains. | | `apex domains list` | List install domains. | | `apex domains add` | Add install domains. | | `apex domains remove` | Remove install domains. | | `apex domains set` | Replace install domains. | | `apex domains claim` | Claim a client hostname for landing pages; prints the CNAME to the Apex edge (`--wait` checks for up to two minutes). | | `apex domains register` | Register a new domain on the Drip Cloudflare account after a price confirmation (`--confirm-price` without a terminal). | | `apex domains status` | List landing-page domains, or check one hostname now. | | `apex domains unclaim` | Remove a landing-page domain from Apex; a registered domain stays on the Drip account. | | `apex handover` | Manage cross-domain identity handover. | | `apex handover get` | Show cross-domain handover settings. | | `apex handover set` | Enable cross-domain identity handover. | | `apex handover off` | Disable cross-domain handover. | | `apex handover qa` | Check static link/form handover parameter carriage. | | `apex analytics` | Read shop analytics with a secret API key. | | `apex analytics order-journey` | Inspect bounded, read-only order evidence and attributed-goal eligibility. | | `apex analytics stats` | Show dashboard analytics stats. | | `apex analytics timeseries` | Show analytics timeseries. | | `apex analytics traffic` | Show traffic sources. | | `apex analytics pages` | Show page analytics. | | `apex analytics geo` | Show geography and device analytics. | | `apex analytics sessions` | Show session analytics. | | `apex analytics realtime` | Show realtime analytics. | | `apex analytics entry-exit` | Show entry and exit page analytics. | | `apex analytics consent-rate` | Estimate analytics consent acceptance from visitor series. | | `apex analytics urls` | Show storefront URL inventory from pageview analytics. | | `apex install` | Check Apex storefront installation. | | `apex install assess` | Assess a public storefront URL and draft an installation assessment and client guide. `--shop ` fills the snippet shop ID; `--format` selects text, Markdown or JSON. | | `apex install kit` | Generate a ready-to-send installation guide as Markdown or JSON. | | `apex install check` | Verify snippet, served config (including purchase triggers), and recent tracking. `--shop-id` also selects the shop for API requests; otherwise the selected shop or `DRIP_SHOP_ID` is used. | | `apex install steps` | Show each installation step (script, consent, test environments, orders, firewall access) with when Apex first and last saw it; exits 6 while a step is waiting. `--live` also checks the storefront page and firewall access. | | `apex flags` | Manage flags. | | `apex flags list` | List flags. | | `apex flags get` | Get a flag. | | `apex flags status` | Check whether a flag publication is live at the edge. | | `apex flags create` | Create a flag. | | `apex flags update` | Update a flag. | | `apex flags update-code` | Edit On variant JS/CSS from files with an optimistic version check. | | `apex flags rules` | Replace or reorder audiences. | | `apex flags rules set` | Replace the live audience list and archive omissions. | | `apex flags rules reorder` | Reorder all live audiences with a parent version check. | | `apex flags experiences` | Manage rule experiences. | | `apex flags experiences create` | Create a rule experience from JSON. | | `apex flags experiences update` | Update a rule experience from JSON. | | `apex flags experiences delete` | Delete an experience when no active or historical rule references it. | | `apex flags activate` | Activate a flag. | | `apex flags pause` | Pause a flag. | | `apex flags archive` | Archive a flag. | | `apex backlog` | Manage backlog. | | `apex backlog list` | List backlog items identified as apex.backlog_item.v1. | | `apex backlog get` | Get a backlog item identified as apex.backlog_item.v1. | | `apex backlog create` | Create a backlog item. | | `apex backlog update` | Update a backlog item. | | `apex backlog archive` | Archive a backlog item with a decline reason. | | `apex backlog restore` | Restore an archived backlog item. | | `apex backlog delete` | Permanently delete a backlog item after confirmation. | | `apex backlog fields` | Manage the shop's own idea fields. | | `apex backlog fields list` | List the shop's idea fields. | | `apex backlog fields create` | Add an idea field. | | `apex backlog fields update` | Update an idea field. | | `apex backlog fields archive` | Archive an idea field; values already on ideas are kept. | | `apex signals` | Manage signals. | | `apex signals list` | List signals; pass `--include-archived` to include archived signals. | | `apex signals get` | Get a signal. | | `apex signals create` | Create a signal. | | `apex signals update` | Update a signal. | | `apex signals delete` | Delete or archive a signal. | | `apex personalizations` | Manage personalizations. | | `apex personalizations list` | List personalizations. | | `apex personalizations get` | Get a personalization. | | `apex personalizations create` | Create a personalization. | | `apex personalizations update` | Update a personalization. | | `apex personalizations activate` | Activate a personalization. | | `apex personalizations pause` | Pause a personalization. | | `apex personalizations archive` | Archive a personalization. | | `apex external-pixels` | Manage external pixel integrations. | | `apex external-pixels snippet` | Generate an external purchase pixel snippet. | | `apex assets` | Manage assets. | | `apex assets list` | List assets. | | `apex assets get` | Get an asset. | | `apex assets upload` | Upload an asset file. | | `apex assets delete` | Delete an asset. | | `apex landing-pages` | Build landing pages with your own tools, then deploy, track and test them with Apex. | | `apex landing-pages list` | List landing pages. | | `apex landing-pages get` | Get a landing page. | | `apex landing-pages create` | Create a landing page to build outside Apex. | | `apex landing-pages pull` | Pull a landing page into a checkout folder. | | `apex landing-pages plan` | Show what applying a checkout would change. | | `apex landing-pages apply` | Apply a checkout as a new version. | | `apex landing-pages preview` | Print a signed preview link (valid 24 hours). | | `apex landing-pages deploy` | Deploy a version to the shop's apex-page.com address or a domain it holds. | | `apex landing-pages versions` | List versions. | | `apex landing-pages rollback` | Deploy an earlier version to the page's current address. | | `apex landing-pages experiment` | Create a draft experiment that splits the live page between two versions. | | `apex landing-pages open` | Print the page's dashboard URL. | | `apex pages` | Manage pages. | | `apex pages list` | List pages. | | `apex pages create` | Create a page; `--help` lists body keys, pattern shapes contradicting `match_type` fail with `match_type_mismatch` unless `--literal` is passed, and `apex schema --json` carries the body schema. | | `apex pages update` | Update a page; `--help` lists body keys, pattern shapes contradicting `match_type` fail with `match_type_mismatch` unless `--literal` is passed, and `apex schema --json` carries the body schema. | | `apex pages delete` | Delete a page. | | `apex segments` | Manage segments. | | `apex segments list` | List segments. | | `apex segments create` | Create a segment. | | `apex segments update` | Update a segment. | | `apex segments delete` | Delete a segment. | | `apex exclusion-groups` | Manage exclusion groups. | | `apex exclusion-groups list` | List exclusion groups. | | `apex exclusion-groups get` | Get an exclusion group. | | `apex exclusion-groups create` | Create an exclusion group; `--help` shows the request body and `apex schema --json` exposes it under the command's `body` key. | | `apex exclusion-groups update` | Update an exclusion group with the same documented body shapes. | | `apex exclusion-groups delete` | Delete an exclusion group after confirmation; use `--force` in non-interactive environments. | | `apex exclusion-groups replace-members` | Replace exclusion group members. | | `apex brand` | Print shop brand context for external agents. | | `apex targeting` | Test experiment targeting. | | `apex targeting test` | Evaluate experiment URL targeting for a page. | | `apex audit-log` | Inspect sensitive audit logs. | | `apex audit-log list` | List audit logs with entity filters and offset pagination. | | `apex inbox` | Read and send inbox messages. | | `apex inbox list` | List account or experiment inbox messages. | | `apex inbox send` | Send an account or experiment inbox message. | | `apex inbox mark-read` | Mark one inbox channel, or all channels, read. | | `apex shops` | Manage organization shops. | | `apex shops list` | List organization shops. | | `apex shops get` | Get a shop. | | `apex shops create` | Create a shop. | | `apex shops update` | Update a shop. | | `apex shops doctor` | Check shop configuration and the three-state purchase-capture battery against observed analytics. | | `apex shops auto-stop` | Read or update auto-stop defaults. | | `apex shops testing-defaults` | Read or update testing defaults. | | `apex shops guardian` | Read or update guardian defaults. | | `apex shops store-profile` | Read and review a store profile. | | `apex shops store-profile get` | Read the store profile. | | `apex shops store-profile review-fact` | Review a store-profile fact. | | `apex shops store-profile review-finding` | Review a semantic finding. | | `apex shops store-profile start-learning-run` | Start a store-profile learning run. | | `apex shops shopware` | Manage Shopware bindings. | | `apex shops shopware prebind` | Bind a Shopware store URL before installation. | | `apex discover` | Read observed tracking shapes and optionally mine a public GTM container. | | `apex schema` | Print the CLI command, flag, and mandatory-option schema and the bundled experiment filesystem file contract. | | `apex skills` | Print or install Apex CLI agent skill files. | | `apex org` | Manage the organization bound to the user credential (`apex orgs` is an alias). | | `apex org get` | Get organization details. | | `apex org update` | Update organization settings. | | `apex org members` | List organization members. | | `apex org invite` | Invite an organization member; the default shop role is `editor` (workspace defaults are internal roles). | | `apex org revoke-invite` | Revoke an organization invitation. | | `apex org set-role` | Change an organization member role. | | `apex org suspend-member` | Suspend an organization member immediately; accepts `--reason` and requires confirmation unless `--force` is passed. | | `apex org reinstate-member` | Reinstate a suspended organization member; requires confirmation unless `--force` is passed. | | `apex org remove-member` | Remove an organization member after confirmation. | | `apex team` | Manage shop-scoped team membership (`apex teams` is an alias). | | `apex team members` | List members for `--shop` or the selected shop. | | `apex team invite` | Invite a member to `--shop` or the selected shop as `editor` or `client_viewer` (Viewer). `--design-approver` also lets them sign off designs once they accept (a Viewer with it is a Client approver); it needs store owner or admin rights. | | `apex team revoke-invite` | Revoke a shop invitation. | | `apex team set-role` | Change a shop member role to `editor` or `client_viewer` (Viewer). | | `apex team remove` | Remove a shop member after confirmation. | | `apex notifications` | Manage notification settings. | | `apex notifications ready` | Configure Ready to launch email recipients and the existing Slack event switch. | | `apex notifications ready get` | Read email and Slack settings for `--shop` or the selected shop. | | `apex notifications ready email` | Choose current shop members to receive ready emails. | | `apex notifications ready email set` | Replace recipients using `--recipients a@example.com,b@example.com`. | | `apex notifications ready email add` | Add current shop members by email. | | `apex notifications ready email remove` | Remove current shop members by email. | | `apex notifications ready email off` | Clear the email recipient list. | | `apex notifications ready slack` | Select the ready event on an existing Slack destination. | | `apex notifications ready slack on` | Enable the ready event; preserve other events and the destination pause state. | | `apex notifications ready slack off` | Disable only the ready event. | | `apex upgrade` | Install the latest Apex CLI (`--dry-run` prints the command). | ## Survey body example Save this body as `survey.json`, then use `apex surveys create --file survey.json` or `apex surveys update --file survey.json`. Both commands also accept the body as inline JSON, `@survey.json`, or a positional file path. Providing a positional body together with `--file` is an error. ```json { "name": "Product feedback", "questions": [{ "type": "single_choice", "prompt": "What would help you decide?", "options": [{"label": "More details"}, {"label": "Reviews"}, {"label": "Delivery info"}] }], "targeting": {"url": [{"type": "simple", "pattern": "/products/", "matchType": "contains"}]}, "sampling_rate": 0.5, "frequency_cap": "once_per_visitor", "card_style": "quiet", "answer_goal": 100 } ``` On create, `name` is required. Server defaults: `questions: []` (each question's `required: true`), `targeting: null` (all pages/visitors), `sampling_rate: 1`, `frequency_cap: "once_per_visitor"`, `card_style: "quiet"`, `answer_goal: null`, `environment_key: "production"`, `placement: "sdk_page"`, `active_from: null`, and `active_to: null`. Update accepts a partial body and keeps omitted fields unchanged. Run `apex surveys create --help` or `apex surveys update --help` for field limits and supported values. ## Platform administrator commands These commands require a platform user API key and are intentionally excluded from the public command index. | Command | Purpose | | --- | --- | | `apex admin shops archive ` | Archive a shop and disable Apex delivery; use `--force` in non-interactive environments. | | `apex admin shops restore ` | Restore an archived shop while leaving its runtime kill switch unchanged. | | `apex admin shops settings list --shop ` | Read every registered per-shop setting, value, override and group with a platform user API key. | | `apex admin shops settings get --shop --key ` | Read one setting; unknown keys list the available registry keys. | | `apex admin shops settings set --shop --key --value ` | Resolve and pin the shop, then make an audited write. Booleans accept on/off/true/false; numbers must be finite; null clears nullable settings. Sensitive writes require `--confirm`; optional `--reason` and `--expected-version` preserve audit context and concurrency. | ## Analytics reads `apex analytics` requires a secret API key with `analytics:read` (or `analytics:*`). Publishable keys cannot access shop analytics. Windowed commands accept `--days`; `pages` also accepts `--limit`. All commands support the standard `--json` envelope. ## Related pages - [Install and authenticate the Apex CLI](/docs/cli/install-auth) - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) - [Agent tools: SOS, brand, schema, and skills](/docs/cli/agent-tools-sos-brand-schema-skills) ## Project code ```sh apex project-code get --shop example.com --json apex project-code diff --file project-code.json --shop example.com --json apex project-code set --file project-code.json --shop example.com --backup previous-code.json --json ``` The input is a partial projectCode object, for example `{ "css": "body { color: red; }" }`. Supported fields are `enabled` (boolean) and `css`, `helperJs`, and `js` (strings or null). Each code field permits at most 20,000 characters. Omitted fields remain unchanged. Null or blank code clears that field. Unknown fields and empty objects are rejected. GET returns complete code, shop identity, and an opaque version. DIFF returns effective before/after values and changed fields without writing. SET creates a new backup file with complete previous code, identity, capture time, and the exact version before PATCH. It never replaces an existing backup. Backup failures prevent the write. An effective no-op returns `unchanged`, without a PATCH or backup file. The backed-up version becomes expectedVersion unchanged. A conflict returns exit 5, keeps the backup, and does not retry. Read the latest state and preview it before a new attempt with a new backup path. Authentication uses exit 2; invalid input uses exit 3. A successful write returns `saved`, its diff, backup path, current version, writtenVersion, and precompile result. A failed precompile is a saved-write warning. All warnings remain in JSON and are also written to stderr. User keys resolve domains or pin UUID context; shop keys must match the requested identity before any backup or write. ### Order journey diagnostics `apex analytics order-journey --platform shopware --order-id --environment production --from --to ` requires a secret or authorized user key with `analytics:read`. Use `--order-number` instead of `--order-id`, never both. The interval is at most 31 days. `--include-qa` includes QA in eligibility; bots remain excluded. `--json` retains the sanitized report, every represented assignment, source availability, coverage and the integration verdict. Exit 0 means the diagnostic was fetched, even when its verdict is `fail` or `inconclusive`. Eligibility means only `eligible_for_attributed_goal_read`, with basis `attributed_goal_counts`. Final statistical contribution is `not_evaluated`. Unknown consent, freshness, missing evidence and failed reads never mean zero or healthy. No proof file is saved. Order journey diagnostics report current, stale, or unknown freshness for each retained receipt and assignment. Present attributed-goal reads identify their evaluated snapshot; they do not establish historical contribution or repair missing producer evidence. ## Error recovery and update notices JSON errors keep the numeric `error.code` exit code and add `error.reason`, `error.next_steps` (an array of `{ command, why }`), and optional `error.request_id`. The request ID comes from `x-request-id`, falling back to `x-vercel-id` (the Apex app) and then `cf-ray`; include it when reporting an API failure. API-provided reason codes are preserved verbatim. Otherwise reasons are `auth_required`, `auth_forbidden`, `shop_context_required`, `shop_context_mismatch`, `install_check_failed`, `validation_failed`, `feature_disabled`, `confirmation_required`, `conflict`, `not_found`, `rate_limited`, `network_timeout`, `network_connect_timeout`, `network_dns`, `network_refused`, `network_reset`, `network_tls`, `network_unknown`, `local_file_error`, or `internal_error`. Inspect `next_steps` for login, key permissions, shop selection, network settings, folder validation, disabled-feature contacts, request-field issues, or rate-limit recovery. Body validation failures carry per-field issues in `error.details.issues`; disabled features name their shop setting. Steps are suggestions, never automatic approval to retry a write. Confirmation errors retain `error.details.retry`. Human output prints recovery steps as `Next:` lines on stderr and request IDs on the error line. Trigger commands now follow the same exit codes as every other command: authentication failures exit **2** (previously 4), invalid local input exits **3** (previously 2), and other API failures follow their HTTP status (validation **3**, conflict **5**, anything else **1**). The `TRIGGERS_AUTH`, `TRIGGERS_CONFIG_ABSENT` and `TRIGGERS_NO_DATA` verdict prefixes are unchanged, so scripts can key on them instead of exit codes. Every JSON envelope may carry `update` and `warnings`. The update object includes `current`, `latest`, optional `minimum`, `required`, `breaking`, `command`, and `notes_url`; warnings use `{ code, message }`. The exact install command is `npm install -g @drip-apex/apex-cli@latest`, and `apex upgrade` runs it (`--dry-run` only prints it). Set `APEX_NPM_BIN` to choose the npm binary. The npm latest version is cached for 24 hours (failed lookups for one hour). Server latest/minimum policy is cached for seven days, so offline commands still enforce a known floor. `APEX_NO_UPDATE_CHECK=1` disables the npm lookup and optional update notice, but never hides a required server-floor upgrade. ## Tracking verification limits `apex tracking verify` reads at most 200 of the newest events from a window of at most 7 days. When the limit is reached, `truncated` is true and totals describe that sample only. The response includes the exact `from`, `to`, `minutes`, and `limit` window. ## Commerce catalog - `apex commerce catalog search [query] [--cursor ] [--limit <1-10>]` - `apex commerce catalog get [--cursor ]` - `apex commerce catalog get --product-id [--cursor ]` Use `--json` for the complete API envelope. Human output lists variant GID, title, SKU, price, cost (`unknown` when absent), and price-test exclusion. ## Commerce results output Human-readable experiment results show a **Profit per visitor (CM1)** section for commerce tests and checkout-owned tests with attributed orders. Null money prints as `-`; degraded reads print their reason and never imply measured zeroes. JSON output preserves the API's `commerce_results` field unchanged, including `pending_days`, `plane`, `decision_grade`, and `confidence_visible`. Checkout-owned tests measure orders from visitors who saw the checkout message and receive the supplementary CM1 section when they have attributed orders. Other non-commerce tests return `commerce_results: null`. An unavailable supplementary section on a checkout-owned test does not mark the analytics as degraded. ## Import past test results `apex results import --file ` uploads one or more CSV or JSON files, inspects their columns, and prints a deterministic preview before confirmation. Use `--dry-run` to stop after preview and `--yes` for non-interactive confirmation. `--json` preserves full learning text and returns the preview envelope for dry runs or merged `{ imported, skipped, test_results }` totals after import. ## Read past test results `apex results list [--source import|native|all] [--limit ] [--all] [--json]` lists past results for the selected shop (default source `all`, page size 50, maximum 200). Use `--all` to follow every page. The table shows legacy ID (or UUID), title, outcome, metric, lift, sample size, dates, and source. Empty lists print `No test results.` `apex results get [--json]` opens a result by legacy ID, test key, or UUID, including its full learning, confidence, dates, page, and device. Missing results exit with code 1 through the existing API failure path. Both commands use shop or user keys with `experiments:read` and verify each row belongs to the selected shop. Imports show their source name and original date range, falling back to import time; native results show source `apex` and creation time. `apex results delete [--yes] [--json]` permanently removes one imported result with `experiments:write`. It first reads and previews the title, outcome, dates, source, and legacy ID. Native results are refused locally (exit 3). Confirm the prompt or pass `--yes`; non-TTY callers without `--yes` receive exit 4 with the exact rerun hint. Removed results cannot be restored in Apex; the full record remains in the audit log. JSON output wraps the API `{ data }` response and verifies the echoed shop. JSON uses the standard CLI envelope: its `data` contains `{ data, pagination }` for lists (merged rows and final pagination with `--all`) or `{ data: result }` for a single result. Invalid `--source` or `--limit` exits 3 before an HTTP request. ## Asset delivery URLs `apex assets upload ` returns `delivery_url`; embed that permanent URL in variation code. The `public_url` is a signed read URL for the API caller and expires after 300 seconds. Never embed it. Delivery is anonymous once the referencing experiment or snippet is published, and resolves through the owning shop's dashboard session or a QA preview before that. Only the absolute https form is recognized as an owned reference; protocol-relative or http forms are not granted at publication. See [Commerce prices](/docs/cli/commerce-prices) for CSV export, import, and revision semantics. ## Commerce definition `apex experiments pull ` writes a top-level `commerce.json` when the experiment has a canonical commerce definition. `plan` shows both the definition change and its derived checkout carriers, warnings, and commerce launch preview. `apply` validates and saves the definition and sibling variation files together. ```json { "revision": 7, "definition": { "schemaVersion": 2, "baseCurrency": "EUR", "versions": { "control": "control", "order": ["control", "sale"] }, "levers": {} }, "serverOwned": { "readonly": true } } ``` Edit `definition.levers` to change or remove a lever. Keep the positive integer `revision` from your pull. A stale revision returns 409 `commerce_definition_revision_conflict`; pull again and reconcile your edit. `serverOwned` is read-only, ignored on save, and recompiled by the server. Edited server output adds the warning `commerce_server_owned_ignored`. ```bash apex commerce definition get [file] apex commerce definition put apex commerce definition put --create ``` `get` writes the same envelope as pull, to the file or stdout. `put` sends only `definition` and `expectedRevision` from the file. `--create` sends a null revision and refuses if a row already exists. Refused writes print the 409 or 422 error code and any blocking issues. A legacy definition pulls as `{ "revision": 7, "legacy": true, "carriers": {} }`. It is read-only. Replace the entire envelope with `{ "revision": 7, "definition": { ... } }` to request a validated replacement. Replacement reports `legacyReplaced`, `carriersEqual`, and `changedCarriers` (variation and action). Existing status and representability guards still apply. Deleting `commerce.json` is refused; remove a lever by omitting it from `definition.levers`. For one release, experiments without a definition continue to pull step 1 files under `commerce/`. Unchanged files stay unchanged. Editing a supported lever file converts the set to a definition, with `commerce_files_v1_converted`, and creates its row only if none exists. Opaque read-only files cannot be converted. Never mix `commerce.json` and step 1 price, shipping, offer or post-purchase files. `commerce/discount-codes.json` stays separate and may coexist with either format. A price version containing exactly one all-scope integer percent rule from -90 to -1, with no ending or overrides, remains a percent arm. Its charged and displayed price follows the baseline captured at launch. Other price rules compile into fixed prices from the resolved baseline. Unchanged levers preserve their carrier revision, checksum and server-owned launch evidence. ## Raw artifact saves and `apex push` Authenticated artifact and workspace reads return `commerceRevision` alongside the artifact hash. Keep both from the read through the edit. A changed price, shipping, offer or post-purchase carrier needs that revision and the original artifact hash for each existing arm explicitly patched. `null` means create only. A missing token never means “use the latest revision.” `apex push` accepts the tokens in `.drip.config.ts`: ```ts export default [{ slug: "spring-sale", name: "Spring sale", commerce: { revision: 7 }, variations: [ { slug: "control", name: "Control", weight: 0.5, isControl: true, artifactHash: "hash-from-control-read" }, { slug: "sale", name: "Sale", weight: 0.5, artifactHash: "hash-from-sale-read", mutations: editedMutations }, ], }]; ``` For a fresh commerce experiment, use `commerce: { revision: null }`. An optional `commerce.definition` supplies the complete canonical v2 definition. Omit `mutations` to retain an existing arm's mutations; `mutations: []` deliberately removes them. DOM-only saves and unchanged carrier echoes do not require a new commerce token or change its revision. Discount-code rules remain independent. Workspace builds through Operator or MCP accept `expectedCommerceRevision`, `expectedArtifactHash`, `expectedWorkspaceRevision` and `expectedFileVersions`. File writes retain their existing file-version precondition. On HTTP 409, review the new state before reapplying your edit. The CLI does not retry with a fresh commerce revision. Unsupported raw changes return HTTP 422 with paths. Raw edits to migrated legacy commerce remain read-only; use canonical definition replacement for an explicit supported replacement. ## Disabled option reservations The schema reserves the options below for later implementation. They are **not available**: explicitly supplying a disabled option, including an empty object, empty array or its effective fallback, returns `commerce_definition_feature_unavailable` at the concrete JSON pointer. For example, `display: { unitPrice: "keep" }` refuses at `/definition/levers/price/display/unitPrice`; `display: {}` refuses at `/definition/levers/price/display`. Invalid values and unknown keys remain schema errors. Omit these fields to keep existing behavior. The internal `commerceOptions` registry in `apps/web/src/lib/commerce/definition/options.ts` holds each typed schema, effective fallback and availability flag. Fallbacks apply only after the owning unit implements the option. They are never inserted into canonical JSON. `priceFloor` is already supported and remains enabled, with its existing `missingCost: "block"` and `belowCost: "warn"` parse defaults when the object is supplied. An omitted floor stays omitted. Paths below are relative to `definition`. `{v}` means a concrete version slug and `{i}` a rule index. | Registry row | Mount | Effective fallback after activation | Activation owner | | --- | --- | --- | --- | | `mode` | `/mode` | `test` | #11397 | | `audience` | `/audience` | `holdoutPercent: 0`, `globalHoldout: respect` | #11397 | | `resultsMetric` | `/results/metric` | `auto` | #11432 | | `resultsBreakdownDimension` | `/results/breakdownDimension` | `channel` | #11422 | | `profitPlanning` | `/planning` | metric `profit`, MDE `0.10`, baseline `28` days, max `42` days | #11423 | | `reportingSale` | `/reporting/sale` | baseline `auto`, last-year alignment `weekday` | #11424 | | `priceDisplay` | `/levers/price/display` | absent preserves legacy display | #11412 display plumbing, later slice | | `unitPrice` | `/levers/price/display/unitPrice` | `update` | #11413 | | `soldOut` | `/levers/price/display/soldOut` | `test_price` | #11415 | | `priceHoldMs` | `/levers/price/display/priceHoldMs` | `2000` | #11416 | | `badge` | `/levers/price/display/badge` | `hide` | #11414 | | `versionBadge` | `/levers/price/versions/{v}/badge`, method `price` only | inherit display badge, then `hide` | #11414 | | `priceFeed` | `/levers/price/feed` | version derived by owner from mode/prices; compare-at `before`, fixed-price markets `skip` | #11398 | | `priceFeedRouteClicks` | `/levers/price/feed/routeClicks` | sources `[google_shopping]`, existing visitors `move`, source match defaults below | #11399 | | `pricePlane` | `/levers/price/plane` | `auto` | #11402, #11405 | | `priceChargeVia` | `/levers/price/chargeVia` | `auto` | #11427 | | `priceMerchantDiscounts` | `/levers/price/merchantDiscounts` | `stack` for price, `best_for_customer` for amount-off | #11427 | | `offerMerchantDiscounts` | `/levers/offer/merchantDiscounts`, every offer type | `best_for_customer` | #11427 | | `amountOffDisplay` | `/levers/price/amountOffDisplay` | `badge` | #11429 | | `priceConditions` | `/levers/price/versions/{v}/conditions`, methods `price` and `amount_off` | subtotal basis `cart`, quantity basis `in_scope`; no thresholds | #11392 | | `priceEnding` | `/levers/price/versions/{v}/rules/{i}/ending/value` | no ending | #11393 | | `discountCodes` | `/levers/discountCodes` | absent preserves existing independent legacy carriers | #11391 | | `selectionAll` | `/levers/price/products/select/all` | `false` predicate on omission; wire accepts only `true` | #11425 | | `selectionExcludeTags` | `/levers/price/products/select/excludeTags` | `[]` | #11394 | | `selectionExcludeCollections` | `/levers/price/products/select/excludeCollections` | `[]` | #11394 | | `selectionExcludeSkus` | `/levers/price/products/select/excludeSkus` | `[]` | #11394 | | `selectionBundles` | `/levers/price/products/select/bundles` | exclude `parents_and_components`, empty marker lists | #11396 | | `selectionIncludeSkus` | `/levers/price/products/select/includeSkus` | absent, no SKU inclusion source | #11395 | | `priceFloor` (enabled) | `/priceFloor` | absent evaluates below-cost policy `warn` | #11409 | Badge values reserve `hide`, `theme`, or a strict custom object with required `text` (1–40 characters, `{percent}` and `{amount}` tokens), optional `translations`, `textColor`, `backgroundColor`, `position` and `surfaces`. Custom colors use `#RRGGBB`; effective colors are white on black, position `theme_slot`, and surfaces product, card, quick view and search. These effective custom defaults and version inheritance belong to the badge implementation. Feed routing reserves optional `sources`, `match` and `existingVisitors`. `match` has only `google_shopping` and `email` arrays of `{param, equals?}`. The effective Google match defaults are `srsltid` presence and `utm_medium=shopping`; email defaults are `utm_medium=email` and `_kx` presence. The feed and routing rows have independent availability gates, as do the display container and its four leaves. Enabling a container does not enable its leaves. The independent discount-code lever reserves optional `scope` (`assigned` or `all_carts`, effective `assigned`) and `versions.{v}` as `{unchanged:true}` or `{policy, except?, messages?, fallbackLanguage?}`. Policy is `allow_all`, `block_all` or `block_except`; exceptions contain optional `codes` and `prefixes`. Messages map up to 20 language keys to 1–120-character strings; `fallbackLanguage` belongs to the policy version and is effectively `en`. Version keys must match `versions.order`. This reservation activates neither code blocking nor a migration of `commerce/discount-codes.json`. The older price-version `discountCodes` field remains refused. Selection `all:true` cannot accompany a nonempty `collections`, `productTypes`, `vendors`, `tags` or `includeProducts` selector. Existing `excludeProducts` behavior is unchanged. Bundle markers reserve optional tag and product-type arrays and metafield `{namespace,key}` pairs. Conditions reserve optional `minimumSubtotal`, `subtotalBasis`, `minimumQuantity` and `quantityBasis`. Empty-condition, matching and SKU-composition behavior remains with the activation owners. Existing `struckThrough`, `alreadyDiscounted`, price, amount-off, offer and shipping behavior is unchanged. Endings `0.95`, `0.99` and `0.00` remain supported; other two-digit endings validate but refuse at `ending/value` until #11393 activates them. No display sidecar, new carrier field, Shopify read or renderer is emitted by these reservations. Owners enable only their own rows with implementation and tests; changing availability alone does not implement a capability. ## Commerce prices Export the effective prices of a canonical commerce definition: ```bash apex commerce price export prices.csv apex commerce price export > prices.csv ``` Without a file, stdout contains CSV even when piped. An explicit `--json` returns `{ data: { csv, revision, contentType } }` inside the standard CLI JSON envelope. The API uses `GET /api/v1/experiments/{id}/commerce/price/csv` and returns that JSON body with a quoted revision in the `ETag` response header. Imports use `PUT` at the same path with a `text/csv` body and `If-Match` header. The CSV contains `product_id,variant_id,sku,product_title,variant_title,base_price` followed by each editable price version slug in definition order. Prices use the base currency's minor units. Compare-at versions are read-only and omitted. Edit price cells, then import: ```bash apex commerce price import prices.csv apex commerce price import prices.csv --expected-revision 7 ``` Import reads the current definition revision first unless you provide `--expected-revision`. It sends that revision as `If-Match`. Use the revision from the definition you exported when you need protection against edits since that export. A stale revision refuses with `commerce_definition_revision_conflict`. Keep the header and version order unchanged. `variant_id` identifies the row; other informational columns are ignored. An empty cell or a value equal to the rules' price removes its override. Other values become overrides. Exporting and importing unedited prices makes no write. Imports print whether anything changed, override counts, and the resulting revision. Unknown or excluded variants, malformed money, duplicate variants, and files larger than 2 MB refuse without saving. A refusal exits nonzero and includes the server's code and message. ## Experiment filesystem and QA commands The experiment filesystem workflow is for complete experiment edits. It pulls supported experiment files to disk, tracks file hashes in `.apex/experiment-filesystem.json`, plans changes through Apex, and applies only supported file paths. ## Choosing a workflow The default loop for agents building or editing a test is `apex experiments pull`, edit supported files, `apex experiments plan`, `apex experiments apply`, then `apex experiments qa status`. | Workflow | Use when | What it writes | Do not use when | | --- | --- | --- | --- | | `apex push` | Syncing config-as-code goals, targeting, and flags by slug. | Server configuration declared in `.drip.config.ts`. | Editing one test's variation files or QA state. | | `apex experiments pull/plan/apply` | An agent is building or editing a test. This is the default. | Supported experiment files and server draft state after review of the plan. | You need a live operator code workbench. | | `apex workspace` | An operator needs a live-preview draft for one variation. | Workspace draft files and published draft variation code. | You need the default full-experiment agent loop. | | `apex harness` | An external `ab-harness` project needs build and QA integration. | External harness project artifacts and QA sessions. | Apex owns the experiment files directly. | ## Set up a commerce test Price, shipping, offer and post-purchase tests are written as files and saved with the Apex CLI. You then check and launch them on the experiment page in the dashboard. Tests that are already running are not affected. 1. Install the CLI once and sign in: `npm install -g @drip-apex/apex-cli`, then follow [Install and sign in](/docs/cli/install-auth). 2. Create a draft experiment for the page the test runs on: `apex experiments create --name "Free shipping from 60"`. 3. Pull it into a folder: `apex experiments pull --out ./free-shipping`. 4. Create `commerce/shipping.json` in the pulled folder (a new draft has no commerce file yet). Use one row in `arms` per variation, with the slugs from `variations/`. This file shows free shipping from 60 in `variant-a` and leaves the control unchanged: ```json { "schemaVersion": 1, "kind": "shipping", "lever": { "catalogSource": "live", "currencyCode": "EUR", "enforcement": "none", "liveFreeShippingThreshold": null }, "arms": [ { "variation": "control", "control": true, "key": "control", "label": "Control", "threshold": null, "treatment": "none", "progressCopy": "", "reachedCopy": "", "bannerCopy": "", "flatRate": null, "percentOff": null, "target": { "productGids": [], "variantGids": [] } }, { "variation": "variant-a", "key": "free-60", "label": "Free shipping from 60", "threshold": 60, "treatment": "free_shipping", "progressCopy": "", "reachedCopy": "", "bannerCopy": "", "flatRate": null, "percentOff": null, "target": { "productGids": [], "variantGids": [] } } ] } ``` Price, offer and post-purchase tests use `commerce/price.json`, `commerce/offer.json` and `commerce/post-purchase.json`. Use `apex commerce catalog search` to find product and variant IDs for price tests. 5. Check it: `apex experiments plan ./free-shipping --json`. The plan lists everything launch would refuse. A threshold below your store's own free-shipping minimum needs checkout enforcement; the plan says so and names the missing settings. 6. Save the draft: `apex experiments apply ./free-shipping --json`. 7. Open the experiment in the dashboard, run QA and launch it there. ## Canonical loop ```bash apex experiments pull homepage-hero-trust-copy --out ./homepage-hero apex experiments plan ./homepage-hero --json apex experiments apply ./homepage-hero --json apex experiments qa status ./homepage-hero --json apex experiments qa probe ./homepage-hero --kind price --json ``` Edit only supported files in the pulled folder: ```text experiment.json targeting.json metrics.json qa.json launch.json variations/control/variation.json variations/variant-a/variation.json variations/variant-a/mutations.json commerce/price.json commerce/shipping.json commerce/offer.json commerce/post-purchase.json variations/variant-a/index.css variations/variant-a/index.js variations/variant-a/reset.js variations/variant-a/anti-flicker.json ``` Commerce drafts are pulled into one typed file per lever. Every variation has an arm row (use `sameAsControl: true` for no treatment), while revisions, checksums, references, launch evidence, and price baselines remain under read-only `serverOwned`. `mutations.json` contains DOM mutations and checkout blocks, not typed commerce carriers. A price lever that covers several products sets `lever.productId` to `"selection"`; launch QA and charged verification then check every listed variant on its own product. For price, shipping, and offer levers, a treatment row with `sameAsControl: true` keeps that lever at control while sibling treatments change it. Plan lists these rows as `same_as_control:` in `commerceLaunchPreview.checked`. The saved treatment has no carrier for that lever. Post-purchase offers are drawn per checkout, so every treatment must carry the offer or none does. One treatment can combine price, shipping, and offer changes. For example, set the treatment row in `commerce/price.json` to a lower price, set the same treatment in `commerce/shipping.json` to a free-shipping threshold, and use `sameAsControl: true` for that treatment in `commerce/offer.json` when its offer should remain unchanged. Plan includes a `combined` preview whose launch-only checks name the shared assignment and, when the arm changes both price and shipping, the `price-shipping-combined` Commerce Lab proof. Applying saves the draft; launching remains a dashboard click. Post-purchase offers cannot join a combined arm in this step because they are drawn per checkout. `.apex/` is CLI-owned metadata. Do not edit it directly. ## Pull, plan, and apply | Command | Arguments | Flags | Behavior | |---|---|---|---| | `apex experiments pull` | `` | `--out ` | Resolves the experiment, pulls editable files, writes CLI metadata. | | `apex experiments plan` | `[folder]` | None beyond global flags | Sends local files to Apex for validation and returns changes, warnings, errors, QA invalidation, and precompile status. | | `apex experiments apply` | `[folder]` | `--accept-qa-invalidation`, `--confirm-targeting-replace`, `--no-asset-upload`, `--sync` | Uploads local or inline images, plans, confirms if QA would be invalidated, applies through Apex, returns a post-write receipt, preserves authored files by default; `--sync` refreshes them from the server. | Successful apply JSON includes `data.receipt` (also in `data.result.receipt`). Older servers without receipts return `data.receipt: null`. Text output prints a Receipt block. The server reads the persisted experiment after the write and publication complete: Note: when running a filesystem `apply`, the CLI returns a post-write receipt that includes the server's persisted experiment state and per-variation content `digests`. Verify the saved experiment state and the variation `digests` directly from the apply response (or by running `apex experiments pull` after the apply) to confirm what was persisted. ```json { "files": [{"path": "experiment/index.js", "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "submitted_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "normalizations": []}], "server_owned_files": ["qa.json", "launch.json"], "server_owned_fields": ["experiment.json#id", "experiment.json#testKey", "targeting.json#authorUrl", "variations/control/variation.json#artifactReview"], "experiment_id": "experiment-id", "status": "draft", "delivery_type": "mutation", "counting_type": "non_program", "traffic_allocation": 0.5, "schedule": { "scheduled_start_at": null, "scheduled_end_at": null }, "goal_count": 2, "qa": null, "written_at": "2020-01-01T00:00:00.000Z", "variations": [{ "id": "variation-id", "slug": "control", "name": "Control", "is_control": true, "redirect_url": null, "digests": { "mutations": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", "custom_js": null, "custom_css": null, "reset_js": null } }] } ``` Variations are sorted control first, then by name. Digests are SHA-256 hex over stored content: recursively key-sorted compact JSON for `mutations` (array order preserved; a variation without stored mutations digests compact `[]`), and raw text for code fields. A null code column yields null; an empty string yields its SHA-256 digest. Recompute these from a subsequent pull to compare content, parsing and canonicalizing mutations instead of hashing the formatted JSON file. `qa` is the latest run's `run_id`, `status`, and ISO `approved_at` or null; no run yields null. Schedules use ISO timestamps or null. `written_at` records receipt construction time. Failed applies have no server receipt. If the readback fails after the write committed, the apply still succeeds with `receipt: null` and a warning in `result.plan.warnings` (the server's final plan; `data.plan` is the pre-apply plan); pull the experiment to verify. The receipt describes persisted state and does not replace the publication outcome or establish QA approval. `apex push` is unchanged. `apex experiments apply` returns confirmation-required exit code `4` in non-interactive mode only when a material change would revoke an existing QA approval and `--accept-qa-invalidation` is not present. A material change to an unapproved draft needs no acknowledgement. Plan JSON distinguishes these states with `qaApproval.present` and `requiredAcknowledgment`. The JSON refusal uses reason `qa_approval_present`, includes `qa_approved_at`, and names the required acknowledgement. Apply also returns confirmation-required exit code `4` in non-interactive mode when a `targeting.json` change attaches a Page and drops URL rules authored through the API, unless `--confirm-targeting-replace` is present. Apply waits up to 90 seconds for the server, unless `APEX_TIMEOUT_MS` explicitly overrides the wait. After a timeout or connection reset, the CLI plans the same files once more: it reports a confirmed apply when the server already holds the changes, or returns `apply_unconfirmed` with the exact `apex experiments apply ` command to run again. When that verification returns a valid plan with zero changes, apply exits `0`. JSON `data` includes `applied: true`, `verifiedAfterTimeout: true`, the effective `timeoutMs`, `result: null`, and `receipt: null`. The server holds the changes, but the timed-out response provides no receipt or publication confirmation. Commerce QA probes can be run for draft or paused experiments; running, completed, and archived experiments are refused. For a draft experiment, human output reports plain success: ```text Apply confirmed: the server already holds these changes (the response timed out after 90s). ``` For every non-draft experiment, including running and paused experiments, JSON also includes `persisted: true`, `publication: "unconfirmed"`, `warnings`, and `next_steps`. `applied: true` confirms that the changes were saved; it does not confirm storefront publication. A non-running experiment can change a goal shared with a running experiment, so publication remains unconfirmed for all non-drafts. For example, these fields appear in `data`: ```json { "applied": true, "persisted": true, "verifiedAfterTimeout": true, "timeoutMs": 90000, "result": null, "receipt": null, "publication": "unconfirmed", "warnings": [ "Storefront publication is unconfirmed; visitors may still receive the previous treatment." ], "next_steps": [{ "command": "apex experiments status ./homepage-hero --json", "why": "Check experiment status before handover; persisted files do not confirm storefront publication." }] } ``` Human output reports the saved apply, the publication warning, and the next step: ```text Apply saved: the server already holds these changes (the response timed out after 90s). Warning: Storefront publication is unconfirmed; visitors may still receive the previous treatment. Next: apex experiments status ./homepage-hero --json (Check experiment status before handover; persisted files do not confirm storefront publication.) ``` Asset upload is enabled by default. The CLI replaces large inline image data URIs and relative image references in variation code files and experiment-level custom code files with hosted Apex delivery URLs. It uploads equal content once per command. Use `--no-asset-upload` to send the code unchanged and let the server validate it. ### targeting.json Supported fields are `url`, `attributes`, `pages`, `pageRules`, `segments`, `pageMatchMode`, `segmentMatchMode`, and `deviceType`; `authorUrl` is server-owned provenance. `deviceType` accepts `all`, `desktop`, `mobile`, or `tablet` (omitted means `all`): assignment is limited to visitors whose detected device type matches; a multi-device audience uses a segment with a `device.type` condition. ## Normalisation contract JSON bodies and checkout JSON files may start with a UTF-8 BOM. Every receipt file carries `submitted_sha256`, hashing the exact submitted UTF-8 bytes including a BOM (or null when that path was not submitted), and `normalizations`. An unchanged file has equal submitted and stored digests and an empty list. Text output lists changed files on one `Normalized on store:` line. Older servers may omit these fields. `apex schema --json` publishes the receipt schema at `files.receipt`. | Normalisation | Files | What changed | |---|---|---| | `bom-stripped` | JSON; variation code | One leading byte order mark was removed. | | `whitespace-trimmed` | Code | Leading or trailing whitespace was trimmed. | | `literal-cleared` | Variation code | A standalone true, false, null or undefined literal became empty code. | | `blank-cleared` | Code | Whitespace-only code became empty code. | | `json-reserialized` | JSON | Parsed JSON was serialized in the stored representation. | | `author-url-mirror` | `targeting.json` | The server recomputed `authorUrl`. | | `page-rules-expanded` | `targeting.json` | Attached Pages expanded page rules and URL rules. | | `server-owned-fields` | `experiment.json`, `variations/*/variation.json` | Server-owned IDs, test key or artifact review were restored. | | `goals-normalized` | `metrics.json` | The same goal IDs were returned in the normalized goal representation. | | `server-owned` | `qa.json`, `launch.json` | Generated server state replaced submitted content. | | `server-rewritten` | Any file | Stored bytes differ for another reason. | Without `--sync`, apply leaves authored files byte-identical, heals only the server-owned `qa.json` and `launch.json`, and records receipt file digests in the session manifest. Use `apex experiments apply --sync` or `apex experiments pull` to refresh authored files. JSON reports `synced`; a failed refresh reports `false`. An older server without receipt file digests leaves the authored-file manifest entries unchanged; the healed server-owned entries are still recorded. A failed `--sync` refresh falls back to the default behaviour. Plan and apply compare variation code after the same normalisation used for storage, so a local trailing newline alone is not a change and does not invalidate QA. Experiment-level code is still compared verbatim. Plan reuses asset URLs cached in the session manifest, leaves uncached references unchanged, and performs no uploads or manifest writes. The `files` example above is abbreviated: actual receipts contain every pull path, and `server_owned_fields` carries one `variation.json#artifactReview` entry per variation. These SHA-256 digests are proof of the stored pull bytes, including JSON whitespace; compare them directly against reviewed files without another pull. | Files | Stored and pulled representation | |---|---| | `experiment/index.js`, `experiment/reset.js`, `experiment/index.css` | Nonblank code preserves whitespace, including final newlines. Blank or missing code stores null; pull writes an empty string. | | `variations/*/index.js`, `variations/*/reset.js`, `variations/*/index.css` | The artifact persistence path trims leading and trailing whitespace, including final newlines. Blank code stores null. The standalone values `true`, `false`, `null`, and `undefined` (case-insensitive) also normalize to null. Pull writes an empty string for null. | | All JSON files | Pull reconstructs supported fields with stable key order, two-space indentation and a trailing newline. Input formatting is not retained. | | `metrics.json` | Goals are ordered primary first, then by goal name. | | `qa.json`, `launch.json` | Entirely server-owned; local edits are ignored by the CLI and healed from the server. | | `experiment.json` | `id` and `testKey` are server-owned. `environmentKey` is writable, and omission preserves its stored value. | | `variations/*/variation.json` | `artifactReview` is server-owned. `jsArtifactId` and `cssArtifactId` are writable artifact references subject to validation, not ignored fields. | | `targeting.json` | Submitted `authorUrl` is ignored; the server recomputes it from authored targeting. | Asset upload may replace references in submitted code with hosted URLs; without `--sync`, these replacements do not rewrite local authored files. The session manifest `.apex/experiment-filesystem.json` caches uploaded assets by the SHA-256 of the uploaded bytes, recording their hosted URL and delivery URL when supplied. Later applies reuse these URLs without uploading unchanged images again, avoiding spurious code changes and QA invalidation. Older manifests without an `assets` map start with an empty cache; cache writes preserve per-file digests, including when apply plans no changes. Apply JSON also includes `dashboardUrl`, or null without shop context, linking to the experiment through the dashboard shop switch route. ## QA status ```bash apex experiments qa status ./homepage-hero apex experiments qa events ./homepage-hero --type custom ``` The target can be a pulled folder, experiment ID, or slug. When you pass a pulled folder, the CLI refreshes local files after reading the QA run. ## QA checklist updates ```bash apex experiments qa check ./homepage-hero \ --item responsive_matrix \ --status pass \ --notes "Desktop and mobile smoke checks passed." \ --evidence @qa-result.json ``` | Flag | Required | Behavior | |---|---|---| | `--item ` | Yes | Checklist item ID. | | `--status ` | Yes | One of `pending`, `pass`, `warning`, `fail`, `accepted`, or `skipped`. | | `--notes ` | No | Checklist notes. | | `--evidence ` | No | JSON object inline or `@path/to/evidence.json`. | QA evidence must be a JSON object. Keep screenshots, logs, and artifacts outside `qa.json`; attach structured references through the QA command. ## QA notes and approval ```bash apex experiments qa notes ./homepage-hero --notes "Ready for stakeholder review." apex experiments qa notes ./homepage-hero --acceptance "Responsive, tracking, and content checks passed." apex experiments qa approve ./homepage-hero apex experiments qa reopen ./homepage-hero ``` `approve` marks QA done after the server-side checklist gates are ready and moves the experiment to `ready_to_launch`. Its JSON includes `experiment_status_before`, `experiment_status_after`, and `transition` with `from`, `to`, and a `reopen` command. `reopen` returns the experiment to QA and clears scheduled launch state. ## Filesystem rules - Never hand-edit `qa.json` or `launch.json` as the source of truth. - Use `apex experiments qa` to update QA runs and approval state. - Use `apex experiments qa events` to read goal and custom events tagged with the latest QA run; pass `--session`, `--minutes`, or `--type` to narrow the readback. - Use `apex experiments plan` before every apply. - If you hit a conflict or stale filesystem state, pull fresh files, reapply your edits, and plan again. ## Related pages - [Price basis for value-bearing treatments](/docs/developers/price-basis-value-treatments) - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) - [Workspace and Operator commands](/docs/cli/workspace-operator-roadmap) - [CLI commands, JSON, and exit codes](/docs/cli/commands-output) ## Per-URL variation blocks `variations//url-blocks.json` is optional. When the file is absent, Apex treats the variation as having no URL blocks. A treatment variation can include the file with this shape: ```json [ { "id": "product-a", "name": "Product A treatment", "urlRules": [{ "type": "simple", "pattern": "https://shop.example/products/a" }], "mutations": [{ "action": "text", "selector": "h1", "text": "A-specific headline" }], "css": ".offer { color: red; }", "js": "window.productTreatment = 'a';" } ] ``` Blocks are ordered and **first match wins**. The matching block substitutes its `mutations`, `css`, and `js` for the variation's flat files. When no block matches, Apex falls back to `mutations.json`, `index.css`, and `index.js` as the implicit default block. If those flat fields are empty, rendering deliberately fails open as a no-op: the visitor remains assigned and contributes to pooled experiment results. Control variations remain control on every URL. Blocks do not have a per-block `resetJs`. Write block `js` to be idempotent. The runtime removes block CSS and runs its normal cleanup when the URL or assignment signature changes, but JavaScript side effects such as globals or DOM changes outside declared mutations remain the author's responsibility, just as they do for flat custom JavaScript. Classification uses `--counting-type standard|setup_test|aa_test|non_program` on `apex experiments create` (default `standard`) and `apex experiments update`. Classification writes are draft-only. In pulled `experiment.json`, `experimentType` and `countingType` equal `delivery_type` and `counting_type` from `apex experiments get --json`. Omitting or setting `countingType` to null preserves the stored value. Classification edits invalidate QA approval; paused and running experiments reject these metadata changes. ## File contract Every JSON file written by `apex experiments pull` is described by the draft 2020-12 document at `apex schema --json` → `data.files`. The document and the local `.apex/experiment-filesystem.json` receipt carry `schemaVersion: 1`. Use `GET /api/v1/experiments/filesystem/schema` to compare a server's current document with the schema bundled in the installed CLI before editing or applying files. This contract covers `apex experiments pull` only. Files created by the older `apex workspace checkout` operator workflow have no JSON Schema contract. ## Compact output and evidence vocabularies `qa check`, `qa notes`, and `qa evidence` accept `--summary`. JSON data contains only `run` (id, status, summary, approvedAt, updatedAt), `changed`, and `validationErrors` (empty on success). Changed values are the updated checklist item, stored notes and acceptanceCriteriaSummary, or evidence records newly added by ID. Without the flag, the full run is returned. | Command | Flag | Values or behavior | |---|---|---| | QA check, notes, evidence | `--summary` | Compact run and changed values. | | QA notes | `--notes `, `--acceptance ` | Either or both in one update. | | QA evidence | `--status ` | captured, passed, warning, failed, pending, not_run | | QA evidence | `--kind ` | screenshot (default), goal, summary, launch_capture, selector_audit, console_network, qa_audit, readiness | Help and `schema --json` list accepted choices. Invalid values fail locally with exit 3. Plan and apply JSON include `requiredAcknowledgment`: `accept-qa-invalidation` when the plan is material to QA and the experiment holds an approval, otherwise null. The `qaApproval` object reports whether an approval is present, its approval time when available, and the experiment status. Apply still accepts the hidden deprecated `--force` alias, printing a notice to stderr. QA status creates a run only when none exists. Reading an existing run never writes to it: `updatedAt` is its last stored mutation time, while `computedAt` is the ISO timestamp when the current checklist and summary were computed. ## Read-only handover status Run `apex experiments status [folder-or-id] --json` before handover. With no argument, it reads the current pulled folder. The JSON envelope's `data` contains `context`, `experiment`, `versions`, `qa`, `next_steps` (`command` and `why`), `ready_for_handover`, and `notes`. Human output shows the same sections. Folder mode compares every supported file with its pulled hash (`unchanged`, `modified`, `added`, or `deleted`) and uses the read-only plan route for saved changes, QA invalidation, blockers, and warnings. ID, slug, and test-key mode reports `versions: null` and recommends pulling a folder first. Status never writes local files, uploads assets, creates a QA run, or changes QA `updatedAt`. It reads the server filesystem's QA snapshot. Missing counts are null; missing identity names are null, particularly with shop credentials. `qa.approval_current` is true when QA is approved, the approval is bound to a saved version (`ready_version_id`, which the server clears whenever saved test content changes), and the pulled folder has no local or saved changes. An older approval without a ready version is not current; status then suggests approving again. `ready_for_handover` is true when the experiment status is `ready_to_launch`, approval is current, every required check is done, and there are no blockers; the only next step is then to launch the test in the dashboard, which stays a human decision. Without a pulled folder, readiness reflects the saved version only. ## Look up products for commerce drafts Use `apex commerce catalog search` to find active products, then `apex commerce catalog get ` to copy exact variant GIDs, base-currency prices, live costs, and subscription or bundle exclusions into a draft. # Commerce launch preview For draft and other pre-launch tests, `apex experiments plan` returns a `commerceLaunchPreview` entry for every authored commerce file. Its warning-level `issues` show settings that launch composition would refuse without changing whether plan or apply succeeds. `checked` names the pure checks performed from the files, while `launchOnly` explains deferred checks and its `when` field says whether each runs at apply or launch. Relationship eligibility, the complete running-offer size budget, and overlap with running tests are read-only plan checks; a failed read appears as an unchecked warning. Discount-code rules live in `commerce/discount-codes.json`. Each arm names a variation and either keeps control behavior with `sameAsControl: true` or sets `block: true`; an optional `message` object can override the default `en` and `de` customer messages. Checkout refuses entered codes for visitors in a blocking arm and shows the message; Apex makes no storefront DOM changes. For example: ```json {"schemaVersion":1,"kind":"discount-codes","arms":[{"variation":"control","control":true},{"variation":"sale","block":true,"message":{"en":"Codes cannot be combined with this sale."}}]} ``` ## Checkout messages `targeting.json` cannot author `surface` or `checkoutBlock`. Retired test-level targeting is read-only history: pulls retain its raw keys, an unchanged pull/apply can remain a no-op, and targeting replacements return `409 checkout_mode_retired`. Keep a checkout message in the arm's `variations//mutations.json` file (`variations/variation-b/mutations.json` for a fresh CLI draft; leave control as `[]`). In a workspace folder, use `src/mutations.json`: ```json [ { "action": "checkoutBlock", "checkoutBlock": { "placement": "checkout", "tone": "info", "eyebrow": "Shop with confidence", "heading": "Free returns for 30 days", "body": "Try your order at home.", "disclosure": "Return items in their original condition." } } ] ``` Each arm accepts one checkout message. The heading is required (1–120 characters after trimming, with visible text); eyebrow, body and disclosure are optional (60, 500 and 300 characters). Placement accepts only `checkout` and defaults to it; tone defaults to `info` and also accepts `success`, `warning` and `critical`. Unknown fields are refused. The legacy `enabled` boolean is deprecated; omit it for new messages (`false` hides the message). Checkout messages work only on A/B tests, outside URL blocks, and cannot share a test with `commerce/post-purchase.json`. Duplicate messages return `duplicate_checkout_block`. Copy cannot state a money amount (a number next to a currency symbol, ISO code or currency word, or a percent next to off, save, sale or Rabatt); apply refuses it with `422 checkout_copy_states_money` (CLI exit 3). A plain percent only warns at launch. Pause a running test before changing its message, repeat its QA checkout, then resume. On paused or running tests, adding the first message to a test with no storefront change or removing the last message is refused with `checkout_exposure_owner_frozen`. Messages can share an arm with DOM mutations, CSS, JS and commerce levers other than post-purchase. Combined tests count visitors on the storefront and show their storefront change only while Apex confirms the message is live. Message-only tests count visitors at checkout; buyers and QA testers must allow analytics, marketing and sale-of-data processing to see the message. Apex recommends a revenue primary goal. Control may carry a message, but it does not count as treatment work. Run `apex experiments plan --json`, then `apex experiments apply --json` (add `--accept-qa-invalidation` if the test already holds QA approval; otherwise exit 4 requests confirmation), then `apex experiments apply --sync` so the folder picks up the stored entry (apply adds `"placement": "checkout"`). `apex experiments status --json` shows the QA checklist and file state, not launch blockers; the dashboard shows those. The CLI validates file names; the server validates contents. `apex schema --json` prints the contract offline. Before launch, follow [Before launch](/docs/experiments/checkout-blocks#before-launch), including adding the Apex Checkout Experiment block in the Shopify checkout editor. For each message arm, open a signed QA link in a real checkout: `apex preview --json --no-open` prints one (treat it as a secret), and the Check panel's Preview links work too. Add a product, go to checkout, allow analytics, marketing and sale-of-data processing if the store asks, and wait for the message; a customer approval link does not count. Launch happens in the dashboard. See [Combined storefront and checkout tests](/docs/experiments/checkout-blocks#combined-storefront-and-checkout-tests). ### Commerce definition files Experiments with a canonical definition pull one top-level `commerce.json` with `revision`, `definition`, and read-only `serverOwned`. `plan` shows definition and derived carrier changes plus launch preview; `apply` saves with revision checking. A stale file refuses with 409 `commerce_definition_revision_conflict`. Server output edits are ignored with `commerce_server_owned_ignored`. Deleting the file is refused; omit a lever from `definition.levers` to remove it. For one release, experiments without a definition still pull `commerce/price.json`, `shipping.json`, `offer.json` and `post-purchase.json`. Changed supported files convert with `commerce_files_v1_converted`. Unchanged and opaque read-only files stay as they are. Mixing step 1 lever files with `commerce.json` refuses with `commerce_definition_declared_twice`. Discount codes remain in the separate `commerce/discount-codes.json` file in both formats. Use [`apex commerce definition get/put`](/docs/cli/commerce-definition) for direct file export and editing. Legacy envelopes remain read-only until replaced by a full definition at their stored revision. A single all-scope percent rule from -90 to -1 without endings or overrides uses the launch-time baseline; other price rules compile to fixed prices. ## Experiment lifecycle commands Human results output names each goal’s counting basis. See [Metric definitions](/docs/experiments/metric-definitions) for the exact rules. The `experiments` group covers experiment CRUD and lifecycle actions. Use IDs or slugs for reads and lifecycle commands. The alias `apex exp` is available for the group, and `apex experiments list` also has the `ls` alias. ## List and inspect ```bash apex experiments list --status draft --limit 20 apex experiments list --url https://shop.example/products/example --json apex experiments get homepage-hero-test apex experiments results homepage-hero-test --currency EUR ``` | Command | Arguments | Flags | Notes | |---|---|---|---| | `apex experiments list` | None | `--status `, `--url `, `--limit `, `--include-archived`, `--fields `, `--full` | Summary by default; status and effective URL targeting are filtered server-side. Experiments without URL rules match every URL. Select columns with `--fields` or request full details with `--full`. | | `apex experiments get` | `` | None beyond global flags | A plain slug and `slug:` both resolve by slug. | | `apex experiments results` | `` | `--currency `, `--wait ` | Returns experiment totals, arms, decision, metrics, series, computed time/source, and warnings. Cold results builds retry for up to 120 seconds by default; use `--wait 0` to disable retries. | Use JSON mode for scripts: ```bash apex experiments results homepage-hero-test --currency EUR --json ``` ## Lifecycle readback Create and get JSON always include these keys; they are never omitted. All three are `null` on a fresh draft. Set values are ISO-8601 UTC timestamps in the exact `YYYY-MM-DDTHH:mm:ss.sssZ` form. | Key | Set by | Cleared by | | --- | --- | --- | | `started_at` | Dashboard launch | Revert to draft | | `scheduled_start_at` | Launch scheduling | Schedule cancellation or QA reopen | | `qa_approved_at` | QA approval | QA reopen | Integrations reading a schedule field must read `scheduled_start_at`. ## Create a draft ```bash apex experiments create \ --name "Homepage hero trust copy" \ --slug homepage-hero-trust-copy \ --description "Tests clearer proof copy in the homepage hero." \ --page-url "https://shop.example/products/hero-bundle" \ --audience segment_vip \ --goal "new:type=click,selector=.hero-cta,name=Hero CTA click,page=" \ --traffic 25 \ --preview ``` {/* Note: The CLI `apex experiments create` accepts absolute page URLs for targeting or source/destination, accepts audience references or `new:` audience bodies to create audiences, and accepts existing goal IDs or inline `new:` goal definitions. In an interactive terminal the command prompts for any missing required values; when `--preview` is requested the CLI prints a QA preview URL (also emitted as `preview_url` in JSON mode). */} | Flag | Required | Behavior | |---|---|---| | `--name ` | Headless: yes | Experiment display name. In a terminal the wizard asks for it when omitted. | | `--slug ` | No | URL-safe slug for GitOps and slug-based lookup. | | `--description ` | No | Draft description. | | `--counting-type ` | No | `standard` (default), `setup_test`, `aa_test`, or `non_program`. | | `--type ` | No | `mutation` (default) or flag-gated `redirect`. | | `--source-url ` | Redirect: yes | Positive source URL targeting rule. Repeat for multiple exact URLs, paths, or wildcard patterns. The first absolute HTTP(S) value also becomes the preview/store URL. | | `--page-url ` | No | Page URL targeting rule. Repeat for multiple patterns. This is additive with the existing `--source-url` spelling. | | `--audience ` | No | Include an existing segment, or create one first from the same JSON body accepted by `apex segments create`. Repeat for multiple audiences. | | `--goal ` | No | Attach an existing goal, or create one from comma-separated `type`, `name`, and goal fields such as `selector`. Field keys are the v1 goal fields; `page` is accepted for `page_id` and hyphenated spellings such as `text-match` and `url-pattern` are accepted too. Click goals are page-scoped in v1, so an inline `type=click` goal needs `page=` (list them with `apex pages list`); in a terminal the wizard asks for the page when it is missing. Wrap a value that contains commas in double quotes, for example `selector=".buy,.checkout"`. Custom goals cannot use reserved event names such as `pageview` or `purchase` (`add_to_cart` and `checkout_started` are allowed). Every inline goal and audience file is checked before anything is created. Repeat for multiple goals; the first is primary. | | `--destination ` | Redirect: yes | HTTP(S) destination for one treatment variation. Repeat to create multiple equally weighted treatments. Invalid or missing redirect destinations fail validation. | | `--traffic ` | No | Percentage input converted to API traffic allocation. Defaults to `100`. | | `--preview` | No | Print a QA URL pinned to the first non-control variation. Needs an absolute HTTP(S) `--page-url`/`--source-url` to build the link, checked before anything is created, so a run that cannot be previewed fails validation instead of leaving an experiment behind. JSON output includes it as `preview_url`. | | `--yes` | No | Disable wizard prompts. Missing required values then return validation exit code `3`. | In an interactive terminal without `--yes`, omitted values are collected by a wizard that lists available audiences and goals and permits `none`. Headless callers should provide `--name` and use flags for the desired targeting and goals. New audiences are created first, then new goals, followed by a single experiment request. If that final request fails, the error lists resource IDs that were already created so callers can clean them up deliberately. JSON output's `data` payload has the shape `{ experiment, segments, goals, preview_url, preview_note }`; `segments` and `goals` contain resources created by this invocation. Inline goal bodies are parsed and completed before the first write, so a malformed or page-less `new:` goal fails validation without creating anything. When the created experiment has no treatment variation yet, the command still succeeds and reports why the link is missing in `preview_note`. Create a redirect experiment with a control on the source page and one treatment on the destination: ```bash apex experiments create \ --name "Product page split URL" \ --type redirect \ --source-url "https://shop.example/products/source" \ --destination "https://landing.example/products/treatment" ``` Redirect creation preserves the public v1 gates: the shop must have redirect tests enabled, at least one positive source rule and treatment destination are required, and destinations must be HTTP(S). A destination must not match the source targeting unless it is the same page with added or changed query parameters, such as `?view=alternate`. API gate failures use the standard JSON error envelope and exit codes. The filesystem and public v1 contracts permit a redirect treatment to retain mutations, CSS, or JavaScript. Those artifacts are source-page-only; Apex keeps the assignment active on the destination for attribution but does not apply the source-authored artifacts there. Filesystem plans warn when a treatment mixes these artifacts with a destination. ## Update ```bash apex experiments update homepage-hero-trust-copy --counting-type aa_test --json apex experiments update homepage-hero-trust-copy --counting-type non_program --results-weekdays mon,tue --results-timezone Europe/Berlin --json ``` `--counting-type` accepts `standard`, `setup_test`, `aa_test`, or `non_program`. It works alone or with the results weekday window. When setting a window, `--results-weekdays` and `--results-timezone` must travel together; use `--clear-results-weekdays` to clear it. Omitting classification leaves it unchanged. Text output reports the classification stored by the server. API edits are draft-only; the CLI passes through the server error for other statuses. ## Lifecycle verbs ```bash apex experiments pause homepage-hero-trust-copy apex experiments complete homepage-hero-trust-copy ``` | Command | Arguments | Behavior | |---|---|---| | `apex experiments pause` | `` | Requests the API to pause a running experiment. | | `apex experiments complete` | `` | Requests the API to complete or stop the experiment. | Starting traffic is dashboard-only so the approval gates remain visible. Lifecycle validation for pause and complete is owned by Apex; if the API rejects a state transition, the CLI reports the API error and exits non-zero. ## Delete ```bash apex experiments delete homepage-hero-trust-copy apex experiments delete homepage-hero-trust-copy --force --json ``` `apex experiments delete` prompts before deleting in an interactive terminal. In non-TTY mode, deletion returns confirmation-required exit code `4` unless you pass `--force`. ## Launch in the dashboard The CLI has no `experiments start` command. Launch is dashboard-only so Apex can keep approval and readiness gates visible; CLI lifecycle commands cannot bypass QA or business approval. Experiment references accept an ID, slug, or test key (for example `BIO-085`). Test keys resolve within the selected shop; use `--shop ` to change it. Bare lowercase references remain slugs. Create and get JSON include `dashboard_url` (null when no shop is known), and text output prints a Dashboard line. The link opens the experiment in the right shop, requires a signed-in dashboard user, and switches that user's active shop. The response's `shop_id` takes precedence over the CLI's pinned shop context. ## Troubleshooting > **Results verification:** When publishing or interpreting material results changes, require revision-specific live-model comparison evidence and an author explanation. {/* release: 9bb0adb */} - Experiment not found: check whether you passed an ID, slug, or `slug:`. - Lifecycle action rejected: inspect the API error in JSON mode for readiness, status, or permission details. - Delete fails in automation: rerun with `--force` only after confirming the target experiment. ## Related pages - [Template tests on Shopify](/docs/experiments/template-tests-shopify) - [Experiment filesystem and QA commands](/docs/cli/experiments-filesystem-qa) - [Push and GitOps with `.drip.config.ts`](/docs/cli/push-gitops) - [CLI commands, JSON, and exit codes](/docs/cli/commands-output) ### List summaries and selected fields `apex experiments list --json` returns code-free summaries with `targeting_urls` and `variation_count`. It omits targeting objects, variations, and goals. Use `--full --json` when you need the full configuration, including code. ```bash apex experiments list --fields id,test_key,name,status --json apex experiments list --fields status,name apex experiments list --full --fields id,name,targeting --json ``` `--fields` selects top-level keys. Requesting a field outside the summary view automatically switches the server to full details, so `--full` is not required. Table columns follow the requested order; JSON always retains `id`. Unknown keys return an error listing allowed keys. `--full` leaves the default table columns unchanged. The existing CR-011 filters remain the way to narrow the experiment set; view and field selection change row content, not which experiments match. Direct API consumers retain the full view by default; the CLI sends `view=summary`. Summary timestamps include `created_at` and `archived_at`; experiments have no `updated_at` field. ## Explaining a number Run `apex experiments results GET-023 --explain --samples` to compare raw events with reported results. Use `--goal ` for a specific attached goal, `--currency EUR` for money, or `--order ` for an order's label trail across arms. `--json` preserves the API body, including its `data` envelope. Each row gets one label, in this order: `bot`, `qa_traffic`, `outside_window`, `duplicate`, `before_exposure`, `exposed_before_window_only`, `filtered_by_goal`, or `counted`. These mean bot traffic, QA traffic, events outside the result window, repeated event/order identity, conversions before the first in-window exposure, visitors exposed only before the window, unmatched or unevaluable goal conditions, and included events respectively. Non-purchase events without an eligible exposure, and placeholder visitor IDs on exposures and non-purchase goals, are also `filtered_by_goal`; purchases keep placeholder IDs, as the results pipeline does. An order belongs to the variation on its earliest row; if that variation is no longer part of the test, the order appears under `other_variations`. An order without any tracked exposure carries `arm_only: true` when counted. The table shows counted, reported, and residual (counted minus reported) for each arm. This is a diagnostic classifier, not the canonical results pipeline. A nonzero residual exposes a disagreement between the diagnostic rules and the results pipeline; it is never forced to zero. Unavailable reported values or currency conversion produce an unavailable residual. The scan includes 30 days before the result window; older sightings cannot be diagnosed. Exposure rows are aggregated without a raw-row cap, so visitor counts remain exact; visitor samples are not provided. Variations missing from the results model appear in `other_variations` and are excluded from the arm diagnostics. Conversion scans above 100,000 rows return `diagnostic_row_limit` with the observed row count and a hint to narrow by `--order`. An order lookup restricts candidates to that order and exposures to its visitors. Because it covers one order, it shows no reported value or residual; run without `--order` to compare with the reported numbers. Timeouts and oversized candidate scans return errors, never partial results. See [metric definitions](https://docs.drip-apex.com/docs/experiments/metric-definitions). ## Profit per visitor for commerce tests and checkout-owned tests `apex experiments results` includes **Profit per visitor (CM1)** for commerce tests and checkout-owned tests with attributed orders. It reports exposed visitors, attributed orders, CM1 per visitor, the range versus control, the verdict, cost coverage, and failed trust checks. Unsupported money is `-`, not zero. A pending or unavailable analytics read prints its degraded reason instead of an empty results table. Checkout-owned tests measure orders from visitors who saw the checkout message and receive the supplementary CM1 section when they have attributed orders. Other non-commerce tests return `commerce_results: null`. An unavailable supplementary section on a checkout-owned test does not mark the analytics as degraded. ## Install and authenticate the Apex CLI The Apex CLI is the command-line interface for experiment operations, draft workspace edits, GitOps pushes, QA evidence, tracking checks, and agent handoffs. Use `apex` as the canonical binary. `drip` is kept as a compatibility alias. ## Prerequisites - Node.js 18 or newer. - An Apex account with access to the shop or workspace you want to manage. - For API-key fallback login, an Apex API key that starts with `apx_`. ## Install ```bash npm install -g @drip-apex/apex-cli apex --help ``` ### Agents and sandboxes Install the CLI once, then call `apex` directly. `apex --help` and `apex schema --json` answer without network access. Do not run `npx @drip-apex/apex-cli@latest` for every command: npx asks the npm registry for the package before the CLI starts, so it prints nothing and hangs when the registry is slow or blocked, which is common in agent sandboxes. Update with `npm install -g @drip-apex/apex-cli@latest`. For local package development, build and link from the monorepo: ```bash npm --prefix packages/cli install npm --prefix packages/cli run build cd packages/cli npm link ``` ## Default browser login Run login in an interactive terminal: ```bash apex login ``` By default, the CLI starts a short-lived callback server on `127.0.0.1`, opens Apex at `/cli/login`, and includes a one-time `state`, local callback URL, and machine name. Approve the CLI login in your browser. Apex posts the generated API key back to the local callback, the CLI validates it by reading one experiment, and then stores credentials at `~/.drip/credentials.json` with file mode `0600`. If the browser does not open automatically, the CLI prints the login URL. Open that URL manually in the same browser session where you are signed in to Apex. ## Manual and API-key login Use `--manual` when you want the CLI to prompt for a key instead of starting browser login: ```bash apex login --manual ``` Use `--api-key` for non-interactive shells: ```bash apex login --api-key apx_your_key_here ``` Use `--base-url` only when you are targeting a non-production Apex environment: ```bash apex login --base-url https://app.example.test ``` In CI or agent runners, prefer environment variables over prompts: ```bash DRIP_API_KEY=apx_your_key_here apex whoami --json ``` `DRIP_API_KEY` takes priority over stored credentials. `DRIP_BASE_URL` can override the API base URL for the process; otherwise the CLI uses `https://app.drip-apex.com`. ## Verify or clear auth ```bash apex whoami apex experiments list --limit 5 apex logout ``` `apex whoami` validates the current key against the API and prints only a redacted key preview. `apex logout` removes the stored credential file, but it does not revoke the API key in Apex. To verify a storefront installation, run `apex install check`. The command resolves its shop from an explicit `--shop-id` first, then the invocation's selected shop (`--shop ` or `apex use`), and finally `DRIP_SHOP_ID`. `--shop-id` selects that shop for every request; a contradictory explicit `--shop` is refused with exit 3 and `shop_context_mismatch`. A definite failed verdict exits 6 and lists `failed_checks` in the JSON error details. ```bash apex --shop shop_123 install check --json ``` ## Troubleshooting - Browser login fails in CI: use `apex login --api-key ` or set `DRIP_API_KEY`. - Login returns exit code `4`: inspect the JSON `error.details.retry` command and rerun only after confirming you meant to use API-key login. - Validation fails with `401`: check that the key is valid and starts with `apx_`. - Validation fails with `403`: the key exists, but it lacks permission to read experiments. - `member_suspended` (exit code `2`): ask an organization owner or admin to reinstate your membership; logging in again will not restore access. - Validation is rate-limited: retry later and check the server-side rate-limit configuration if this happens on first login. {/* CLI network/connectivity errors now include actionable causes. */} - When the CLI cannot connect to the API, the error message now reports the underlying cause (for example: DNS lookup failure, TCP connection refused, TLS certificate validation failure, or a socket timeout) and includes a machine-readable `reason` field in JSON output where applicable. Verify network, DNS, and proxy settings when you see one of these causes. ## Related pages - [CLI commands, JSON, and exit codes](/docs/cli/commands-output) - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) - [Workspace and Operator commands](/docs/cli/workspace-operator-roadmap) ## Push and GitOps with `.drip.config.ts` `apex push` syncs local experiment definitions from a config file to the Apex API. It is idempotent by slug: the CLI sends each experiment to the slug upsert endpoint and reports whether the API created or updated it. ## Config discovery By default, the CLI looks in the current working directory for: ```text .drip.config.ts .drip.config.js drip.config.ts drip.config.js ``` You can also pass a config path: ```bash apex push --config ./experiments/.drip.config.ts ``` TypeScript configs are bundled through `esbuild` before import. JavaScript configs are imported directly. ## Supported exports The config can export one experiment, an array of experiments, or an object with an `experiments` array. ```ts export default { experiments: [ { slug: "homepage-hero-trust-copy", name: "Homepage hero trust copy", description: "Tests clearer proof copy in the homepage hero.", trafficAllocation: 0.25, storeUrl: "https://example-store.test/", targeting: { url: { type: "contains", value: "/" } }, variations: [ { slug: "control", name: "Control", weight: 0.5, isControl: true, mutations: [] }, { slug: "proof-copy", name: "Proof copy", weight: 0.5, mutations: [ { selector: "[data-hero-subtitle]", action: "text", value: "Trusted by thousands of returning customers." } ], css: "[data-hero-subtitle] { letter-spacing: 0; }", js: "console.info('Apex variant loaded');", resetJs: "console.info('Apex variant reset');", antiFlickerSelectors: ["[data-hero-subtitle]"] } ], goals: [ { id: "00000000-0000-4000-8000-000000000010" } ] } ] }; ``` ## Experiment fields | Field | Required | API field sent by CLI | |---|---|---| | `slug` | Yes | Upsert key in the URL. | | `name` | Yes | `name` | | `description` | No | `description` | | `trafficAllocation` | No | `traffic_allocation`, defaulting to `1` when omitted. | | `storeUrl` or `store_url` | No | `store_url` | | `targeting` | No | `targeting` | | `variations` | Yes | Mapped to variation payloads. | | `goals` | No | `goal_ids` from each goal object's `id`. | Variation fields map both camelCase and snake_case names where the CLI supports them: `isControl` or `is_control`, `css` or `custom_css`, `js` or `custom_js`, `resetJs` or `reset_js`, and `antiFlickerSelectors` or `anti_flicker_selectors`. ## Dry run ```bash apex push --dry-run apex push --config ./experiments/.drip.config.ts --dry-run --json ``` Dry run loads and validates the config shape, checks that every experiment has a unique slug, and prints what would be pushed without calling the API. ## Push ```bash apex push --json ``` The result includes counts for created, updated, and failed experiments plus a per-slug result array. If any experiment fails, the command exits non-zero and includes the partial result details. Before a push, the CLI uploads large inline image data URIs and existing relative image files next to the config. It replaces each reference with the hosted Apex delivery URL and writes one upload line to stderr. Equal file content is uploaded once per command. Use `--no-asset-upload` to keep the source unchanged and let the server validate it. ## Pairing with SDK definitions Use `.drip.config.ts` as the local GitOps source for experiment definitions that pair with SDK-side [`defineExperiment`](/docs/sdk/config-definitions) or similar code-owned definitions. Keep slugs stable, keep API keys out of config, and use placeholders in examples rather than real shop IDs or client domains. ## Related pages - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) - [Experiment filesystem and QA commands](/docs/cli/experiments-filesystem-qa) - [CLI commands, JSON, and exit codes](/docs/cli/commands-output) - [Code-owned SDK definitions](/docs/sdk/config-definitions) ## Tracking, goals, targeting, preview, and harness These command groups support measurement setup, live verification, targeting diagnostics, and external harness preview workflows. ## Tracking manifests ```bash apex tracking inspect \ https://example-store.test/products/example \ https://checkout.example-store.test/thank-you \ --shop-id 00000000-0000-4000-8000-000000000020 \ --out ./.apex/tracking-manifest.json apex tracking plan ./.apex/tracking-manifest.json apex tracking qa ./.apex/tracking-manifest.json ``` | Command | Arguments | Flags | Behavior | | ----------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `apex tracking inspect` | `` | `--shop-id `, `--endpoint `, `--out ` | Writes a manifest with normalized URLs, domains, recommended cookie domain, snippet, handoff settings, placeholder steps/goals, and QA checks. | | `apex tracking plan` | `[manifest]` | None beyond global flags | Prints the implementation plan from a manifest. | | `apex tracking qa` | `[manifest]` | None beyond global flags | Prints the QA checklist from a manifest. | | `apex tracking verify` | None | `--minutes `, `--limit `, `--experiment `, `--event-type ` | Reads recent live tracking events for the current shop. | | `apex events export` | None | `--from `, `--to `, `--experiment `, `--event-type `, `--qa `, `--limit `, `--out `, `--json-summary` | Exports every raw event in a window of at most 31 days whose end is not in the future. | `apex tracking verify` accepts comma-separated event types: ```bash apex tracking verify --minutes 120 --limit 20 --event-type pageview,purchase --json ``` Human output includes a compact properties preview. Verification is capped and intended for spot checks; use `apex events export` for complete counts and properties up to 32,768 characters per event (up to 20 event types per export): ```bash apex events export \ --from 2026-08-01T00:00:00Z \ --to 2026-08-02T00:00:00Z \ --event-type pageview,purchase \ --out events.ndjson ``` The export follows opaque keyset cursors until the server reports the final page, then compares the number written with the first page's total. Use `--qa exclude` for non-QA traffic or `--qa only` for QA traffic; the default `include` matches raw storage. In JSON mode (`--json` or piped stdout), `--out` is required: rows go to the file and stdout carries one summary envelope. In an interactive terminal rows print to stdout unless `--out` is set; `--json-summary` asks for the summary envelope there too. Invalid timestamps and windows return `invalid_window` with `invalid_timestamp`, `to_not_after_from`, `window_too_long`, or `to_in_future` in the details. A failed run removes the partial `--out` file so it can be retried; an existing file is refused and is never overwritten. ## Purchase-capture verification runbook Run the read-only battery after publishing purchase tracking: ```bash apex shops doctor --check purchase_capture apex tracking verify --event-type goal,conversion,purchase,checkout_completed,order_created --minutes 10080 --json apex install check --shop-id "$APEX_SHOP_ID" ``` The doctor verdict is deliberately three-state. `OK` requires a served purchase plane, a mature publication-anchored window, known historical order cadence, and captured revenue events. `DARK` means a stage is absent or orders are expected while capture is zero. `NO_DATA` means the window is too young or cadence is unknown; it is never treated as healthy. The duplicate-order warning starts above 2%. Magnitude mismatch uses `|revenue - sum(price * quantity)| > max(0.01, 10% of revenue)` as an advisory signal because shipping and tax may legitimately differ. Reconciliation excludes pre-publication and consent-suppressed orders. ### Control-goal discriminator When purchase is dark, add temporary **diagnostic, read-only observations** for sibling GA4 events already emitted by the page (for example `begin_checkout` and `add_payment_info`) and compare them with `purchase` over the same publication-anchored window. Sibling events present while purchase is absent isolates the checkout-to-confirmation boundary; all siblings absent points earlier, at the data-layer or consent boundary. Do not turn diagnostic siblings into business conversion goals, and remove the diagnostic trigger after the boundary is identified. ### Generic rescue patterns - **Session-storage purchase rescue:** immediately before a checkout redirect, store the minimum purchase context (stable order ID, revenue, currency, and line items) in `sessionStorage`. On the confirmed return page, validate and emit it once, then delete it. Namespace the key, set a short expiry, and deduplicate by order ID so reloads cannot double-count. - **PSP redirect-return rescue:** when a payment service skips the normal thank-you page, recognize only its verified success-return route, reconstruct the purchase from trusted return-page context, and re-emit the same canonical purchase payload once. Require a stable transaction ID, expire the rescue record, and never infer success from an unverified query parameter alone. Both recipes preserve the existing write plane; the CLI commands above only observe and reconcile what that plane produced. ## Goals - Measurement choice and value-source fields render only for custom goals; existing non-custom value goals keep their stored configuration and remain editable (grandfathered). ```bash apex goals list --limit 50 apex goals create \ --name "Add to cart click" \ --type click \ --selector "[data-add-to-cart]" \ --page product-detail \ --count-once apex goals update 00000000-0000-4000-8000-000000000030 \ --event-name purchase_completed \ --capture '[{"field":"order_id","source":"property","key":"order_id"}]' ``` | Command | Flags | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `apex goals list` | `--limit ` | | `apex goals create` | `--name `, `--type `, `--page-id `, `--page `, `--selector `, `--text-match `, `--url-pattern `, `--event-name `, `--count-once`, `--capture ` | | `apex goals update` | ``, plus the same editable fields as create, with `--no-count-once` available; pass `--page-id null` or `--page null` to clear page scope. | Goal type values are `click`, `pageview`, `custom`, and `revenue`. Capture can be inline JSON or `@path/to/capture.json`. Click goals require a page. Run `apex pages list` to find its ID or key. Pass `--page-id ` for a UUID with no lookup or `pages:read` permission, or `--page ` for an ID or a key resolved by lookup (keys need `pages:read`). Use only one of these options. **Pages from the CLI.** `apex pages create --help` and `apex pages update --help` list every body key; `apex schema --json` includes their body schemas. Set `match_type` to match the pattern: `exact` (the create default), `contains`, `wildcard`, or `regex`. The CLI refuses wildcard or regex shapes under a contradictory match type with `match_type_mismatch`; pass `--literal` to store the patterns as written. An update without a match type leaves it unchanged and skips checks that depend on its stored value. ```bash apex pages create '{"name":"Product pages","url_pattern":"/products/*","match_type":"wildcard","include_rules":["/collections/*/products/*"],"trigger":{"mode":"immediate"}}' ``` ## Targeting tests ### Targeting schema An experiment with no targeting reads `targeting` as `null` in v1 responses. Any non-null targeting carries both `url` and `authorUrl` arrays. `apex experiments pull` also returns both arrays in `targeting.json`, using canonical rule objects: `{ type: "simple" | "regex", pattern: string, include: boolean, matchType?: "exact" | "contains" | "wildcard" | "regex" }`. v1 writes accept only this canonical shape; older rows stored in legacy shapes (bare strings, `urls`, `rules`) are normalized on read. No migration rewrites stored rows; the compatibility projection runs only on read. The SDK enforces `deviceType` (edge falls back to the SDK for this rule); `apex targeting test` evaluates URL rules only and reports a device restriction as a runtime caveat rather than checking it. ```bash apex targeting test https://example-store.test/products/example --status all apex targeting test https://example-store.test/products/example --status draft,running --json ``` `apex targeting test` evaluates URL targeting for the supplied page and returns matching count, experiment rows, and caveats from the API. Note: tests that target multiple pages or audiences can now declare a match mode of `ANY` or `ALL` (the SDK, edge engine, and web evaluators honor this). When multiple selections are present the setup UI exposes an `ANY/ALL` choice to require either any matching selection or all selected ones. Existing tests retain the prior any-match behavior. ## QA preview URL ```bash apex preview homepage-hero-trust-copy apex preview homepage-hero-trust-copy variant-a --store-url https://example-store.test/products/example --no-open ``` `apex preview` resolves the experiment by ID or slug, defaults to the first non-control variation, and builds a forced QA URL with `drip_force`, `drip_experiment_id`, `drip_variation_id`, `drip_qa=1`, and `drip_debug=1`. It opens the browser unless `--no-open` is present. Use `drip_force=EXP:~` to hold one experiment at Original: it receives no assignment, exposure, stickiness, or page changes. Other tests run normally. Combine it with a pin, for example `drip_force=EXP:~,OTHER:VAR`, to hold EXP while previewing OTHER. A hold does not survive a cross-domain redirect handover. ### Forced preview isolation and coexistence The QA panel’s **Coexist with live tests** toggle sets the same `drip_force_coexist=1` choice for the session. Pre-launch QA and ready-to-launch experiments appear in the panel only when the server verifies the QA session’s authorization for this shop. Three authorizations count: a signed QA link opened from the dashboard (a client link without debug does not list them), an active harness session for the shop, or a preview-read capability scoped to the shop. These experiments render only when pinned. `drip_qa=1` alone lists running tests. A variation pin in `drip_force` (or legacy experiment/variation pair) isolates the SDK preview to the forced experiments and any experiment handles listed in `drip_exp`. Other experiments, personalizations, and flags are suppressed and produce no exposure or tracking. This also applies to cookie-persisted and QA assistant forces. The assistant's activation diagnostics show `suppressed_by_forced_preview`. Add `drip_force_coexist=1` to restore coexistence for the QA session. The SDK stores this choice in the `drip_force_coexist` cookie alongside `drip_qa_force`, renewing both for 30 minutes on QA force persistence. Navigation preserves the choice; `drip_force_coexist=0` disables it, and exiting QA clears it. The runtime snapshot exposes `forcedPreviewCoexist`. Without a force, running experiments targeting the page can be assigned unless an exclusion group separates them. Assignments apply in served config order. When multiple experiments claim the same mutation selector in QA mode, the QA bundle’s debug output warns `Shared container : / and /; later wins`: the later assignment wins on overlapping mutations. This diagnostic includes selected URL-block mutations and does not change rendering. The QA assistant runtime snapshot exposes the same selector and experiment/variation pairs in `conflicts`. The conflict warning and snapshot conflicts list are available only in the QA bundle, not in the live bundle. {/* Release note: signed dashboard QA links list on-store QA and ready-to-launch tests after server verification. When you open a QA session from a signed dashboard QA link the panel can show running, QA, and ready-to-launch tests; a `Coexist with live tests` toggle lets a pinned preview run alongside live tests when needed. */} ## AB Test Harness ```bash apex harness init ./ab-test-harness/experiments/homepage-hero apex harness dev ./ab-test-harness/experiments/homepage-hero --no-open apex harness publish ./ab-test-harness/experiments/homepage-hero ``` | Command | Arguments | Flags | Behavior | | ---------------------- | ---------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | | `apex harness init` | `` | `--force` | Writes `apex.harness.json` from existing harness metadata, unless it already exists. | | `apex harness dev` | `` | `--no-open` | Runs `ab-harness build`, uploads the artifact to an Apex harness session, opens or prints the preview URL, and watches for rebuilds. | | `apex harness publish` | `` | None beyond global flags | Runs `ab-harness verify`, builds, uploads, and publishes the latest harness artifact to the Apex draft variation. | Set `AB_HARNESS_BIN` when the harness executable is not named `ab-harness` on PATH. Harness dev uses `experimentName` and `targetUrl` only when it creates the experiment. Selectors and the cleanup reset script are sent only when the harness config sets them. ## Related pages - [Workspace, Operator, and roadmap commands](/docs/cli/workspace-operator-roadmap) - [Experiment filesystem and QA commands](/docs/cli/experiments-filesystem-qa) - [Push and GitOps with `.drip.config.ts`](/docs/cli/push-gitops) `apex preview --proxy --json` returns the existing `session_id`, `preview_url`, and `expires_at` plus `qa_run: { id, created, refreshed, automated_evidence_count }`. The receipt names the QA run created or refreshed by recording this preview. `automated_evidence_count` counts the normalized `evidenceRecords` across all evidence kinds, matching the list shown by `apex experiments qa status`. Human output prints `QA run created` or `QA run refreshed (N automated evidence records)`. ## Decision latency before launch ```bash apex tracking latency MOM-027 --days 7 apex tracking latency MOM-027 --days 7 --qa-session --json ``` Compare forced-preview and natural loads before launching an above-the-fold change. `--days` accepts 1–30 (default 7). The table shows samples, decision p50/p95/p99, config-fetch p50/p95 in milliseconds, and inline/cache/network/none counts. `--json` includes the API body in the standard CLI envelope, including assignment-ready percentiles and `position: "runtime_inline_no_change"`. All buckets include only page-load events (`perf_event_kind = 'init'` and exclude route-change events and bot traffic. Forced rows contain the experiment's resolved ID in `forced_experiment_ids`. Natural rows are shop-wide loads with no forced IDs and `qa_mode = 0`. The optional QA-session bucket covers that session; it can overlap forced rows. Fewer than five measured samples produce null percentiles (`-` in the table). An unavailable analytics read returns an error, never a zero-sample success. For page-readable timestamps during forced previews, see [SDK decision timing](/docs/sdk/runtime-api#decision-timing). The runtime remains unchanged. Natural runtime decides within tens of milliseconds of SDK start when config ships inline with the snippet (measured p50 12 ms / p95 54 ms on a large storefront). QA previews, screenshot-mode previews and cookie-forced sessions take the QA config lane and fetch fresh config over the network (measured p50 1.3 s / p95 7 s); they are not representative of launch behaviour. URL-forced sessions without a `qa` parameter keep the inline or cached config, so these measurements do not describe them. No config preload or anti-flicker hold is planned. ## Tracking verification limits `apex tracking verify` reads at most 200 of the newest events from a window of at most 7 days. When the limit is reached, `truncated` is true and totals describe that sample only. The response includes the exact `from`, `to`, `minutes`, and `limit` window. ## Workspace, Operator, and roadmap commands Use `apex workspace` for variation-level draft workspace files used by Apex Operator and code workbench flows. The command group also has the `operator` alias. ## Check out a workspace ```bash apex workspace checkout 00000000-0000-4000-8000-000000000001 \ --variation variant-a \ --out ./homepage-hero ``` | Command | Arguments | Flags | Behavior | | ------------------------- | ----------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `apex workspace checkout` | `` | `--variation `, `--out ` | Pulls one variation workspace and writes `.apex/session.json`. | | `apex workspace status` | `[folder]` | None beyond global flags | Shows local files, hashes, sizes, and changed paths. | | `apex workspace dev` | `[folder]` | `--no-open`, `--once` | Creates an exact harness session, uploads the current preview artifact, opens or prints the preview URL, and watches files unless `--once` is used. | | `apex workspace publish` | `[folder]` | None beyond global flags | Uploads changed files, builds the workspace through Apex, refreshes local session metadata, and publishes external artifacts when configured. | ## Supported workspace files ```text src/index.js src/index.scss src/info.js src/mutations.json experiment.config.json metrics.json targeting.json dev-doc.md brand.md goals/.json segments/.json network/.recipe.json ``` `brand.md` is generated read-only context. Local edits to `brand.md` are ignored during publish. ## Preview workflow ```bash apex workspace status ./homepage-hero --json apex workspace dev ./homepage-hero --no-open apex workspace dev ./homepage-hero --once ``` `dev` reads the local session, creates a harness session for the exact experiment and variation, uploads the current `src/index.js` and optional `src/index.scss`, then writes `.apex/harness-session.json` with the preview metadata. Without `--once`, it watches the workspace and uploads new preview versions after local file changes. Dev never changes the experiment's name, store URL, reset script or anti-flicker selectors. The preview opens on `experiment.config.json` `storeUrl` (or the experiment's store URL). When Apex refuses the session, nothing is created and the error lists each blocking issue with its code (`error.code` `harness_artifacts_blocked`). Preview URLs include short-lived harness tokens. Treat them like secrets. ## Publish workflow ```bash apex workspace publish ./homepage-hero ``` Publish validates JSON files, uploads changed supported files except `brand.md`, asks Apex to build the workspace, and refreshes local session metadata from the server. When `experiment.config.json` opts into external delivery with `externalCode`, `deliveryMode: "external"`, or `codeDeliveryMode: "external"`, publish also creates a harness session, uploads the current artifact, and publishes that harness session. Publishing syncs draft artifacts back to Apex. It does not launch public traffic. ## Roadmap handoffs Use roadmap commands when your API key can access approved ideas and you need a build brief. ```bash apex roadmap approved --unbuilt --brief apex roadmap approved --query "homepage" --limit 10 apex roadmap handoff 00000000-0000-4000-8000-000000000002 --instructions apex roadmap handoff 00000000-0000-4000-8000-000000000002 --out ./handoff.json ``` | Command | Flags | Behavior | | ----------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | `apex roadmap approved` | `--limit `, `--cursor `, `--query `, `--unbuilt`, `--built`, `--brief`, `--json` | Lists approved roadmap ideas, optionally filtered to linked or unlinked experiments. | | `apex roadmap handoff` | `--out `, `--instructions` | Reads the full build handoff for one approved item, or just the instructions text. | ## Related pages - [Experiment filesystem and QA commands](/docs/cli/experiments-filesystem-qa) - [Preview and harness commands](/docs/cli/tracking-goals-targeting-preview-harness) - [Agent tools: SOS, brand, schema, and skills](/docs/cli/agent-tools-sos-brand-schema-skills) ### Export contract `--instructions` cannot be combined with `--out` (or its legacy alias `--output`); file exports always contain the full JSON contract. `apex roadmap handoff --json` returns `apex.roadmap_handoff.v1` in the CLI wrapper's `data`. `--out` writes that exact contract object. `apex backlog get` and `apex backlog list` identify each item as `apex.backlog_item.v1`. Both have `schema_version: 1`. The backlog `version` is an independent concurrency token, not a schema version. There is one item contract: the handoff embeds the closed item shape under `item`; backlog get/list expose the same fields alongside the existing raw row fields. Select the contract properties from a raw row before validating the closed schema. Every key below is required. Unknown optional values are explicit `null`; acceptance criteria are an empty array unless metadata contains an array consisting only of strings. | Key | Type | Nullable | Source | | -------------------------------- | --------------- | -------- | --------------------------------------------------------------------------------------------------------- | | `contract`, `schema_version` | string, integer | No | Contract id, literal `1` | | `id`, `title`, `status`, `stage` | string | No | Backlog row; stage is the approval lifecycle | | `ticket_key` | string | Yes | `metadata.backlog_key` | | `hypothesis`, `device` | string | Yes | Backlog row | | `approval` | object | No | Roadmap decision status (`proposed` without a decision), item stage, and decision's nullable `decided_at` | | `page` | object | No | Nullable `label`, `url`, `type`; row page fields and known page vocabulary | | `research_hub` | object | No | Nullable `brand_id`, `idea_id` from row; `company_id` is currently null | | `acceptance_criteria` | string array | No | `metadata.acceptance_criteria`, otherwise `[]` | | `mockups` | object | No | `precedence: "primary_first"`, nullable `primary`, array `others`; active visual refs in order | | `main_metric_goal_id` | string | Yes | Backlog row | | `created_at`, `updated_at` | string | No | Row timestamps serialized as ISO dates | The handoff additionally guarantees: | Key | Type | Nullable | Source | | --------------------- | ------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `item` | object | No | Shared item projection above, without its standalone discriminator | | `experiment` | object | Yes | Linked experiment: `id`, `test_key`, `slug`, `name`, `status`, `variation_id`, `variation_slug`, `variation_name`, `target_url` (each nullable) | | `instructions` | string | No | Build brief | | `goals` | object array | No | Handoff metrics: `name`, `role` (primary/secondary), nullable `apex_goal_id` and `event`; `apex_goal_id` is the linked experiment goal or null | | `mockups` | object | No | Handoff visuals, including linked experiment references after backlog references | | `acceptance_criteria` | string array | No | Same as `item.acceptance_criteria` | | `harness`, `scoring` | object | No | Existing handoff metadata blocks | | `source` | string | No | Existing handoff source | | `metadata` | object | No | Existing block retained, including `schemaVersion: 1` | Visual refs carry nullable `url`, `storagePath`, `label`, `description`, `kind`, and non-null `source`. The primary ref takes precedence over every entry in `others`. Missing goal ids or events stay null; built-in metric names do not invent stored goals. A removal, rename, semantic change, or nullable-field change requires a new `.v2.` schema and contract id. Pin the version you consume. - [Roadmap handoff schema](https://github.com/drip-apex-labs/Apex/blob/main/docs/contracts/experiment-os/apex.roadmap_handoff.v1.schema.json) and [fixture](https://github.com/drip-apex-labs/Apex/blob/main/docs/contracts/experiment-os/fixtures/roadmap_handoff.approved_item.json). - [Backlog item schema](https://github.com/drip-apex-labs/Apex/blob/main/docs/contracts/experiment-os/apex.backlog_item.v1.schema.json) and [fixture](https://github.com/drip-apex-labs/Apex/blob/main/docs/contracts/experiment-os/fixtures/backlog_item.approved.json). ## Consent integration for privacy teams Apex separates storefront rendering from analytics consent. This lets a visitor receive a stable page treatment without Apex writing a persistent identifier or sending tracking events before the store's consent rule allows it. This page explains product behavior and implementation choices; it is not legal advice. The store's privacy owner or DPO should decide which consent categories and regional rules apply. ## What can happen before consent - Apex can evaluate eligibility and render the assigned variation. - The visitor identifier exists only in browser memory for that page load. - Anti-flicker can hide and reveal treatment elements so the control does not flash first. The memory-only identifier is temporary. A reload can produce a new identifier and assignment until consent permits persistence. ## What stays blocked While consent is required but not granted, Apex treats tracking as blocked and persistent storage as unavailable: - no analytics, exposure, goal, heatmap, revenue, or other tracking events are sent; - no persistent visitor or assignment identity is written; - existing Apex consent-controlled visitor, goal, signal, config-cache, session, QA, and identity context storage is cleared where possible. After consent is granted, the SDK can enable the configured storage mode, persist the active anonymous assignment, and start permitted tracking. Revoking consent stops tracking again and clears consent-controlled storage. ## Connect a consent manager For a custom CMP, initialize Apex in consent mode and pass every consent change to `setConsent`: ```js Drip.init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", requireConsent: true, hasConsent: false }); cmp.on("consentChanged", ({ analyticsAllowed }) => { window.drip.setConsent(analyticsAllowed); }); ``` The queued equivalent before the hosted SDK finishes loading is: ```js window.drip.push({ type: "setConsent", granted: true }); ``` Apex can also read supported providers through `consentMode`: | Provider | Configuration Apex reads | |---|---| | `onetrust` | A required OneTrust group, using `OnetrustActiveGroups` or a configured getter path. | | `usercentrics` | A matching service name, ID, template ID, or data processor from `UC_UI`. | | `cookiebot` | A Cookiebot consent category such as `statistics`. | | `ccm19` | A matching accepted embedding name or ID. | | `shopify` | Shopify Customer Privacy consent purposes (`analytics` by default). The storefront script loads the API and the SDK polls until it answers, as with Pandectes. | | `pandectes` | A Pandectes consent purpose (`analytics`, `marketing`, `preferences`, or `sale_of_data`), read from the Pandectes API with a Shopify `customerPrivacy` fallback. | | `cookiescript` | A CookieScript category from `CookieScript.instance.currentState()`. Analytics aliases select `performance`, and custom category names remain available. | | `custom` | A configured window getter, or explicit `setConsent` calls. | ```ts consentMode: { enabled: true, provider: "usercentrics", requiredGroup: "Apex" } ``` Test the initial denied state, later grant, later revocation, and a fresh page load in every region where the configuration is used. ## Why the script itself must not be consent-gated Place the Apex snippet directly in ``. A tag manager or CMP that loads the script only after consent prevents Apex from installing anti-flicker before the first paint. Visitors can then see the original page and a late jump to the treatment. Loading the script is not the same as granting analytics consent. With consent mode enabled, Apex can render a consistent page using memory-only identity while its tracking and persistent-storage gates remain closed. ## Review checklist for a DPO - Confirm which CMP category or service authorizes Apex storage and analytics. - Confirm the initial page-load state is denied until the CMP provides a positive signal. - Confirm variation rendering before consent is acceptable for the store's policy. - Verify no Apex network tracking or persistent identity appears before consent. - Verify grant and revocation update the SDK without reloading the page. - Record the configured storage mode and the store's retention policy. ## Related pages - [Consent modes for developers](/docs/sdk/consent-modes) - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) ## Consent and regional compliance Apex can render storefront variations separately from analytics tracking. This helps you configure experiments for stores that need consent before persistent storage or analytics events begin. This page is product guidance, not legal advice. Work with your privacy or legal team to decide which settings are required for each region and storefront. ## Consent mode Use consent mode when your store needs Apex to wait for consent before using persistent storage or tracking. The SDK treats consent as required when `requireConsent` is true, when an initial `hasConsent` value is provided, or when `consentMode.enabled` is true. Apex supports these SDK consent providers: | Provider | Typical use | |---|---| | `custom` | Your storefront owns consent state and calls `setConsent()`. | | `onetrust` | The store uses OneTrust consent groups. | | `usercentrics` | The store uses Usercentrics services. | | `cookiebot` | The store uses Cookiebot consent categories. | | `ccm19` | The store uses CCM19 accepted embeddings. | | `shopify` | Follow Shopify’s own cookie banner using its Customer Privacy API. The storefront script loads it; the SDK polls as with Pandectes. | | `pandectes` | The store uses the Pandectes GDPR Compliance app (Shopify) consent purposes. | | `cookiescript` | The store uses CookieScript consent categories. | See [SDK consent modes](/docs/sdk/consent-modes) for copy-paste developer examples. ## Do Not Track and storage Apex runtime settings can respect browser Do Not Track and can change storage mode between local storage, session storage, cookies, and memory. Memory storage is the most temporary mode; it does not persist assignment identity across page loads. When consent is denied, the SDK clears Apex visitor, goal, signal, config-cache, session, QA, and identity context storage where possible. ## Platform install notes Shopify, WooCommerce, Shopware, headless, and custom installs all use the same privacy principle: configure the runtime for the consent rules that apply to the storefront. - Shopify installs should also respect Shopify customer privacy settings and the enabled web pixel behavior. - WooCommerce installs load the Apex snippet through the WordPress plugin, so confirm your cookie banner rules allow or block that script according to your policy. - Shopware installs declare the Apex statistical cookie entry in the app manifest and load the storefront bridge for confirmed active installs. - Custom and headless installs can call the SDK consent API directly. ## Regional checklist - Decide whether Apex can render variations before analytics consent or must wait entirely. - Enable provider-backed consent mode when a consent manager is present. - Use Do Not Track support when your policy requires it. - Use a storage mode that matches your retention and consent expectations. - Document which identifiers your support team uses for export and deletion requests. - Test consent granted and consent denied paths before launching an experiment in a regulated region. ## Related - [SDK consent modes](/docs/sdk/consent-modes) - [GDPR export and deletion](/docs/data-privacy/gdpr-export-deletion) - [Runtime controls, SDK, anti-flicker, and consent](/docs/settings-account/runtime-sdk-controls) - [Install Apex on WooCommerce](/docs/setup/woocommerce) - [Install Apex on Shopware](/docs/setup/shopware) ## Developer access and data Apex developers and technical partners usually work through the dashboard, the public API, webhooks, the SDK, and platform integrations. Keep access server-side when secrets are involved, and use the narrowest key or role that can complete the task. ## Access paths | Access path | Use it for | |---|---| | Dashboard | Managing shops, experiments, goals, setup, team members, API keys, and runtime controls. | | Public API v1 | Server-side automation for experiments, variations, goals, webhooks, screenshots, harness workflows, and privacy requests. | | SDK | Storefront assignment, rendering, tracking, consent, feature flags, and runtime diagnostics. | | Webhooks | Receiving Apex events in your backend. | | Platform apps and plugins | Shopify, WooCommerce, and Shopware connection flows. | Do not put secret API keys, webhook signing secrets, or platform credentials in browser code. ## Data stores | Store | What Apex uses it for | |---|---| | Product database | Shops, experiments, variations, goals, pages, segments, users, organizations, API keys, webhooks, privacy requests, and audit-oriented product state. | | Event analytics | Storefront events such as visitors, exposures, clicks, conversions, revenue, performance, and experiment results. | | Asset and evidence storage | QA screenshots, generated artifacts, and review evidence when those features are enabled. | | Edge runtime | Snippet delivery, experiment config, event ingest, previews, and routing support. | The public event schema is documented in [Public event schema](/docs/developers/event-schema). Worker routes are documented in [Worker public endpoints](/docs/developers/worker-endpoints). ## Data controls Apex includes runtime settings for consent mode, Do Not Track, storage mode, screenshot handling, debug controls, and shop or experiment kill switches. Workspaces may configure evidence retention. Per-category opt-outs for AI prompts, screenshots, tool outputs, observability mirroring, and model calls are not available. Use these controls before launch, not after a privacy review finds a mismatch. ## Safe implementation rules - Use server-side API calls for secret keys and privacy workflows. - Rotate keys when people leave the project or an integration is replaced. - Keep webhook receivers idempotent; webhook retries and duplicate delivery can happen. - Treat visitor IDs as pseudonymous personal data when your policy or region requires it. - Use consent mode and storage settings that match the storefront's regional requirements. - Do not export raw event data to third-party systems unless the merchant has approved that destination. ## Related - [GDPR export and deletion](/docs/data-privacy/gdpr-export-deletion) - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [API keys and public API v1](/docs/settings-account/api-keys) - [Webhook payload reference](/docs/developers/webhooks) ## GDPR export and deletion Apex supports shop-scoped visitor export and deletion workflows for privacy requests. These endpoints are server-to-server only: call them from your backend with an Apex API key that has the matching data permission. The privacy workflow is based on the Apex visitor ID. If a shopper has used multiple browsers, cleared cookies, or checked out without a linked Apex assignment, one visitor ID may not represent every record you hold about that person. ## Before you start - Find the Apex visitor ID for the request. - Use a secret API key from your server, never from browser code. - Make sure the key has `data:export` for export requests or `data:delete` for deletion requests. - Keep your own record of the customer request and the identifier you used. ## Export visitor data Send a POST request to `/api/v1/data-export` with a visitor ID. The optional `limit` is capped at 50,000 rows. The examples below read your secret API key from the `APEX_API_KEY` environment variable. ```bash curl -X POST "https://app.drip-apex.com/api/v1/data-export" \ -H "Authorization: Bearer $APEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"visitor_id":"visitor_123","limit":10000}' ``` A completed export returns a request ID, visitor ID, status, and the Tinybird event export for that shop and visitor. To check the latest export request later, use either the request ID or visitor ID: ```bash curl "https://app.drip-apex.com/api/v1/data-export?request_id=req_123" \ -H "Authorization: Bearer $APEX_API_KEY" ``` ## Delete visitor data Send a POST request to `/api/v1/data-deletion` with a visitor ID. ```bash curl -X POST "https://app.drip-apex.com/api/v1/data-deletion" \ -H "Authorization: Bearer $APEX_API_KEY" \ -H "Content-Type: application/json" \ -d '{"visitor_id":"visitor_123"}' ``` The request is recorded when the API returns `accepted`. Deletion usually finishes before the response. While recent event writes for the shop are still settling, it can stay `queued` for up to about an hour. Copies of erased data that arrive later are found and removed by follow-up checks. Deletion completes within the 30 days promised in the DPA. Apex deletes matching Tinybird event rows and redacts matching local Operator messages, Operator run/event payloads, and screenshot job results. Apex keeps a minimal privacy request audit row with status, processed time, and error information if something fails. To check deletion status: ```bash curl "https://app.drip-apex.com/api/v1/data-deletion?visitor_id=visitor_123" \ -H "Authorization: Bearer $APEX_API_KEY" ``` ## What Apex removes or returns Export reads matching analytics events for the shop and visitor ID from the configured Tinybird event datasource. Deletion removes matching analytics events from the configured Tinybird deletion datasource list. It also redacts local Apex artifacts that contain the visitor ID in Operator messages, Operator runs, Operator event payloads, and screenshot job results. Deletion is scoped by shop ID and visitor ID. It does not delete your ecommerce platform's customer, order, or consent records; handle those in your store platform or customer-data system. ## Troubleshooting - `401` or `403`: confirm you are using a server-side secret API key with the right data permission. - `400`: include either `visitor_id` in the POST body or `request_id` / `visitor_id` when checking status. - `404`: no matching privacy request exists for that shop and identifier. - `429`: the API key rate limit was reached; retry after the rate-limit window. - `failed` status: retry after checking the returned error and your Apex support channel. ## Related - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [Developer access and data](/docs/data-privacy/developer-access-data) - [API keys and public API v1](/docs/settings-account/api-keys) ## Public API surface map # Public API v1 The public Apex API is the `/api/v1/*` surface defined by the served OpenAPI contract. Contract routes require an API key unless listed under "Routes without API-key authentication". The same document also describes the explicitly listed dashboard session operations. Use [`/api/v1/openapi.json`](/api/v1/openapi.json) for methods, request bodies, responses, and schemas. Dashboard handlers outside `/api/v1/*` are session-scoped application internals. Only those registered under "Dashboard session routes" appear in this map. Separately, `GET /api/exp` is the SDK's storefront-configuration transport. It takes a `shopId`, needs no session or API key, and is served with a public 30-second cache. It is an SDK delivery endpoint, not part of the v1 contract; integrate through the hosted SDK script rather than calling it directly. The route inventory below is verified against `src/lib/api/openapi-spec.ts`; its drift test fails if the OpenAPI paths, their operation-level servers, their security declarations, or this list change independently. OpenAPI parameter syntax (`{id}`) is used verbatim. Worker operations declare the events host as their server and are listed with the full URL. Dashboard session operations declare `/` as their server and use absolute `/api/*` paths. ## Contract routes - `/api/v1/account/profile` - `/api/v1/admin/client-links` - `/api/v1/admin/customers` - `/api/v1/admin/customers/{orgId}` - `/api/v1/admin/customers/{orgId}/move-stores` - `/api/v1/admin/customers/{orgId}/setup` - `/api/v1/admin/customers/{orgId}/unique-visitors` - `/api/v1/admin/settings` - `/api/v1/admin/shops/{id}/archive` - `/api/v1/admin/shops/{id}/restore` - `/api/v1/admin/shops/{id}/settings` - `/api/v1/admin/shops/{id}/tracking-health` - `/api/v1/admin/tracking/quarantine` - `/api/v1/analytics/entry-exit` - `/api/v1/analytics/geo` - `/api/v1/analytics/order-journey` - `/api/v1/analytics/pages` - `/api/v1/analytics/profit` - `/api/v1/analytics/realtime` - `/api/v1/analytics/sessions` - `/api/v1/analytics/stats` - `/api/v1/analytics/suggest-goals` - `/api/v1/analytics/timeseries` - `/api/v1/analytics/traffic` - `/api/v1/analytics/url-inventory` - `/api/v1/analytics/visitor-series` - `/api/v1/assets` - `/api/v1/assets/{id}` - `/api/v1/assets/{id}/restore` - `/api/v1/audit-logs` - `/api/v1/backlog` - `/api/v1/backlog/fields` - `/api/v1/backlog/fields/{id}` - `/api/v1/backlog/{id}` - `/api/v1/backlog/{id}/approve` - `/api/v1/backlog/{id}/archive` - `/api/v1/backlog/{id}/design` - `/api/v1/backlog/{id}/reject` - `/api/v1/backlog/{id}/restore` - `/api/v1/brand-context` - `/api/v1/commerce/catalog` - `/api/v1/consent` - `/api/v1/cost-settings` - `/api/v1/data-deletion` - `/api/v1/data-export` - `/api/v1/environments` - `/api/v1/events/export` - `/api/v1/exclusion-groups` - `/api/v1/exclusion-groups/{id}` - `/api/v1/exclusion-groups/{id}/members` - `/api/v1/exclusion-groups/{id}/restore` - `/api/v1/experiment-events` - `/api/v1/experiments` - `/api/v1/experiments/by-slug/{slug}` - `/api/v1/experiments/conflicts` - `/api/v1/experiments/filesystem/schema` - `/api/v1/experiments/generate` - `/api/v1/experiments/{id}` - `/api/v1/experiments/{id}/commerce` - `/api/v1/experiments/{id}/commerce/price/csv` - `/api/v1/experiments/{id}/commerce/price/rows` - `/api/v1/experiments/{id}/complete` - `/api/v1/experiments/{id}/duplicate` - `/api/v1/experiments/{id}/filesystem` - `/api/v1/experiments/{id}/filesystem/apply` - `/api/v1/experiments/{id}/filesystem/plan` - `/api/v1/experiments/{id}/idea` - `/api/v1/experiments/{id}/pause` - `/api/v1/experiments/{id}/qa` - `/api/v1/experiments/{id}/qa/commerce-probes` - `/api/v1/experiments/{id}/qa/events` - `/api/v1/experiments/{id}/qa/evidence` - `/api/v1/experiments/{id}/qa/run` - `/api/v1/experiments/{id}/readiness` - `/api/v1/experiments/{id}/results` - `/api/v1/experiments/{id}/results/ad-roas` - `/api/v1/experiments/{id}/results/by-channel` - `/api/v1/experiments/{id}/results/explain` - `/api/v1/experiments/{id}/screenshots` - `/api/v1/experiments/{id}/start` - `/api/v1/experiments/{id}/variations/generate` - `/api/v1/experiments/{id}/variations/{variationId}/build` - `/api/v1/experiments/{id}/variations/{variationId}/files` - `/api/v1/experiments/{id}/variations/{variationId}/files/{path}` - `/api/v1/experiments/{id}/variations/{variationId}/revisions` - `/api/v1/experiments/{id}/variations/{variationId}/workspace` - `/api/v1/external-pixels/snippet` - `/api/v1/external-tracking` - `/api/v1/flags` - `/api/v1/flags/{id}` - `/api/v1/flags/{id}/activate` - `/api/v1/flags/{id}/archive` - `/api/v1/flags/{id}/code` - `/api/v1/flags/{id}/conclude` - `/api/v1/flags/{id}/experiences` - `/api/v1/flags/{id}/experiences/{experienceId}` - `/api/v1/flags/{id}/pause` - `/api/v1/flags/{id}/qa-devices` - `/api/v1/flags/{id}/qa-devices/{identity}` - `/api/v1/flags/{id}/reopen` - `/api/v1/flags/{id}/rules` - `/api/v1/flags/{id}/rules/reorder` - `/api/v1/flags/{id}/status` - `/api/v1/forms` - `/api/v1/forms/detect` - `/api/v1/forms/{key}` - `/api/v1/forms/{key}/errors` - `/api/v1/forms/{key}/fields` - `/api/v1/forms/{key}/fields/{identity}` - `/api/v1/forms/{key}/sessions` - `/api/v1/forms/{key}/summary` - `/api/v1/funnels` - `/api/v1/funnels/{id}` - `/api/v1/funnels/{id}/restore` - `/api/v1/funnels/{id}/run` - `/api/v1/goals` - `/api/v1/goals/{id}` - `/api/v1/goals/{id}/restore` - `/api/v1/harness/sessions` - `/api/v1/harness/sessions/{id}/publish` - `/api/v1/harness/sessions/{id}/versions` - `/api/v1/inbox/messages` - `/api/v1/inbox/read-state` - `/api/v1/inspect` - `/api/v1/install-domains` - `/api/v1/install-status` - `/api/v1/install/assess` - `/api/v1/key` - `/api/v1/keys` - `/api/v1/keys/self/shop` - `/api/v1/keys/{id}` - `/api/v1/landing-pages` - `/api/v1/landing-pages/domains` - `/api/v1/landing-pages/domains/{id}` - `/api/v1/landing-pages/domains/{id}/verify` - `/api/v1/landing-pages/{id}` - `/api/v1/landing-pages/{id}/deploy` - `/api/v1/landing-pages/{id}/experiment` - `/api/v1/landing-pages/{id}/filesystem` - `/api/v1/landing-pages/{id}/filesystem/apply` - `/api/v1/landing-pages/{id}/filesystem/plan` - `/api/v1/landing-pages/{id}/filesystem/upload` - `/api/v1/landing-pages/{id}/filesystem/upload-grant` - `/api/v1/landing-pages/{id}/preview` - `/api/v1/landing-pages/{id}/rollback` - `/api/v1/landing-pages/{id}/versions` - `/api/v1/me` - `/api/v1/me/context` - `/api/v1/notifications/ready/email` - `/api/v1/notifications/ready/slack` - `/api/v1/org` - `/api/v1/org/invitations` - `/api/v1/org/invitations/{id}` - `/api/v1/org/members` - `/api/v1/org/members/{id}` - `/api/v1/pages` - `/api/v1/pages/{id}` - `/api/v1/personalizations` - `/api/v1/personalizations/{id}` - `/api/v1/personalizations/{id}/activate` - `/api/v1/personalizations/{id}/archive` - `/api/v1/personalizations/{id}/pause` - `/api/v1/preview-sessions` - `/api/v1/roadmap/approved` - `/api/v1/roadmap/items/{id}/handoff` - `/api/v1/runtime-settings` - `/api/v1/runtime-settings/recipes` - `/api/v1/screenshots/{jobId}` - `/api/v1/segments` - `/api/v1/segments/{id}` - `/api/v1/shops` - `/api/v1/shops/{id}` - `/api/v1/shops/{id}/audit-logs` - `/api/v1/shops/{id}/auto-stop` - `/api/v1/shops/{id}/engine` - `/api/v1/shops/{id}/engine/guardrails` - `/api/v1/shops/{id}/engine/interventions/{iid}/rollout` - `/api/v1/shops/{id}/engine/pause` - `/api/v1/shops/{id}/engine/proposals/{pid}/approve` - `/api/v1/shops/{id}/engine/proposals/{pid}/reject` - `/api/v1/shops/{id}/engine/proposals/{pid}/veto` - `/api/v1/shops/{id}/engine/start` - `/api/v1/shops/{id}/engine/stop` - `/api/v1/shops/{id}/guardian` - `/api/v1/shops/{id}/install-instructions` - `/api/v1/shops/{id}/plugin-token` - `/api/v1/shops/{id}/shopware/prebind` - `/api/v1/shops/{id}/store-profile` - `/api/v1/shops/{id}/store-profile/facts/{factId}` - `/api/v1/shops/{id}/store-profile/findings/review` - `/api/v1/shops/{id}/store-profile/learning-runs` - `/api/v1/shops/{id}/testing-defaults` - `/api/v1/signals` - `/api/v1/signals/{id}` - `/api/v1/sos` - `/api/v1/sos/attachment-uploads` - `/api/v1/sos/mine` - `/api/v1/surveys` - `/api/v1/surveys/{id}` - `/api/v1/surveys/{id}/complete` - `/api/v1/surveys/{id}/duplicate` - `/api/v1/surveys/{id}/pause` - `/api/v1/surveys/{id}/start` - `/api/v1/targeting/test` - `/api/v1/team/invitations` - `/api/v1/team/invitations/{id}` - `/api/v1/team/members` - `/api/v1/team/members/{id}` - `/api/v1/telemetry/cli` - `/api/v1/test-results` - `/api/v1/test-results/import/confirm` - `/api/v1/test-results/import/inspect` - `/api/v1/test-results/import/preview` - `/api/v1/test-results/{id}` - `/api/v1/tracking/discovery` - `/api/v1/tracking/health` - `/api/v1/tracking/latency` Compare forced-preview and natural page-load decision latency. - `/api/v1/tracking/simulate-trigger` - `/api/v1/tracking/verify` Look up one test phone or account with `identity=` (exact `visitor_id` or `user_id`, 1 to 128 characters after trimming). Use the device UUID before identify and the app user id after identify. With identity, `minutes` defaults to 60 and is capped at 1,440; without it, the default remains 120 and the maximum is 10,080. Events include platform, app version, SDK version and environment. Requires a private API key with `experiments:read` permission. - `/api/v1/usage/unique-visitors` - `/api/v1/variations` - `/api/v1/variations/{id}` - `/api/v1/variations/{id}/restore` - `/api/v1/webhooks` - `/api/v1/webhooks/{id}` - `/api/v1/webhooks/{id}/deliveries` - `/api/v1/webhooks/{id}/restore` - `/api/v1/webhooks/{id}/test` ## Organization member suspension Owners and admins can use `PATCH /api/v1/org/members/{id}` with `{ "suspended": true, "reason": "Offboarded" }` to suspend a member, or `{ "suspended": false }` to reinstate them. The optional reason must contain non-whitespace text and is only allowed when suspending. Send role or `default_shop_role` changes in a separate request; combining them with `suspended` returns `400`. The member list and PATCH response include nullable `suspended_at`, `suspended_by`, and `suspended_reason` fields. Repeating the current state returns `200` without adding another audit entry. Self-suspension and self-reinstatement return `400` with `cannot_suspend_self`; suspending the last active owner returns `400` with `last_owner`. Missing members return `404` with `not_found`. Admins cannot suspend or reinstate owners; both requests return `403`. A suspended member's user key receives `403` with `member_suspended` when accessing that organization. Account context discovery excludes suspended organizations. Access to other active memberships remains available, and reinstatement restores organization access. CLI login approval refuses an account key when every organization membership is suspended. Shop API keys carry no user identity and are not affected by member suspension. ## Dashboard session routes These routes return `404` while the server rollout is off. Brand undo uses explicit `all_with_source`, copies-only `all`, or one `shop_id`, with per-store atomic receipts and access redaction. These POST operations require a dashboard session cookie and an explicit `x-apex-shop` header; they do not accept API keys. Both require `experiments:write`, and restoring a running test also requires `experiments:launch` and the ordinary resume safety checks, including QA. Preview is read-only. Apply binds the reviewed completion and preview version, requires an idempotency key and a reason, and restores the same run and assignments. A committed refresh failure returns HTTP 200 with receipt warnings; retry the same key to repair publication. Already-sent external effects cannot be recalled, and queued retries or in-flight sends may still deliver. - `/api/experiments/{id}/undo-completion` - `/api/experiments/{id}/undo-completion/preview` - `/api/flags/{id}/experiences` - `/api/flags/{id}/experiences/{experienceId}` - `/api/flags/{id}/rules` - `/api/flags/{id}/rules/reorder` - `/api/multi-shop-programs/{id}/actions/undo-completion` - `/api/multi-shop-programs/{id}/actions/undo-completion/preview` ## Worker runtime routes Served by the events Worker; the OpenAPI operations declare `https://events.drip-apex.com/v1` as their server. Neither route exists on the dashboard host. - `https://events.drip-apex.com/v1/assign` - `https://events.drip-apex.com/v1/handover/verify` ## Routes without API-key authentication These operations declare `security: []` in the contract. Handover verification requires a valid `X-Drip-Shop` header and a signed, unexpired `verifyToken` in the JSON body. The token must match the resolved shop. A missing or invalid shop header returns `400` before token verification. An invalid, expired, or mismatched token returns `401`. No ingest API key is required. - `https://events.drip-apex.com/v1/handover/verify` Note: This release adds the `/api/v1/analytics/order-journey` analytics contract for bounded order-journey diagnostics and Shopify order correlation; see `docs/specs/spec-6686-order-journey.md` for the contract details. ## Results for an audience and written summaries Call `GET /api/v1/experiments/{id}/results?segment=device:mobile` for mobile or `?segment=device:desktop` for desktop. Omit both slice parameters for All. `exclude_segment=dim:value` reads the complement; `segment` wins if both are present. Dimensions are `device`, `country`, `traffic_source`, `platform`, `campaign`, `visitor_kind`, `landing_page` and `weekday`. Values match observed stored values exactly. `currency` remains an optional three-letter ISO code. Every response includes `segment_applied: { requested: { dim, value, mode } | null, applied, reason }`. Invalid or unknown requests, unobserved values and unavailable preflights return the unsliced read with `applied: false` and respectively `invalid_value`, `unknown_dimension`, `unobserved_value` or `preflight_unavailable`. Check this field before labelling numbers as mobile or desktop. Store-order `commerce` is unsliced. For an applied segment it becomes `{ unavailable: true, reason: "not_sliced_by_segment" }` without store money or orders. Tracked `revenue` stays sliced. Per-segment ARPU is directional: `revenue.perTreatment[].result.significance` carries `reason: "revenue_significance_uncertified"` and `unavailableContext: "view"`; there is no certified per-device revenue confidence. The primary-goal probability is `decision.judgedConfidence` and measures that goal, not ARPU. Use these fields for a dashboard card, pairing each treatment with control: | Card number | Field or calculation | | --- | --- | | Users | `arms[].visitors` | | Transactions | Revenue goal metric `controlDisplayOrders` / `variantDisplayOrders`; check their source fields (attributed or pixel). `controlCount` / `variantCount` are converting visitors, not orders. | | CR | Display orders / arm visitors | | AOV | Revenue goal metric `controlAov` / `aov` (revenue / display orders) | | ARPU | `arms[].revenue` / `arms[].visitors` | | Each delta | Treatment / control − 1; undefined when control is zero | | Runtime revenue delta | (Treatment ARPU − control ARPU) × treatment visitors | Arm revenue includes matched orders without a tracked visit while arm visitors exclude those visits. The metric's `controlValuePerVisitor` / `valuePerVisitor` use its goal population, so they can differ from card ARPU. Keep these definitions distinct. Commerce-system order truth is available only on the unsliced call. The unsliced response also includes `narrative`, either null or `{ learning, psychology, insights: [{ title, description }], follow_ups, stakeholder_summary, generated_at }`. `generated_at` is the cached row's ISO creation time. Only a summary matching the current v15 input hash is returned. When `narrative_unavailable_reason` is `generating`, generation runs after the numbers response; a later All call returns the cached summary. Other reasons are `gated` (the conversion significance floor), `not_narratable` (no readable narrative dataset) and `request_unsliced` (a slice was requested). Sliced requests never look up or generate a summary. Count-every goals in summaries report events per visitor, not a percentage conversion rate. Concurrent cold All calls can each generate for the same hash; the existing unique-key upsert retains one row. CLI: `apex experiments results --segment device:mobile` or `--exclude-segment device:mobile`. MCP `get_results` accepts `segment` and `exclude_segment` with the same values and reports whether the slice applied. ### Structured test-result imports - `GET /api/v1/test-results` lists past results for the selected shop with `source=import|native|all`, `limit` (1–200, default 50), and cursor pagination in `{ data, pagination }`. - `GET /api/v1/test-results/{id}` reads a result by UUID, legacy ID, or test key in `{ data }`; missing or foreign-shop results return 404. Both reads accept shop or user keys with `experiments:read`. Each row carries `result_source` (`import` or `native`), classified like the Program page: a row with a source ID or source name is imported, including results migrated from the backlog. - `DELETE /api/v1/test-results/{id}` permanently removes one imported result with `experiments:write` and JSON `{ "confirm": true }`, returning the removed row in `{ data }`. The full record is retained in the audit log. Native results return 409, absent or foreign-shop results return 404, missing confirmation returns 400, and an unavailable required audit returns 500 without removing anything. - `POST /api/v1/test-results/import/inspect` accepts multipart `files` and returns detected columns and values. - `POST /api/v1/test-results/import/preview` accepts the same `files` plus a JSON `mapping` part and returns mapped rows, row errors, and warnings without writing. - `POST /api/v1/test-results/import/confirm` accepts JSON `{ rows }` in batches of at most 500 and writes imported test results. All three routes require `experiments:write`; user API keys must also pin a shop context. ### Rollout code edits `PATCH /api/v1/flags/{id}/code` requires `flags:write`. Send `custom_js` and/or `custom_css` (null clears; omission preserves), plus `expected_updated_at` from **the On variant** in `GET /flags/{id}`, or `expected_artifact_hash` from a prior code-save receipt. Stale versions return 409. Only On code and its version are changed; targeting, schedule, rollout, status, flag identity and Off stay intact. Edited code replaces that side's external artifact reference, if present. The response reports `saved`, `artifact_hash`, `publication`, `reviewStatus`, `blockingIssues` and `warningIssues` (inside `data` on success). Blocking issues refuse persistence. A 502 with `saved: true` means the save committed but shop publication failed; read back before retrying. Pending/warning publication statuses remain distinct from persistence. Render evidence is not accepted by this endpoint; `needs_review` is advisory for an already active rollout. CLI: `apex flags update-code --js ./on.js [--css ./on.css]`. The CLI reads the current On version before saving (requiring `flags:read` as well) and never automatically retries a conflict. ## Public event schema Apex events flow from the SDK, Shopify Pixel, external checkout pixels, or server integrations to the Worker, then to Tinybird. The shared `@drip/event-schema` package owns the public field list, validation patterns, and Tinybird column mapping. ## Incoming event shape The Worker accepts batches as an array of events, or as an object with an `events` array plus ingest auth: ```json { "events": [ { "type": "goal", "eventId": "evt_demo_1001", "shopId": "11111111-1111-4111-8111-111111111111", "visitorId": "visitor_demo_123", "sessionId": "session_demo_123", "experimentId": "22222222-2222-4222-8222-222222222222", "variationId": "33333333-3333-4333-8333-333333333333", "url": "https://example.com/products/demo", "data": { "goalId": "__add_to_cart__", "revenue": 49, "currency": "EUR" }, "ts": 1783502400000 } ], "_shopId": "11111111-1111-4111-8111-111111111111", "_signature": "px_signed_ingest_token_here" } ``` Accepted aliases: | Canonical meaning | Accepted incoming keys | |---|---| | Shop ID | `shop_id`, `shopId` | | Visitor ID | `visitor_id`, `visitorId` | | Session ID | `session_id`, `sessionId` | | User ID | `user_id`, `userId` | | Event type | `event_type`, `type` | | Event ID | `event_id`, `eventId` | | Experiment ID | `experiment_id`, `experimentId` | | Variation ID | `variation_id`, `variationId` | | Properties | `properties` as a JSON object string or object, or `data` as an object | | Timestamp | `timestamp`, `ts` | `event_type` must match `/^[a-zA-Z0-9_.:-]{1,120}$/`. Properties must be a JSON object after normalization. Apex rejects common sensitive property names such as `email`, `phone`, `password`, `ssn`, `credit_card`, and `card_number`. ## Outgoing field reference After normalization, Apex writes these fields: | Field | Required | Type or rule | Notes | |---|---:|---|---| | `event_id` | No | string | Generated when missing. Used for bounded dedupe when present. | | `shop_id` | Yes | string | Validated from ingest auth. | | `visitor_id` | Yes | string | Required after aliases; missing values are quarantined. | | `session_id` | Yes | string | Defaults to `unknown` if missing. | | `user_id` | Yes | string | Empty string when unknown. | | `event_type` | Yes | pattern string | Same pattern as incoming `event_type`. | | `experiment_id` | Yes | string | Empty string for unattributed events. | | `variation_id` | Yes | string | Empty string for unattributed events. | | `url` | Yes | string | Cleaned event URL. | | `properties` | Yes | JSON object string | Sanitized event properties plus `event_id`. | | `bot` | Yes | `0` or `1` | Worker bot evidence flag. | | `live_run_id` | Yes | string | Extracted from `liveRunId` or `live_run_id`; empty when absent. | | `order_key` | Yes | string | Derived from order, booking, checkout, or attribution properties. | | `revenue` | No | finite number | Promoted from properties when present and valid. | | `currency` | No | 3 uppercase letters | Promoted from `currency`, `currencyCode`, or `currency_code`. | | `duration` | No | finite number | Promoted from properties. | | `scroll_depth` | No | finite number | Promoted from `scroll_depth` or `scrollDepth`. | | `goal_id` | No | string | Promoted from `goalId` or `goal_id`. | | `referrer` | No | string | Promoted from properties. | | `utm_source` | No | string | Promoted from `utm_source` or `utmSource`. | | `utm_medium` | No | string | Promoted from `utm_medium` or `utmMedium`. | | `utm_campaign` | No | string | Promoted from `utm_campaign` or `utmCampaign`. | | `device` | Yes | string | Derived from the request user agent. | | `country` | Yes | string | Derived from Cloudflare request metadata or `unknown`. | | `timestamp` | Yes | parseable datetime string | Derived from `timestamp`, `ts`, or receipt time. | | `qa_mode` | Yes | `0` or `1` | `1` when incoming `qa_mode` is true. | | `qa_session_id` | Yes | string | Set from the preview link's `drip_qa_session` parameter and preserved for the browser session so QA event readback can isolate one run. Empty string when absent. | `user_id`, `experiment_id`, `variation_id`, `live_run_id`, `order_key`, and `qa_session_id` intentionally use empty strings when absent for Tinybird compatibility. ## Server API source requirements Events sent with a tracking source key through `x-apex-source-key` or `Authorization: Bearer apx_src_...` must include a stable `event_id`. Apex uses that ID for replay protection and can quarantine missing IDs for server API sources. ## Related pages - [Worker public endpoints](/docs/developers/worker-endpoints) - [Shopify Pixel integration](/docs/developers/shopify-pixel) - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) ## External checkout pixels Use external checkout pixels when the purchase happens on a provider domain where the Apex SDK is not loaded, but the provider lets you add JavaScript to a confirmation page. ## Runtime pattern 1. Configure external tracking handover for the provider domain in Apex runtime settings. 2. Choose the handover query parameter, for example `drip_bucket` or `ref`. 3. Generate a provider snippet from `/api/external-pixels/snippet`. 4. Install the snippet on the provider confirmation page. 5. Call the generated global function with the provider's booking or order variables. The SDK appends the handover payload only when a visitor clicks or submits toward a configured external domain. It can append to links and forms. The default handover parameter is `drip_bucket`, the default TTL is 120 seconds, and signed handover is enabled unless disabled in runtime settings. ## Generate the snippet ```http POST /api/external-pixels/snippet Content-Type: application/json { "provider": "fareharbor", "handoverParam": "ref", "globalName": "trackFareHarborPurchase" } ``` The response includes the shop ID, Worker endpoint, sanitized provider, handover parameter, global name, and the snippet. `handoverParam` keeps only letters, numbers, underscores, and dashes. `globalName` must be a valid JavaScript identifier or Apex falls back to `apexExternalPurchase`. ## Provider-side call Adapt the generated function call to the provider's variables: ```js trackFareHarborPurchase({ ref: booking.onlineRef, revenue: booking.totalInCents / 100, currency: booking.currency, bookingId: booking.pk, orderId: booking.order && booking.order.uuid, itemId: booking.item && booking.item.pk, itemName: booking.item && booking.item.name, metadata: { channel: "external_checkout" } }); ``` The snippet also reads the configured handover query parameter from `location.href` when `ref` or `onlineRef` is not passed. ## Attribution formats The snippet accepts: | Format | Notes | |---|---| | Apex base64url payload | Decodes `visitorId` and `assignments` from the SDK handover payload. | | Compact legacy format | `apex:visitorId:experimentId:variationId`. | | Direct function inputs | `experimentId` and `variationId` can be passed directly when no handover reference is available. | When no assignment exists, the snippet still sends one unattributed event using `visitorId: "external_pixel"` and `sessionId: "external_pixel"`. ## Event sent to Apex The generated snippet posts to `{endpoint}/events` with `_shopId` and `_signature`: ```json { "events": [ { "type": "goal", "data": { "goalId": "__revenue__", "revenue": 129, "currency": "EUR", "orderId": "booking_demo_1001", "bookingId": "booking_demo_1001", "provider": "fareharbor", "externalRef": "handover_payload_here", "source": "external_pixel" }, "shopId": "11111111-1111-4111-8111-111111111111", "visitorId": "visitor_demo_123", "sessionId": "external_pixel", "experimentId": "22222222-2222-4222-8222-222222222222", "variationId": "33333333-3333-4333-8333-333333333333", "url": "https://checkout.example.com/confirmation", "ts": 1783502400000 } ], "_shopId": "11111111-1111-4111-8111-111111111111", "_signature": "px_signed_ingest_token_here" } ``` `goalId` defaults to `__revenue__`. `revenue` accepts numbers, numeric strings, or `totalInCents` divided by 100. `metadata` keys are copied into the event data. ## What this does not cover External checkout pixels fire only where the provider runs the snippet. Funnel intent events such as checkout-step views, add-to-cart, or form friction still need provider events, `postMessage`, custom storefront JavaScript, or a native integration. ## Related pages - [Worker public endpoints](/docs/developers/worker-endpoints) - [Public event schema](/docs/developers/event-schema) - [InitConfig reference](/docs/sdk/init-config) ## Developer overview Use these pages when integrating Apex with custom storefronts, automation, API clients, webhooks, pixels, and privacy workflows. ## Runtime and SDK - [SDK reference](/docs/sdk/install-quickstart) - [App SDK for Ionic, Angular, Capacitor and Cordova apps](/docs/app-sdk/overview) - [Runtime controls, SDK, anti-flicker, and consent](/docs/settings-account/runtime-sdk-controls) - [API keys and public API v1](/docs/settings-account/api-keys) - [Webhook payload reference](/docs/developers/webhooks) - [Public event schema](/docs/developers/event-schema) - [Worker public endpoints](/docs/developers/worker-endpoints) - [Shopify Pixel integration](/docs/developers/shopify-pixel) - [External checkout pixels](/docs/developers/external-checkout-pixels) - [GDPR export and deletion](/docs/data-privacy/gdpr-export-deletion) - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [Developer access and data](/docs/data-privacy/developer-access-data) - [Generated OpenAPI reference](/docs/api/reference) ## Price basis for value-bearing treatments Apex can model staging and production storefronts as environments of one **shop**, or as two independent shops. A preview resolves domains for its shop and environment. It does not translate treatment values for whichever storefront happens to render them. This distinction matters whenever a treatment displays a price, discount, shipping amount, threshold, or other value that can differ between storefronts. ## Literal treatment values stay literal Treatment artifacts store authored values. Mutation `text` and `html` fields and `custom_js` are delivered as saved; Apex does not recognize a price inside them or replace it with the current storefront's value. For example, a treatment that contains `1,13 €` will still display `1,13 €` when previewed on a staging shop whose page currently shows `1,10 €`. A successful staging preview proves that the saved treatment rendered. It does **not** prove that a hardcoded production price matches staging data, or that Apex selected a price for the previewed environment. Before approving a value-bearing treatment, identify both: 1. the shop and storefront domain used for QA; and 2. the basis of every authored value (live page data, staging data, or production data). ## Recommended patterns Choose one pattern deliberately. ### Read the live value from the page Prefer a live-computed treatment when the value should follow the storefront being viewed. Read the authoritative value from the page DOM at apply time, derive the treatment copy or calculation from it, and re-evaluate after relevant SPA or product-variant updates. This is usually the safest option for presentation-only treatments that must work unchanged across independently configured shops. Use stable selectors and fail closed: if the expected source value is absent or cannot be parsed, do not render a fabricated price. ### Maintain one experiment per shop When staging and production intentionally use different fixed values, create a separate experiment in each shop and author the values for that shop. QA and approve each experiment against its own storefront. Do not copy a production artifact to a staging shop and treat a visually successful render as price verification. ### Record the basis when hardcoding If a fixed production value is intentional, make the basis explicit in the QA evidence. Use a note such as: ```text Price basis: production. Hardcoded values were checked against on . The staging storefront has independent prices and is used only to verify rendering. ``` List each fixed value when the treatment contains more than one. A screenshot without this note is ambiguous: it cannot tell a reviewer whether the displayed value came from the page, staging data, or production data. ### Use environments within one shop For a same-shop setup, attach the staging domain to a non-production environment. Install that environment's snippet tag on the staging storefront, and author the experiment in that environment with values checked against staging data. The tag delivers experiments assigned to its environment, and config delivery filters by `environment_key`. When the experiment is ready, promote it to create a production draft. Promotion copies the treatment, including literal values. Re-check every storefront-dependent value against production data before you start the production experiment. ## What `environment_key` does not do `Experiment.environment_key` selects experiments for config delivery within one shop. It supports a same-shop staging workflow, but it never pairs two shops. If a merchant's staging storefront is modeled as a separate shop, it remains fully independent. Each shop has its own `shop_id`, domains, experiments, variations, and configuration. An `environment_key` in one shop does not map that shop to another shop or translate treatment values between them. First confirm whether the staging storefront is an environment of the production shop or a separate shop. Then use the matching value pattern above. ## QA checklist - Confirm the active shop and preview domain. - Inventory every literal price, discount, threshold, and other storefront-dependent value in mutations and custom JavaScript. - For live-computed values, verify the source selector, parsing behavior, SPA/variant updates, and fail-closed state. - For per-shop experiments, verify that each shop's artifact contains its own values. - For same-shop environments, verify staging values before promotion and production values in the promoted draft. - For hardcoded production values, add an explicit `Price basis: production` note and cite the evidence used to validate them. - Treat the preview as rendering evidence, not as automatic price-source validation. ## Related pages - [Install Apex on a headless React storefront](/docs/setup/headless-react) - [Experiment filesystem and QA commands](/docs/cli/experiments-filesystem-qa) - [QA and previews](/docs/experiments/qa-and-previews) ## SDK examples The full SDK reference now lives in the [SDK reference section](/docs/sdk/install-quickstart). Start there for the current install snippet, npm package usage, InitConfig table, runtime API, React bindings, feature flags, and migration notes. ## Install snippet ```html ``` ## Track an event ```js Drip.track("add_to_cart", { product_id: "gid://shopify/Product/123", value: 49.00, currency: "EUR" }); ``` ## Runtime config ```http GET /api/exp?shopId=shop_uuid { "experiments": [...], "profile": { ... }, "runtime": { ... }, "signals": [...], "delivery": { ... } } ``` ## Full reference - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) - [InitConfig reference](/docs/sdk/init-config) - [Runtime API](/docs/sdk/runtime-api) - [React bindings](/docs/sdk/react-bindings) ## Shopify Pixel integration The Apex Shopify Web Pixel runs inside Shopify's sandboxed pixel iframe. It subscribes to standard Shopify customer events, maps them to Apex events, and posts them to the Worker event pipeline. ## Settings The pixel receives settings from Shopify's `webPixelCreate` configuration: | Setting | Purpose | |---|---| | `shopId` | Public Apex shop ID on outgoing events. | | `endpoint` | Worker base URL; the pixel posts to `{endpoint}/events`. | | `ingestShopId` | Shop ID covered by the signed ingest token. | | `ingestSignature` | Signed pixel ingest token sent as `_signature`. | | `enabledEvents` | Shopify event names Apex should forward. | | `trackPageViewed` | Controls whether `page_viewed` is forwarded. | | `queueUnattributedMs` | Time to buffer events waiting for SDK assignment sync; default 3000 ms. | | `maxBufferedEvents` | Maximum buffered unattributed events; default 20. | ## Assignment sync The storefront SDK publishes assignment state through the Shopify analytics bridge: ```js Shopify.analytics.publish("drip_assignment_sync", { visitorId: "visitor_demo_123", sessionId: "session_demo_123", assignments: [ { experimentId: "22222222-2222-4222-8222-222222222222", variationId: "33333333-3333-4333-8333-333333333333" } ], qaMode: false }); ``` The pixel normalizes that payload, stores it in memory, and persists it best-effort to Shopify pixel local storage and a cookie named `drip_assignment_sync_v1` for two hours. ## Event mapping | Shopify event | Apex event type | Key fields | |---|---|---| | `checkout_completed` | `goal` | `goalId: "__revenue__"`, `funnelStep: "purchase_completed"`, revenue, currency, order ID, line items | | `checkout_started` | `goal` | `goalId: "__checkout_started__"`, checkout value fields | | `checkout_contact_info_submitted` | `goal` | `goalId: "__checkout_contact_info_submitted__"` | | `checkout_address_info_submitted` | `goal` | `goalId: "__checkout_address_info_submitted__"` | | `checkout_shipping_info_submitted` | `goal` | `goalId: "__checkout_shipping_info_submitted__"` | | `payment_info_submitted` | `goal` | `goalId: "__payment_info_submitted__"` | | `product_added_to_cart` | `goal` | `goalId: "__add_to_cart__"`, product, variant, price, quantity | | `product_removed_from_cart` | `product_removed_from_cart` | Product, variant, price, quantity | | `cart_viewed` | `cart_viewed` | Cart total and item count | | `product_viewed` | `product_viewed` | Product ID, title, type, vendor, price | | `collection_viewed` | `collection_viewed` | Collection ID and title | | `search_submitted` | `search_submitted` | Query and result count | | `page_viewed` | `pageview` | Page title and URL, only when `trackPageViewed` is true | | `alert_displayed` | `alert_displayed` | Message, alert type, area, target field, discount code when present | | `ui_extension_errored` | `ui_extension_errored` | Error and extension metadata | | `drip_checkout_experiment_viewed` | `experiment_viewed` | Checkout-block experiment and variation IDs | | `drip_checkout_message_rendered` | `checkout_message_rendered` | Checkout message shown for a test that also changes the storefront: experiment and variation IDs, `assignment_epoch`, `extension_version`; not an exposure, so results never count it | Checkout line items list a bundle per component: each product in the bundle becomes its own line with its own price, and carries `bundleParentLineId`, `bundleParentVariantId` and `bundleParentProductId` so the lines can be grouped back into the bundle. A bundle whose component prices do not add up to the bundle line price stays as one line flagged `bundleExpansion: "unreconciled"`. Each line also carries its own `discountAllocations` (amount, title and type) when Shopify allocated a discount to it. Most mapped events add `source: "shopify_pixel"` in the event data before sending. Both checkout-block events use `source: "shopify_checkout_block"` so checkout extension exposures stay distinguishable. ## Event IDs and dedupe The pixel creates stable IDs when Shopify or commerce data provides one: 1. Shopify event `id` 2. Shopify event `eventId` 3. Shopify event `clientId` 4. Mapped `orderId` 5. Mapped `order_id` 6. Mapped `checkoutToken` Stable IDs use this shape: ```text shopify_pixel:{event_type}:{source_id} ``` If no stable source identifier exists, the pixel generates a random event ID. The Worker performs bounded event ID dedupe before publishing to Tinybird. ## Unattributed buffer When no assignment sync is available yet, the pixel builds an unattributed event with `visitorId: "shopify_anonymous"` and `needs_attribution: true`. If buffering is enabled, the event is kept in memory until assignment data arrives or `queueUnattributedMs` expires. When assignments arrive, the buffer replays each queued event with `needs_attribution: false` and one event per active assignment. If the timer expires, the buffer drains as unattributed. Assignment events always enable the pixel's attribution buffer before replaying queued events. ## Related pages - [Public event schema](/docs/developers/event-schema) - [Worker public endpoints](/docs/developers/worker-endpoints) - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) ## Webhook payload reference Apex webhooks notify your system after selected experiment, Guardian, screenshot, and publication events. Webhooks are shop-scoped. Each endpoint has a URL, subscribed event list, active flag, and generated signing secret. ## Create an endpoint Create webhooks with the public API: ```http POST /api/v1/webhooks Authorization: Bearer apx_your_key_here Content-Type: application/json { "url": "https://example.com/apex/webhooks", "events": ["experiment.started", "experiment.completed"] } ``` The URL must be HTTPS. The create response includes the generated `secret` once. List, read, and update responses do not include it. ## Event names These are the event names accepted by the public webhook create/update schema: | Event | When Apex enqueues it | |---|---| | `experiment.started` | An experiment starts from the dashboard launch flow or scheduled launch worker. | | `experiment.completed` | An experiment is completed from the dashboard, a brand run, or a public API request. | | `experiment.significant` | Auto-stop pauses an experiment because a significant winner was found. | | `experiment.guardian_triggered` | Guardian detects a warning and alerts without pausing. | | `experiment.guardian_paused` | Guardian pauses an experiment for a configured safety finding. | | `variation.winning` | Auto-stop identifies the winning variation for the primary goal. | | `screenshots.completed` | A screenshot job finishes and stores screenshot results. | | `project.published` | Runtime publication records a shop revision when publication webhooks are enabled. | ## Delivery envelope Every outbound delivery body is JSON. Apex serializes the stored event payload, then adds delivery metadata: ```json { "event": "experiment.started", "experiment_id": "11111111-1111-4111-8111-111111111111", "experiment_name": "PDP trust badge", "delivery_id": "22222222-2222-4222-8222-222222222222", "timestamp": "2026-07-08T12:00:00.000Z" } ``` Headers: | Header | Value | |---|---| | `Content-Type` | `application/json` | | `X-Drip-Signature` | `sha256=` followed by the hex HMAC SHA-256 of the exact request body using the webhook secret. | | `X-Drip-Event` | Event name, for example `experiment.started`. | | `X-Apex-Delivery-Id` | Delivery row ID. Use this for idempotency. | | `User-Agent` | `Drip-Webhooks/1.0` | ## Signatures Verify the raw body bytes before parsing or transforming the payload: ```ts import crypto from "node:crypto"; export function verifyApexWebhook(rawBody: string, signatureHeader: string, secret: string) { const expected = "sha256=" + crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const received = Buffer.from(signatureHeader); const expectedBytes = Buffer.from(expected); return received.length === expectedBytes.length && crypto.timingSafeEqual(received, expectedBytes); } ``` ## Payload shapes Common metadata is always added by the dispatcher: | Field | Type | Notes | |---|---|---| | `event` | string | Event name. | | `delivery_id` | string | Unique Apex delivery ID. | | `timestamp` | ISO datetime string | Delivery serialization time. | Event-specific fields: | Event | Fields | |---|---| | `experiment.started` | `experiment_id`, `experiment_name` | | `experiment.completed` | `experiment_id`, `experiment_name` | | `experiment.significant` | `experiment_id`, `experiment_name`, `goal_id`, `winning_variation_id` | | `variation.winning` | `experiment_id`, `experiment_name`, `variation_id`, `goal_id` | | `experiment.guardian_triggered` | `experiment_id`, `experiment_name`, `kind`, `action`, `variation_id`, `goal_id`, `details` | | `experiment.guardian_paused` | `experiment_id`, `experiment_name`, `reason`, `variation_id`, `goal_id` | | `screenshots.completed` | `job_id`, `experiment_id`, `results` | | `project.published` | `project_id`, `revision`, `environment_key`, `status`, `published_at` | `goal_id` can be `null` where the triggering condition does not apply to a specific goal. `details` and `results` are JSON objects whose internal shape depends on the Guardian finding or screenshot job output. ## Retries and failure behavior Apex queues matching active webhooks and processes pending deliveries asynchronously. A delivery is claimed as `processing`, sent with a 10 second timeout, and marked delivered only when the receiver returns a 2xx status. Failed fetches and non-2xx responses are retried up to five attempts with exponential backoff. Retry delay starts at 15 seconds and is capped at 30 minutes. Processing deliveries older than 10 minutes are treated as stale and can be claimed again. After the final failed attempt, the delivery is marked `dead`. The parent webhook's consecutive failure counter is incremented. After three consecutive dead deliveries, Apex disables the webhook and sets `disabled_at`. ## Inspect deliveries Use the delivery log endpoint to debug recent attempts: ```http GET /api/v1/webhooks/22222222-2222-4222-8222-222222222222/deliveries?status=dead&limit=20 Authorization: Bearer apx_your_key_here ``` Valid `status` filters are `queued`, `processing`, `delivered`, `failed`, and `dead`. `limit` defaults to 50 and is clamped from 1 to 100. ## Related pages - [Public event schema](/docs/developers/event-schema) - [Public API surface map](/docs/developers/api/overview) - [Apex MCP tool reference](/docs/mcp/tool-reference) ## Worker public endpoints The Apex Worker serves storefront-facing runtime routes. This page covers only the public subset: event ingest, runtime config, short install snippets, landing-page routes such as `/guide`, preview proxy routes, and health checks. Internal Worker routes are intentionally excluded. ## Base hosts Use the configured Worker base URL for your shop. Production installs usually use: ```text https://events.drip-apex.com ``` Custom-domain edge traffic can also send events to: ```text /_drip/events ``` ## `POST /events` and `POST /_drip/events` Use these endpoints for analytics event ingest. `/events` is accepted on control hosts. `/_drip/events` is accepted as the edge event path for custom-domain traffic. ```http POST /events Content-Type: application/json { "events": [ { "type": "goal", "eventId": "evt_demo_1001", "shopId": "11111111-1111-4111-8111-111111111111", "visitorId": "visitor_demo_123", "sessionId": "session_demo_123", "data": { "goalId": "__add_to_cart__" }, "ts": 1783502400000 } ], "_shopId": "11111111-1111-4111-8111-111111111111", "_signature": "px_signed_ingest_token_here" } ``` Behavior: | Rule | Implemented behavior | |---|---| | Method | `POST` only. Other methods return 405. | | Auth | Requires ingest auth through `x-drip-shop` and `x-drip-signature`, body `_shopId` and `_signature`, or a valid tracking source key. | | Body | Must be a JSON array of events or an object with `events`. | | Batch size | Empty batches return 400. More than 200 events returns 413. | | Rate limits | Per-shop event rate limits apply and can return 429 with `Retry-After`. | | Invalid events | Invalid events can be quarantined; if every event is quarantined the response is 202 with `ingested: 0`. | | Dedupe | Events with duplicate `event_id` values are filtered before publish. | | Publish | Uses an event queue when configured and returns 202, otherwise forwards NDJSON to Tinybird and returns Tinybird's status/body. | See [Public event schema](/docs/developers/event-schema) for fields and aliases. ## `GET /api/exp` `/api/exp` is the public runtime config endpoint served by the web app and fetched by the Worker snippet builder. It returns the shop's edge config payload: experiments, runtime settings, signals, profile, and delivery manifest. ```http GET /api/exp?shopId=11111111-1111-4111-8111-111111111111 ``` Query parameters: | Parameter | Notes | |---|---| | `shopId` | Required. | | `env` or `environment` | Optional runtime environment key. | | `drip_qa`, `drip_force`, `drip_experiment_id`, `drip_variation_id` | QA/forced assignment parameters used by preview and QA flows. | | `drip_force=EXP:~` | Holds EXP at Original with no assignment, exposure, stickiness, or mutations in the SDK and at the edge. A hold alone leaves other tests running normally and does not survive a cross-domain redirect handover. Combine holds and variation pins with commas. | | `drip_force_coexist=1` | SDK QA opt-out from forced preview isolation. By default only forced experiments and `drip_exp` preview handles are assigned. The choice persists in the 30-minute `drip_force_coexist` cookie alongside the QA force; exiting QA clears it. Shared-container conflict warnings and the snapshot `conflicts` list are available only in the QA bundle (QA mode), not in the live bundle. | | `drip_harness` | Signed QA preview or harness token, forwarded on the QA lane even without a force pin. The SDK retains it in the 30-minute `drip_qa_harness` cookie and clears it on QA exit or explicit reset. | | `drip_debug`, `debug`, `apex_debug`, `apex_config_refresh` | Debug/cache-bypass controls. | The QA panel’s **Coexist with live tests** toggle sets the same `drip_force_coexist=1` choice for the session. Pre-launch QA and ready-to-launch experiments appear in the panel only when the server verifies the QA session’s authorization for this shop. Three authorizations count: a signed QA link opened from the dashboard (a client link without debug does not list them), an active harness session for the shop, or a preview-read capability scoped to the shop. These experiments render only when pinned. `drip_qa=1` alone lists running tests. {/* Release note: when opened from a signed dashboard QA link the QA panel can list on-store QA and ready-to-launch tests after server verification; pinned previews may be run alongside live tests using the Coexist toggle. */} Responses include `ETag`, `Cache-Control`, `CDN-Cache-Control`, `Vercel-CDN-Cache-Control`, and `X-Apex-Config-Cache`. QA and debug responses are no-store. Public responses cache briefly unless runtime delivery is disabled. ## `GET /s/{shopId}.js` The short snippet endpoint serves a synchronous JavaScript install script: ```html ``` Preconnect to the snippet origin before loading it. Keep the script tag synchronous: it guarantees the anti-flicker guard, assignment, and goal tracking are ready before the page is interactive. An `async` tag is an explicit opt-in for after-hydration shops with anti-flicker off that accept the measurement trade-off (early interactions can be missed or mis-attributed). The path must match `/s/{uuid}.js`. The response includes anti-flicker setup, SDK code, and inlined experiment config. Invalid shop IDs return JavaScript comments with a 400 status. Per-IP snippet rate limits can return 429 and `Retry-After`. ## Landing-page paths such as `/guide` The Worker does not hardcode a standalone `/guide` branch. It serves landing-page routes dynamically when a published route manifest exists for the request host and normalized path. That means `/guide` is public when Apex has registered a published landing page route for that host/path. Landing-page responses: | Behavior | Notes | |---|---| | Methods | `GET` and `HEAD`. | | Routing | Host and normalized path are looked up in KV. | | Storage | Published HTML and assets are read from R2. | | Variants | If a landing-page experiment variant is configured, the Worker assigns by visitor ID and serves variant HTML. | | Headers | Responses include `x-apex-landing-page-id`, version, publish ID, cache key, and `x-drip-edge: landing-page`. | ## `GET /api/preview` and `/api/preview/state` Preview routes run on the preview host and proxy a signed preview session. | Endpoint | Purpose | |---|---| | `/api/preview?pst=...&url=https%3A%2F%2Fexample.com%2Fproducts%2Fdemo` | Fetches the target page, rewrites HTML/CSS through the preview proxy, strips frame-blocking headers, injects the preview bridge, and stores cookies per session. | | `/api/preview/state?pst=...` | Returns preview session state as JSON. | Rules: - `pst` is required. Missing tokens return 401. - Only `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE`, and `OPTIONS` are accepted. - `/api/preview` requires a `url` query parameter. - The target host must be allowed by the signed preview session payload. - Responses use no-store caching. ## `GET /health` The health endpoint returns Worker status on control hosts: ```json { "ok": true, "service": "apex-worker", "environment": "production", "release": "release-id-or-null", "queue_enabled": true, "queue_name": "event-queue", "distributed_rate_limits": { "events_per_shop": true, "snippet_per_ip": true }, "dead_letter_queue": { "ok": true }, "checked_at": "2026-07-08T12:00:00.000Z" } ``` The response always sets `Cache-Control: no-store`. ## Related pages - [Public event schema](/docs/developers/event-schema) - [Shopify Pixel integration](/docs/developers/shopify-pixel) - [External checkout pixels](/docs/developers/external-checkout-pixels) - [SDK SPA and router modes](/docs/sdk/spa-router) ## Apex Operator and tool-led building > **Simple terms** > > Operator is the AI builder. It uses browser, page, code, selector, network, screenshot, filesystem, and persistence tools to help build and QA experiments. Apex Operator is the model-led experiment builder. It gives the model compact tools for inspecting pages, network traffic, state, selectors, artifacts, QA evidence, and persistence. ## Routes - `/experiments/[id]/variations/[varId]` - Operator logs overlay in the variation editor - `/api/operator-chat-session` - `/api/operator-events` - `/api/operator-runs` - `/api/operator-evidence` - `/api/operator-network` - `/api/operator-screenshot` ## Implementation behavior - On installed stores, Operator browser tools can use an active on-site editor session to inspect and interact with the real merchant page instead of the proxy iframe. - The compact on-store dock can generate and save a treatment from live DOM context. Full tool-led QA, evidence review, and launch gating remain in the dashboard Operator workflow. - Operator sessions gather current experiment, variation, page, workspace, and store-profile context. - Tools let the model inspect DOM, network, console, page state, selectors, files, screenshots, artifacts, and saved preview state. - Operator writes structured changes to variation mutations or code workspaces instead of relying on one-off text suggestions. - Operator never puts attached image data into variation code. In variant build mode, it uses `upload_asset` for an image from the current turn and puts the returned hosted URL in the variation. - External handoff now prefers the Apex CLI workflow: install `@drip-apex/apex-cli`, run `apex login`, check out canonical workspace files, dogfood with `apex workspace dev`, then publish with `apex workspace publish`. - The CLI uses the same canonical workspace file model as in-app Operator: `src/index.js`, `src/index.scss`, `src/info.js`, `src/mutations.json`, `experiment.config.json`, `targeting.json`, `goals/*.json`, `segments/*.json`, `network/*.recipe.json`, and `dev-doc.md`. - Workspace goal sync: adding a goal to a paused or running test no longer blocks saving code edits; the build now re-syncs workspace goal files from the database when appropriate (see `docs/specs/spec-6654-workspace-goal-sync-heal.md`). {/* user-note: "Adding a goal to a paused or running test no longer blocks saving code edits.","ticket":"#6654" */} - Logs preserve chat messages, tool calls, evidence, failure states, artifacts, and network details for review. - The product principle is general tool affordance first: avoid hardcoding one test type as the only supported workflow. ## How to use it - Use Operator to inspect a target page, propose a variant, build it, and collect QA evidence. - Review every generated change before launch. - If a model variation finishes just after the run budget expires, it is now saved as a draft (not discarded); the run failure card shows the draft state and a retry option. {/* user-note: "Operator no longer throws away a finished variation when it runs out of time; you see what was saved and can retry." */} - Operator runs now persist every observed event and record their exit branch; uploads finalize once expected events arrive, recover missing events by sequence, and retain capture sequences after clearing logs. {/* user-note: "Operator runs persist every event and record their exit branch; uploads finalize after every expected event arrives, recover missing events by sequence, retain capture and absolute sequences after clearing logs, and send a bounded terminal event tail on page exit." */} - Open the in-editor logs overlay when a run fails, produces unexpected output, or needs auditability. - Give Operator specific constraints, but let it use general tools for page inspection and implementation. - For handoff runs, let the external agent inspect the live target URL and relevant storefront/domain context itself; screenshots are useful evidence, not a hard prerequisite. {/* A clarifying question produced by the Operator may now include a structured "blocker" payload that the Operator checks against its selector evidence. When the Operator emits a supported clarifying question the turn ends without extra repair rounds, and the run records whether the clarifying ending came from the model or from a runtime gate. This behaviour is source-backed by the runtime clarification implementation in the release. */} ## When not to use it - Do not treat Operator output as automatically launch-safe. - Do not use it for deterministic one-click workflows that normal forms already handle faster. - Do not constrain Operator to one narrow playbook when a general page-inspection tool can solve the task better. ## Implementation source - Core code lives under `src/features/apex-operator`. - APIs include `/api/operator-chat-session`, `/api/operator-events`, `/api/operator-runs`, `/api/operator-evidence`, `/api/operator-network`, and `/api/operator-screenshot`. - The variation editor mounts the Operator UI and uses saved tool results to persist mutations, code workspace changes, artifacts, and evidence. - External handoff uses `@drip-apex/apex-cli` with `apex workspace checkout`, `apex workspace dev`, and `apex workspace publish` against canonical workspace files shared with in-app Operator. ## Data and API - APIs include `/api/operator-chat-session`, `/api/operator-events`, `/api/operator-runs`, `/api/operator-evidence`, `/api/operator-network`, and `/api/operator-screenshot`. - CLI workspace APIs include `/api/v1/experiments/[id]/variations/[variationId]/workspace`, file read/write routes, build, and revisions. - Models include operator sessions, events, artifacts, evidence, review records, and code workspace snapshots. - Uses store profile facts, assets, backlog context, experiment setup, and preview browser state. ## Failure modes - Poor output usually means missing target URL, missing store context, or vague instruction. - Tool failure can come from blocked storefront access, auth-required pages, CSP, or preview host mismatch. - A disconnected on-site tab falls back to the dashboard preview transport; reopen Edit on store when real-origin verification is required. - Never assume generated code is launch-ready without QA and visual inspection. - Operator preview limits: storefront pages up to 8 MB now load in the visual editor preview; when a page cannot be loaded the Operator turn ends quickly (within about one minute) and shows a simple failure message instead of a long timeout. {/* user-note: {"user":"Large storefront pages now open in the Operator preview, and when a page cannot load the Operator tells you right away.","ticket":"#6398"} */} - Operator evidence and reads: Operator now collects page evidence in parallel with a 20 second deadline per read so a slow storefront does not stall a build for minutes. {/* user-note: {"user":"The Operator starts building sooner on slow store pages."} */} - Preview preparation: the visual editor prepares the preview once per Operator turn instead of before every model call, reducing repeated work during a single turn. {/* internal-change */} - Inspection behavior: Operator no longer substitutes slow host-component or commerce-surface reads for the model's inspection calls; when evidence is missing the model is informed instead, and successful host-context reads clear subsequent missing-evidence hints. {/* internal-change */} ## Variant builder, visual mutations, and code mode > **Simple terms** > > The builder is where a control or treatment variation is prepared. It can save simple DOM mutations or a richer code workspace. The builder authors the actual storefront change for a variation. It supports visual mutations, custom CSS/JS, file-based code workspaces, previews, revisions, and generated changes. ## Routes - `/experiments/[id]/variations/[varId]` ## Implementation behavior - Visual mutations are saved as structured JSON actions such as text, HTML, style, attribute, remove, insert before, and insert after. - The SDK applies mutations idempotently after targeting and assignment succeed. - Code mode stores generated workspace files, preview builds, build status, and revision metadata for more complex variants. - Selector generation and mutation generation APIs help produce or refine changes from prompts and page context. - Revision history lets teams inspect earlier builder states and roll forward from known working versions. ## How to use it - Start with visual mutations for simple copy, style, layout, or insertion changes. - Use code mode when the change needs component logic, assets, or multi-file behavior. - Save frequently, preview the forced variation, then run QA before launching traffic. - For draft controls, use control edits for baseline-only setup or fixes, and preview the forced control arm before launch. Non-draft controls remain read-only but retain preview access; control edits apply only to visitors assigned to control. - Put helpers or code that every arm needs in experiment-level CSS/JS instead of copying them into control and treatment variations. - Keep each variation code field at or below 500,000 characters. Apex refuses data URIs longer than 4,096 characters. Upload images on the Assets page, with `apex assets upload`, through an Operator attachment, or through automatic CLI asset upload. Then use the hosted delivery URL in custom code or mutations. ## When not to use it - Do not use code mode for a copy or color change that a structured mutation can handle. - Do not use control-only edits for the treatment idea: launch readiness still requires a saved treatment change. - Do not launch a generated variant without previewing and saving the final state. ## Implementation source - The editor route is `src/app/(dashboard)/experiments/[id]/variations/[varId]/page.tsx`. - Operator and builder logic lives under `src/features/apex-operator` plus variation handlers under `/api/experiments/[id]/variations/[variationId]`. - Mutation payloads are stored on `Variation.mutations`; code workspace files, revisions, and builds use the experiment variation workspace APIs. ## Data and API - Prisma `Variation.mutations`, variation weight/control fields, code workspace models, preview artifact records, and revision records. - APIs: `/api/variations/[varId]`, `/api/generate-mutations`, `/api/generate-selector`, `/api/experiments/[id]/variations/[variationId]/workspace`, `/api/experiments/[id]/variations/[variationId]/build`, `/api/experiments/[id]/variations/[variationId]/files`, and `/api/experiments/[id]/variations/[variationId]/revisions`. - SDK mutation engine and preview-session runtime. ## Failure modes - Change flashes or appears late: tune anti-flicker, selector timing, or target container. - Mutation misses element: inspect selector, iframe/shadow DOM boundaries, and dynamic rendering. - Code preview fails: review build status, generated files, unsupported browser APIs, and asset paths. ## Checkout-block experiments Checkout message tests change content in the Apex block in Shopify checkout. The old checkout mode is retired; add a checkout message to a test's arm instead. Existing retired tests stay read-only history and cannot resume. Authored targeting must omit `surface` and `checkoutBlock`; arm mutations still use `action: "checkoutBlock"`. ## Before launch 1. Connect the store's Shopify app. Checkout messages require Shopify Plus (including eligible development stores). Apex checks the current plan before launch. 2. Have your Apex contact add the Checkout extension tests add-on and turn on checkout tests for the store. 3. Add the Apex Checkout Experiment block in the Shopify checkout editor, normally at the information step. Apex cannot see placement directly, so an unplaced block appears as a missing QA checkout. If the store offers Shop Pay, turn on "Include block in Shop Pay" for the block. 4. Add the message directly to an arm, outside URL blocks. A checkout message cannot share a test with a post-purchase offer. 5. Keep the test in production or the store's active environment, and keep the store's checkout messages within the content budget. 6. Open each message arm's QA link and go to a real checkout. A new test needs a QA checkout within 30 days for every message arm, including a control with its own message. Changing the message or assignment epoch requires another QA checkout. A test that already ran can resume with an older matching receipt. 7. Mark QA done and launch once the remaining launch checks pass. If Apex is retrying a checkout update, wait and try again. ## Combined storefront and checkout tests A test can change the storefront and show a checkout message together. These tests count visitors on the storefront. Newly published and freshly built configs include the storefront change only while Apex can confirm the checkout message is live at checkout. When the message stops, Apex stops serving the storefront change through the same channels it uses for a paused test. Pages already open, browser and edge caches, and customer edge Workers can keep it until they refresh. When the test also changes price, shipping or offers, Apex removes it from checkout pricing the same way it does for a paused test, within about a minute, so carts that already received the change lose it too. The results API reports, per version, how many checkout starts showed the message (`checkout_health`). It flags `partial_treatment.material` when many did not. The result then mixes the full change with the storefront change alone, which can point the other way. Checkout messages cannot state a price, discount or amount yet. Apex refuses money amounts both when saving and at launch. Remove the amount and run the message's QA checkout again. ## After launch Apex confirms the message at Shopify. Until then the status says "Apex has not confirmed this checkout message at checkout yet". Apex keeps checking and pauses a test whose message it cannot confirm within 30 minutes. Fix the reason and resume the test. While Apex is stopped or checkout tests are off, Apex turns the checkout message off and holds confirmation once Shopify shows it is off. It shows again when Apex starts or checkout tests are on. A running test cannot gain or change a checkout message: pause it first, change the message, run its QA checkout and resume. ## Launch warnings - A primary goal that happens before checkout exposure can prevent automatic stopping from deciding the test. A revenue goal avoids this. - Other checkout tests share the placement. A shopper in both sees both messages, one under the other. Pause one if shoppers should see only one. - A percentage may state a discount. Remove it if it does; ordinary percentages such as "100 % recyclable" can launch as is. - If your store uses Shop Pay, turn on Include block in Shop Pay for the Apex block in the checkout editor, or Shop Pay buyers will not see the message. - If Apex is stopped for the store, ask your Apex contact to start it so the checkout message can show. ## Data and API Checkout message content is stored on variation records and served through the Shopify extension. APIs under `/api/experiments/[id]` and `/api/variations/[varId]` use the same save guards. For checkout-only tests, checkout exposure and Shopify revenue events supply the measurement; combined tests are measured from storefront exposure. Previewing content alone does not prove delivery or attribution. ## Feature flags > **Simple terms** > > Feature flags are controlled switches or payloads for engineering rollouts, not visual A/B test DOM changes. Runtime toggles for off/on variants, typed payloads, rollout percentages, targeting, and source-experiment promotion. ## Routes - `/flags` ## Implementation behavior - Flags use the same targeting and rollout concepts as experiments but return a decision payload instead of DOM mutation behavior. - A flag has status, key, rollout, targeting, variants, default/off value, and optional source experiment. - Decision debugger explains why a visitor would receive a variant. - Flags can be activated, paused, archived, or created from successful experiments. ## How to use it - Use flags when engineering needs a controlled rollout or remote configuration. - Keep payloads typed and stable so application code can consume them safely. - Test decisions with the debugger before activating. - Archive flags that are permanently shipped in application code. ## When not to use it - Do not use a flag when you need experiment analytics and variant conversion tracking. - Do not put secret or unsafe values in flag payloads consumed by the browser. - Do not keep temporary rollout flags alive after the feature is permanently shipped. ## Implementation source - Pages live under `src/app/(dashboard)/flags`. - Handlers live under `/api/flags` plus activate, pause, and archive routes. - Flags share targeting and rollout ideas with experiments but return typed decision payloads instead of DOM mutations. ## Data and API - Flag and flag variant models plus targeting JSON. - APIs under `/api/flags`, `/api/flags/[id]`, `/api/flags/[id]/activate`, `/api/flags/[id]/pause`, `/api/flags/[id]/archive`, and experiment-to-flag creation. - Runtime decisions are delivered through configuration and flag evaluation endpoints. ## Failure modes - Application sees default value: flag paused, key mismatch, targeting miss, or rollout percentage excludes visitor. - Bad payload breaks consuming app code: validate schema and defaults before activation. - Old flags create confusion: archive retired rollouts. ## Ordered audiences through the API and CLI A personalization can show different versions to different audiences. Audiences are evaluated in order. Each audience chooses an experience and a rollout; its holdback sees the flag's Off version. When “stop after match” is enabled, later audiences are skipped after a match, including a holdback assignment. Use `apex flags experiences create` or `update` with a JSON file to save the experience's value, visual changes, CSS or JavaScript. Use `apex flags rules set` to replace the audience list and `apex flags rules reorder` to change its order. The API offers the same actions under a flag's `rules` and `experiences` paths. Writes check the parent flag version to prevent overwriting another editor's changes. If a save reports a conflict, reload the flag before saving again. Removing an audience preserves its history. Experiences that an active or historical audience references cannot be deleted. The flag's Off and On versions stay in place, and goals remain attached to the whole personalization. A flag with no audiences keeps its existing rollout behavior. ## Guardrails, planned tests, and auto-pause > **Simple terms** > > Guardrails are the rules that keep tests from launching or being interpreted too casually. Defines the statistical and operational rules that determine when tests can launch, pause, and be interpreted. ## Routes - `/settings/test-rules` (Settings → Test rules; `/settings/testing` and `/settings/guardrails` forward here) - `/experiments/new` - `/experiments/[id]` ## Implementation behavior - Shop guardrail settings hold default minimum runtime, sequential significance preferences, auto-pause behavior, and planned-test defaults. - Experiment setup can opt into planned fixed-horizon or sequential readback depending on the selected settings. - Runtime and analytics code use these fields to label status, warnings, and decision readiness. - SRM warnings should be reviewed as measurement-quality context before using a test result for a decision. - When SRM or other measurement-quality warnings are present, the Results UI now exposes metric estimates and uncertainty intervals for inspection while blocking decision actions until the measurement issues are resolved. (Release: 1dcc2ad) - Auto-pause defaults protect against obvious risk but do not replace human review of experiment quality. ## How to use it - Configure guardrails before creating a large batch of experiments. - Choose planned-test settings during experiment creation when sample size and duration matter. - Use sequential mode when ongoing monitoring is expected. - Review warnings before starting, pausing, or completing a test. ## When not to use it - Do not use guardrails as a replacement for a clear hypothesis and primary metric. - Do not disable runtime/sample warnings just to ship more tests. - Do not switch statistical modes mid-test without documenting why. ## Implementation source - Settings UI lives under `src/app/(dashboard)/settings/test-rules` (`_components/test-rules-page.tsx`) and experiment creation/detail routes. - Settings are read and written through testing-defaults APIs and experiment update flows. - Experiment stats and result components read planned-test and sequential fields to label decision readiness. ## Data and API - Guardrail settings on shop and experiment records. - APIs under `/api/shops/testing-defaults`, experiment creation/update endpoints, and analytics readback endpoints. - Results views read planned-test and sequential fields for interpretation. ## Failure modes - Premature winner call: test does not meet runtime, sample, or statistical assumptions. - Unexpected pause: auto-pause threshold or guardrail was triggered. - Inconsistent readback: experiment settings changed after launch; inspect audit history. ## Ideas and backlog > **Simple terms** > > The backlog is where rough test ideas live before they are ready to become experiments. The planning layer for experiment concepts before they become tests. It stores hypotheses, evidence, source context, comments, priority, and handoff notes. ## Routes - `/pipeline/backlog` - `/pipeline/backlog?idea=` (one idea, opened in the modal) - `/backlog/capture` `/backlog`, `/backlog/new`, and `/backlog/` are permanent redirects into those three addresses. ## Implementation behavior - Win prediction evidence opens as a full-width panel under the score band and the Score row; the screen viewer opens full-page captures at a readable size. {/* source: release .release-diff.patch changelog fragment */} - Ideas can be created manually, imported from Research Hub context, or generated from Operator and store-profile evidence. - Imported Research Hub items now show the Research Hub score they arrived with and surface a Research Hub linkage strip in the detail view; the linkage strip's links are shown only to authorized viewers. - Each idea keeps status, priority, source, target surface, expected impact, comments, archive state, and optional linked experiment. - The detail page turns a qualified idea into an experiment without losing the original evidence trail. - Codex handoff and Research Hub snapshots preserve external research context for future builders. - Every idea has a Design tab: mockups, links, inspiration images, and internal build notes. Mockups can be uploaded or picked from the Research Hub images the idea arrived with; picking places a reference, it does not copy the file. - Each link carries a switch that says whether the client sees it. The Add link dialog pre-selects the switch for Figma and Loom and leaves it off for everything else; a link saved through the API without the flag stays internal whatever its kind. Build notes and inspiration images are always internal, and the dashboard read redacts them for client roles. ## Your own fields A shop defines its own idea fields once, and every idea then carries values for them. - Types: short text, long text, number, yes or no, single choice, date, link, and person. - Each field can be required, and each field can be marked visible to the client. - The idea list filters on any field with `?field.=` and sorts with `?sort=field.`. A sort by a field covers at most 5000 matching ideas; past that, filter or search the list first. - Batch import maps a spreadsheet column to an existing field, creates a new field for it, or skips it. `skip` is a reserved key, so no field can use it. - Archiving a field keeps the values already stored on ideas and stops new writes to it. - A client sees only the fields the shop marked client-visible, and cannot filter or sort by the others. ## How to use it - Create ideas when evidence is not ready to launch as an experiment. - Attach page context and proof so Operator can reason about the target surface. - Move high-confidence ideas into experiments from the detail route. - Archive stale ideas instead of deleting useful historical context. ## When not to use it - Do not put live-test configuration only in an idea; create an experiment when it needs runtime behavior. - Do not use ideas as a dumping ground without evidence, target page, or expected metric. - Do not delete old idea context when archiving is enough to keep history clean. ## Implementation source - Pages live under `src/app/(dashboard)/backlog`. - Handlers live under `/api/backlog`, including detail, comments, archive, restore, Codex handoff, landing-page brief, and create-experiment actions. - Backlog items can link to experiments so the original hypothesis and evidence remain attached. ## Data and API - Backlog and comment models plus imported research metadata. - APIs: `/api/backlog`, `/api/backlog/[id]`, `/api/backlog/[id]/comments`, `/api/backlog/[id]/design`, `/api/backlog/[id]/archive`, `/api/backlog/[id]/restore`, `/api/backlog/[id]/codex-handoff`, `/api/backlog/[id]/landing-page-brief`, and `/api/backlog/[id]/create-experiment`. - The design board is read and written on its own route, `GET`/`PATCH /api/v1/backlog/{id}/design`. A `PATCH` replaces only the keys it carries and requires `expected_version` from the last read, because it replaces whole lists after reading them; a stale version answers 409 with `backlog_item_cas_miss`. - Field definitions live at `/api/backlog/fields` and `/api/backlog/fields/[id]`, with `/api/v1/backlog/fields` for API keys. Values ride on each idea's `custom_fields`.- Linked experiments store the relationship back to the originating idea. ## Failure modes - Weak ideas produce weak experiments: add evidence, page URL, target user, and expected metric before handoff. - Imported items may need cleanup if external source fields are incomplete. - Archived ideas disappear from default lists but can be restored. - A Research Hub image placed as a mockup and later removed from the idea keeps its place on the board and reads as not available, instead of disappearing without a word. ## Metric definitions # Metric definitions This page states how Apex turns raw events into the numbers on the results page, the results API, and `apex experiments results`. Use it when you reconcile raw events with reported numbers. ## Results window - The window runs in UTC. It starts at the later of the first launch and the last results reset. It ends when the experiment ended, or now, rounded down to the minute. - Changing the allocation, weights, or variations starts a new assignment epoch for visitor bucketing, but it does not move the results window. To count only data after such a change, reset the results. - Custom date ranges on the results page use the shop's reporting time zone. - If the experiment uses a weekday filter, only visitors whose first exposure fell on a selected weekday are counted. - Results only include the experiment's environment, which is production by default. ## Visitor A visitor is a distinct visitor ID with at least one exposure event for that variation inside the window. Exposure events are experiment views and rendered personalizations. Being assigned without seeing the experiment, or firing a goal event, does not make someone a visitor. - A visitor exposed to two variations counts in both. - Bot traffic is always excluded. - QA-session traffic is excluded unless you explicitly include it. - Placeholder visitor IDs that cannot identify a real person are excluded from visitor counts. - For purchase goals, an order that Apex matched to a variation on the server without a tracked exposure adds its visitor to that variation's population. ## Conversions - **Count once:** each exposed visitor counts at most once per variation in the window. - **Count every:** every qualifying event counts, so conversions can exceed visitors. Purchase and revenue goals always count distinct buyers. A value goal follows its own setting: set to count every, it sums the value of every qualifying event. - Only goal events at or after the visitor's first exposure to that variation in the window count. Earlier conversions are dropped, and the results page shows a warning when this is material. - Duplicate events with the same event ID count once. Resetting results starts a new window and re-arms count-once goals. A reset "from now" also starts a new assignment epoch: returning visitors are re-bucketed with the same deterministic hash, so they keep their variation unless the allocation changed. A reset to a past date keeps the current epoch. ## Purchases A purchase conversion is a distinct buyer, not an order. An order counts for an experiment when all of these hold: 1. The order belongs to the variation recorded on the order's first event. It does not depend on which variation the visitor saw last. One order can count in several experiments. 2. The order was first seen inside the window. 3. The same visitor was exposed to that variation in the window before the order, or the visitor has no exposure to that variation at all and the order was matched on the server. There is no attribution expiry: any order from the first exposure to the end of the window counts. Orders placed before exposure, and orders from visitors exposed only before the window started, are not counted. Orders are identified by their order key, falling back to the event ID. Repeat events for the same order within 60 seconds of its first sighting are merged. Zero-value orders are included. Line-item-filtered goals count only orders from exposed visitors whose captured products match the filter. Orders without product data are excluded unless the goal is set to include them. ## Revenue - Revenue is converted into the requested currency at the daily rate for the order date. - Orders without a currency are assumed to be in the shop currency, unless that setting is turned off. - Orders that cannot be converted still count as purchases but are left out of money totals. - The revenue read is withheld only when no attributed order can be valued. - Revenue statistics cap single orders at the 99th percentile so one outlier cannot decide a test. Reported totals are not capped. ## Conversion rate Conversion rate is conversions divided by the variation's population. For purchase goals the population includes the server-matched order visitors described above. For count-every goals the rate is events per visitor and can exceed 100%. In the API, `arms[].primaryRate` and `primaryConversions` always use unique converters, even when the primary goal counts every event. The goal's `counting` field states which basis the statistics use. ## Quality checks **Sample-ratio mismatch (SRM).** When enabled, Apex tests whether the observed split between variations differs implausibly from the configured allocation. A mismatch blocks winner and loser verdicts and auto-stop. Confidence values stay visible for diagnosis. **Population inconsistency.** When a variation has more unique converters on the primary goal than visitors in its population, the numbers cannot be valid. The API then removes primary conversions, rates, lift, confidence, the time series, segments, the funnel, and revenue inference. The results page hides the headline rates, lift, confidence, and primary series, and marks the primary goal row with a warning. See [experiment results](/docs/experiments/results) and [metrics and goals](/docs/experiments/metrics-goals). ## Metrics, goals, and conversion tracking See [Metric definitions](/docs/experiments/metric-definitions) for exposure, counting, conversion-rate, revenue, and quality-gate rules. > **Simple terms** > > Metrics tell Apex what success means for a test: a click, pageview, custom event, revenue event, or Shopify event. Creates reusable success events for experiments: click, pageview, custom, revenue, and Shopify-connected conversion goals. ## Routes - `/goals` - `/goals/new` - `/goals/new/visual` ## Implementation behavior - A `Goal` stores type, selector, URL pattern, event name, capture rules, text matching, revenue settings, and count-once behavior. - Click goals bind to selectors in the SDK; optional capture rules can store element text, value, attribute, or dataset values. - Pageview goals match URL patterns after the SDK sees a page event. - For "goal-only" Pages that use the SDK's `pollOnRules` option, the SDK now starts a bounded immediate-mode retry (the same behavior as targeting Pages) so JavaScript-condition goals attach reliably without waiting for a URL change. - Custom and revenue goals are tracked through SDK calls or Shopify event integrations. - Experiments attach goals through `ExperimentGoal`, with one goal marked primary for readback and launch criteria. ## How to use it - Create reusable metrics before building an experiment. - The experiment list and detail header show a 'ready-to-launch' status. The experiment QA tab contains Open forced preview, Copy preview URL, and Show QR controls when a preview URL is available. - Use click goals for UI actions, pageview goals for route arrival, custom goals for product events, and revenue goals for order value. - Verify selectors in QA before launch. - Mark one primary metric per experiment and keep secondary metrics for diagnostics. ## When not to use it - Do not use a click goal when the real outcome is revenue or checkout completion. - Do not use fragile selectors that depend on generated class names if a stable selector exists. - Do not mark multiple goals as primary for the same experiment decision. ## Implementation source - Metric pages live under `src/app/(dashboard)/goals`. - Handlers live under `/api/goals`, `/api/generate-selector`, and goal-verification experiment QA routes. - The SDK binds goal listeners and sends matching events through the Worker into Tinybird. ## Data and API - Prisma models: `Goal`, `ExperimentGoal`, `ShopifyEventSetting`. - APIs: `/api/goals`, `/api/goals/[id]`, `/api/generate-selector`, `/api/experiments/[id]/qa/goal-verification`. - Events land in Tinybird `events` with `event_type`, `experiment_id`, `variation_id`, `properties`, and timestamp. ## Failure modes - Selector does not fire: inspect DOM timing, selector specificity, and whether the element is replaced after SDK binding. - Revenue missing: confirm Shopify/web pixel settings or custom revenue payload shape. - Conversion rate looks wrong: check count-once rules, bot/internal traffic, and whether the primary goal is attached to the right experiment. Note: After an experiment's results are reset (a new assignment epoch), goals configured with `count-once` will "re-arm" so returning visitors' conversions can be counted again. Returning visitors keep their previously-assigned variation and are not re-randomized by this reset. (See release notes for the committed behavior.) ## Migrating a test from Kameleoon ## What carries over The JavaScript and CSS pair you use in Kameleoon carries over to an Apex variation. Apex stores the files as `src/index.js` and `src/index.scss`. You can edit them in the [variation code workspace](/docs/experiments/builder-visual-code), then build the variation without rewriting the test into a different format. ## Step by step 1. Create an experiment and add a treatment variation. 2. Open the variation's code editor. To work from your terminal or an AI client instead, use the [CLI experiment filesystem](/docs/cli/experiments-filesystem-qa) or [MCP tools](/docs/mcp/tool-reference). 3. Paste the test JavaScript into `src/index.js` and the styles into `src/index.scss`. 4. Build the variation. Fix every build or artifact safety error before continuing. 5. Open the preview link. Apex preview sessions work for 1 to 168 hours and do not require the Apex SDK on the previewed site. 6. Set the experiment's [audience and targeting](/docs/experiments/targeting-audiences) and [goals](/docs/experiments/metrics-goals). 7. Complete the [QA checklist](/docs/experiments/qa-and-previews). Check the intended URL, desktop and mobile layouts, interactions, goal tracking, and the control experience before starting traffic. ## Differences that bite **Artifact safety.** Apex rejects dynamic `innerHTML =` values and assignments to `.src` or `.href`. Build DOM with `createElement` and `textContent`. Clone an existing image when you need its source instead of assigning a URL. **Hydrating storefronts.** For a React, Vue, Nuxt, or similar storefront, use `spa.applyTiming=after_hydration` with `spa.domChangeTrigger=true`. This avoids applying the change before the framework finishes rendering. Follow [framework coexistence](/docs/sdk/framework-coexistence) and [SPA and router modes](/docs/sdk/spa-router). **Shopify Dawn forms.** Dawn can render a second, hidden add-to-cart form. Select the visible form and name the intended product form in your code instead of using the first matching form. **Baqend Speed Kit.** Speed Kit can serve cached HTML that changes what an experiment sees. Follow [Speed Kit's A/B testing guidance](https://www.speedkit.com/docs/ab-testing) before QA and launch. **Flicker and selectors.** Configure [anti-flicker behavior](/docs/sdk/anti-flicker) for the page's render timing. Anchor selectors on stable attributes rather than generated classes or fragile element positions. ## Parity table | Kameleoon capability | Apex surface | Guide | | --- | --- | --- | | Experiments | Experiments and variation builder | [Variation builder](/docs/experiments/builder-visual-code) | | Personalizations | Personalization experiments | [Personalization](/docs/experiments/personalization) | | Goals | Experiment metrics and goals | [Metrics and goals](/docs/experiments/metrics-goals) | | Audiences and targeting | Experiment targeting | [Targeting and audiences](/docs/experiments/targeting-audiences) | | QA previews | QA workspace and preview links | [QA and previews](/docs/experiments/qa-and-previews) | | Guardrails | Auto-pause guardrails | [Guardrails and auto-pause](/docs/experiments/guardrails-auto-pause) | | Backlog ideas | Ideas and backlog | [Ideas and backlog](/docs/experiments/ideas-backlog) | | Heatmaps | Behavior tab | [Experiment results](/docs/experiments/results) | | Clarity session tagging | Settings, Integrations | No guide yet | | Surveys | On the roadmap | Not available yet | ## A prompt for your AI assistant ```text Convert this storefront test for an Apex variation. Return exactly two files: src/index.js and src/index.scss. Do not use a dynamic value on the right side of innerHTML =. Do not assign to any element's src or href property. Create DOM with createElement and textContent, and clone an existing image if one is needed. Make apply idempotent so repeated calls do not duplicate elements, handlers, or styles. Anchor selectors on stable attributes, not generated classes or element position. Name the exact, visible add-to-cart target the script will modify and explain how the script avoids hidden duplicate forms. Keep all styles in src/index.scss. Source test: [paste the Kameleoon JavaScript and CSS here] ``` ## Where to get help Start with the [builder guide](/docs/experiments/builder-visual-code) for workspace and build errors, then use the [QA guide](/docs/experiments/qa-and-previews) for preview or launch checks. If the test still fails, contact the Apex team with the experiment ID, variation ID, target URL, build error, and the shortest steps that reproduce the problem. ## Edit a draft on the live store > **Simple terms** > > Edit on store opens the draft on the merchant's real page. Changes remain draft-only until the experiment is reviewed and launched from Apex. The on-site editor is the high-fidelity editing surface for stores with the Apex SDK installed. It runs over the merchant page in an isolated editor layer, so selectors, storefront scripts, cart behavior, consent tools, and session state match the environment shoppers use. ## Routes - `/experiments/[id]` - `/experiments/[id]/variations/[varId]` - `/api/editor-sessions` ## How to use it 1. Open a draft experiment or one of its treatment variations. 2. Choose **Edit on store**. Apex opens the experiment URL in a new tab and connects the editor automatically. 3. Pick an element to edit text, image source, color, spacing, visibility, or other supported styles. 4. Switch treatment variations from the editor toolbar when the session covers more than one treatment. 5. Watch the save status. Every edit is saved back to the same draft used by Quick preview and Apex Operator. 6. Exit the editor to revoke the session and reload the unedited store page. The persistent **Draft - not live** badge is authoritative. The editor cannot launch traffic or change an experiment's status. ## Operator and browser tools - The compact on-store Operator dock can produce and save a treatment from the current live DOM context. - The full Operator remains available in the dashboard. When the on-site tab is connected, its existing browser tools run against that real tab through the scoped editor relay instead of the proxy iframe. - Full QA, action proof, screenshots, review, and launch approval still belong to the normal Operator and QA workflow. ## Device preview The device button opens the same merchant page in a 390 x 844 same-origin frame and reapplies the current draft. If the merchant's own `frame-ancestors` or related browser policy blocks self-framing, Apex restores the page and shows a fallback instead. Use browser device mode or open the page on a mobile device in that case. ## Security behavior - The launch credential is short-lived, single-use, and placed in the URL fragment so it is not sent to the merchant server. - The SDK removes the fragment before exchanging it. - The exchanged bearer stays in memory and is restricted to one shop, experiment, and exact merchant origin. - Artifact reads and writes are limited to non-control variations in that experiment. - Exiting revokes the session. A hard navigation or reload ends the editor unless Apex starts a fresh session. ## Quick preview fallback **Quick preview** remains available before the SDK is installed and for shareable proxy previews. It is useful for draft review, but interactive cart, checkout, consent, authentication, and session-dependent behavior can differ from the live store. Use Edit on store as the fidelity surface when it is available. ## Failure modes - Editor does not appear: confirm the Apex SDK is installed on the exact target page, then retry from the dashboard. - Launch opens but does not connect: the single-use token may have expired or the target origin may not match the shop configuration. - Save conflict: the draft changed in another editor. Reload the variation before continuing so newer work is not overwritten. - Custom JavaScript cannot be undone in place: exit or reload the page. The editor says when a reload is required. - Mobile frame is blocked: use browser device mode or a real mobile device. ## Implementation source - Session and scoped editor APIs live under `src/app/api/editor-sessions`. - The dashboard launch surface is `src/features/apex-operator/ui/on-site-editor-launch.tsx`. - The storefront bootstrap and isolated editor runtime live under `packages/sdk/src/editor-bootstrap.ts` and `packages/sdk/src/editor`. - All writes use the shared variation artifact save service, including artifact hashes, workspace mirrors, snippet compilation, and cache invalidation. ## Experiment creation and lifecycle > **Simple terms** > > An experiment is the actual test that can be shown to visitors. It owns the variants, targeting, traffic, metrics, QA, and launch status. The core A/B testing workflow: create a draft, configure targeting and metrics, build variants, QA, launch, monitor, pause, complete, archive, or restore. ## Routes - `/experiments` - `/experiments/new` - `/experiments/[id]` ## Implementation behavior - An `Experiment` owns status, traffic allocation, targeting JSON, schedule, planned-test settings, sequential settings, and optional backlog source. - Each experiment has `Variation` records with weight, control flag, mutation payloads, code workspace metadata, and preview state. - Primary and secondary metrics are connected through `ExperimentGoal`; a primary metric is required for meaningful launch/readback. - Status transitions gate runtime eligibility: draft is not delivered, running can be assigned, paused stops new delivery, completed feeds readback, archived hides from default operations. - Conflict checks compare targeting and schedule against other live or scheduled tests before launch. ## How to use it - Create the experiment from an idea or from `/experiments/new`. - Set the target URL/audience, traffic allocation, and schedule before building variants. - Attach at least one primary metric and any secondary metrics. - Build the variants, run QA, resolve blockers, then start the experiment. - Pause for emergencies, complete after enough evidence, archive when it should leave the operating list. ## When not to use it - Do not launch an experiment for a change you already know you want to ship to everyone; use a feature flag or direct implementation. - Do not create a test without a measurable primary metric. - Do not run overlapping tests on the same page/audience unless the conflict is intentional and understood. - Commerce launch gates: the platform now blocks different commerce test types (for example, price-changing tests vs offer/discount tests) from launching against the same product or product variant to prevent conflicting charge- affecting changes. This is enforced at launch; resolve or separate overlapping commerce tests before starting them. (See release 93d1c3e.) ## Implementation source - Pages live under `src/app/(dashboard)/experiments`. - Dashboard handlers live under `/api/experiments`, `/api/variations`, and experiment subroutes for start, pause, complete, archive, restore, QA, goals, and code workspaces. - The Prisma models are `Experiment`, `Variation`, `Goal`, and `ExperimentGoal`; public automation uses `/api/v1/experiments`. ## Data and API - Prisma models: `Experiment`, `Variation`, `Goal`, `ExperimentGoal`, `Page`, `Segment`, `AuditLog`. - Internal APIs: `/api/experiments`, `/api/experiments/[id]`, `/api/experiments/[id]/start`, `/api/experiments/[id]/pause`, `/api/experiments/[id]/complete`, `/api/experiments/[id]/archive`, and `/api/experiments/[id]/restore`. - Public APIs: `/api/v1/experiments`, `/api/v1/experiments/{id}`, `/api/v1/experiments/{id}/start`, `/pause`, `/complete`, `/duplicate`. ## Failure modes - Cannot launch: check missing primary metric, missing variant, no URL/audience, zero traffic allocation, or unresolved QA blockers. - Visitors not assigned: experiment is not running, targeting does not match, or the SDK is not installed on that page. {/* Release notes: This release (8b7bc16) restores hover, press, and keyboard focus feedback for setup controls and ensures selection pickers remain reachable near viewport edges. */} ## Personalization > **Simple terms** > > Personalization chooses a visitor-specific experience from ordered rules and signals. Signal-driven storefront experiences with ordered rules, payloads, targeting, rollout controls, and decision debugging. ## Routes - `/personalization` ## Implementation behavior - Personalization rules evaluate visitor and store signals in order and return the first eligible experience. - Experiences can use targeting, payload values, rollout percentage, and safe status controls. - Decision debugger shows matched and rejected rules so teams can understand runtime behavior. - The runtime uses the same event, segment, and signal foundation as experiments and flags. ## How to use it - Create personalization only after the signal or audience is trustworthy. - Order specific rules above broad fallback rules. - Roll out gradually and inspect decisions before expanding traffic. - Use analytics and visitor trails to validate that the experience is reaching the intended group. ## When not to use it - Do not use personalization when you need a clean randomized A/B test result. - Do not personalize from signals that are missing, noisy, or legally sensitive. - Do not create broad rules above specific rules unless the broad rule is meant to catch most traffic. ## Implementation source - Pages live under `src/app/(dashboard)/personalization`. - Handlers live under `/api/personalizations` plus activate, pause, and archive routes. - Runtime decisions use segment, signal, rollout, and payload records tied to the active shop. ## Data and API - Personalization, rule, payload, segment, signal, and decision records. - APIs under `/api/personalizations`, `/api/personalizations/[id]`, `/api/personalizations/[id]/activate`, `/api/personalizations/[id]/pause`, and `/api/personalizations/[id]/archive`. - Uses SDK events, store facts, visitor context, and segment rules. ## Failure modes - Wrong experience appears: rule order, broad targeting, or stale visitor signals. - No experience appears: paused status, rollout exclusion, missing signal, or targeting miss. - Hard-to-debug outcomes: use the decision debugger and visitor explorer together. ## Audiences and experiences Ordered audiences let one personalization show different experiences to different visitors. Each audience has its own targeting, rollout and experience. Audiences run in order; a matching audience can stop later ones from running, including when its visitor receives the holdback version. The flag API and `apex flags` CLI can create experiences, replace the audience list and reorder audiences. Removing an audience keeps its performance history. The existing `apex personalizations` commands and personalization API paths keep their format and automatically use the current data after a personalization has moved to the flag model. ## QA workspace, previews, screenshots, and launch gating > **Simple terms** > > QA is the safety check before traffic. It proves the variant renders, tracking works, and blockers are resolved. QA proves that a variant renders correctly, tracking fires, screenshots are captured, and launch blockers are resolved before visitors see a test. ## Routes - `/experiments/[id]?tab=qa` - `/experiments/[id]/variations/[varId]` ## Implementation behavior - Installed stores can use **Edit on store** as the high-fidelity draft surface. The editor runs on the real merchant origin and saves into the same variation artifacts used by the dashboard. - **Quick preview** remains the zero-install proxy fallback. Cart, checkout, consent, authentication, and other session-dependent behavior can differ from the live store and should be verified on-site before launch. - Preview sessions force a specific experiment and variation through cookies or preview parameters. - QA run APIs inspect the experiment setup, target page, goals, screenshots, and runtime behavior. - Screenshot jobs can capture desktop, iPhone, and iPad views and persist result URLs. - Goal verification checks whether the selected metric can be observed or triggered in the preview path. - Launch gates combine setup blockers, QA state, guardrails, and experiment status. ## How to use it - Open QA from an experiment before pressing start. - Preview every variation on the intended URL and key device sizes. - Run screenshots and goal verification for the primary metric. - Fix every blocker or consciously document the exception before launch. ## When not to use it - Do not use QA as a substitute for setting up the experiment correctly. - Do not approve a test from desktop screenshots only when mobile traffic matters. - Do not ignore a failed goal verification just because the visual change looks right. ## Implementation source - The QA tab is the `?tab=qa` view of `src/app/(dashboard)/experiments/[id]/page.tsx`, with deeper runs inside the variation editor. - Handlers include `/api/experiments/[id]/qa/run`, `/api/experiments/[id]/qa/screenshots`, `/api/experiments/[id]/qa/goal-verification`, `/api/preview`, and `/api/preview-sessions`. - Screenshot jobs and preview sessions persist evidence used by launch gates. ## Data and API - Prisma `ScreenshotJob`, preview session records, operator evidence, goal verification records, and experiment QA state. - APIs: `/api/experiments/[id]/qa/run`, `/api/experiments/[id]/qa/goal-verification`, `/api/experiments/[id]/qa/screenshots`, `/api/preview`, `/api/preview-sessions`. - Public screenshot readback exists under `/api/v1/screenshots/[jobId]`. ## Failure modes - Preview shows control: force cookie/session missing or targeting does not match. - Edit on store does not connect: confirm the SDK is installed on the target URL and that its origin belongs to the active shop. - Screenshot job stuck: check browser worker availability and target page accessibility. - Goal verification false negative: selector may need user interaction, delayed render, or a different URL state. QA preview links carry `drip_qa_session`, which is exposed as `qa_session_id` on events. QA-mode events are sent with `qa_mode=1` and excluded from experiment results unless a read explicitly sets `include_qa=1`. Consent requirements and Do Not Track still hold events; inspect `Drip.getDiagnostics().customEventsHeld` to see whether a custom event can reach the wire, and `Drip.getDiagnostics().tracking` for the blocked reason and queued event count. See the [SDK runtime API](/docs/sdk/runtime-api) for the full diagnostics contract. ## Results, imported outcomes, and readback See [Metric definitions](/docs/experiments/metric-definitions) before interpreting visitors, conversions, revenue, rates, or quality gates. > **Simple terms** > > Results is where Apex turns tracked or imported test data into a decision and a learning. Turns live and completed experiment data into decisions, learnings, and future backlog evidence. ## Routes - `/results` - `/experiments/[id]/results` - `/experiments/[id]` ## Implementation behavior - Apex reads assignment and conversion events from Tinybird by experiment, variation, goal, and time window. - Sequential settings and planned-test snapshots support always-valid or fixed-horizon readback depending on setup. - Imported results let teams capture external testing outcomes and preview them before confirmation. - Completed experiment outcomes can be summarized into store learnings and used by the next backlog cycle. ## How to use it - Review results after enough runtime and sample have accumulated. - Results include a read-only “Runs” selector on the Results page to view earlier assignment runs; the revert dialog indicates the page shows the new run by default. - Targeting evaluations may now include an explicit page/segment match mode (`ANY` or `ALL`) when experiments select multiple pages or audiences; check the experiment setup to see which mode was used when interpreting results. - Test results now show an average conversion value (ACV) for non-money value goals and use AOV for money goals in exports and labels; small per-visitor values are no longer rounded to zero. - Compare primary metric lift, secondary metric movement, revenue, traffic quality, and screenshots. - Use import preview before committing external result data. - Use audience breakdowns by shop-defined custom dimensions to segment results (e.g. loyalty_tier, storefront_variant) when available. - Record the conclusion so Launchpad and future ideas retain the learning. - When measurement-quality warnings appear (for example SRM/unreliable-data), the Results UI now shows valid metric estimates and uncertainty intervals for inspection while keeping experiment decision actions blocked until measurement quality is resolved. (Release: 1dcc2ad) {/* user-note: "Accessibility and layout: Results breadcrumb, metadata and tabs plus the Pipeline roadmap and approvals text now meet a 4.5:1 contrast floor; Results no longer nests a second main landmark, and each goal's results table is a named, keyboard-scrollable region. Long test keys on the Pipeline timeline now truncate instead of wrapping into the title. Ticket: #5808" */} {/* release-note: Results page adds a platform filter. When a platform filter (Web / App / All) is active, the results readback bypasses the rollup fast path to ensure platform-specific accuracy. */} ## When not to use it - Do not call a winner before the test has enough runtime, sample, and measurement quality. - Do not mix imported external results with Apex-tracked results without checking dates and variant mapping. - Do not use secondary metrics as the main decision unless the experiment was intentionally set up that way. ## Implementation source - Results routes live under `src/app/(dashboard)/results` (the program index) and `src/app/(dashboard)/experiments/[id]/results` (one test's readout), plus experiment detail analytics sections. - Handlers include `/api/test-results`, `/api/test-results/import/preview`, `/api/test-results/import/confirm`, and `/api/analytics/experiment/[id]`. - Readback joins Prisma experiment context with Tinybird visitor/conversion/revenue/event pipes. ## Data and API - Tinybird pipes for visitors, conversions, timeseries, revenue, behavior, and dashboard stats. - APIs: `/api/analytics/experiment/[id]`, `/api/test-results/import/preview`, and `/api/test-results/import/confirm`. - Models include experiment outcome, imported result, learning, and audit records. ## Failure modes - No results: no runtime traffic, wrong shop ID, event ingestion failure, or the test was never running. - Unexpected lift: inspect traffic allocation, targeting, internal traffic, and goal attachment. - External import mismatch: validate variation names, dates, sample counts, and metric units before confirm. ## Targeting, pages, audiences, and segments > **Simple terms** > > Targeting answers the question: who is eligible to see this thing, and on which pages? Defines who and what page qualifies for experiments, feature flags, and personalization rules. ## Routes - `/audience` - `/audience/new` - `/audience/:id/edit` - `/pages` - `/pages/new` ## Implementation behavior - Runtime page matching supports exact, wildcard, and regex URL rules. - Editing a page's URL rules now updates any tests, feature flags, and personalization that reference the page and republishes the shop (see ticket #6219). {/* user-note: "Page rule edits apply to the tests that use the page right away; you no longer need to remove and re-add the page. Ticket: #6219" */} - Segments combine page, traffic, behavior, customer, device, and Shopify/customer signals into reusable audience definitions. - SDK-backed mutation experiments evaluate device, browser, browser version, OS, cookie, URL parameter, language, referrer, UTM, and visitor audiences client-side. - Redirect experiments evaluated at the edge can only use URL, page, and URL-only segment criteria; use mutation experiments for non-URL audience criteria. - Post-purchase tests with storefront targeting only reach checkouts that went through the storefront cart. Tests without storefront targeting also reach Buy it now and express checkouts; traffic allocation is applied per checkout. - Experiment targeting JSON can reference direct URL rules or saved segment rules depending on the workflow. - Legacy pages redirect into the current audience system, but the underlying runtime still understands URL patterns. ## How to use it - Create broad page groups for repeated target surfaces such as PDP, cart, homepage, and collection pages. - Use exact URLs for one-page tests, wildcard for families of pages, and regex only when pattern control is needed. - Build saved audiences when the same visitor logic will be reused across tests, flags, or personalization. - Audience editor: the AND / OR connector between audience conditions is now editable in place; choosing OR merges blocks and choosing AND splits a row into its own block. {/* user-note: "You can switch AND and OR between audience conditions without deleting and re-adding them." */} {/* user-note: "Audience editor: URL-parameter and cookie criteria in the audience rule builder now stack inside the value column instead of overflowing it. Ticket: #5906" */} - Review targeting before launch when an experiment does not appear in preview. ## When not to use it - Do not use a broad audience when a page-specific test only belongs on one surface. - Do not use regex unless exact or wildcard matching cannot express the rule. - Do not rely on behavior/customer signals before those signals are actually collected for the shop. ## Implementation source - Audience pages live under `src/app/(dashboard)/audience`; legacy page routes live under `src/app/(dashboard)/pages`. - Handlers live under `/api/segments` and `/api/pages`. - Matching logic is shared by dashboard targeting utilities and the SDK runtime URL/segment evaluation. ## Data and API - Prisma models: `Segment` with `rules` JSON, `Page`, and `Experiment.targeting` JSON. - APIs: `/api/segments`, `/api/pages`, and analytics page data endpoints. - SDK URL matcher evaluates current location before assignment and mutation. ## Failure modes - No match because of protocol, trailing slash, query strings, or an overly narrow regex. - Audience rule depends on a signal that has not been collected yet. - Multiple eligible tests can conflict; check conflict output and traffic allocation. {/* Release notes: This release (8b7bc16) improves the audience editor UI: long authored URL-rule lists are kept visually compact on mobile with an expand affordance for keyboard and pointer users, and the editor preserves accessible focus and interaction feedback. */} ## Template tests on Shopify Shopify can render an alternate template when a storefront URL includes a `view` query parameter. Apex supports this as a redirect test on the same page. For example, the source page can be: ```text https://shop.example/products/example ``` The treatment destination can be: ```text https://shop.example/products/example?view=alternate ``` ## Prepare the Shopify template 1. Create the alternate template in the Shopify theme. 2. Assign the template a suffix, such as `alternate`. 3. Open the clean product URL with `?view=alternate` and confirm the template renders. 4. Confirm the Apex snippet runs on both versions of the page. ## Create the test in Apex Select **Redirect test** in the new-test form. Use the clean page URL as the page or store URL. Use the same URL with `?view=` as the treatment destination. Apex redirects treatment visitors once. A visitor already on the treatment URL is not redirected again. Control visitors stay on the clean URL. ## Create the test with the CLI ```bash apex experiments create \ --name "Product template test" \ --type redirect \ --source-url "https://shop.example/products/example" \ --destination "https://shop.example/products/example?view=alternate" ``` The shop must have redirect tests enabled. Configure the goal and complete the quality assurance checks in the dashboard before launch. ## Preview and QA Open the treatment URL directly to check the alternate template. Then use the Apex quality assurance workspace to preview both variations on desktop and mobile. Confirm that the primary goal records on both page versions. Direct entry is different from assignment. A control-assigned or unassigned visitor who opens the `?view=` URL directly sees the alternate template. Apex does not redirect that visitor back to the clean URL. ## Search indexing and links Shopify emits a canonical URL for the clean page. This helps search engines identify the main page. Do not link the `?view=` URL in public navigation, campaigns, or other indexed content. Keep it as the test destination. ## Related pages - [QA workspace, previews, screenshots, and launch gating](/docs/experiments/qa-and-previews) - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) ## Access, authentication, and workspace selection > **Simple terms** > > This is the front door and tenant switchboard. It decides who is signed in, what workspace they belong to, and which shop every action applies to. Controls who can enter Apex, which workspace they operate in, and which shop becomes active for every dashboard and API action. ## Routes - `/login` - `/signup` - `/verify-email` - `/auth/reset-password` - `/orgs/new` - `/join` - `/join/[code]` ## Implementation behavior - Supabase Auth creates and refreshes the user session; protected dashboard routes require that session before rendering. - Apex stores the active shop in the `drip_active_shop` httpOnly cookie so server components and route handlers resolve the same tenant. - Company workspaces focus on one brand; agency workspaces add client management and cross-client navigation. - Shop records hold the storefront domain, display name, owner, worker URL, and integration metadata used by the runtime. ## How to use it - Sign up or log in, verify email, then create or join a workspace. - Open your DRIP client link to set up a store before creating goals, experiments, audiences, API keys, or webhooks. Users without a store can open `/join` to resume a usable link or ask DRIP for one. - Use the workspace and shop switchers before making changes for a client or brand. - Accept store or organization invitations from the invitation acceptance routes. ## When not to use it - Do not use shop switching as a permissions workaround; assign the user to the correct workspace or store. - Do not create duplicate shops just to test setup; use the existing staging or test workspace when possible. - Do not troubleshoot product data until the active shop and workspace are confirmed. ## Implementation source - Authentication pages live under `src/app/(auth)`, workspace creation under `src/app/(dashboard)/orgs/new`, and client-link setup under `src/app/(join)/join`. - Tenant resolution depends on Supabase Auth plus the active-shop cookie used by dashboard route handlers. - Shop and organization membership state is stored in Prisma models in `prisma/schema.prisma` and managed through `/api/shops` and `/api/orgs` handlers. ## Data and API - Supabase Auth user and session records. - Prisma models: `Organization`, `OrganizationMember`, `Shop`, `ShopUser`, `Invitation`, `OrgInvitation`. - APIs: `/api/shops`, `/api/orgs`, `/api/org-invitations`, `/api/invitations`. ## Failure modes - No active shop: select an existing store, or open `/join` to resume setup or ask DRIP for a client link. - Wrong tenant data: switch the active workspace before editing experiments or integrations. - Expired invitation: request a new invitation from a workspace or store admin. ## Coexistence with other testing tools Never run two testing tools on the same element or page area. Divide ownership clearly while migrating. - **Statsig:** Do not run an Apex test and a Statsig experiment on the same area. Apex can read `statsig_experiment` from the dataLayer as a segment. - **GemPages:** Pause its A/B test or pin it to one variant before Apex owns that area. - **Shoplift:** Let running tests finish, move each area to Apex, and remove the app at the end. - **AB Lift:** Keep one owner per area. Stop the test there before starting it in Apex, and create no new tests there once Apex is live. - **PostHog:** Do not test one element with both tools. Keep Apex heatmaps and replay off where PostHog records. - **VWO, Optimizely, Kameleoon, and Convert:** Never run both tools on the same element or page area. ## Install Apex on a custom storefront For non-native storefronts, Apex loads through a small script snippet from the Apex Worker. Apex provides the shop-specific snippet during setup. Use this path for custom storefronts, Webflow, headless builds, and other sites where a developer can add a script to the global document head. The Installation page provides the exact .js snippet for your shop; onboarding shows the Worker path for developer handoff. ## Add the script Place it in the global <head>, before your other scripts. It loads synchronously so the anti-flicker guard can prevent visible content flashes (guard is capped at 1.2 s). ```html ``` The preconnect warms the Worker connection before the snippet is discovered. Keep the synchronous tag: it arms the anti-flicker guard before first paint and guarantees assignment and goal tracking are active before the page becomes interactive. An `async` tag is available as an explicit opt-in for stores that treat render blocking as a hard constraint, but it weakens two guarantees: the anti-flicker guard arms only after the script arrives (never combine `async` with scoped or full anti-flicker), and the page can become interactive before assignment and goal tracking are ready, so early shopper actions may be missed or mis-attributed. Only choose it deliberately, with those measurement implications accepted, on after-hydration stores with anti-flicker off. Code that depends on the SDK must wait for `window.Drip` to exist. ## Handle single-page apps The Apex SDK observes browser history changes, popstate, hashchange, and the Navigation API when configured to do so. It also tracks pageviews on page load and SPA navigation through the SDK analytics pipeline. For headless or SPA stores, install the snippet once in the global shell rather than re-adding it inside each route component. ## Work with consent tools If your consent manager blocks analytics before consent, keep Apex behind the same consent rules your store applies to testing and analytics scripts. The Installation page includes a consent-mode example that starts tracking only after your cookie banner callback grants consent. ## Confirm events arrive 1. Publish the snippet to the storefront. 2. Open the live storefront and visit a tracked page. 3. Return to Apex onboarding or Installation and run the verification check. ## Troubleshooting - If nothing appears, confirm the snippet URL exactly matches the one Apex generated for the shop. - If only some pages track, confirm the snippet is loaded by the shared layout or global head. - If a consent manager is installed, grant consent during the test visit and verify again. ## Related - [Install Apex on a headless React storefront](/docs/setup/headless-react) ## Glossary Use this glossary when Apex uses a product or analytics term you do not use every day. ## A/B test A comparison between two or more versions of a storefront experience. Apex calls the test an experiment. ## Assignment The version a visitor receives in an experiment. Apex keeps assignments sticky so the same visitor keeps seeing the same variation while the assignment is valid. ## Control The unchanged version of the experience. Results for other variations are compared against the control. ## Conversion A visitor action that counts toward a goal, such as clicking Add to cart, reaching a page, firing a custom event, or placing an order. ## Experiment The test container. It holds status, variations, targeting, traffic allocation, schedule, goals, QA, and results. ## Goal The success metric for an experiment. Apex supports click, pageview, custom, and revenue-style goals. One goal should be marked primary for the experiment decision. ## Guardrails Rules and warnings that protect test quality. Guardrails cover launch readiness, runtime, statistical interpretation, traffic health, and auto-pause behavior. ## SDK The JavaScript runtime that runs on the storefront. It loads experiment config, checks targeting, assigns visitors, applies variations, and sends events. ## Snippet The shop-specific script tag that loads Apex on a storefront. ## Variation A version inside an experiment. A variation can be the comparison control or a treatment; both can carry storefront artifacts, while launch readiness still requires a treatment change. ## Visitor ID The pseudonymous identifier Apex uses to keep assignment and event data connected for a browser visitor. ## Worker The edge service that serves Apex snippets and config, receives events, supports previews, and routes analytics data. ## Consent mode A runtime setting that lets Apex wait for a consent provider before using persistent storage or tracking. See [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) and [SDK consent modes](/docs/sdk/consent-modes). ## Related - [What is Apex?](/docs/getting-started/what-is-apex) - [Experiment creation and lifecycle](/docs/experiments/overview) - [Install Apex on a custom storefront](/docs/setup/custom) ## Install Apex on a headless Nuxt storefront This guide installs Apex on a Nuxt 3 storefront backed by Shopify. Add the loader once through your central Nuxt plugin, then configure the SPA and consent presets in Apex. ## Install the loader Replace `` with the shop ID shown in Apex. Choose one loader variant; do not install both. ### Direct load (recommended) Use the direct loader when consent is configured in the Apex dashboard. The SDK then loads before consent, and Apex's consent integration controls storage and tracking. Create `plugins/apex.ts` — a **universal** plugin, not `.client.ts`. During server-side rendering, `useHead` writes the script tag into the served HTML, so the browser discovers the SDK with the initial document instead of after the client bundle boots. (A `nuxt.config.ts` `app.head.script` entry achieves the same; use the plugin form when the shop ID comes from runtime config.) ```ts export default defineNuxtPlugin(() => { useHead({ script: [ { key: "apex-loader", src: "https://events.drip-apex.com/s/.js", async: true, }, ], }); }); ``` The async loader does not block rendering, and because the tag is server-rendered it starts downloading with the initial document. It is still best-effort for the first paint: an async script can finish after the browser begins painting. If the shop requires the strongest pre-paint guarantee, use the synchronous loader pattern generated by Apex instead. ### CMP-gated load (OneTrust) If your storefront requires OneTrust to activate the Apex tag itself, emit the loader as an inert Targeting-cookie script (same universal-plugin file): ```ts export default defineNuxtPlugin(() => { useHead({ script: [ { key: "apex-loader", src: "https://events.drip-apex.com/s/.js", type: "text/plain", class: "optanon-category-C0004", }, ], }); }); ``` OneTrust changes this tag into an executable script after the visitor grants the `C0004` category. This stricter gate means the SDK cannot run before consent, so the pre-paint anti-flicker path cannot protect the first paint. When flicker matters, prefer direct loading plus Apex's own consent gating. ## Apply the Nuxt SPA preset In the Apex dashboard, open the shop's **Installation** settings and set two controls: 1. **Apply timing** (`spa.applyTiming`): select **After hydration**. 2. **DOM changes** (`spa.domChangeTrigger`): enable it. **After hydration** waits for the Nuxt application to hydrate before mutations are applied — the SDK recognizes a Nuxt page through `window.__NUXT__` and the `#__nuxt` root and holds mutations until hydration completes. **DOM changes** re-evaluates eligible mutations after Nuxt swaps or updates the view; enable it explicitly, because it is what keeps mutations applied across client-side updates on Nuxt. These two controls are the complete SPA setup for Nuxt. ## Configure OneTrust consent In the Apex dashboard, choose the **OneTrust consent** integration. Its default category is `C0004`, the standard OneTrust Targeting Cookies group. Keep that value when Apex belongs in the same category as your testing and analytics tools, or replace it with the category ID your consent policy assigns to Apex. The SDK reads the category list from `window.OnetrustActiveGroups`. It starts consent-controlled storage and tracking only when the configured group is present. Test both a denied visit and a newly granted visit after changing the category. If you selected the CMP-gated loader above, OneTrust controls when the SDK code loads as well as what the in-dashboard integration permits. With the recommended direct loader, the SDK is available for pre-paint work while the integration still gates consent-controlled behavior. ## Handle client-side navigation Nuxt's default history navigation works with Apex; install the plugin once rather than once per page. On client-side route changes, the SDK observes the URL and re-evaluates after the new view is rendered. With `domChangeTrigger` enabled, later component and DOM updates also cause eligible mutations to be re-applied. See [SPA and router modes](/docs/sdk/spa-router) for router overrides, manual activation, and debounce settings. ## Read feature flags with Vue composables Install the SDK package release containing the Vue bindings, then add `DripPlugin` to your existing universal loader plugin. Keep the hosted loader configured above and omit SDK init config when installing the plugin. Declare the private ingest token key in `nuxt.config` so the `NUXT_APEX_INGEST_TOKEN` environment variable fills it on the server only. Its value must be exactly the Edge Worker's `APEX_INGEST_TOKEN`: the middleware verifies the worker's HMAC with it, and any other value rejects every forwarded envelope as `invalid_signature`. ```ts // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { apexIngestToken: "", // server-only; NUXT_APEX_INGEST_TOKEN = the Edge Worker's APEX_INGEST_TOKEN }, }); ``` ```ts import { DripPlugin } from "@drip-apex/sdk/vue"; export default defineNuxtPlugin((nuxtApp) => { // Keep the useHead loader configuration from the installation section. const event = import.meta.server ? useRequestEvent() : undefined; nuxtApp.vueApp.use(DripPlugin, undefined, { serverAssignments: event?.context.apexAssignments, nuxt: { useRequestHeaders, useCookie, useState }, }); }); ``` ```vue ``` ## Render variants on the server With the customer-deployed Edge Worker 1.1.16 or later, the worker forwards the visitor id and a signed assignment envelope to the origin so the origin can render variants without a hydration flicker. The worker strips incoming client assignment headers and signs its own exact JSON payload with hex HMAC-SHA256; the header contract and a verification recipe are in [Server-rendered variants](/docs/settings-account/edge-routing-domains#server-rendered-variants). Verify edge-forwarded assignments in Nitro before your universal plugin consumes them. Keep the ingest token in private runtime config. ```ts // server/middleware/apex-assignments.ts import { readServerAssignments } from "@drip-apex/sdk/vue"; export default defineEventHandler(async (event) => { const ingestToken = useRuntimeConfig(event).apexIngestToken; const result = readServerAssignments( getRequestHeaders(event), { drip_uid: getCookie(event, "drip_uid") }, { // Omit the token when it is not configured: forwarded headers are then // rejected with `no_token`, while cookie-only bucketing still works. ...(typeof ingestToken === "string" && ingestToken ? { ingestToken } : {}), // Your cached list of this shop's running experiments, filtered to the // ones eligible for this request. It supplies control status, values and // flag keys; without it every forwarded assignment counts as control. experiments: await eligibleApexExperiments(event), }, ); event.context.apexAssignments = result; if (result.rejected) console.warn("Apex assignments rejected:", result.rejected); }); ``` Rejected headers render defaults with no assignments; the middleware logs only the rejection reason. Attribution-only entries (`attributableOnly: true`) are dropped from rendering. Forwarded headers contain IDs, not flag metadata: pass eligible `experiments` to derive control status, variation values, and feature flags. Experiments missing from `experiments` are treated as control (`isControl: true`, `index: 0`, undefined value and feature flag), so flags fail closed while `useApexVariant` still exposes the forwarded variation id. For cookie-only bucketing, pass `experiments` in the third argument containing only experiments already eligible for this request. It uses the same visitor and variation hashes as the client SDK. It does not implement browser targeting or sticky assignment storage; use forwarded decisions for those cases. No header and no cookie returns no assignment and leaves bucketing to the client. ```vue ``` The plugin serializes verified decisions in `useState("apex-assignments")`. `useApexVariant` uses that payload for the first client render, even if the snippet has already loaded, then follows live runtime decisions. Never serialize the ingest token. See [Vue and Nuxt bindings](/docs/sdk/vue-bindings) for all composables and the provider boundary. ## Preserve revenue attribution Shopify's `cartAttributesUpdate` mutation replaces the full attributes array. In a Nuxt composable, read the cart's current attributes and merge the Apex stamp before every update. Re-sync after `cartCreate` and later cart mutations because an assignment can arrive after the first cart write. ```js export function useApexCartAttribution(storefrontClient) { async function syncStandardAttribution(cartGid) { const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, { variables: { cartId: cartGid }, }); const attributes = window.drip?.mergeCartAttributes?.(cart?.attributes) ?? cart?.attributes ?? []; const result = await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, { variables: { cartId: cartGid, attributes }, }); return result; } return { syncStandardAttribution }; } ``` Call `syncStandardAttribution(cart.id)` after `cartCreate` succeeds and after each Storefront API cart mutation that may follow a new assignment. **Charge-affecting experiments must use the signed flow.** For price, shipping, offer, or any other test that changes what the customer is charged, write every attribute returned by `window.drip.getSignedCartAttributes(cartGid)`, then call `confirmCartAttributesWritten(cartGid)` only after `cartAttributesUpdate` succeeds with no `userErrors`. Never run an unsigned `mergeCartAttributes()` re-sync after a signed write on the same cart: it replaces the signed authorization, and commerce authorization fails closed at checkout. `getSignedCartAttributes` returns only the Apex entries. `cartAttributesUpdate` replaces the cart's full attributes array, so merge the signed entries with the cart's other attributes before writing — never write the signed list alone, or you erase gift notes and other apps' attributes. An empty signed result is fail-closed: skip the write entirely and try again on the next cart mutation. ```js export function useApexSignedCartAttribution(storefrontClient) { async function syncSignedAttribution(cartGid) { const signed = (await window.drip?.getSignedCartAttributes?.(cartGid)) ?? []; if (signed.length === 0) { // Fail-closed: the SDK is not loaded yet (e.g. consent-gated loader) or // authorization is not ready. Do not write, and do not substitute // unsigned attributes on this cart. return null; } const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, { variables: { cartId: cartGid }, }); const signedKeys = new Set(signed.map((entry) => entry.key)); const preserved = (cart?.attributes ?? []).filter( (attribute) => !signedKeys.has(attribute.key), ); const result = await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, { variables: { cartId: cartGid, attributes: [...preserved, ...signed] }, }); const userErrors = result?.cartAttributesUpdate?.userErrors; if (!Array.isArray(userErrors)) { throw new Error("Shopify did not return cart attribute write status"); } if (userErrors.length === 0) { const acknowledged = window.drip.confirmCartAttributesWritten(cartGid); if (!acknowledged) { // The SDK rejected the acknowledgement (e.g. the staged treatment went // stale while the write was in flight). Authorization stays pending — // re-run this sync on the next cart mutation. return null; } } return result; } return { syncSignedAttribution }; } ``` Cart attributes have no compare-and-set on the Storefront API, so a concurrent attribute write between the read and the update can be overwritten. Route all cart mutations in your app through one serialized cart store (queue writes; never fire `cartAttributesUpdate` concurrently), and re-run the sync after every later cart mutation — the same rule as the standard flow above. Keep the signed and unsigned helpers on separate code paths. Orders can then be attributed server-side through Shopify order webhooks. ## Verify the installation 1. Publish an active experiment that targets a page in the Nuxt storefront. 2. Open a fresh browser session. If OneTrust is enabled, exercise the consent state you intend to test. 3. Confirm that the visit receives an experiment assignment and that the assigned variation appears. 4. Navigate to another Nuxt route without a full reload and confirm another pageview arrives. 5. In Apex, open **Installation** for the shop and run the verification check. Then open the experiment's results dashboard and confirm the visit and pageview are present. ## Related - [SPA and router modes](/docs/sdk/spa-router) - [Runtime API](/docs/sdk/runtime-api) - [Install Apex on a headless React storefront](/docs/getting-started/headless-react) ## Install Apex on a headless React storefront For headless React storefronts, Apex still starts with the global SDK snippet. The snippet handles anti-flicker protection, assignment, persistence, pageview tracking, and event delivery before your React tree finishes rendering. Use the npm React bindings when a test needs native branch rendering for cart logic, product modules, or state-coupled UI. Apex owns the assignment and stats; your components only read the active variation or flag. ## Add the script to your app shell Place the shop-specific snippet once in the root document head, such as Next.js app/layout.tsx or the Hydrogen app shell. The preconnect starts the Worker connection before the browser discovers the script. ```html ``` The synchronous tag above is the safe default: it arms the anti-flicker guard before first paint and has assignment plus goal tracking active before the page is interactive. An async variant exists as an explicit opt-in for after-hydration stores with anti-flicker off that treat render blocking as a hard constraint — it does not change flicker behavior there, but the page can become interactive before assignment and tracking are ready, so early shopper actions may be missed or mis-attributed: ```html ``` Keep synchronous loading whenever anti-flicker is scoped or full — an async tag arms the guard only after the script arrives — for immediate-apply themes, and whenever conversion attribution matters more than the ~250–400 ms render-blocking cost (it usually does). ## Keep preview and staging traffic out of live results A storefront host that is not listed under any environment receives the default environment, which is production: its visits are bucketed into live tests and counted in results. Branch previews such as `.vercel.app` and local builds on `localhost` fall into this case unless you route them. You can route them in two ways: 1. **Register the hosts.** In Settings > Developers > Environments, add the preview hosts to your staging environment's domains. A wildcard such as `*.vercel.app` or `*.preview.example.com` covers every subdomain, and `localhost` covers local builds. 2. **Select the environment on the snippet URL.** Add `?env=` to the snippet URL for non-production builds. The explicit key wins over the host mapping. For example, in Next.js on Vercel: ```tsx const env = process.env.NEXT_PUBLIC_VERCEL_ENV === "production" ? "" : "?env=staging"; ``` ## Headless / strict CSP checklist For `script-src 'strict-dynamic' 'nonce-…'` storefronts: 1. Render the shop-specific loader with the page's fresh server-generated nonce: ``. A `sdk.drip-apex.com` host allowlist is not a substitute under `strict-dynamic`. 2. Add `https://events.drip-apex.com` to the existing `connect-src` directive. This client-owned one-line CSP change is mandatory for event POSTs. 3. Keep the existing `style-src 'unsafe-inline'` allowance if Apex anti-flicker is enabled; the anti-flicker style needs no Apex-side change. 4. Verify a pageview and conversion request in browser developer tools after deployment. Live CSP verification is a downstream client/staging check, not something the package build can certify. The Worker-served snippet copies its nonce to a secondary SDK tag when the configured SDK-delivery policy creates one. Other scripts created by that trusted SDK inherit trust under `strict-dynamic`. For an npm install, the SDK is compiled into the storefront's application bundle, so authorize the application bundle through the site's existing CSP path; there is no Apex loader tag to nonce. GTM is an alternative only when the GTM bootstrap is already nonced: install Apex from that trusted container, accepting that tag-manager timing cannot provide the direct head tag's early anti-flicker guarantee. The Worker's `x-drip-csp-nonce` edge-injection support is not involved when Oxygen or another headless platform fronts and serves the HTML. In that topology the application must put the nonce on the Apex tag itself. ## SPA navigation The SDK observes browser history changes, popstate, hashchange, and the Navigation API automatically. Install the snippet in the global shell rather than adding it per route. For React route transitions that re-render product or cart modules, set routerMode to history, navigation, or auto and enable spa.domChangeTrigger when Apex should re-assert eligible changes after DOM updates. ## Consent tools Apex integrates with OneTrust, Cookiebot, Usercentrics, CCM19, Pandectes, and CookieScript through consentMode: { enabled: true, provider: "..." }. For any other CMP, use the custom provider with a getter that returns whether tracking consent is currently granted. Keep Apex behind the same consent policy you apply to testing and analytics scripts, then run a granted-consent test visit during verification. ## Branch rendering in React (npm package) Install @drip-apex/sdk, wrap your app in DripProvider, and read useVariation(experimentId) or useFeatureFlag(flagId) inside components. This is the right path for tests that need native React state, cart behavior, or module-scoped rendering while Apex handles assignment, persistence, and stats. See the [React SDK reference](/docs/sdk/react-bindings) for every provider prop and hook. ```tsx import { DripProvider, useFeatureFlag, useVariation } from "@drip-apex/sdk/react"; export function StorefrontProviders({ children }: { children: React.ReactNode }) { return ( {children} ); } function ProductOffer() { const trustBadge = useVariation("product-trust-badge"); const showBundle = useFeatureFlag("bundle-module"); return ( <> {trustBadge?.id === "instant-access" ? : null} {showBundle ? : } ); } ``` ## Purchase attribution with Storefront API carts ### Checkout attribution on a shared registrable domain If Shopify's analytics bus is unavailable, Apex writes the current assignment sync to a first-party cookie after tracking consent is granted. A checkout on the same registrable domain can read the cookie through the Apex web pixel and attribute purchase events. For example, `shop.example.com` and `checkout.example.com` can share the cookie. The cookie cannot cross registrable domains. A custom storefront and a checkout on `myshopify.com` do not share it. Keep the cart-attribute integration below as the webhook-attribution hardening path. It is required for every charge-affecting price, shipping, or offer experiment. Headless carts must carry Apex attribution into Shopify. Because Shopify's cartAttributesUpdate mutation replaces the complete attributes array, load the cart's current attributes and pass them to window.drip.mergeCartAttributes() before updating. The method normalizes valid existing attributes to Array<{key, value}>, replaces only keys present in the fresh Apex stamp, and preserves all other attributes. When Apex has no fresh stamp, existing attribution remains unchanged. Re-sync after cartCreate and every cart mutation because assignments can arrive after first paint. Orders can then attribute server-side through Shopify order webhooks. **Charge-affecting experiments use the signed flow instead.** If the shop runs price, shipping, or offers experiments (anything where the treatment changes what the customer is charged), the cart's `apex_assignment_authorization` must come from `window.drip.getSignedCartAttributes(cartGid)` — write every entry it returns to the cart and call `confirmCartAttributesWritten(cartGid)` after the mutation succeeds with no `userErrors`. Do not follow a signed write with an unsigned `mergeCartAttributes()` re-sync on the same cart: the unsigned stamp replaces the signed authorization, and commerce authorization then fails closed, suppressing the treatment at checkout. Use `mergeCartAttributes()` for standard (non-charge-affecting) attribution only. ```ts const { cart } = await storefrontClient.query(CART_ATTRIBUTES_QUERY, { variables: { cartId }, }); const attributes = window.drip?.mergeCartAttributes?.(cart?.attributes) ?? cart?.attributes ?? []; await storefrontClient.mutate(CART_ATTRIBUTES_UPDATE, { variables: { cartId, attributes, }, }); ``` ### Optional cart-metafield mirror for BI Headless shops can opt in to mirroring the same Apex key/value stamp into Shopify cart metafields. This mirror is advisory only. Never use it as attribution truth. The signed [cart-attribute flow documented above](#purchase-attribution-with-storefront-api-carts) remains the authoritative attribution plane. #### Required order-metafield setup Shopify copies a cart metafield to an order only when a matching order metafield definition exists. The definition must use the same namespace and key. It must also enable the `cartToOrderCopyable` capability. This capability requires Shopify Admin API version `2026-04` or later. Create one `ORDER` definition for each mirrored key. The default namespace is `apex`. | Key | Type | | --- | --- | | `apex_assignments` | `json` | | `apex_assignment_authorization` | `single_line_text_field` | | `apex_experiment_id` | `single_line_text_field` | | `apex_variation_id` | `single_line_text_field` | | `apex_assignment_epoch` | `single_line_text_field` | | `apex_visitor_id` | `single_line_text_field` | | `apex_session_id` | `single_line_text_field` | The following Admin GraphQL mutation creates the definition for `apex_assignments`. Run it against API version `2026-04` or later. Repeat it for each key in the table. Use `single_line_text_field` for the other keys. ```graphql mutation CreateApexAssignmentsOrderMetafield { metafieldDefinitionCreate( definition: { name: "Apex assignments" namespace: "apex" key: "apex_assignments" ownerType: ORDER type: "json" capabilities: { cartToOrderCopyable: { enabled: true } } } ) { createdDefinition { id namespace key type { name } } userErrors { field message code } } } ``` After these definitions exist, Shopify can copy matching cart metafields to the completed order. BI and extract, load, transform tools can then ingest the experiment context. #### Write the current mirror ```ts const metafields = window.drip?.getCartMetafields?.() ?? []; await storefrontClient.mutate(` mutation SetApexCartMetafields($metafields: [CartMetafieldsSetInput!]!) { cartMetafieldsSet(metafields: $metafields) { metafields { key namespace type value } userErrors { field message } } } `, { variables: { metafields: metafields.map((metafield) => ({ ...metafield, ownerId: cartId })), }, }); ``` The returned keys are composite Shopify keys such as `apex.apex_assignments`. Each object can be spread with `ownerId` as shown above. This helper is headless-only because the Storefront API is Shopify's cart-metafield write path. Liquid and theme storefronts cannot write this mirror. `getCartMetafields()` omits empty values. Apex cart attributes use empty values to clear prior treatment stamps. `cartMetafieldsSet` only upserts the supplied keys, so an omitted key can retain a stale value on the cart. For optional cleanup, delete the complement before or after the upsert. Use the same namespace option for both helpers. ```ts const CART_METAFIELD_DELETE = `mutation DeleteStaleApexCartMetafield($input: CartMetafieldDeleteInput!) { cartMetafieldDelete(input: $input) { deletedId userErrors { field message } } }`; await Promise.all((window.drip?.getCartMetafieldStaleKeys?.() ?? []).map((key) => storefrontClient.mutate(CART_METAFIELD_DELETE, { variables: { input: { ownerId: cartId, key } } }))); ``` See the [`getCartMetafields` runtime API reference](/docs/sdk/runtime-api#getcartmetafields) for the return shape and namespace option. ## Confirm events arrive 1. Publish the snippet to the headless storefront. 2. Open the live storefront and visit a tracked page. 3. If consent is required, grant consent in the banner during the test visit. 4. Return to Apex onboarding or Installation and run the verification check. ## Related - [Choose and document the price basis for value-bearing treatments](/docs/developers/price-basis-value-treatments) - [Install Apex on a custom storefront](/docs/setup/custom) - [Install Apex on Shopify](/docs/setup/shopify-app) ## Launchpad > **Simple terms** > > Launchpad is the home screen that tells an operator what to do next for the selected shop. The daily command center for a shop. It shows readiness, active tests, draft blockers, recent learning, and the next best action. ## Routes - `/` ## Implementation behavior - Launchpad Tinybird reads are bounded in production: 15s queue / 8s execution budget for reads reachable from the home render (see release 114fcd5). - For experiments that are part of a multi-shop program, Launchpad shows the program's pooled brand decision (pooled lift, confidence) on the live row and annotates the row with the number of pooled stores; when the pooled read is unavailable the row falls back to the store's own metrics. {/* source: release 187a6ff */} - Pooled results are only surfaced to internal roles; client roles and unauthenticated viewers always see the per-store numbers and are not exposed to program membership. {/* source: release 187a6ff */} - When a linked idea appears in the month strip, Launchpad now places and labels the linked idea by its primary test instead of the first-created link. {/* source: release 35283b7 */} - Launchpad reads the active shop and aggregates setup state from goals, audiences, store profile, installation, drafts, and running experiments. - It computes a readiness stage such as setup, launch, active learning, or readback based on what exists and what is blocking progress. - The next-move card points to the highest-leverage route with a reason and confidence so operators do not hunt through the app. - Learning proof comes from recent experiment outcomes, store profile findings, and signals that are already persisted. ## How to use it - Start from Launchpad before creating new work. - Follow the highlighted next move if setup or launch readiness is unclear. - Open active and draft tests directly from the Launchpad lists. - Use readiness checks to decide whether to install, add metrics, build variants, QA, or read results. ## When not to use it - Do not use it as the source of truth for detailed experiment math; open the experiment or analytics pages for that. - Do not treat a next-move suggestion as approval to launch without QA. - Do not diagnose SDK install issues from Launchpad alone; use Realtime Analytics and installation checks. ## Implementation source - The route is `src/app/(dashboard)/page.tsx` with supporting dashboard data calls. - It composes shop setup state from experiments, goals, audiences, installation, analytics summaries, and store profile state. - Navigation cards link into the same underlying routes documented in the complete catalog. ## Data and API - Reads `Shop`, `Experiment`, `Variation`, `Goal`, `ExperimentGoal`, store profile, signal, and installation state. - Analytics summaries come from dashboard and experiment analytics APIs. - Navigation connects into `/backlog`, `/experiments`, `/analytics`, `/store-profile`, and `/installation`. ## Failure modes - Empty Launchpad usually means no shop, no installation, or no foundations yet. - A next move that looks stale usually points to incomplete setup data rather than a broken recommendation. {/* user-note: "Launchpad: a live test with no readable confidence now shows 'Confidence unavailable' instead of an empty bar. Ticket: #6088" */} - Analytics cards stay thin until SDK events reach the Worker and Tinybird. ## Verify your Shopify tracking Apex separates platform connection from live telemetry. A connected Shopify app means Apex has the Shopify install record and pixel state. Receiving data means Apex has seen recent storefront telemetry for the shop. Onboarding uses the go-live verification check to read recent installation signals and show when Apex is receiving data. ## Confirm the app embed 1. Open the Shopify theme editor for the published theme. 2. Confirm the Apex app embed is enabled. 3. Save the theme after changing the embed state. ## Send a test visit 1. Open the live storefront in a normal browser tab. 2. Visit a product or collection page. 3. Return to Apex onboarding and run the install verification again. ## Understand the states - Connected means Apex found the platform app connection before traffic verification. - Receiving data means Apex received a fresh storefront pageview and visitor signal during the verification window. - Partial means Apex has some telemetry, but one or more checks still need a signal. ## Troubleshooting - Embed not enabled: enable the Apex app embed in the live theme and save. - Wrong theme published: repeat the embed step on the current published theme. - Consent blockers: if a consent manager blocks scripts or storage before consent, grant test consent and send another visit. - Telemetry delay: wait briefly, then run verification again. ## Related - [Install Apex on Shopify](/docs/setup/shopify-app) ## Install Apex on Shopify The Apex Shopify app connects your Shopify store to Apex through Shopify OAuth. In Apex, the install button starts /api/shopify/auth?shopify_domain=<store>.myshopify.com for your permanent Shopify domain. After OAuth succeeds, Apex stores the Shopify installation, registers Shopify web pixel settings for commerce events, and configures the storefront app embed settings used by the theme extension. ## Before you start - Use the permanent .myshopify.com domain for the store, not only the public storefront domain. - Install from Apex onboarding or the Installation page so the app connects to the right Apex shop. - You need to be the Shopify store owner, or staff with the 'Manage and install apps and channels' permission. - You need the 'Themes' staff permission (access to the published theme) to enable the app embed. ## Install the app 1. Open Apex onboarding or the Installation page for the shop. 2. Choose the Shopify install action. Apex redirects you to Shopify OAuth for the selected .myshopify.com store. 3. Approve the requested Shopify app access in Shopify. 4. Return to Apex after the callback completes. ## Turn on the storefront embed The OAuth callback prepares the app embed configuration, but the live theme still needs the Apex embed enabled in Shopify. From Apex, open Installation and use the app embed action. Shopify opens the theme editor with the Apex embed selected. Enable it and save the live theme. ## What Apex receives The Shopify web pixel forwards enabled Shopify customer events to the Apex event endpoint. The current pixel subscribes to checkout, cart, product, collection, search, alert, UI extension error, and checkout experiment events. Apex also syncs active experiment assignment data to the pixel so Shopify events can be attributed to the right experiment and variation when assignment data is available. ## Troubleshooting - If OAuth rejects the domain, confirm it is a valid *.myshopify.com host. - If the app is connected but no storefront events arrive, confirm the Apex app embed is enabled and saved on the live theme. - If you changed themes after setup, enable the embed again on the published theme. ## Related - [Verify your Shopify tracking](/docs/setup/shopify-verify) - [Install Apex on a headless React storefront](/docs/setup/headless-react) ## Install Apex on Shopware The Apex Shopware app connects your Shopware store through the Shopware app manifest flow. Shopware reads the Apex manifest, registers the app with Apex, confirms the install with Shopware Admin API credentials, and then loads the Apex storefront bridge for confirmed active installations. Use the Shopware path when you want Apex to load through the Shopware app system instead of pasting a manual script into the theme. ## Before you start - Have Shopware Admin access with permission to install apps. - Get the `ApexDrip` private-app package from your Apex contact. - Make sure the Shopware server can reach the Apex app host over HTTPS. - Keep the app active after installation; Apex treats deactivated or deleted app installs as inactive. ## Send the package to your developer Give your Shopware developer the `ApexDrip` package — it is delivered to you directly by your Apex contact (request it if you have not received it; there is no public download URL). The public dynamic manifest is available for inspection only; it is not an `app:install` argument: ```txt https://app.drip-apex.com/api/shopware/manifest ``` The distributable package contains the private shared secret required for Shopware to sign registration. Do not publish or commit the package. ## Install the Shopware app 1. Extract or copy the package so the app is at `custom/apps/ApexDrip` in the Shopware project. 2. Run these commands from the Shopware project root: ```bash bin/console app:refresh bin/console app:validate ApexDrip bin/console app:install --activate ApexDrip ``` 3. Approve the `Apex by Drip` app in Shopware Admin. 4. Let Shopware complete registration and confirmation. This gives Apex the shop-specific credentials it needs for the app connection. 5. Confirm the app is installed and active in Shopware Admin. ## Confirm storefront tracking After the app is confirmed, the Shopware storefront bridge resolves the current Shopware shop URL to the connected Apex shop and injects the Apex Worker snippet. 1. Open the live storefront in a normal browser session. 2. Visit a product page or checkout flow page. 3. In Apex, open Installation or the Shopware analytics page and confirm the connection is active. 4. Run a test purchase only when you are ready to verify revenue tracking. The storefront bridge also passes Shopware order identity on checkout finish pages when the order number is visible in the page markup. That lets Apex connect server-side order events back to the visitor assignment when the storefront and webhook data line up. ## Optional: first-party ingest domain You can serve the Apex snippet and send SDK config, assignment, and event traffic through a subdomain of your own store domain. Ask your Apex contact to register the subdomain as the shop's ingest domain. Then add the CNAME and Cloudflare validation TXT records they provide; the CNAME must point to `edge.drip-apex.com`. Apex continues using the shared `events.drip-apex.com` host while DNS or TLS validation is pending. Once Cloudflare reports both the hostname and SSL certificate as active, the Shopware storefront bridge automatically loads the snippet from your first-party domain. No change to the Shopware app package is required. ## What Apex receives The manifest declares the Apex statistical cookie entry, app lifecycle webhooks, and commerce webhooks. Apex receives: - App lifecycle events for installed, activated, deactivated, and deleted states. - Order and refund events including order placed, order written, refunded, partially refunded, and cancelled signals. - Product update signals used to refresh product and cost context. - Storefront assignment and tracking events through the same Apex Worker snippet used by other storefronts. The app requests read access for product, order, customer, sales channel, currency, and language data. Apex uses those permissions for commerce attribution, revenue analytics, and storefront context. For storefront purchases, Apex carries attribution in a Worker-signed envelope stored in Shopware’s own line-item payload. The order webhook verifies the signature before linking the order. Apex does not add attribution text to the shopper’s order-comment field; the legacy parser is retained only for orders created by older cached storefront scripts. ## Troubleshooting - If registration fails, confirm `SHOPWARE_APP_SECRET` was injected into the packaged manifest and matches the Apex web runtime value. - If the app is installed but not connected in Apex, confirm the Shopware confirmation step completed and the app is active. - If no storefront events arrive, confirm the storefront can load the Apex storefront script and that consent rules are not blocking analytics before consent. - If revenue is missing, confirm the order webhook events return successfully and the connected shop is active in Apex. - If you uninstall or deactivate the app, reinstall or reactivate it before expecting new storefront or webhook data. ## Related - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [SDK consent modes](/docs/sdk/consent-modes) - [Worker public endpoints](/docs/developers/worker-endpoints) ## How Apex detects your store The first onboarding step scans your storefront so Apex can pick the right connection path before you touch any code. You enter your store URL and brand name, and Apex reads the public storefront to detect the commerce platform and any storefront tools it can see. Nothing is installed during the scan. It only reads publicly available storefront signals to recommend how to connect Apex. ## What the scan reads - Platform signals: markup, scripts, and globals that identify Shopify, Shopify Hydrogen, Shopware, or a custom storefront. - Commerce setup: product and checkout structure used to prepare your brand profile. - Storefront tools: analytics, review, and marketing scripts that are detectable from the public page. ## Platform detection and confidence Apex reports a detected platform with a confidence level. When detection is confident and the platform has a native Apex integration, the step recommends a one-click connect path. When detection is ambiguous or the store is custom, you can choose the platform yourself and Apex switches to a script-based connection. You can always correct the detected platform before continuing. ## Detected storefront tools Detected tools are shown only when Apex sees matching storefront scripts or browser globals. If a detected tool has a native Apex integration, the step highlights a one-click connect action for it. An empty tools list is normal — it only means no analytics, review, or marketing tools were confidently detected from the storefront. ## Next step After you confirm the platform, Apex prepares your brand profile and moves you to the connection step, where you install the app or add the script for your platform. ## Related - [Install Apex on Shopify](/docs/setup/shopify-app) - [Install Apex on a custom storefront](/docs/setup/custom) ## What is Apex? Apex is an experimentation platform for ecommerce teams. It helps you test storefront changes, measure which version performs better, and keep experiments safe enough to run on a live store. Use Apex when you want to answer questions like: does this product page message increase add-to-cart clicks, does a new trust badge improve checkout starts, or does a different landing page convert more visitors? ## The core workflow 1. Create an experiment in Apex. 2. Choose the page, audience, traffic split, and success metric. 3. Build one or more variations. 4. QA the preview. 5. Launch the test. 6. Watch results, guardrails, and revenue signals. 7. Pause, complete, or archive the experiment when you have enough evidence. ## Core concepts An experiment is the test itself. It owns the name, status, target page or audience, traffic allocation, schedule, metrics, QA state, and results. A variation is one version inside an experiment. The control is the comparison baseline and can include baseline-only setup or fixes. Treatment variations carry the tested change; either arm can include visual edits, CSS, or JavaScript when the experiment requires it. A goal is the action that defines success. Common goals include clicks, pageviews, custom events, and revenue. Experiments should have one primary goal so Apex knows which result matters most. An assignment is the version a visitor receives. Apex keeps assignments sticky so a visitor does not bounce between versions during the same experiment. Guardrails are safety rules and warnings around launch, runtime, sample health, revenue, and interpretation. They help you avoid calling a winner too early or ignoring obvious risk. ## How the pieces fit The Apex dashboard is where your team creates experiments, manages goals, reviews QA, launches tests, and reads results. The Apex SDK runs on the storefront. It loads the active experiment config, checks targeting, assigns visitors to variations, applies changes, and sends events. The Apex Worker sits at the edge. It serves the shop-specific snippet and experiment config, receives storefront events, handles preview and routing paths, and forwards analytics events for reporting. Analytics events land in the event pipeline and power dashboards for visitors, conversions, revenue, funnels, heatmaps, and experiment results. ## Install paths Choose the install path that matches your store: - [Install Apex on Shopify](/docs/setup/shopify-app) - [Install Apex on WooCommerce](/docs/setup/woocommerce) - [Install Apex on Shopware](/docs/setup/shopware) - [Install Apex on a headless React storefront](/docs/setup/headless-react) - [Install Apex on a custom storefront](/docs/setup/custom) ## Related - [Glossary](/docs/getting-started/glossary) - [Experiment creation and lifecycle](/docs/experiments/overview) - [Metrics, goals, and conversion tracking](/docs/experiments/metrics-goals) - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) ## Install Apex on WooCommerce The Apex WooCommerce plugin connects a WordPress/WooCommerce store to Apex with a short-lived install token. In Apex, the WooCommerce install action creates a token that starts with `apx_woo_` and expires after 48 hours. In WordPress, you upload the plugin zip, paste the token in WooCommerce settings, and save. After the token is claimed, the plugin stores the returned Apex shop ID, snippet URL, webhook URL, and webhook secret. It then injects the Apex storefront snippet and provisions WooCommerce order webhooks. ## Before you start - Use WordPress 6 or newer, WooCommerce 7 or newer, and PHP 7.4 or newer. - Have WordPress admin access with permission to install plugins and manage WooCommerce settings. - Generate the token from Apex for the same shop you are connecting. - Use the latest `apex-woocommerce.zip` plugin package from Apex or your implementation handoff. ## Generate the install token 1. In Apex, open Settings, then Installation, then WooCommerce. 2. Generate a WooCommerce install token. 3. Copy the token. It is only useful for this connection flow and expires after 48 hours. If the token expires, generate a new one in Apex and paste the new value into WordPress. ## Upload the plugin zip 1. In WordPress Admin, open Plugins, then Add New. 2. Choose Upload Plugin. 3. Upload `apex-woocommerce.zip`. 4. Install and activate the plugin. The plugin adds an Apex settings page under WooCommerce settings. ## Connect in WooCommerce settings 1. In WordPress Admin, open WooCommerce, then Settings, then Apex. 2. Paste the `apx_woo_...` install token into Install token. 3. Save changes. 4. Confirm the page shows Connected with an Apex shop ID. When the save succeeds, the plugin claims the token with Apex, stores the returned connection details, and creates native WooCommerce webhooks for order created and order updated events. ## Confirm storefront tracking The connected plugin injects the Apex snippet early in `wp_head` and loads a small WooCommerce bridge script. Visit the storefront in a normal browser session and then check Apex Installation or analytics to confirm events are arriving. The bridge sends page context into `window.dataLayer` for virtual pageviews, add-to-cart clicks, checkout starts, and purchases. On checkout, the plugin stamps the Apex visitor ID, session ID, and experiment assignments onto the WooCommerce order when the Apex identity cookie is present. ## What Apex receives The WooCommerce plugin can send: - Storefront tracking events through the Apex snippet. - Page, product, cart, and order context from the WooCommerce bridge. - Native WooCommerce order created and order updated webhooks. - Signed refund events when a WooCommerce order is refunded. - A recent-order sync from the settings page, limited to recent orders and an allowlist of Apex attribution and cost metadata. Webhook requests are signed with the plugin webhook secret. Apex rejects inactive installations, unknown store URLs, invalid signatures, missing topics, and missing delivery IDs. ## Troubleshooting - If WordPress shows a registration error, generate a fresh token in Apex and confirm the token was copied completely. - If the settings page does not show Connected, confirm WooCommerce is active and the WordPress server can reach Apex over HTTPS. - If storefront events do not arrive, confirm the plugin is active and the connected settings page shows a snippet URL. - If order events are missing, open WooCommerce settings, Apex, and confirm native webhook IDs are present. - If historical orders are missing, use Sync recent orders from the Apex settings page after the connection is healthy. ## Related - [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance) - [SDK consent modes](/docs/sdk/consent-modes) - [Public event schema](/docs/developers/event-schema) ## Build landing pages with your own tools > **Simple terms** > > You build the page. Apex keeps its versions, tracks its goals, deploys it, and tests one version against another. A landing page built outside Apex lives in a checkout folder. Any tool can build it: Claude Code, Codex, a Vite or Astro build, or plain HTML. Apex stores every applied version, serves the deployed one, injects the shop's Apex snippet, and creates the goals the page declares. ## The workflow ```bash apex landing-pages create --name "Fall sale" --out ./fall-sale # or: apex landing-pages pull --out ./fall-sale # build ./fall-sale/index.html and its assets apex landing-pages plan ./fall-sale # what would change apex landing-pages apply ./fall-sale --yes # a new version, plus a signed preview link (24 hours) apex landing-pages deploy ./fall-sale # live at https://.apex-page.com/fall-sale apex landing-pages rollback ./fall-sale --version 1 apex landing-pages experiment ./fall-sale --control 1 --variant 2 # a draft split test; start it in the dashboard ``` The same steps exist as MCP tools (`create_landing_page`, `pull_landing_page`, `plan_landing_page`, `apply_landing_page`, `preview_landing_page`, `deploy_landing_page`, `list_landing_page_versions`, `rollback_landing_page`, `create_landing_page_experiment`). Existing-domain commands are available as `list_landing_page_domains`, `claim_landing_page_domain`, `check_landing_page_domain` and `remove_landing_page_domain`. Buying a domain through `register_landing_page_domain` is available only in local stdio MCP; the hosted connector used by ChatGPT, Claude and other remote clients does not expose it. See the [MCP tool reference](/docs/mcp/tool-reference). Production preview links use short `https://preview.apex-page.com/` addresses. Each signed link expires after 24 hours. Previews are sandboxed, are not indexed by search engines, and do not load the shop's Apex snippet. Staging and local previews stay on their app origin. ## The checkout folder | Path | What it is | |---|---| | `index.html` | The page. Required at the root. | | `assets/…` and other files | Stylesheets, scripts, images and fonts. At most 300 files and 25 MB in total. | | `apex-landing-page.json` | The manifest: name, slug, host, path, snippet, shop hosts, goals and experiment. | | `context/` | Read-only context from Apex: brand kit, catalog, offers, reviews, and the idea the page tests. Never uploaded. | | `.apex/landing-page.json` | The marker that ties the folder to its page and pulled version. | Allowed file types: html, css, js, mjs, json, svg, png, jpg, jpeg, webp, avif, gif, ico, woff, woff2, txt, xml and webmanifest. Build with relative asset URLs (Vite `base: "./"`): Apex serves assets from a versioned path. ## apex-landing-page.json ```json { "name": "Fall sale", "slug": "fall-sale", "path": "/fall-sale", "snippet": true, "shop_hosts": ["shop.example.com"], "goals": [ { "key": "buy", "name": "Buy button", "type": "click", "selector": "a.buy" }, { "key": "signup", "name": "Newsletter", "type": "custom", "event_name": "newsletter_signup" } ] } ``` - `host`: a domain the shop holds. Leave it out to deploy to the shop's instant host. - `path`: where the page lives on the host. Default: `/`. - `snippet`: Apex adds the shop's snippet to the page when it serves it. Turn it off only when the page loads the snippet itself. - `shop_hosts`: links to these hosts carry the visitor's identity, so a purchase on the shop counts for the page's test. Default: the shop's install domains. - `goals`: each goal is created on the shop by its `key` and named `: `. A changed goal is updated when the version that changes it is deployed. A removed goal is kept. - `experiment`: optional control and variant version numbers. ## Where a page goes live **Instant host.** Every shop has its own address on apex-page.com: `https://.apex-page.com/`. The label is the shop's brand name: its domain without `www.`, a platform host such as `.myshopify.com` and the top-level domain (snocks.com becomes `snocks`, shop.co.uk becomes `shop`, eu.brand.com becomes `eu-brand`). It is assigned at the shop's first deploy and never changes, also when the shop's domain changes later. No DNS setup is needed. A shop must be live on its own domain to deploy here. **Claimed domain.** Set `host` in the manifest, or pass `--host`, to deploy to a domain the shop holds, such as `offers.shop.example.com`. Claim a client's hostname with `apex domains claim offers.shop.example.com` (add the CNAME it prints, then `apex domains status offers.shop.example.com`), or register a new domain on the Drip account with `apex domains register`. A host on apex-page.com other than the shop's own address is refused. ## Cookies on apex-page.com Each shop's pages run on their own subdomain, so one shop's pages cannot read another shop's storage. On apex-page.com every Apex cookie is a host-only `__Host-` cookie, such as `__Host-drip_uid` for the visitor id. Another subdomain cannot set a `__Host-` cookie, and Apex reads no other cookie there, so a cookie another shop's page places on the parent domain never reaches Apex. Pages on apex-page.com never share cookies with the Apex dashboard. If an experiment on an apex-page.com page targets visitors by a cookie, the page must set that cookie with the `__Host-` prefix (for example `__Host-plan=pro; Path=/; Secure`); the targeting rule still names it `plan`. ## Rollback and experiments `rollback` deploys an older version to the page's current address. `experiment` creates a draft test that splits the live page between two versions; start it in the dashboard. While a test splits the page, a deploy or rollback keeps serving the test's two versions; the deployed version takes over when the test ends. ## Related pages - [Landing pages overview](/docs/landing-pages/overview) - [MCP tool reference](/docs/mcp/tool-reference) - [CLI commands and output](/docs/cli/commands-output) ## Landing pages and landing-page QA > **Simple terms** > > You build a landing page in a workspace checkout and deploy it with the Apex CLI. Apex keeps every version, shows its preview and QA evidence, publishes it, and runs experiments on it. Keeps the versions, previews, QA evidence, publish state, and experiments of landing pages that are built outside Apex and deployed with the CLI. ## Routes - `/landing-pages` - `/landing-pages/new` (redirects to the list with a deploy notice) - `/landing-pages/[id]` - `/landing-pages/[id]/qa` ## Implementation behavior - Landing pages have typed page records, versions, publish status, preview URLs, and immutable published bundles. - Pages are built outside Apex. The in-app page builder (brief, compose, chat, blocks, and gates) was retired on 2026-10-02. - Each deploy adds an immutable version. QA checks a specific version before it becomes the published one. - Published bundles are served through the configured landing-page runtime and storage path. ## How to use it - Build the page in a workspace checkout, then run `apex landing-pages deploy`. - Open the page in Apex to preview each version and review its QA evidence. - Publish only after the QA route shows the intended version and target. - Unpublish to take a page offline, or archive it to take it offline and grey it out in the list. ## When not to use it - Do not use landing pages for a small change to an existing product page; use an experiment variant instead. - Do not publish a version that has not been previewed and QAed. ## Implementation source - Pages live under `src/app/(dashboard)/landing-pages`. - Handlers include `/api/landing-pages`, `/api/landing-pages/[id]/publish`, `/api/landing-pages/[id]/qa`, `/api/landing-pages/[id]/experiments`, and version render routes. - Brief and grounding reads (`/api/landing-pages/briefs`, `/api/landing-pages/grounding/*`) give a workspace checkout the catalog, offers, reviews, and brief data to build from. - Landing page records, versions, artifacts, and publish state are persisted separately from experiment variations. ## Data and API - Landing page, version, QA, publish, and artifact records. - APIs under `/api/landing-pages`, `/api/landing-pages/[id]/publish`, `/api/landing-pages/[id]/qa`, `/api/landing-pages/[id]/versions/[versionId]/render`, and `/api/landing-pages/preview/[versionId]`. - The public v1 API (`/api/v1/landing-pages`) is read-only and backs the CLI's landing-page reads. ## Failure modes - Preview mismatch: make sure the version being QAed is the latest deployed version. - Publish target unavailable: check storage and runtime configuration and the deployed bundle state. MP4 (`video/mp4`) and WebM (`video/webm`) videos are hosted directly by Apex. Each version holds at most 300 files and 100 MB; each video is limited to 50 MB, and other files to 3 MB. `apply` automatically obtains a 15-minute upload grant for videos above 3 MB and sends resumable 8 MB parts straight to the Worker, retrying failed parts with progress output. The Worker verifies the declared size and SHA-256 before apply records the file. Pull and signed previews also fetch large videos directly from the Worker. Video supports byte ranges, including Safari's `bytes=0-1` probe; rollback preserves the older bytes. ## Install and configure the Apex MCP server The Apex MCP server lets model clients read and manage Apex experiments through the Model Context Protocol. Use it when you want ChatGPT, Codex, Claude Desktop, Claude Code, Cursor, or another MCP client to list tests, inspect a draft, create a draft or pause an experiment, read goals, pull brand context, or generate QA preview URLs. ## Prerequisites - Node.js 18 or newer. - An Apex API key with the permissions needed for the tools you plan to call. - Either `APEX_API_KEY` in the MCP process environment or a saved user/shop CLI credential at `~/.drip/credentials.json`. User keys minted by `apex login` are supported. The server reads configuration in this order. Legacy `DRIP_API_KEY` and `DRIP_BASE_URL` names remain working fallbacks: | Setting | Required | Default | Notes | |---|---:|---|---| | `APEX_API_KEY` | Yes, unless CLI credentials exist | None | Takes priority over saved credentials; `apx_uk_…` is treated as a user key. | | `APEX_BASE_URL` | No | `https://app.drip-apex.com` | Use only for non-production Apex environments. | | `DRIP_ORG` / `DRIP_SHOP` | For user-key context | Saved context | Override the org/shop selected by `apex use`. | | `~/.drip/credentials.json` | Fallback | None | Shared with `apex login`; the saved user key is preferred over a saved shop key. | ## Install Run the packaged server with `npx`: ```bash npx -y @drip-apex/mcp-server ``` For local monorepo development, build the package and run the compiled server: ```bash npm --prefix packages/mcp-server install npm --prefix packages/mcp-server run build node packages/mcp-server/dist/server.js ``` ## Claude Desktop `APEX_API_KEY` needs `@drip-apex/mcp-server` 0.2.0 or newer; on 0.1.3 set `DRIP_API_KEY` instead. Add this to `claude_desktop_config.json`: ```json { "mcpServers": { "drip-ab-testing": { "command": "npx", "args": ["-y", "@drip-apex/mcp-server"], "env": { "APEX_API_KEY": "apx_your_key_here", "APEX_BASE_URL": "https://app.drip-apex.com" } } } } ``` If you already authenticated with `apex login` on the same machine, you can omit `env` and let the server read `~/.drip/credentials.json`: Resolution is `APEX_API_KEY` (with `DRIP_API_KEY` fallback), saved user key, then saved shop key. Set `DRIP_ORG`/`DRIP_SHOP` to override saved user-key context. ```json { "mcpServers": { "drip-ab-testing": { "command": "npx", "args": ["-y", "@drip-apex/mcp-server"] } } } ``` ## Claude Code Use a project `.mcp.json` or user-level MCP config with the same server block: ```json { "mcpServers": { "drip-ab-testing": { "command": "npx", "args": ["-y", "@drip-apex/mcp-server"], "env": { "APEX_API_KEY": "apx_your_key_here", "APEX_BASE_URL": "https://app.drip-apex.com" } } } } ``` For a local checkout: ```json { "mcpServers": { "drip-ab-testing": { "command": "node", "args": ["./packages/mcp-server/dist/server.js"], "env": { "APEX_API_KEY": "apx_your_key_here", "APEX_BASE_URL": "https://app.drip-apex.com" } } } } ``` ## Cursor Add this to `.cursor/mcp.json` or Cursor's global MCP config: ```json { "mcpServers": { "drip-ab-testing": { "command": "npx", "args": ["-y", "@drip-apex/mcp-server"], "env": { "APEX_API_KEY": "apx_your_key_here", "APEX_BASE_URL": "https://app.drip-apex.com" } } } } ``` ## Verify Restart the MCP client, then ask it to list Apex experiments. A valid setup can call `list_experiments`. If the server exits immediately with "No API key found", set `APEX_API_KEY` for that MCP client or run: ```bash apex login ``` ## Troubleshooting - `No API key found`: set `APEX_API_KEY` in the MCP config or create `~/.drip/credentials.json` with `apex login`. - `HTTP 401`: the key is missing, expired, revoked, or not in the `apx_` key family. - `HTTP 403`: the key exists but does not have permission for the requested API endpoint. - Non-production testing: set `APEX_BASE_URL` to the target Apex base URL and keep production credentials out of that config. ## Related pages - [MCP tool reference](/docs/mcp/tool-reference) - [Install and authenticate the Apex CLI](/docs/cli/install-auth) - [API keys and public API v1](/docs/settings-account/api-keys) ## Remote connector Use `https://app.drip-apex.com/api/mcp` to connect without installing a local MCP server. - **Claude web:** Open Settings, Connectors, add a custom connector, and enter the endpoint URL. Choose Connect and sign in to Apex. ### ChatGPT directory plugin Apex is preparing its listing and is not available in the directory yet. Once it is published, find Apex in the ChatGPT plugin directory, choose Connect, sign in to Apex, and review the consent page before approving. Select Apex in your chat and ask it to list your experiments. Until publication, use a custom connector. ### ChatGPT custom connector Enable Developer mode in ChatGPT Settings under Security and login, open ChatGPT Plugins, select the plus button, and name the connector Apex. Choose a public MCP connection with `https://app.drip-apex.com/api/mcp` and OAuth authentication. Create the connection, inspect the discovered tools, and sign in to Apex. Account and workspace policies determine whether custom plugins are available. See [OpenAI's connection instructions](https://developers.openai.com/plugins/deploy/connect-chatgpt). ### Codex Codex connects to the same MCP endpoint directly. It does not need the ChatGPT directory listing. Add the connector and authenticate through OAuth: ```bash codex mcp add apex --url https://app.drip-apex.com/api/mcp codex mcp login apex ``` Sign in to Apex, approve the selected shop and permissions, then restart your Codex session and inspect its MCP tools. Ask it to list Apex experiments. These steps work before and after directory publication. See [OpenAI's MCP configuration instructions](https://developers.openai.com/codex/mcp). The Apex approval page names the connector, lists its requested scopes, and lets you choose an organization and a shop from your memberships. Review those permissions before approving. The connector receives a scoped Apex user key valid for 30 days; it never receives your Apex password. Reconnect after expiry, as refresh tokens are not issued yet. Revoke access in **Settings → Account → CLI keys**. Revocation takes effect through the same key validation used by the API. Requests keep the approved organization and shop context; manually minted user keys without a saved shop can send `X-Apex-Shop`. Read-only connectors should request `shops:read`; connectors that edit experiments also need `shops:write`. Your current shop role still limits what a connector can do. ### Start without a shop You can approve the remote connector for your organization before you create a shop. Choose “No shop yet, I will create one from the connector”, grant `shops:write`, then ask for `create_shop` with your shop name, domain and platform. An unbound connector accepts either `shops:read` or `shops:write` to connect and list tools. Creating the shop requires `shops:write`; reading shop data requires `shops:read`. Grant both for the full setup flow. The connector binds to that shop once. Ask for `get_install_instructions`, install Apex on the storefront, then use `get_install_status` to check incoming data. Complete human QA and launch in Apex. `create_shop` reuses an unarchived shop with the same normalized domain in your organization. If the shop is created but connector binding fails, the response includes its shop id. Retry `create_shop` with the same domain to bind it. For Shopify shops, installation instructions use the snippet until Apex enables the public app link. Apex operators set `SHOPIFY_PUBLIC_INSTALL_CONFIRMED=1` in Vercel after confirming public distribution in the Shopify Partner Dashboard. The variable defaults off; while off, `shopify_oauth_url` is omitted and the instructions say “The Shopify app link follows once Apex enables it.” ## Usage telemetry Starting with MCP server 0.5.0, local tool calls send best-effort usage records to Apex. Hosted connector calls are recorded by the remote server. Records include the tool name, duration, outcome, API call count, first error status/code/request ID and redacted route, MCP server package version, argument names, and authenticated account/shop context. Argument values, variation file contents, and API keys are never included in usage records. Set `APEX_TELEMETRY=0` in the local MCP server's environment to disable usage telemetry. Local beacons are also disabled under `NODE_ENV=test`, have an 800 ms timeout, and cannot change tool results. The environment opt-out applies to the local server; hosted connector calls retain their server-side usage records and audit entries. Operators can use `npm run cli-usage -- --client apex-mcp` for local calls or `--client apex-mcp-remote` for hosted calls. Hosted usage retains that fixed client label and the server package version. Add `--assistant` to compare remote calls by the OAuth key name (`MCP - `), grouped across users and keys. On initialize messages, a separate audit entry captures the connected client's name and version, each bounded to 40 characters. The hosted server is stateless; a later tool request does not inherit that initialization metadata. Tools declare read-only, destructive, idempotent and open-world hints so MCP clients can group approvals. Trigger/file replacement, integration recipes, builds, pause and complete retain their destructive hints. Strict reads and whole-value trigger/file replacement are idempotent; supply `expected_version` when replacing a file. These hints do not change API permissions. Removing a connector in ChatGPT or running `codex mcp logout apex` removes the client's local connection. Revoke its key in Apex **Settings → Account → CLI keys** to prevent further API access. For listing operators, the deployment environment variable `OPENAI_APPS_CHALLENGE_TOKEN` supplies the exact plain-text response at `https://app.drip-apex.com/.well-known/openai-apps-challenge`. It is uncached and returns 404 when unset. Configure its value through Bitwarden and the deployment environment; never add a value to the repository. ## Apex MCP tool reference The MCP server exposes Apex build, analysis, backlog, configuration, landing page, and preview tools over stdio and the hosted connector. Each tool calls the Apex public API with the configured API key and returns text content to the MCP client. Failed API calls return an error text block and mark the tool result as an error. Earlier parameter names (`id_or_slug`, `experiment_id_or_slug`, `id`) are still accepted. Experiment launch is dashboard-only. The MCP server does not expose a `start_experiment` tool, so approval and readiness gates remain visible during launch. ## Experiment tools | Tool | Purpose | Inputs | Output | |---|---|---|---| | `list_experiments` | List experiments, optionally filtered by status. | `status`: optional `draft`, `running`, `paused`, or `completed`; `limit`: optional number from 1 to 100. | Markdown list of experiments with name, ID, slug, status, traffic allocation, store URL, creation date, and variations. Adds "More results available" when the API page has more rows. | | `get_experiment` | Fetch one experiment by UUID or slug. | `experiment_id`: experiment UUID or slug. | Markdown experiment summary with the same formatted fields as `list_experiments`. | | `get_results` | Read a compact results summary without filling the chat context with the raw results payload. | `experiment_id`: experiment UUID or slug. | Experiment, run window, primary goal, per-variation visitors, conversions, rate, lift, confidence or probability, and verdict. The output is capped at 4,000 characters and names unavailable metrics when analytics are degraded. | | `create_experiment` | Create a draft A/B test. | `name`: required string; `slug`: optional string; `description`: optional string; `traffic_allocation`: optional percentage from 0 to 100, defaulting to 100. | Markdown confirmation with the created experiment summary. | | `pause_experiment` | Pause a running experiment. | `experiment_id`: experiment UUID. | Text confirmation that the named experiment is now paused. | | `complete_experiment` | Complete or stop an experiment. | `experiment_id`: experiment UUID. | Text confirmation that the named experiment is now completed. | ## Variation build tools | Tool | Purpose | Inputs | Output | |---|---|---|---| | `list_variation_files` | List files in a variation workspace. | `experiment_id`: experiment UUID or slug; `variation_id`: variation UUID, public ID, slug, or a name unique within the experiment. | JSON file list with path, kind, contentType, sha256, sizeBytes, updatedAt and readonly. The canonical editable files are `src/index.js` and `src/index.scss`. | | `read_variation_file` | Read one workspace file. | `experiment_id`; `variation_id`; `path`: relative workspace path. | JSON file record with those metadata fields and the requested content. Absolute paths and paths containing empty, `.` or `..` segments are rejected. | | `write_variation_file` | Replace one workspace file. | `experiment_id`; `variation_id`; `path`; `content`: complete file content; optional `expected_version`: the file's `sha256` from `read_variation_file`. | JSON ok/changed result and file metadata without echoed content or revision IDs, or a conflict error when the file changed since it was read. Run `build_variation` before previewing the change. | | `build_variation` | Compile the variation workspace after edits. | `experiment_id`; `variation_id`. | Build status, or build error lines when compilation fails. | Write a checkout message with `write_variation_file` to `src/mutations.json` as `[{ "action": "checkoutBlock", "checkoutBlock": { "heading": "Free returns for 30 days" } }]`; each message arm's QA checkout and launch happen in the dashboard. ## Landing page tools These tools mirror `apex landing-pages` for pages built outside Apex. Files travel inline as `{ path, content, encoding }`, with `encoding` `utf8` (default) or `base64`. See [Build landing pages with your own tools](/docs/landing-pages/build-and-deploy). | Tool | Purpose | Inputs | Output | |---|---|---|---| | `create_landing_page` | Create a landing page to build outside Apex. | `name`: required; `slug`, `idea_key`: optional. | JSON page record. | | `pull_landing_page` | Read the page checkout. | `id`: landing page id; `paths`: optional list of at most 32 file paths to read. | JSON manifest, file list with sha256, and context; with `paths`, those files' contents. | | `plan_landing_page` | Show what an apply would change, without writing. | `id`; `manifest`: the `apex-landing-page.json` object; `files`: every page file. | JSON plan: added, changed and removed files, manifest and goal changes, deploy target. | | `apply_landing_page` | Apply files and manifest as a new current version. | `id`; `manifest`; `files`; optional `base_version_id` from `pull_landing_page`, so a concurrent change is refused. | JSON version number, version id, signed preview URL and goal changes. Only files the plan names are uploaded. | | `preview_landing_page` | Get a signed preview URL (24 hours). | `id`; optional `version` number. | JSON preview URL and expiry. | | `deploy_landing_page` | Deploy a version live. | `id`; optional `version_id` (default: current), `host` (a domain the shop holds; default: the manifest host, else `.apex-page.com`), `path`. | JSON live URL, host, path and version. | | `list_landing_page_versions` | List versions, newest first. | `id`. | JSON versions with current and live flags. | | `rollback_landing_page` | Deploy an older version to the page's current address. | `id`; `version_number` or `version_id`. | JSON live URL and version. | | `create_landing_page_experiment` | Create a draft test that splits the page between two versions. | `id`; `control` and `variant` version numbers; optional `name`, `primary_goal` (a manifest goal key). | JSON draft experiment. Start it in the dashboard. | | `list_landing_page_domains` | List the shop's landing-page domains. | Optional `host`. | JSON domains with mode, status and DNS target. | | `claim_landing_page_domain` | Claim a client hostname for landing pages. | `host`. | JSON domain with the CNAME record to add at the DNS provider. | | `register_landing_page_domain` (local stdio only) | Register a new domain on the Drip Cloudflare account. Billable and non-refundable. Unavailable through the hosted connector. | `name`; optional `years` (1 to 10); `confirm_price` `{ amount, currency }`. | Without `confirm_price`, an error carrying the price quote; with the quoted total, the registered domain. | | `check_landing_page_domain` | Check one hostname against Cloudflare and DNS now. | `host`. | JSON domain status. | | `remove_landing_page_domain` | Remove a landing-page domain from Apex. Refused while a published page serves it. | `host`. | JSON removed domain. A registered domain stays on the Drip account. | ## Idea tools | Tool | Purpose | Inputs | Output | |---|---|---|---| | `create_idea` | Create a backlog idea. | `title`: required; `description`, `page_url`, and `hypothesis`: optional strings. | Created idea ID and Apex backlog key. | | `list_ideas` | List backlog ideas, optionally filtered by status. | `status`: optional; `limit`: optional 1–100; `cursor`: optional continuation token. | JSON page of the full backlog records returned by `/api/v1/backlog` (including scores, visual references, custom fields, linked experiments and decisions), with the owner's email removed, plus has_more/next_cursor pagination and the returned filters when present. | | `get_idea` | Read one backlog idea. | `idea`: idea UUID or Apex backlog key. | The same full backlog record, with the owner's email removed. | ## Goal and brand tools | Tool | Purpose | Inputs | Output | |---|---|---|---| | `list_goals` | List conversion goals for the active shop behind the API key. | None. | Markdown bullet list with goal name, type, and ID, or "No goals found." | | `get_brand_context` | Read the shop brand context before writing customer-facing copy. | None. | Pretty-printed JSON containing the brand context returned by `/api/v1/brand-context` unchanged (the route already excludes internal notes): hasProfile, mode, status, summary, voice, guardrails, proofRequirements, clientNotes, clientAssetExcerpts, guidelinesText and promptBlock where available. | ## Preview tools | Tool | Purpose | Inputs | Output | |---|---|---|---| | `generate_preview_url` | Build a forced QA URL for a variation on a store where the Apex SDK is installed. | `experiment_id`: required UUID or slug; `store_url`: optional absolute storefront URL override; `variation_name`: optional exact variation name. | Markdown block with experiment name, selected variation, and a URL containing `drip_force`, `drip_experiment_id`, `drip_variation_id`, `drip_qa=1`, and `drip_debug=1`. | | `create_preview_session` | Create a proxy preview for a prospect or store without the Apex SDK. | `experiment_id`: experiment UUID or slug; `variation_id`: optional variation UUID, public ID, slug, or unique name; `target_url`: optional absolute storefront URL, defaults to the experiment's store URL; `ttl_hours`: optional integer from 1 to 168, default 168. | Proxy preview URL and expiry timestamp. | When `variation_name` is omitted, `generate_preview_url` selects the first non-control variation and falls back to the first variation. The experiment must have a `store_url` or the caller must pass `store_url`. ## Shop setup - `create_shop` creates a self-serve shop through Apex provisioning and binds the connector to it once. Pass `name`, `domain`, `platform` and optionally `currency` and `origin`. Requires `shops:write`. It calls shop creation directly, so a key with only `shops:write` can create a shop. A domain-binding conflict reuses an authorized, unarchived owner with the same normalized domain in your key organization. If binding fails after creation, it returns the shop id and asks you to retry with the same domain. A bound connector returns its existing shop instead of creating another. Output includes `shop_id`, `domain`, `platform`, `message`, and provisioning `setup` when known; unknown domain or platform is `null`. Existing-shop recovery without setup returns `setup_status: "existing"` and: "Existing shop reused; setup steps were not re-run. Call get_install_status for the current install state." Concurrent calls share provisioning only for the same credential, normalized domain and platform; a binding race returns the shop that won the binding. - `get_install_instructions` returns plain installation steps and structured snippet fields. Shopify shops include the app connection link once public installation is confirmed; WooCommerce shops include plugin steps and a link to the installation token panel. Requires read access. A key without a bound shop can select its shop with `X-Apex-Shop`. - `assess_storefront` takes `url` and optional `shop_id`, assesses the public storefront, and returns platform, consent, bot protection, CSP, testing tools, an installation kit and a forwardable Markdown guide. Read-only; loads the page and probes `/cart.js`. - `get_install_status` checks the same installation telemetry as onboarding. It returns `shop_id`, `overall` (`verified`, `partial`, `missing` or `unavailable`), `last_event_time` (a timestamp or `null`), `connected`, `receivingData`, optional `domains`, and `next_action`. Unavailable telemetry is reported explicitly. The top-level status and last event fields use only `overall` and `last_event_time`. These tools are shared by stdio and the remote connector. Shop-bound tools ask you to call `create_shop` when the remote connector has no shop. QA approval and launch stay in the Apex app. Shopify instructions include `shopify_oauth_url` only when Apex operators have set `SHOPIFY_PUBLIC_INSTALL_CONFIRMED=1` in Vercel after confirming public distribution in the Shopify Partner Dashboard. This variable defaults off. Until enabled, Shopify shops receive snippet instructions and “The Shopify app link follows once Apex enables it.” ## Related pages - [Install and configure the Apex MCP server](/docs/mcp/install-configure) - [Experiment lifecycle commands](/docs/cli/experiments-lifecycle) - [Webhooks](/docs/developers/webhooks) ## Configuration and diagnostics `get_runtime_settings`, `set_data_layer_triggers` and `apply_integration_recipe` return the complete public API response unchanged, including every runtime-settings field, version fields, `precompile` compile/publication outcomes and warnings when present. `set_data_layer_triggers` accepts `data_layer_triggers` (up to 20 trigger objects) and optional `expected_version`: the ISO datetime `version` returned by `get_runtime_settings`, or `null` when no prior settings exist. It forwards that value as `expectedVersion` to the PATCH endpoint so concurrent edits return the API conflict error unchanged. Read the settings again before retrying a conflict. `discover_tracking_schema` returns aggregate event/field types, timing, storage and trigger diagnoses with observation-window, consent and coverage fields; it does not return raw events. `build_variation` returns the build result as the API reports it (status, build counts and byte sizes, attached goal IDs, metrics source, publication status, diagnostics and warnings when available); only stack and trace fields inside diagnostics are removed. ## Live page inspection These read-only tools load the selected shop's public page through Apex's Browserbase browser. They need no experiment, variation, or installed SDK. Use them before writing variation code; `generate_preview_url` and `create_preview_session` show an existing variation instead. | Tool | Inputs | Result | | --- | --- | --- | | `inspect_page` | `url`, optional `viewport` (`desktop` or `mobile`) | Page map and section candidates | | `inspect_dom` | `url`, `selector` or `text`, optional `viewport` | Matching elements and their computed styles | | `screenshot` | `url`, optional `viewport` and CSS `selector` | PNG or JPEG image and a short text summary | Desktop uses 1280 × 900 and mobile uses 390 × 844. URLs must match the shop's domain, origin, or an experiment's store URL. Off-host redirects are refused. User keys need `shops:read`, an authorized selected shop, and experiment-read permission; shop keys need `experiments:read`. Each tool call consumes one inspection from the shop's UTC-day allowance (default 10). `inspect_page` runs page map and sections in one browser session; `inspect_dom` runs DOM context and styles in one session. The 11th tool call is refused. Inspection shares the remote preview limit of two sessions per shop, with a 10-minute session TTL. The API returns a plain reason when the allowance is exhausted, inspection is disabled, or no usable browser result is available. Screenshots use `image/png` for selector captures and may use `image/jpeg` for full-page frame captures. They are limited to 3,500,000 characters including the data URL prefix. A failed browser attempt still consumes its inspection. ## Launch stays in the app Assistants can build, preview, and collect QA evidence. Approving QA and starting a test remain your clicks on the experiment page in Apex. The launch-handoff widget declares an OpenAI widget CSP with no fetch domains and image resource domains limited to the app origin and, on the remote server, the configured Supabase screenshot storage origin. ### `run_qa` Capture screenshots for an experiment and attach them to its QA run. Pass `experiment_id`, optional `devices` (`desktop`, `mobile`, default both), and optional `notes`. A completed call returns `status: "completed"`, the QA run ID, signed per-device screenshot URLs, evidence checksum, and changed readiness items. If capture outlasts the call, it returns `status: "pending"`, `screenshot_job_id`, progress, and a retry delay without attaching evidence or writing notes. Call `run_qa` again with the same `experiment_id` and `screenshot_job_id`; the resume call ignores `devices` and finishes that existing job instead of starting another capture. ### `request_launch` Read an experiment's launch checklist without changing it. Pass `experiment_id`. Returns the remaining human steps, readiness items, latest QA screenshots, preview URL, and a link to open Apex to approve QA and launch. ChatGPT can display the shared launch-handoff widget. ## `lookup_commerce_catalog` Search active Shopify products with `query`, or retrieve one exact product with `handle` or `product_id`. Product lookup returns live base-currency prices, costs, and price-test exclusions. The tool is read-only and accepts optional `cursor` and search-only `limit`. ## Data model ## Tenancy and access Workspace, shop, team, invitation, profile, API credential, audit, and privacy-request ownership boundaries. - `Organization` - `OrgMember` - `OrgInvitation` - `Shop` - `ShopMember` - `Invitation` - `UserProfile` - `ApiKey` - `AuditLog` - `PrivacyRequest` ## Experimentation Core test setup, targeting, metrics, variants, QA runs, screenshots, heatmaps, archives, behavior insight, and result readback. - `Experiment` - `Variation` - `Goal` - `ExperimentGoal` - `Segment` - `Signal` - `Page` - `ExperimentQaRun` - `ScreenshotJob` - `HeatmapScreenshot` - `ExperimentArchive` - `ExperimentBehaviorInsight` - `TestResult` ## Operator and workbench Persisted agent runs, chat messages, tool timelines, artifacts, code workspaces, revisions, preview sessions, and usage logging. - `OperatorRun` - `OperatorMessage` - `OperatorEvent` - `CodeArtifact` - `ExperimentWorkspace` - `ExperimentWorkspaceFile` - `ExperimentWorkspaceRevision` - `PreviewSession` - `AiUsageLog` ## Store intelligence Runtime controls, store checks, evidence, findings, brand profile, technical profile, learning runs, facts, and reference assets. - `ShopRuntimeSettings` - `ShopAuditRun` - `ShopAuditEvidence` - `ShopAuditFinding` - `ShopBrandProfile` - `ShopTechnicalProfile` - `ShopLearningRun` - `ShopStoreEvidence` - `ShopStoreFact` - `Asset` ## Advanced runtime Flags, typed payloads, personalized experiences, generated landing pages, published versions, QA runs, external harness sessions, and saved funnels. - `FeatureFlag` - `FeatureFlagVariant` - `Personalization` - `PersonalizationRule` - `PersonalizationExperience` - `LandingPage` - `LandingPageVersion` - `LandingPagePublish` - `LandingPageQaRun` - `HarnessSession` - `Funnel` ## Shopify and delivery Shopify OAuth/install state, product cost data, pixel event settings, webhook endpoints, and webhook delivery records. - `ShopifyInstallation` - `ShopifyVariantCost` - `ShopifyEventSetting` - `Webhook` - `WebhookDelivery` ## Enums The controlled vocabularies used by status filters, lifecycle actions, targeting types, workspace roles, and runtime delivery modes. - `ShopRole`: client-facing memberships use `owner`, `editor`, and `client_viewer` (shown as Owner, Editor, and Viewer). Stored `admin`, `viewer`, `analyst`, and `client_approver` values are legacy compatibility values and are not offered by client invite or role-change surfaces. - `ExperimentStatus` - `GoalType` - `PatternType` - `ShopRole` - `InvitationStatus` - `OrgRole` - `WorkspaceType` - `SegmentType` - `SegmentStatus` - `BacklogStatus` - `BacklogActionType` - `FeatureFlagStatus` - `FeatureFlagDeliveryType` - `PersonalizationStatus` - `PersonalizationDeliveryType` - `LandingPageStatus` - `LandingPagePublishMode` - `LandingPageType` - `ApiKeyType` - `PrivacyRequestType` - `PrivacyRequestStatus` - `AuditAction` ## Complete feature catalog ## Access and onboarding | Route | Title | Audience | Description | |---|---|---|---| | `/login` | Login | Both | Email/password authentication, Supabase session creation, password reset entry, and redirect handling for protected app routes. | | `/signup` | Signup | Both | New-account entry point. Users can create an Apex account before creating or joining a workspace. | | `/verify-email` | Verify email | Both | Email verification holding page after signup. It keeps unverified users out of protected workspace surfaces. | | `/auth/reset-password` | Reset password | Both | Password update flow for users with a valid Supabase reset token. | | `/orgs/new` | Create workspace | Admin | Creates a company or agency workspace. Agency workspaces unlock client management; company workspaces focus on one operating company. | | `/join` | Store setup entry | Both | Signed-in users without a store resume their usable client link or ask DRIP for one. | | `/join/[code]` | Client link | Both | Creates or signs into an account, confirms the store, collects its details, and completes the link's billing requirements. | | `/invitations/accept` | Store invitation acceptance | Both | Accepts store-level invitations with invalid, missing, accepted, declined, and expired states. | | `/org-invitations/accept` | Workspace invitation acceptance | Both | Accepts organization invitations and adds the user to the workspace with the invited role. | ## Daily operating surfaces | Route | Title | Audience | Description | |---|---|---|---| | `/` | Launchpad | Operator | Shop-level command center with readiness stage, blockers, next move, setup checks, active tests, draft tests, and recent learning proof. | | `/backlog` | Ideas | Operator | Prioritized backlog of concepts, approved ideas, linked drafts, source snapshots, comments, archive/restore state, and create-experiment action. | | `/backlog/capture` | Capture an idea | Operator | Captures an idea in one box, drafts its structure, and adds it to the backlog. | | `/pipeline/backlog?idea=` | Idea | Operator | Opens one idea in the backlog modal with its evidence, prioritization, comments, Research Hub snapshot, and linked experiment creation. | | `/experiments` | Experiments | Both | Experiment inventory with status filters, launch readiness, setup progress, draft blockers, linked backlog items, and create-test entry. | | `/experiments/new` | New experiment | Both | Creates a test with store URL, targeting, traffic allocation, schedule, goal selection, planned-test options, variants, and optional backlog linkage. | | `/experiments/[id]` | Experiment detail | Both | Full test workspace: setup checklist, goals, variants, status actions, launch blockers, analytics, QA actions, schedule, targeting, and developer metadata. | | `/experiments/[id]?tab=qa` | QA tab | Both | Forced preview URLs, variant verification, QA run state, screenshot evidence, and launch-readiness checks before traffic starts. | | `/experiments/[id]/variations/[varId]` | Variation editor | Both | The deepest builder surface: preview browser, Operator chat, selector tooling, mutation authoring, code mode, workspace files, build status, preview controls, QA evidence, artifacts, and save/apply behavior. | | `/experiments/[id]/checkout-block` (redirects to the normal test; checkout messages are authored on arms) | Retired checkout editor redirect | Developer | Redirects to the ordinary test detail page; current checkout messages are authored on test arms. | | `/results` | Results | Operator | Readback surface for individual outcomes, imported external results, overall impact, and learnings that shape the next experiment. | ## Foundations | Route | Title | Audience | Description | |---|---|---|---| | `/assets` | Assets | Both | Upload and organize reference files, images, PDFs, and prompt material. Asset usage is tracked so teams know where a file is consumed. | | `/goals` | Metrics | Both | Reusable click, pageview, custom, revenue, and Shopify-connected goals. Metrics can be primary or secondary inside tests. | | `/goals/new` | New metric | Both | Creates success events with selectors, text match, URL patterns, event names, revenue settings, count-once behavior, and capture rules. | | `/goals/new/visual` | Visual metric builder | Operator | Legacy visual builder redirect into the current metric creation path. | | `/audience` | Audiences | Both | Reusable visitor groups and page groups for experiments and personalization, including page, traffic, behavior, and customer-signal rules. | | `/audience/new` | New audience | Both | Rule-builder for URL, page, traffic source, device, behavior, Shopify/customer attributes, exact, wildcard, and regex targeting. | | `/signals` | Signals | Operator | Visitor and storefront facts that Apex captures and reuses for audiences, feature flags, personalization, and Operator context. | | `/store-profile` | Store profile | Operator | Brand memory and technical memory: profile facts, audit runs, evidence, storefront checks, findings, learning runs, and brand guideline presets. | | `/pages` | Pages | Both | Legacy page targeting redirect. Current targeting is handled by audiences and segment rules while runtime still supports URL patterns. | | `/pages/new` | New page | Both | Legacy new-page targeting redirect into the current audience and segment creation flow. | ## Analytics | Route | Title | Audience | Description | |---|---|---|---| | `/analytics` | Analytics overview | Operator | Top-level analytics tabs for overview, acquisition, behavior, funnels, traffic, visitors, realtime, ecommerce, quality, and experiment performance. | | `/analytics/audience` | Audience analytics | Operator | Audience composition, visitor segments, and time-windowed audience behavior. | | `/analytics/traffic` | Traffic sources | Operator | Traffic source, campaign, and acquisition quality breakdowns across 7, 30, and 90 day windows. | | `/analytics/visitors` | Visitor explorer | Operator | Visitor records, sessions, identifiers, and event trails for debugging individual journeys. | | `/analytics/realtime` | Realtime analytics | Both | Live event console for verifying install health, pageview flow, custom events, and QA traffic. | | `/analytics/funnels` | Funnels | Operator | Full-funnel analysis with breakdowns, detected pages, saved funnels, run endpoint, and conversion-path summaries. | | `/analytics/funnels/new` | Funnel builder | Operator | Builds funnels from detected URLs, custom steps, saved funnel definitions, and run-now analysis. | | `/analytics/flow` | Flow | Operator | Path-flow view for entry pages, exit pages, movement between URLs, and behavioral drop-off patterns. | ## Advanced product surfaces | Route | Title | Audience | Description | |---|---|---|---| | `/landing-pages` | Landing pages | Operator | Pages built in a workspace checkout and deployed with `apex landing-pages deploy`, listed with format, versions, publish state, and live address. | | `/landing-pages/new` | New landing page | Operator | Redirects to `/landing-pages` with a notice that pages are deployed with the CLI. | | `/landing-pages/[id]` | Landing page | Operator | Read view: versions with QA state and previews, publish state and live address, experiments that use the page, unpublish and archive. | | `/landing-pages/[id]/qa` | Landing page QA | Operator | Version-level preview links, QR codes, QA runs, approval state, and launch gating for landing pages. | | `/flags` | Feature flags | Developer | Runtime toggles with status, rollout percentage, source experiment, delivery type, targeting, typed values, and archive/pause/activate actions. | | `/flags/new` | New feature flag | Developer | Creates native Apex flags that SDK callers can evaluate and dashboards can toggle. | | `/flags/[id]` | Feature flag detail | Developer | Off/on variants, targeting, decision debugger, payload value, typed values, rollout control, source experiment, and lifecycle metadata. | | `/personalization` | Personalization | Both | Signal targeting, surfaces, priority, runtime status, rollout, and inventory of personalized experiences. | | `/personalization/new` | New personalization | Both | Creates signal-driven storefront experiences with matching rules and structured experience payloads. | | `/personalization/[id]` | Personalization detail | Both | Experience payloads, targeting, ordered rules, decision debugger, safe rollout, source, and lifecycle metadata. | ## Installation and runtime administration | Route | Title | Audience | Description | |---|---|---|---| | `/settings/installation` | Store connection | Admin | Shopify connection state, app embed instructions, manual snippet, install verification, snippet settings, and setup flow. | | `/settings/developers?tab=script` | Script and runtime | Developer | Manual script install (paste, open site, verify), the 13 groups of runtime controls with Save and publish, and the external pixel. Consent settings live in Integrations. | | `/settings/developers?tab=edge` | Edge routing | Developer | DNS proxy mode in five steps: live address, origin, DNS records, the DNS and SSL checks, and go-live. | | Legacy: `/installation/*` redirects to `/settings/installation/*` | Redirect | Admin/Developer | Old `/installation` routes now redirect to the `/settings/installation` tree to keep existing links working. | | `/settings/developers?tab=inspector` | Inspector | Developer | Snippet size, size over the wire, experiments in the payload, edge cache lifetime, and payload composition. | | Legacy: `/settings/installation/{manual,runtime,edge-routing,snippet-size}` | Redirect | Developer | 308 to the matching Developers tab. | | `/settings/test-rules` | Test rules | Admin | Shop-wide auto-pause defaults, sequential significance settings, fixed-horizon planned test options, minimum runtime, and per-test override model. | | `/settings/integrations` | Profit analysis | Admin | Shopify integration and reconnect flow for order, product, inventory, cost, reconciliation, and profit-analysis scopes. | ## Workspace, team, and support | Route | Title | Audience | Description | |---|---|---|---| | `/clients` | Clients | Admin | Agency-workspace client list. Switch into a client workspace before working on that store’s tests and profiles. | | `/settings/team` | Store team or agency team | Admin | Team members, store invitations, workspace members, roles, and invite management. Content changes by workspace type. | | `/settings/store` | Store and workspace settings | Admin | Workspace name, slug, type, ownership, and organization-level settings. | | `/org/members` | Workspace members | Admin | Organization-level membership list and role management for workspace admins and owners. | | `/settings/account` | Account | Both | User display name and image shown across Apex comments, ownership, and team surfaces. | | `/settings/developers` | Developers | Developer | Five tabs: keys and webhooks, script and runtime, edge routing, inspector, IDs and references. API keys (publishable, test secret, live secret) show their secret once. Webhooks are added and edited in a dialog with the events, the signing secret and the recent deliveries. | | Legacy: `/webhooks`, `/webhooks/new`, `/webhooks/[id]` | Redirect | Developer | 308 to the Keys and webhooks tab; a webhook id opens its edit dialog. | | `/experiments/[id]/variations/[varId]` logs overlay | Operator logs | Both | Review persisted Operator runs, chat transcripts, tool/event timelines, artifacts, statuses, filters, and failure states inside the variation editor. | | `/help` | Help | Both | In-app resources, workflow links, FAQs, setup guidance, API docs link, and practical troubleshooting entry points. | ## Internal and brand surfaces | Route | Title | Audience | Description | |---|---|---|---| | `/docs` | Documentation | Both | The complete Apex documentation system: quickstart, product shots, operating workflow, feature manuals, route catalog, API surface map, data model map, endpoint reference, and troubleshooting. | | `/brand` | Brand book | Both | Public Apex brand book surface and visual system reference. | | `/brand-guidelines` | Brand guidelines redirect | Both | Redirects to the current brand documentation surface. | ## Help, docs, brand surfaces, and generated references > **Simple terms** > > This is the documentation and support layer for users, operators, and developers. The support layer for understanding Apex, finding help, sharing brand context, and inspecting machine-readable API documentation. ## Routes - `/docs` - `/help` - `/brand` - `/api/v1/openapi.json` ## Implementation behavior - The docs route uses the supplied Apex Docs HTML design as the visual source of truth and renders the product manual from generated MDX content. - The complete catalog documents every app route surface, while the API surface map documents route-handler families. - The generated endpoint reference is built from the local OpenAPI spec so public API docs stay aligned with code. - Help and brand routes provide user support and brand-facing collateral. ## How to use it - Use Feature manuals for how each product area works. - Use Complete feature catalog when you need the route-level map. - Use API surface and Endpoint reference when integrating or debugging automation. - Use Help for support flows that are not specific to implementation details. ## When not to use it - Do not treat docs as a replacement for live QA or current production monitoring. - Do not rely on the OpenAPI reference for internal dashboard APIs; it only covers public v1. - Do not let screenshots drift after major UI changes. ## Implementation source - Docs live in `content/docs` and render through the generated manifest/MDX pipeline; `src/app/docs/layout.tsx` provides the shell. - OpenAPI source lives in `src/lib/api/openapi-spec` and is served at `/api/v1/openapi.json`. - Product shots live under `public/docs/product-shots`. ## Data and API - Docs page data lives in `content/docs` and the docs manifest/parser pipeline. - OpenAPI source lives in `src/lib/api/openapi-spec` and is served at `/api/v1/openapi.json`. - Product shots are stored in `public/docs/product-shots`. ## Failure modes - Docs drift when new routes or APIs are added without updating the manual and catalogs. - OpenAPI only covers public v1 endpoints; internal APIs need the API surface map. - Product shots need to be refreshed after major UI changes. ## Anti-flicker modes Anti-flicker hides content while the SDK resolves assignments and applies mutations. The default is scoped hiding: Apex hides only selectors referenced by active experiment mutations and `antiFlickerSelectors`. ## Modes ```ts import { init } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", antiFlickerMode: "scoped", antiFlickerTimeoutMs: 1200 }); ``` | Mode | Behavior | Use when | |---|---|---| | `scoped` | Hides mutated selectors only. | Most production installs. | | `full` | Hides the full page until reveal or timeout. | Legacy compatibility or QA forcing when scoped selectors are not known before fetch. | | `custom` | Injects `antiFlickerCustomCss`. | You need exact project-specific hiding CSS. | | `off` | Disables anti-flicker CSS. | Server-rendered variants, tests, or pages where flicker is less risky than hiding. | ## Custom CSS ```ts init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", antiFlickerMode: "custom", antiFlickerCustomCss: ` [data-apex-critical] { opacity: 0 !important; } `, antiFlickerTimeoutMs: 900 }); ``` The SDK removes anti-flicker styles after mutations apply, when the timeout expires, or when config fetch fails. ## Async config fetch When the SDK fetches config by `shopId`, it arms anti-flicker during fetch if no fresh cache is available and `antiFlickerMode` is not `"off"`. If fetched config arrives after the reveal budget, `lateConfigPolicy` decides whether to skip it or apply it. ```ts init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", antiFlickerMode: "scoped", antiFlickerTimeoutMs: 1200, lateConfigPolicy: "skip" }); ``` ## Deprecated aliases Use `antiFlickerMode` and `antiFlickerTimeoutMs`. Removed legacy names log a one-time warning and are ignored. ## Related pages - [InitConfig reference](/docs/sdk/init-config) - [Migration notes](/docs/sdk/migration-notes) ## Code-owned definitions and `.drip.config.ts` The SDK exports helpers for typed, validated code-owned experiment definitions. Use them when experiments live near application code or when feature flags need typed values. ## defineExperiment ```ts import { defineExperiment, init } from "@drip-apex/sdk"; const productBadge = defineExperiment({ slug: "product-page-trust-badge", name: "Product page trust badge", trafficAllocation: 0.5, variations: [ { id: "control", weight: 0.5, isControl: true, mutations: [] }, { id: "badge", weight: 0.5, mutations: [ { selector: "[data-add-to-cart]", action: "insertAfter", html: "

Secure checkout and fast support.

" } ] } ], goals: [ { id: "add-to-cart-click", type: "click", selector: "[data-add-to-cart]" } ] }); init({ experiments: [productBadge], endpoint: "https://events.drip-apex.com" }); ``` `defineExperiment` returns a frozen `Experiment` object with the slug as the experiment ID. It throws `ExperimentDefinitionError` when required fields, variation weights, control variation rules, or click goal selectors are invalid. ## defineExperiments ```ts import { defineExperiments } from "@drip-apex/sdk"; export const experiments = defineExperiments([ { slug: "homepage-proof-copy", name: "Homepage proof copy", variations: [ { id: "control", weight: 0.5, isControl: true }, { id: "proof", weight: 0.5, mutations: [] } ] }, { slug: "cart-empty-state", name: "Cart empty state", variations: [ { id: "control", weight: 0.5, isControl: true }, { id: "helpful-links", weight: 0.5, mutations: [] } ] } ]); ``` ## defineFlagExperiment ```ts import { defineFlagExperiment } from "@drip-apex/sdk"; export const progressUiFlag = defineFlagExperiment({ slug: "checkout-progress-ui", name: "Checkout progress UI", rolloutPercent: 25, offValue: false, onValue: true }); ``` `defineFlagExperiment` creates a two-variation experiment where `control` is off and `enabled` is on. `rolloutPercent` is clamped between 0 and 100. ## `.drip.config.ts` Pair code-owned definitions with the CLI when you want GitOps sync into Apex: ```ts import { defineExperiments, defineFlagExperiment } from "@drip-apex/sdk"; export default { experiments: [ ...defineExperiments([ { slug: "homepage-proof-copy", name: "Homepage proof copy", variations: [ { id: "control", weight: 0.5, isControl: true }, { id: "proof", weight: 0.5, mutations: [] } ] } ]), defineFlagExperiment({ slug: "checkout-progress-ui", name: "Checkout progress UI", rolloutPercent: 25 }) ] }; ``` Then push with the CLI: ```bash apex push --config ./.drip.config.ts --dry-run apex push --config ./.drip.config.ts ``` ## Related pages - [Push and GitOps with `.drip.config.ts`](/docs/cli/push-gitops) - [Feature flags in code](/docs/sdk/feature-flags) - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) ## Consent modes Consent mode lets Apex render variants while delaying persistent storage and tracking until consent is available. The SDK treats consent as required when `requireConsent` is `true`, `hasConsent` is provided, or `consentMode.enabled` is `true`. ## Custom consent Use `setConsent()` when your site owns consent state: ```ts import { init, setConsent } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", requireConsent: true, hasConsent: false }); window.addEventListener("privacy:accepted", () => { setConsent(true); }); ``` When consent is denied, the SDK clears Apex visitor, goal, signal, config-cache, session, QA, and identity context storage where possible. ## Provider modes ```ts import { init } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", consentMode: { enabled: true, provider: "onetrust", requiredGroup: "C0004" } }); ``` | Provider | How the SDK reads consent | Default group | |---|---|---| | `custom` | Reads `getter` from `window` when configured, otherwise waits for `setConsent()`. | None | | `onetrust` | Reads `getter` or `window.OnetrustActiveGroups`; listens for OneTrust update events. | Any non-empty value when `requiredGroup` is omitted | | `usercentrics` | Calls `UC_UI.getServicesBaseInfo()` and matches service name, ID, template ID, or data processor. | `Apex` | | `cookiebot` | Reads `Cookiebot.consent` category booleans. Aliases such as `analytics` map to `statistics`. | `statistics` | | `ccm19` | Reads `CCM.acceptedEmbeddings` and matches embedding name or ID. | `Apex` | | `shopify` | Reads only Shopify’s Customer Privacy API. The storefront script loads the API; `requiredGroup` selects `analytics`, `marketing`, `preferences`, or `sale_of_data`. | `analytics` | | `pandectes` | Calls the Pandectes consent purpose methods (for example `analyticsProcessingAllowed()`), falling back to Shopify's `customerPrivacy` API. `requiredGroup` selects the purpose: `analytics`, `marketing`, `preferences`, or `sale_of_data`. | `analytics` | | `cookiescript` | Calls `CookieScript.instance.currentState()` and reads the selected category. Analytics aliases map to `performance`, marketing aliases map to `targeting`, and preference aliases map to `functionality`. Custom category names remain available. | `analytics` | | `borlabs` | Reads `BorlabsCookie.Consents` or calls `BorlabsCookie.checkCookieConsent()` when available. | `statistics` | ## ConsentModeSettings | Field | Type | Default | When to use | |---|---|---|---| | `enabled` | `boolean` | `false` | Turn provider-backed consent detection on. | | `provider` | `"custom"`, `"shopify"`, `"onetrust"`, `"usercentrics"`, `"cookiebot"`, `"ccm19"`, `"pandectes"`, or `"cookiescript"` | Not set | Select the consent integration. | | `getter` | `string` | Provider default | Dot-path on `window` for custom or OneTrust consent values. | | `requiredGroup` | `string` | Provider default | Required group, category, service, or embedding identifier. | ## Change listeners When `consentMode.enabled` is true, the SDK listens for common consent manager browser events, including OneTrust, Cookiebot, Usercentrics, CCM19, Shopify Customer Privacy (`visitorConsentCollected`), Pandectes (`PandectesInitialized` and Shopify's `visitorConsentCollected`), and CookieScript events. On change, the SDK rechecks consent, storage mode, project prerequisites, and tracking permission. The `custom`, `shopify`, `pandectes`, `cookiescript`, and `borlabs` providers also poll once per second for up to ten minutes; the event-driven providers (OneTrust, Cookiebot, Usercentrics, CCM19) never poll, even with a `getter`. Polling stops after consent grants tracking. If the consent manager has not exposed its state when the script runs, the SDK starts in memory mode and keeps any stored identity. The SDK adopts that identity after consent is granted. When `init` later resolves with consent granted the SDK writes the consent cookie so the visitor identity survives the first navigation after consent (examples: Cookiebot, OneTrust, Usercentrics, CCM19). Only an explicit denial or Do Not Track clears the stored identity. ## Counting while consent is pending While consent is unknown, the SDK still assigns the visitor and renders the variant, and it keeps the assignment exposure and the first performance measurement in memory. Nothing is sent. When consent is granted later on the same page, the assignment exposure is sent, and it is also sent when the visitor leaves or backgrounds the page shortly after accepting. The performance measurement and the regular analytics stage (pageview, goals) still wait for the page-wide apply gate; a visitor who leaves before that gate opens is dropped from those events in every arm. Every arm follows the same rules, so traffic splits stay balanced on stores where most visitors accept cookies after the page has loaded. An explicit denial discards the buffered records. If the visitor grants consent afterwards on the same page, the SDK records a fresh exposure at that time. ## Related pages - [InitConfig reference](/docs/sdk/init-config) - [Runtime API](/docs/sdk/runtime-api) - [Runtime controls, SDK, anti-flicker, and consent](/docs/settings-account/runtime-sdk-controls) ## Feature flags in code Apex feature flags are experiment-backed. A flag is on when the visitor is assigned to a non-control variation. Typed values come from the assigned variation `value`. ## Define a flag ```ts import { defineFlagExperiment, init } from "@drip-apex/sdk"; const checkoutCopyFlag = defineFlagExperiment({ slug: "checkout-cta-copy", name: "Checkout CTA copy", rolloutPercent: 50, offValue: "Continue to checkout", onValue: "Secure my order" }); init({ experiments: [checkoutCopyFlag], endpoint: "https://events.drip-apex.com" }); ``` `defineFlagExperiment` creates two variations: | Variation | Weight | Value | Meaning | |---|---|---|---| | `control` | `1 - rolloutPercent / 100` | `offValue` or `false` | Flag off. | | `enabled` | `rolloutPercent / 100` | `onValue` or `true` | Flag on. | ## Evaluate from initialized SDK state ```ts import { evaluateFlag, evaluateFlagValue } from "@drip-apex/sdk"; const enabled = evaluateFlag("checkout-progress-ui"); const ctaCopy = evaluateFlagValue("checkout-cta-copy", "Continue to checkout"); ``` Pass context when evaluating outside the current page URL or current SDK attributes: ```ts const enabled = evaluateFlag("checkout-progress-ui", { url: "https://example-store.test/products/demo-product", userId: "user-example-123", attributes: { customer_tier: "vip" } }); ``` ## Evaluate a standalone experiment list The `@drip-apex/sdk/flags` subpath exposes pure helpers that accept an experiment array: ```ts import { defineFlagExperiment, evaluateFlagValue } from "@drip-apex/sdk/flags"; const flag = defineFlagExperiment({ slug: "search-layout", name: "Search layout", rolloutPercent: 10, onValue: "compact", offValue: "default" }); const layout = evaluateFlagValue("search-layout", [flag], { userId: "user-example-123", url: "https://example-store.test/search" }, "default"); ``` ## React flag hooks ```tsx import { useFeatureFlag, useFeatureFlagValue } from "@drip-apex/sdk/react"; function SearchResults() { const compact = useFeatureFlag("search-layout"); const columns = useFeatureFlagValue("search-columns", 3); return ; } ``` ## Related pages - [Code-owned definitions and `.drip.config.ts`](/docs/sdk/config-definitions) - [React bindings](/docs/sdk/react-bindings) - [Runtime API](/docs/sdk/runtime-api) ## SSR framework coexistence Apex is designed to coexist with storefronts that server-render HTML and then hydrate it in the browser. The SDK mutes its own DOM writes so its observers do not react to Apex work. If React, Vue, or another renderer later restores server HTML over an active treatment, Apex re-applies the affected text, HTML, attribute, or style on the next animation frame. Inserted and removed nodes are not re-applied by this sentinel. Existing SPA re-evaluation remains the fallback for those structural changes. ## Choose when mutations apply The shop runtime setting `spa.applyTiming` has two modes: | Mode | Behavior | Trade-off | |---|---|---| | `immediate` | Apex applies during the initial render path. If hydration overwrites a treatment, Apex corrects it within about one animation frame. | Best chance of a clean first paint; a framework overwrite can still produce a very short correction. | | `after_hydration` | Apex waits for hydration before applying DOM mutations. The anti-flicker hide remains active until apply or its existing 1200ms safety cap. | Avoids the framework/Apex double render, but the affected element stays hidden while hydration settles. The hydration gate fails open after the configured timeout. | `spa.hydrationTimeoutMs` controls the hydration fail-open window from 500 to 10000ms and defaults to 3000ms. Configure both values under **Installation → Storefront runtime controls → SPA and dynamic page support**. ```ts spa: { applyTiming: "after_hydration", hydrationTimeoutMs: 3000 } ``` ## Explicit lifecycle hooks Use the browser API after the hosted SDK has loaded: ```js window.drip("hold"); // increments the mutation hold counter window.drip("release"); // decrements it; apply resumes when it reaches zero window.drip("hydrated"); // declares framework hydration complete // Method equivalents window.drip.hold(); window.drip.release(); window.drip.hydrated(); ``` Holds are nestable. A forgotten hold fails open after `spa.hydrationTimeoutMs`, records a performance marker, and logs a warning. `hold` and `release` also work when `applyTiming` is `immediate`. If an integration cannot call the API directly, dispatch the equivalent hydration event: ```js document.dispatchEvent(new CustomEvent("drip:hydrated")); ``` ### Nuxt 3 Fire the signal from a client plugin after the app mounts: ```ts // plugins/apex-hydrated.client.ts export default defineNuxtPlugin((nuxtApp) => { nuxtApp.hook("app:mounted", () => window.drip?.hydrated?.()); }); ``` ### Next.js App Router Mount a tiny client component once in the root layout: ```tsx "use client"; import { useEffect } from "react"; export function ApexHydrated() { useEffect(() => window.drip?.hydrated?.(), []); return null; } ``` ## Re-initialize widgets after HTML replacement An `html` treatment replaces a subtree. When Apex has to re-apply that HTML after a framework overwrite, event listeners inside the subtree no longer exist. Apex does not automatically re-run variation JavaScript. Instead it dispatches `drip:mutation-reapplied` on `document`: ```ts document.addEventListener("drip:mutation-reapplied", (event) => { const detail = event.detail; if (detail.selector !== "[data-apex-swiper]" || detail.action !== "html") return; document.querySelectorAll("[data-apex-swiper]").forEach((element) => { if (element.swiper) element.swiper.destroy(true, true); new Swiper(element, { slidesPerView: 1, loop: true }); }); }); ``` Event detail contains `experimentId`, `variationId`, `selector`, and `action`. Keep the handler idempotent because a framework can overwrite the same subtree more than once. ## Consent is separate Hydration hooks only control mutation timing. Use the existing `setConsent(true|false)` API or a configured consent provider for storage and tracking permission. Do not use a consent manager to block the Apex script itself; see [Consent integration](/docs/data-privacy/consent-integration). ## Related pages - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) - [Consent modes](/docs/sdk/consent-modes) - [Store connection and installation](/docs/settings-account/installation) ## `init()` and InitConfig reference Call `init(config)` once per page load or app shell. The SDK accepts inline experiments through `experiments`, or it fetches running experiments when both `shopId` and `endpoint` are present. ```ts import { init } from "@drip-apex/sdk"; const assignments = init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", antiFlickerMode: "scoped", trackingStart: "idle" }); ``` `init()` returns the assignments available immediately. When the SDK fetches config asynchronously, the first return value can be an empty array; read later assignments with `getAssignments()` or listen for the `drip_initialized` browser event. ## Core fields > Note: Data layer triggers were extended in the release `6f8189c` to support curated named-event properties, mapping transforms, and ordered fallback paths. See the SDK "triggers" runtime-settings and trigger-simulation fixtures in the repository for behavior and examples. | Field | Type | Default | When to use | |---|---|---|---| | `experiments` | `Experiment[]` | Not set | Provide inline, code-owned experiments instead of fetching by `shopId`. An empty array is valid and still initializes analytics. | | `signals` | `RuntimeSignalDefinition[]` | `[]` | Define custom runtime signals that targeting can capture from URLs, DOM, globals, data layer events, or SDK events. | | `shopId` | `string` | Not set | Fetch published config from the Worker and attribute events to an Apex shop. Required for hosted config fetches. | | `shopCurrency` | `string` (ISO 4217) | Not set | The shop's reporting currency. When a revenue call omits a currency, the SDK stamps this value on the event and marks it `currency_source: "shop_default"`. Worker-delivered `/config` and the Apex snippet carry it, so merchants do not normally set it by hand. When no shop currency is configured, such an event is sent without a currency field. | | `storeOrigin` | `string` | Not set | Set the canonical storefront origin used to resolve host-agnostic redirect source rules. Worker-hosted config normally supplies this automatically. | | `storeOrigins` | `string[]` | `[]` | List registered public storefront origins that may resolve host-agnostic redirect source rules. When present, this takes priority over `storeOrigin`. | | `endpoint` | `string` | Not set | Base Worker URL for `/config`, `/events`, handover minting, and order identity calls. A trailing `/events` is normalized away. | | `auditEndpoint` | `string` | Apex production audit endpoint | Override signed storefront audit snapshots when using the audit build. Most installs leave this unset. | | `editorBundleUrl` | `string` | Derived from the installed SDK script | Versioned URL for the lazy on-site editor bundle. Apex-hosted snippets provide this automatically; custom delivery setups can point it at their matching `editor.js` asset. | | `optionalCoreAssets` | `Partial>` | Supplied by Apex delivery | Immutable optional replay, surveys, preview-switcher, forms and price-display core assets (preview is supplied to QA configs only; forms only when the shop has tracked forms; price only with price experiments). URLs must use HTTPS on the SDK script or configured delivery origin, with a versioned `/sdk//` path and SHA-384 SRI. | | `replayRecorderUrl` | `string` | Derived from the installed SDK script | Versioned URL for the lazy session-replay recorder artifact. Apex-hosted snippets provide this automatically; custom delivery setups can point it at their matching recorder asset. | | `ingestShopId` | `string` | `shopId` | Send a different shop identifier in event ingestion headers when Worker-delivered config needs a separate ingest identity. | | `ingestSignature` | `string` | Not set | Sign event ingestion, handover minting, and order identity requests when the Worker provides a signature. Do not hardcode secrets in page code. | | `proxyEventDelivery` | `{ enabled: boolean; path?: string }` | Disabled | Use same-origin Shopify App Proxy event delivery with automatic direct fallback. Apex sets this per shop; merchants do not configure it. | | `assignmentCookieSync` | `boolean` | `true` | When Shopify's analytics bus is unavailable (bus-less headless storefronts), write the assignment sync into a first-party cookie on the registrable domain so the checkout pixel can attribute purchases. Set `false` to disable the fallback; controlled per shop via runtime settings. | | `assetFallback` | `boolean` | `false` | Marks every event from this boot with `asset_fallback` when the loader recovered the SDK through the store's own `/apps/apex` path. Set by the Apex loader; merchants do not configure it. | | `attributes` | `Record` | `{}` | Add targeting attributes for this page or visitor, merged with runtime signal-derived attributes. | | `userId` | `string` | Generated visitor ID | Override the visitor/bucketing identity, for example with a stable logged-in user identifier. | | `url` | `string` | `location.href` | Evaluate targeting against a supplied URL instead of the current browser location. Useful for tests and controlled rendering. | | `autoTrack` | `boolean` | `true` | Set to `false` when you do not want automatic assignment exposure events. Manual event calls still work when tracking is available. | | `storefrontVitalsSampleRate` | `number` from `0` to `1` | `0.1` | Set the per-page sampling rate for storefront Core Web Vitals and delivery-timing measurements. | | `configCache` | `boolean` | `true` | Set to `false` to disable localStorage config cache for fetched shop config. Cache is also disabled when storage is blocked. | | `configCacheTtlMs` | `number` | `300000` | Change the freshness window for cached fetched config. Values must be positive numbers. | | `cartAttributeWrite` | `"sdk"` or `"external"` | `"sdk"` | Select the cart-attribute write owner. With `"external"`, the SDK does not write cart attributes itself; the storefront owns the write. | | `cartAttributeScope` | `"consented"` or `"all"` | `"consented"` | This is an admin-managed setting. `"consented"` (default) writes cart attributes only after tracking consent is granted. `"all"` behaves the same once consent is granted; while tracking is unavailable it additionally maintains a stamp that contains the experiment-arm label only, with no identifiers, in both SDK-owned and external integration modes. | | `lateConfigPolicy` | `"skip"` or `"apply"` | `"skip"` | Choose whether fetched config that arrives after the anti-flicker reveal deadline should still apply. QA and screenshot forcing uses `"apply"`. | | `onExperimentViewed` | `(assignment: Assignment) => void` | Not set | Run a callback for each viewed assignment after the SDK has assigned and started exposure tracking. | | `holdout_v1_enabled` | `boolean` | `false` | Enable the global program holdout gate. When enabled with a valid percentage, holdout visitors receive no experiment assignments. | | `holdout_pct` | `number` | Not set | Set the global holdout percentage above `0` and up to `10`. The gate remains inactive unless `holdout_v1_enabled` is `true`. | | `holdout_config_updated_at` | `string` or `null` | Not set | Carry the server-issued holdout configuration timestamp so cached assignments and later events remain bound to the cohort-producing configuration epoch. | | `geo` | `{ country?: string }` | Not set | Request-scoped edge geography supplied by the Apex Worker (from Cloudflare geo) so `geo.country` targeting can match. Set by the runtime on Worker-delivered config; you do not normally set this by hand. | Session replay needs no Content Security Policy allowance beyond the SDK script and `connect-src` for the events host. Blob workers are optional. When the browser blocks the compression worker, the recorder compresses replay blocks on the main thread. ## Runtime modes | Field | Type | Default | When to use | |---|---|---|---| | `executionMode` | `"auto"`, `"edge"`, or `"client"` | `"auto"` | Keep `"auto"` for edge-first delivery with client fallback. Use `"edge"` only when an edge runtime has already initialized the page. | | `platformHint` | `"auto"`, `"shopify"`, `"spa"`, or `"shopware"` | Auto-detected | Hint platform-specific router and surface behavior when auto-detection cannot identify the storefront. | | `routerMode` | `"auto"`, `"history"`, `"navigation"`, or `"none"` | `"auto"` | Top-level SPA route observation mode. `spa.routerMode` takes priority when present. | | `activationMode` | `"auto"`, `"observer"`, or `"manual"` | `"observer"` | Top-level mutation activation mode. `spa.activationMode` takes priority when present. | | `antiFlickerMode` | `"scoped"`, `"full"`, `"custom"`, or `"off"` | `"scoped"` | Control how Apex hides content before variants apply. Use `custom` with `antiFlickerCustomCss`. | | `antiFlickerTimeoutMs` | `number` | `1200` | Auto-reveal timeout for anti-flicker protection. Non-positive or invalid values fall back to `1200`. | | `antiFlickerCustomCss` | `string` | Not set | CSS injected only when `antiFlickerMode` is `"custom"`. | | `trackingStart` | `"idle"` or `"immediate"` | `"idle"` | Decide whether analytics setup waits for idle time or starts immediately after initialization. | | `trackingIdleTimeoutMs` | `number` | `2000` | Fallback timeout for `trackingStart: "idle"`. Non-positive or invalid values fall back to `2000`. | | `customJsPolicy` | `"strict"` or `"hybrid"` | `"hybrid"` | Control variation and project custom JavaScript execution. Use `"strict"` to block custom JS in production-sensitive installs. | | `customCodeErrorBudget` | `number` | `5` | Exceptions allowed per experiment or variation custom-code item in one document, including its registered observer, timer, animation-frame and event callbacks. Integer from `1` to `100`; invalid values use `5`. At the limit, owned callbacks stop, resources are cleaned up, and the item stays stopped across SPA navigation. Shop runtime settings can override this option. | | `requireConsent` | `boolean` | `false` | Gate tracking and persistent storage until consent is granted. Variants can still render under strict consent mode. | | `hasConsent` | `boolean` | Consent cookie or provider state | Seed the initial consent state when using `requireConsent` or custom consent controls. | ## Settings objects | Field | Type | Default | When to use | |---|---|---|---| | `profile` | `ConfigProfile` | Not set | Server-provided profile metadata. The SDK currently reads `profile.platform` after `platformHint`. | | `projectPrerequisites` | `ProjectPrerequisites` | No prerequisite rules | Gate variation display and tracking with project-level `showVariationsRule` and `startTrackingRule` expressions. | | `exclusionGroups` | `ExclusionGroupConfig[]` | `[]` | Server-provided mutual-exclusion groups. When multiple member experiments match, Apex keeps one weighted winner per group and suppresses the others. | | `consentMode` | `ConsentModeSettings` | Disabled | Configure provider-backed consent detection for custom, OneTrust, Usercentrics, Cookiebot, CCM19, Pandectes, or CookieScript. | | `debug` | `DebugSettings` | Disabled | Enable restricted SDK console diagnostics by query parameter, user ID, always-on mode, or active server state. | | `spa` | `SpaSettings` | Auto router, observer activation | Configure SPA router observation, DOM re-evaluation, debounce timing, iframe allowance, and reinit-only mode. | | `pageTrigger` | `PageTriggerSettings` | `{ defaultMode: "direct" }` | Switch initial activation and route behavior to URL change, DOM change, or manual page triggers. | | `externalTracking` | `ExternalTrackingSettings` | Disabled | Preserve bucketing across external checkout, booking, or lead-flow domains by rewriting configured links and forms. | | `dataLayerTriggers` | `DataLayerTriggerSettings[]` | `[]` | Listen to data layer events, map event fields into runtime signals, re-evaluate assignments, and optionally track pageviews or revenue. | | `exposureDestinations` | `ExposureDestinationsSettings` | Disabled | Send assignment exposure metadata to `dataLayer`, Hotjar, or custom queue globals. | | `projectCode` | `ProjectCodeSettings` | Disabled | Apply advanced project-level CSS, helper JS, or startup JS before or after assignments. | | `environments` | `RuntimeEnvironmentSettings` | Production | Select an environment key for fetched config and config-cache isolation. | | `bucketingCleanup` | `BucketingCleanupSettings` | Enabled with `180` day retention | Clean stale Apex goal and config-cache storage. Set `enabled: false` to opt out. | | `dataProtection` | `DataProtectionSettings` | `localStorage`, no encoding, DNT off | Choose visitor storage mode, encoded storage values, Do Not Track behavior, and cookie domain. | | `semanticTracking` | `SemanticTrackingSettings` | Disabled | Capture sampled semantic visibility or click events for configured selectors. | | `aggregateMode` | `AggregateModeSettings` | Disabled | Server-managed flags for the order attribution rail. Read-only from the served config. Cart-attribute behavior is controlled by `cartAttributeScope`, not by this setting. | | `commerce` | `CommerceSettings` | Not set | Server-provided, presentation-only commerce settings. `commerce.price` carries the versioned price-plane projection rendered on product pages. `commerce.shipping` carries assigned display-only free-shipping thresholds, treatment types, and progress/reached copy rendered from the live Shopify cart subtotal. | | `runtimeControl` | `RuntimeControlSettings` | Enabled runtime | Server-authoritative kill-switch state. When `disabled` is true, runtime diagnostics show the disabled state. | | `deliveryState` | `"no_visual_tests"`, `"non_critical_visual"`, `"critical_visual"`, `"edge_managed"`, or `string` | Not set | Worker/snippet metadata included in SDK performance telemetry. | | `scriptCacheSource` | `"edge"`, `"kv"`, `"origin"`, `"precompile"`, `"precompiled"`, or `string` | Not set | Worker/cache source label included in SDK performance telemetry. | | `installSurface` | `"direct_script"`, `"shopify_app_embed"`, or `string` | Not set | Install surface label included in SDK performance telemetry. | | `snippetUrl` | `string` | Not set | Worker-provided snippet URL used for the per-visitor debug-build handoff in restricted debug modes. | | `integrationContext` | `ServedIntegrationContext` | Not set | Worker-provided configuration and selected SDK provenance. Omitted for legacy or unavailable metadata; do not generate it in storefront code. | | `deliveredSdk` | `ServedIntegrationContext["sdk"]` | Not set | Worker-provided identity of the executing SDK artifact (variant, digest, policy revision), bound when the loader delivered it. Retained for the running artifact and never adopted from a config refresh; do not generate it in storefront code. | | `sdkVersion` | `string` | Not set | Worker-provided immutable SDK artifact version for release-attributed performance telemetry. | | `sdkDeliveryMode` | `"global"`, `"pinned"`, `"canary"`, or `string` | Not set | Worker-provided SDK policy mode for release-attributed performance telemetry. | | `sdkDeliverySource` | `"embedded"`, `"external"`, or `string` | Not set | Worker-provided artifact source for distinguishing embedded and immutable external delivery. | | `sdkPolicyRevision` | `string` | Not set | Worker-provided global SDK delivery policy revision used to compile the snippet. | | `runtimeProfile` | `"full"`, `"empty_project"`, `"nonvisual_assignment"`, or `string` | `"full"` | Server-authoritative runtime profile included in performance telemetry. Unknown profiles retain full behavior. | | `delivery` | `SnippetDeliveryManifest` | Not set | Worker-compiled delivery manifest for precompiled critical mutation delivery. Most hand-written installs omit it. | | `runtimeFeatures` | Runtime feature profile object | Not set | Worker-selected feature profile for live, heatmap, debug, QA, harness, or audit builds. | | `perfFlags` | `PerfFeatureFlags` | All current perf flags enabled | Runtime kill-switch flags for staged performance features. | | `qa_warnings` | Array of forced-experiment warning objects | Not set | Server-classified QA diagnostics for forced experiments missing from the served config (archived, runtime-disabled, or not found). QA mode only; the QA assistant renders them. | | `qa_assets` | `"signed"`, `"unsigned"` or `"expired"` | Not set | Server-reported asset capability of a forced QA preview. When it is not `"signed"`, the QA assistant shows that test images cannot load on this link and points to the signed link on the Apex QA tab. Forced QA previews only. | | `qaPreviewSignedUntil` | Number (epoch seconds) | Not set | Server verdict that this QA link is a verified signed Apex link, valid until the given time. The QA build then keeps that tab's QA session in session storage even when analytics consent was declined; no visitor ID or analytics. Signed QA previews only; never sent on the public lane. | | `qaCheckoutToken` | `string` | Not set | Server-signed checkout QA marker for a verified signed Apex QA link. The QA build writes it to the Shopify cart so the Apex checkout block can show the forced test's checkout message in a real checkout. Signed QA previews only; never sent on the public lane. | | `goals` | Array of workspace goal objects | Not set | Every non-archived workspace goal. QA mode only; the QA assistant renders them. | | `setupCapture` | `SetupCaptureSettings` | Not set | Worker-provided time-gated diagnostic capture settings (enabled, expiresAt, urlPatterns, layerNames, maxEventsPerSession). When active and unexpired, the SDK lazily loads the capture module and emits shape-token diagnostics; raw merchant or customer values never leave the browser. | | `setupCaptureUrl` | `string` | Not set | Worker-provided URL of the lazily loaded setup-capture module artifact. Only fetched while `setupCapture` is active. | | `surveys` | Array of survey objects | Not set | Worker-provided published on-page surveys, in display priority order. The SDK shows at most one per page view after its delay, sampling, frequency cap, schedule and targeting checks, and only once tracking consent is granted. Do not generate it in storefront code. | | `surveyRendererUrl` | `string` | Not set | Worker-provided URL of the lazily loaded survey card (`drip.survey.js`). Only fetched when a survey is about to show. | | `forms` | Array of `{ key, selector, complete, autodetect? }` (at most 20) | Not set | Worker-provided tracked forms for this shop. `complete` is `{ goal }`, `{ page }` or `{ call: true }` (use `window.drip.forms.complete(key)`). Loaded only together with `formsAnalytics.enabled` and tracking consent; never records typed values. Do not generate it in storefront code. | | `formsAnalytics` | `{ enabled: boolean }` | `{ enabled: false }` | Worker-provided per-shop form and checkout analytics switch. Form tracking stays off unless Apex enables it for the shop; it never records typed values. Do not generate it in storefront code. | ### Data layer trigger mappings Each `dataLayerTriggers[].when` entry is a payload condition: `{ path, op, value? }` with `op` one of `eq`, `neq`, `gt`, `lt`, `exists`, or `not_exists`. A trigger may carry one to five entries; all of them must match before the trigger maps signals, queues a replay, or tracks an event. `path` is a payload-only dot path with the same property and numeric-index grammar as mapping paths. `eq`, `neq`, `gt`, and `lt` require `value` (`gt`/`lt` a number; the SDK converts the payload value to the configured type before comparison); `exists` and `not_exists` forbid it. `exists` matches values other than `undefined` and `null`. `not_exists` matches `undefined` or `null`. A missing or unconvertible path fails `eq`, `neq`, `gt`, and `lt`. Thus, `neq` does not fire a trigger when the configured path is missing. Number conditions use the same conversion as signal mappings. They accept comma decimals such as `19,90` and grouped comma decimals such as `1.234,56`. Each `dataLayerTriggers[].mappings[].from` value reads a path from the matching data layer entry. The mapping writes that value to the runtime signal named by `to`. Use an array of one to five paths for ordered fallback. Apex uses the first path with a raw value. It selects that path before value transforms or type conversion. Named events and pageviews can define a top-level `properties` array. When this array exists, the event contains only `source`, `dataLayerEvent`, and resolved mapped properties. Without this array, Apex keeps the full data layer payload. Mappings cannot replace `source` or `dataLayerEvent`. Revenue and named-event property mappings use payload paths by default. Prefix a property source with `signal:` to read a stored or runtime signal instead: Whenever a data layer revenue trigger fires, Apex records the purchase at shop scope and attributes it to every running test assigned to the visitor. ```ts dataLayerTriggers: [{ eventName: "purchase", trackEventName: "purchase_complete", properties: [ { from: ["customer.type", "signal:customer_type"], to: "customerType", valueType: "string", transform: { trim: true, lowercase: true, map: { vip: "wholesale" }, default: "retail" } } ], trackRevenue: { enabled: true, revenueFrom: ["ecommerce.value", "value"], properties: [ { from: ["order.margin", "signal:estimated_margin"], to: "margin", valueType: "number", transform: { scale: 0.01 } } ] } }] ``` A mapping transform runs in this order: resolve the raw path or signal, trim and lowercase strings, apply `map`, convert to `valueType`, apply numeric `scale`, then use a converted `default` if no value remains. A map accepts at most 20 exact-match entries. Map keys and values accept at most 100 characters. A scale must be finite and non-zero. Number conversion accepts standard JavaScript numeric strings. It also accepts comma decimals such as `19,90` and grouped comma decimals such as `1.234,56`. Ambiguous forms such as `1,2,3` stay unresolved. A plain path such as `customer_type` reads only the matching data layer entry. Missing sources do not add a property unless the mapping defines a valid default. ### Custom dimensions from dataLayer The SDK can now capture configured custom dimensions from matching `dataLayer` entries and stamp their current values onto analytics events (dimensions are sent as event-level metadata). Configure these captures via your runtime `signals` and `dataLayerTriggers` mappings; the SDK resolves the mapped value at event time and includes it on any tracked event that matches the trigger. See the release `ddf5a2c` for the implementing change and ticket `#4184`. The `signal:` prefix does not apply to `mappings[]`, `revenueFrom`, `orderIdFrom`, `currencyFrom`, `costFrom`, or `profitFrom`. Each element in these fallback arrays must be a payload path. Mapping transforms never apply to the five revenue anchors. ## Redirect experiments Redirect experiments run before visual mutations. Apex persists the assigned arm, queues an `experiment_viewed` exposure using navigation-safe delivery, preserves the source query string and hash (destination parameters win), and then replaces the current location. A loop guard keeps a visitor on the page when it already matches the destination. Mutations, variation CSS, and variation JavaScript may coexist with a redirect destination, but they are source-page artifacts only. If the assignment is preserved on the destination for attribution, the SDK does not apply those artifacts there: the destination document itself is the treatment. For destinations on another hostname, Apex adds the temporary `_apex_assignment` query parameter with the fresh visitor and assignment handover. This name is reserved for Apex redirect tests. Do not add `_apex_assignment` to merchant-authored destination URLs; when it is already present, Apex preserves the merchant value and does not add its assignment carry. Carry is consumed only on the configured destination entry path, not on other pages of the same host. The destination **must install the same shop's Apex snippet** so the SDK can import that identity and assignment, keep conversion attribution sticky, and avoid rebucketing or recording a second exposure. Consent mode deliberately omits this cross-origin carry until consent is granted: the destination mints its own visitor identity. Activity across the two origins before consent is granted cannot be linked. If the destination config is temporarily unavailable, Apex retains an eligible carry in session storage only until a later config can validate it or the handover TTL expires. ## Related pages - [Consent modes](/docs/sdk/consent-modes) - [SPA and router modes](/docs/sdk/spa-router) - [Anti-flicker modes](/docs/sdk/anti-flicker) - [Migration notes for deprecated aliases](/docs/sdk/migration-notes) ## Install and quickstart the Apex SDK The Apex SDK runs experiments, applies DOM mutations, evaluates flags, and sends storefront events to the Apex Worker. Use the hosted script for most storefront installs. Use the npm package when your app needs typed code-owned experiments, React bindings, or direct module imports. ## Prerequisites - An Apex shop identifier from the installation screen. - The public Worker endpoint, usually `https://events.drip-apex.com`. - For npm usage, Node.js 18 or newer in your storefront build pipeline (the SDK package itself targets external consumers; developing inside the Apex monorepo requires Node.js 22.22+). ## Hosted script Add the hosted SDK directly in `` and initialize it with a placeholder shop ID. Do not load it through a tag manager or place the script behind a consent gate: late loading prevents the anti-flicker guard from protecting the first paint. Consent should gate Apex storage and tracking through `setConsent`, not script delivery. ```html ``` Use this path for Shopify theme snippets, Shopware plugins, WooCommerce templates, static storefronts, or headless sites where Apex should own assignment, anti-flicker, pageview tracking, and mutation delivery. ### Headless / strict CSP On a storefront whose `script-src` uses `'strict-dynamic'` and a per-request nonce, host allowlisting alone does not authorize Apex. Put the same server-generated nonce on the shop-specific Apex loader tag (never hard-code or reuse a nonce). The `/s/.js` snippet self-initializes — do **not** add the `window.Drip.init` queue shim here; that shim is only for the generic hosted `drip.js` flow, and including it makes the loader detect an existing runtime and skip loading the real SDK: ```html ``` If an existing integration must keep the generic hosted `drip.js` flow, put that same nonce on all three parser-inserted script blocks — the queue bootstrap, loader, and `Drip.init` call. Under `strict-dynamic`, noncing the external loader does not authorize its inline siblings: ```html ``` The trusted Apex snippet propagates that nonce when its delivery policy loads a secondary SDK bundle. Experiment JavaScript files created by the trusted SDK also inherit trust through CSP `strict-dynamic`; npm-imported SDK code is part of your already-authorized application bundle and has no separate loader tag. If Google Tag Manager is already loaded by a nonced tag, it can install Apex as a Custom HTML tag and the trusted GTM execution chain authorizes the Apex script. This trades away early anti-flicker protection, so use the direct synchronous nonced tag when first-paint mutation delivery matters. The browser must also be allowed to send events. Add exactly this origin to the site's existing `connect-src` directive: ```text https://events.drip-apex.com ``` Without that client-side CSP edit, assignment may run but every Apex event POST is blocked; Apex cannot work around it. The standard anti-flicker style works unchanged when the existing policy allows inline styles (for example `style-src 'unsafe-inline'`). Edge-injection's `x-drip-csp-nonce` path applies only when Apex proxies and rewrites the HTML response, not when Oxygen or another headless origin serves the page. See the [headless strict-CSP runbook](/docs/getting-started/headless-react#headless--strict-csp-checklist) for the complete install-path checklist. ### Flicker contract The default scoped anti-flicker guard hides only elements that an active experiment can change. Apex reveals them as soon as mutations resolve. The guard has a 1200ms safety cap, so a failed config request or missing selector cannot leave the storefront hidden indefinitely. Hydrating Nuxt and Next.js storefronts can keep immediate application with next-frame correction, or opt into applying after hydration. See [SSR framework coexistence](/docs/sdk/framework-coexistence) for the timing modes and handshake hooks. ## Preview and staging environments A storefront host that is not listed under any environment receives the default environment, which is production. Preview deployments and local builds then count as live traffic. Either add their hosts to a staging environment in Settings > Developers > Environments (wildcards such as `*.vercel.app` and `localhost` are accepted), or add `?env=` to the shop-specific snippet URL in non-production builds, for example `https://events.drip-apex.com/s/.js?env=staging`. The explicit key wins over the host mapping. ## npm package Install the SDK when you want module imports: ```bash npm install @drip-apex/sdk ``` Initialize with fetched experiments: ```ts import { init, track, trackGoal } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", trackingStart: "idle" }); track("add_to_cart", { product_id: "gid://shopify/Product/example", value: 49, currency: "EUR" }); trackGoal("product-page-cta-click", { source: "custom-button" }); ``` Initialize with local experiments: ```ts import { defineExperiment, init } from "@drip-apex/sdk"; const heroCopy = defineExperiment({ slug: "homepage-hero-copy", name: "Homepage hero copy", variations: [ { id: "control", weight: 0.5, isControl: true }, { id: "proof-copy", weight: 0.5, mutations: [ { selector: "[data-hero-title]", action: "text", text: "Trusted by growing teams" } ] } ] }); init({ experiments: [heroCopy], endpoint: "https://events.drip-apex.com" }); ``` ## Track your first conversion ```ts import { trackRevenue } from "@drip-apex/sdk"; trackRevenue(89.99, { orderId: "order-example-1001", currency: "EUR" }); ``` `trackRevenue` sends a `goal` event with the reserved `__revenue__` goal ID for each active assignment. ## Related pages - [InitConfig reference](/docs/sdk/init-config) - [Runtime API](/docs/sdk/runtime-api) - [SSR framework coexistence](/docs/sdk/framework-coexistence) - [Consent integration](/docs/data-privacy/consent-integration) - [React bindings](/docs/sdk/react-bindings) - [Code-owned definitions and `.drip.config.ts`](/docs/sdk/config-definitions) ## Migration notes The legacy init aliases below are no longer supported. A stale key logs a one-time warning and is ignored, so migrate it to the current field. ## Removed aliases | Removed field | Current field | Required migration | |---|---|---| | `deferTracking` | `trackingStart` | Replace `true` with `"idle"` or `false` with `"immediate"`. | | `antiFlicker` | `antiFlickerMode` | Replace `true` with `"full"` or `false` with `"off"`. | | `antiFlickerTimeout` | `antiFlickerTimeoutMs` | Rename the timeout field. | ## Before ```ts import { init } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", deferTracking: true, antiFlicker: true, antiFlickerTimeout: 1500 }); ``` ## After ```ts import { init } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", trackingStart: "idle", antiFlickerMode: "full", antiFlickerTimeoutMs: 1500 }); ``` For most production installs, prefer `antiFlickerMode: "scoped"` over the legacy full-page hide behavior. ## Config fetch naming Use `endpoint`, not `workerUrl`, in SDK examples. The SDK normalizes the base endpoint and appends the correct path for `/config`, `/events`, `/v1/handover/mint`, and order identity calls. ## Related pages - [InitConfig reference](/docs/sdk/init-config) - [Anti-flicker modes](/docs/sdk/anti-flicker) - [Install and quickstart the Apex SDK](/docs/sdk/install-quickstart) ## React bindings Import React bindings from `@drip-apex/sdk/react`. The provider dynamically imports the browser SDK so React apps can avoid server-side DOM access during module evaluation. ## DripProvider ```tsx import { DripProvider } from "@drip-apex/sdk/react"; export function StorefrontApp({ children }: { children: React.ReactNode }) { return ( { console.log("Apex assignment", assignment.experimentId, assignment.variationId); }} > {children} ); } ``` `DripProvider` accepts all `InitConfig` fields except it redefines `onExperimentViewed` as a React prop and requires `children`. ## Assignment hooks ```tsx import { useAssignments, useExperiment, useLoading, useVariation } from "@drip-apex/sdk/react"; export function ProductHero() { const loading = useLoading(); const allAssignments = useAssignments(); const heroAssignment = useExperiment("product-hero-layout"); const heroVariation = useVariation("product-hero-layout"); if (loading) return null; return (
0 ? "yes" : "no"}>

{heroVariation?.id === "proof-led" ? "Built for confident purchase decisions" : "A better way to buy"}

{heroAssignment ? Variant: {heroAssignment.variationId} : null}
); } ``` | Hook | Returns | Use | |---|---|---| | `useExperiment(experimentId)` | Assignment or null | Read the assignment for an experiment ID or slug. | | `useVariation(experimentId)` | Variation summary or null | Render based on assigned variation ID and control state. | | `useAssignments()` | `Assignment[]` | Inspect all active assignments. | | `useLoading()` | `boolean` | Hide assignment-dependent UI until provider initialization completes. | ## Feature flag hooks ```tsx import { useFeatureFlag, useFeatureFlagValue } from "@drip-apex/sdk/react"; export function CheckoutButton() { const progressUiEnabled = useFeatureFlag("checkout-progress-ui"); const copy = useFeatureFlagValue("checkout-cta-copy", "Continue to checkout"); return ( ); } ``` | Hook | Returns | Use | |---|---|---| | `useFeatureFlag(flagId)` | `boolean` | Returns true for non-control flag assignments. | | `useFeatureFlagValue(flagId, fallback)` | Assigned value or fallback | Returns the assigned variation value or fallback. | ## Tracking hooks ```tsx import { useTrack, useTrackGoal, useTrackRevenue } from "@drip-apex/sdk/react"; export function AddToCartButton() { const track = useTrack(); const trackGoal = useTrackGoal(); const trackRevenue = useTrackRevenue(); return ( ); } ``` | Hook | Returns | Use | |---|---|---| | `useTrackGoal()` | `(goalId, data?) => void` | Track a configured Apex goal. | | `useTrack()` | `(type, data?) => void` | Track a custom event. | | `useTrackRevenue()` | `(revenue, data?) => void` | Track revenue through the SDK. | ## Cart-metafield hook `useGetCartMetafields()` returns the provider's cart-metafield callback. The callback resolves the same advisory mirror as the browser `getCartMetafields()` API. ```tsx import { useGetCartMetafields } from "@drip-apex/sdk/react"; export function useApexCartMetafieldWriter() { const getCartMetafields = useGetCartMetafields(); return () => getCartMetafields({ namespace: "apex" }); } ``` See the [headless React setup](/docs/getting-started/headless-react#optional-cart-metafield-mirror-for-bi) for the Shopify write and stale-key cleanup steps. ## Provider boundary Every hook throws if used outside ``. Keep the provider in your app shell so route-level components share one SDK initialization. ## Related pages - [SPA and router modes](/docs/sdk/spa-router) - [Runtime API](/docs/sdk/runtime-api) - [Headless React setup](/docs/getting-started/headless-react) ## Runtime API Import runtime functions from `@drip-apex/sdk`, or call the browser globals after the hosted script has loaded. The globals are compatibility surfaces, but they do not all have the same shape. ## Browser global surfaces | Surface | Shape and function names | |---|---| | `window.Drip` | The hosted IIFE's module namespace. It exposes the named SDK exports, including `init`, `track`, `trackStep`, `trackGoal`, `trackRevenue`, `setConsent`, `flush`, `getAssignments`, `getExperiments`, `getRuntimeTools`, and the flag and diagnostics functions documented below. | | `window.drip` | The callable queue-compatible runtime API. It exposes `init`, `track`, `step` and its additive alias `trackStep`, goal/revenue/consent/lifecycle methods, `flush`, assignment/cart/experiment helpers, `getTools` and its additive alias `getRuntimeTools`, `get`, flag evaluation, signals, diagnostics, and `push`. | | `window.Apex` | The same callable runtime API object as `window.drip`, with the same function names. | | `window.apexTools` | The runtime-tools object itself, not the callable API. It exposes custom-JS and page-trigger helpers such as `waitForElement`, `elementInView`, and `domChanged`; do not call `getRuntimeTools` on this object. | The tables below use the npm/`window.Drip` export names. On `window.drip` and `window.Apex`, `trackStep`/`step` and `getRuntimeTools`/`getTools` are equivalent aliases. ## Initialization and state | Function | Signature | Use | |---|---|---| | `init` | `(config: InitConfig) => Assignment[]` | Initialize the SDK with inline experiments or fetched config. | | `reInit` | `(forceReapply?: boolean) => Assignment[]` | Re-evaluate assignments manually, optionally forcing mutations to reapply. | | `recheckPrerequisites` | `() => void` | Re-run project prerequisite gates and start or stop analytics if consent or state changed. | | `getAssignments` | `() => Assignment[]` | Read current active assignments. | | `getExperiments` | `() => Experiment[]` | Read loaded experiment configs. | | `getRuntimeTools` | `() => ApexRuntimeTools` | Get the runtime tools object used by custom JS and page triggers. | | `getDiagnostics` | `() => SdkDiagnostics` | Read public diagnostics; debug-only fields appear only when debug mode is enabled. | | `getCartAttributes` | `() => CartAttributeEntry[]` | Build the authoritative cart-attribute attribution stamp for a headless Shopify cart. | | `getCartMetafields` | `(options?: { namespace?: string }) => Array<{ key: string; type: string; value: string }>` | Build an optional BI mirror for `cartMetafieldsSet`. `key` is composite (`namespace.attributeKey`); namespaces accept 2–255 ASCII letters, digits, `_`, or `-`, and invalid values fall back to `apex`. See the [headless integration guide](/docs/getting-started/headless-react#optional-cart-metafield-mirror-for-bi). | | `getCartMetafieldStaleKeys` | `(options?: { namespace?: string }) => string[]` | Return every mirrorable composite key missing from the current `getCartMetafields()` output. Pass these keys to `cartMetafieldDelete` to clear stale optional mirror values. Use the same namespace option for both helpers. | ## Tracking | Function | Signature | Use | |---|---|---| | `track` | `(type: string, data?: Record, experimentId?: string, variationId?: string) => void` | Send a custom event and trigger matching custom goals. Defaults to the first active assignment when IDs are omitted. | | `trackStep` | `(name: string, data?: Record) => void` | Send a virtual funnel step as `funnel_step`; dedupes once per session unless `data.oncePerSession` is `false`. | | `trackGoal` | `(goalId: string, data?: Record) => void` | Trigger a configured Apex goal by ID for current assignments. | | `trackRevenue` | `(revenue: number, data?: RevenueEventData) => void` | Record each purchase at shop scope and attribute it to every running test assigned to the visitor. | | `flush` | `() => Promise` | Start analytics if ready and flush queued events. Resolves immediately when no tracker exists. | ```ts import { flush, track, trackGoal, trackRevenue, trackStep } from "@drip-apex/sdk"; track("add_to_cart", { product_id: "example-product", value: 49 }); trackStep("postcode_submitted"); trackGoal("checkout-started"); trackRevenue(119, { orderId: "order-example-1002", currency: "EUR" }); await flush(); ``` ## Consent | Function | Signature | Use | |---|---|---| | `setConsent` | `(granted: boolean) => void` | Persist consent state, clear storage when denied, and recheck assignment/tracking prerequisites. | ## Feature flags | Function | Signature | Use | |---|---|---| | `evaluateFlag` | `(flagId: string, context?) => boolean` | Return true when the visitor is assigned to a non-control variation. | | `evaluateFlagValue` | `evaluateFlagValue(flagId, fallback?, context?)` | Return the assigned variation value, or the fallback when unavailable. | ```ts import { evaluateFlag, evaluateFlagValue } from "@drip-apex/sdk"; const enabled = evaluateFlag("checkout-progress-ui"); const ctaCopy = evaluateFlagValue("checkout-cta-copy", "Continue to checkout"); ``` ## Diagnostics ```ts import { getDiagnostics } from "@drip-apex/sdk"; const diagnostics = getDiagnostics(); console.log({ initialized: diagnostics.initialized, assignments: diagnostics.assignments, configSource: diagnostics.configSource, runtime: diagnostics.runtime }); ``` `getDiagnostics()` is safe for support tooling because it redacts consent and tracking-block details unless SDK debug mode is enabled. All builds expose `customEventsHeld` and `tracking`. The latter reports whether the tracker is started, whether calls are blocked, the reason (`consent_required`, `dnt`, `screenshot_mode`, or `not_started`), how many events are waiting behind an activation gate, and whether sent events are QA-tagged. This lets QA distinguish a console call from an event that reached the wire. ## Related pages - [Feature flags in code](/docs/sdk/feature-flags) - [Consent modes](/docs/sdk/consent-modes) - [InitConfig reference](/docs/sdk/init-config) ## Decision timing Read `window.drip.getDiagnostics().load.timing` (or the imported `getDiagnostics().load.timing`) in any build, including a forced preview. The same values are page-readable on `window.__dripPerf` without enabling debug or analytics. | Diagnostics field | `window.__dripPerf` field | Meaning | | --- | --- | --- | | `navigationStartAt` | `navigation_start_at` | Navigation origin, epoch milliseconds from `performance.timeOrigin`, with a `Date.now() - performance.now()` fallback. | | `configFetchedAt` | `config_fetched_at` | Config fetch resolved, epoch milliseconds. Absent when no successful fetch occurred. | | `decisionAt` | `decision_at` | Assignment ready, the existing `assignment_ready_at` mark, in epoch milliseconds. | | `firstMutationAt` | `first_mutation_at` | First DOM mutation, epoch milliseconds, when one occurred. | | `revealAt` | `reveal_at` | Anti-flicker reveal, epoch milliseconds, when recorded. | | `decisionLatencyMs` | `decision_latency_ms` | `decisionAt - navigationStartAt`, milliseconds since navigation. | | `forcedExperimentIds` | `forced_experiment_ids` | Experiment IDs in the active force map. Empty for natural loads. | The initial page's `decisionAt`, `decisionLatencyMs`, and `load.assignmentReadyAt` remain unchanged on SPA route changes. `timing.lastRouteChange` is `null` until a route evaluation completes, then contains `{ startedAt, decisionAt, decisionLatencyMs }`. Both timestamps are epoch milliseconds; its latency is `decisionAt - startedAt` for that route evaluation, independent of how long the page has been open. The matching `window.__dripPerf` fields are `last_route_change_started_at`, `last_route_change_decision_at`, and `last_route_change_decision_latency_ms` (all `null` before the first route evaluation). A `route_change` `sdk_perf` event uses this route-local latency; an `init` event uses navigation-relative latency. `window.__dripPerf.qa_session_id` identifies the QA session (also available as `getDiagnostics().qaSessionId`). Unreached timestamps are absent, not zero. Timing diagnostics do not change consent or telemetry gates. The `sdk_perf` event includes `decision_latency_ms` and `forced_experiment_ids` when telemetry is allowed. ```js const timing = window.drip.getDiagnostics().load.timing; console.table(timing); console.log(window.__dripPerf.decision_latency_ms); ``` The runtime remains unchanged. Natural runtime decides within tens of milliseconds of SDK start when config ships inline with the snippet (measured p50 12 ms / p95 54 ms on a large storefront). QA previews, screenshot-mode previews and cookie-forced sessions take the QA config lane and fetch fresh config over the network (measured p50 1.3 s / p95 7 s); they are not representative of launch behaviour. URL-forced sessions without a `qa` parameter keep the inline or cached config, so these measurements do not describe them. No config preload or anti-flicker hold is planned. ## SPA and router modes The SDK observes route and DOM changes so assignments stay correct on headless, single-page, and app-router storefronts. Keep the defaults unless you have a specific framework conflict. ## Router resolution The SDK resolves platform and router mode in this order: | Setting | Behavior | |---|---| | `platformHint` | Explicit platform wins unless it is `"auto"`. | | `profile.platform` | Server-delivered platform profile is used when no explicit platform hint is set. | | Auto detection | Detects Shopify runtime, Shopware markers, SPA root markers, or the Navigation API. | | `spa.routerMode` | Overrides top-level `routerMode`. | | `routerMode: "auto"` | Uses the Navigation API when available, otherwise history observation. Shopify can resolve to `"none"` unless DOM change mode is enabled. | ## Common SPA setup ```ts import { init } from "@drip-apex/sdk"; init({ shopId: "demo-shop", endpoint: "https://events.drip-apex.com", platformHint: "spa", spa: { routerMode: "auto", activationMode: "observer", domChangeTrigger: true, debounceMs: 250 } }); ``` Use this for Next.js, Hydrogen, Remix, Nuxt, SvelteKit, Vue, or React apps where URL changes do not reload the page. ## SpaSettings | Field | Type | Default | When to use | |---|---|---|---| | `routerMode` | `"auto"`, `"history"`, `"navigation"`, or `"none"` | `"auto"` | Choose how route changes are detected. | | `activationMode` | `"auto"`, `"observer"`, or `"manual"` | `"observer"` | Choose mutation application behavior. `"manual"` applies immediately without wait observers. | | `reinitOnly` | `boolean` | `false` | Disable automatic route observation and rely on manual `reInit()` calls. | | `domChangeTrigger` | `boolean` | Auto-enabled for URL change, DOM change, and `platformHint: "spa"` | Re-evaluate when the DOM changes after route or component updates. | | `debounceMs` | `number` | `250` for DOM observation and post-reveal reassertion | Tune dynamic DOM re-evaluation frequency. | | `allowIframes` | `boolean` | `false` | Allow SDK initialization inside iframes. | ## Page trigger modes `pageTrigger.defaultMode` controls initial activation: | Mode | Behavior | |---|---| | `direct` | Default. Evaluate and apply on initialization. | | `url_change` | Use URL-change behavior and route observation. | | `dom_change` | Enable DOM-change activation. | | `manual` | Skip initial assignment application until manual trigger logic activates. | If `pageTrigger.defaultMode` is `"manual"` or `spa.reinitOnly` is `true`, the SDK does not apply initial assignments automatically. Call `reInit(true)` after your app has rendered the relevant DOM. ## Manual route recheck ```ts import { reInit, trackStep } from "@drip-apex/sdk"; router.events.on("routeChangeComplete", () => { reInit(true); trackStep("route_rendered", { oncePerSession: false }); }); ``` ## Related pages - [Runtime API](/docs/sdk/runtime-api) - [React bindings](/docs/sdk/react-bindings) - [Headless React setup](/docs/getting-started/headless-react) ## Vue and Nuxt bindings Import composables from `@drip-apex/sdk/vue`. Vue 3.5 or later is an optional peer dependency. The browser entry keeps Node signature verification out of client bundles. ## Install ```sh npm install @drip-apex/sdk vue ``` These bindings ship in `@drip-apex/sdk` 0.4.0 and later. Upgrade any 0.3.0 installation before importing the `./vue` export. ## Plugin ```ts import { createApp } from "vue"; import { DripPlugin } from "@drip-apex/sdk/vue"; import App from "./App.vue"; createApp(App).use(DripPlugin, { shopId: "demo-shop", endpoint: "https://events.drip-apex.com", platformHint: "spa", }).mount("#app"); ``` If you already install the hosted snippet, use `app.use(DripPlugin)` without config. It reads the live `window.drip` runtime and connects when a delayed snippet initializes. `provideDrip(config?, options?)` establishes the same context for descendants from a component's setup function. Initialization errors are available through `useDrip().error`. ## Assignment composables ```vue ``` | Composable | Returns | |---|---| | `useAssignments()` | `Ref` | | `useExperiment(id)` | `Ref` | | `useVariation(id)` | Ref containing `{ id, isControl }` or null | | `useLoading()` | `Ref` | Refs update after runtime re-evaluation, including route changes, consent changes, and `reInit`. Vue templates unwrap refs; use `.value` in JavaScript. ## Feature flag composables ```vue ``` `useFeatureFlag` returns a boolean ref. `useFeatureFlagValue` returns the assigned value or the fallback in a ref. Choose `T` to match the flag's configured value type; the generic does not validate remote values. ## Tracking composables ```ts import { useTrack, useTrackGoal, useTrackRevenue } from "@drip-apex/sdk/vue"; const track = useTrack(); const trackGoal = useTrackGoal(); const trackRevenue = useTrackRevenue(); function onPurchase(orderId: string) { track("checkout-complete"); trackGoal("purchase"); trackRevenue(49, { currency: "EUR", orderId }); } ``` Call these from client interactions after the runtime loads. They use the provider's runtime and its consent rules. ## SSR with Nuxt Use a universal plugin to pass Nuxt's request-scoped composables. Vue-only applications do not need a Nuxt dependency. ```ts // plugins/apex.ts import { DripPlugin } from "@drip-apex/sdk/vue"; export default defineNuxtPlugin((nuxtApp) => { const event = import.meta.server ? useRequestEvent() : undefined; nuxtApp.vueApp.use(DripPlugin, undefined, { serverAssignments: event?.context.apexAssignments, nuxt: { useRequestHeaders, useCookie, useState }, }); }); ``` ```vue ``` `useApexVariant` returns a ref containing `{ id, isControl }` or null. It renders the `useState("apex-assignments")` payload during hydration, then adopts live assignments. A visitor without a `drip_uid` cookie and without forwarded assignments receives no server assignment; the client buckets later. `readServerAssignments(headers, { drip_uid }, options)` verifies forwarded decisions when `options.ingestToken` is supplied. It never throws on untrusted input: a missing or invalid signature, a malformed or duplicated header, an oversized envelope, an unsupported version, or a missing token returns `{ assignments: [], source: "none", rejected }` with the reason (`missing_signature`, `invalid_signature`, `malformed`, `duplicate_header`, `oversized`, `unsupported_version`, `no_token`, or `server_only` when the browser bundle is asked to verify) and no unsigned fallback. Read `rejected` before treating an empty list as "no experiments". The signature is hex HMAC-SHA256 over the exact JSON header bytes. Without a header it uses the cookie and the shared SDK bucketing kernel over `options.experiments`. Pass only experiments already eligible for this request to cookie bucketing. This helper does not evaluate browser targeting, sticky assignment storage, exclusion groups, schedules, or commerce authorization. Prefer verified forwarded assignments for those configurations. Never expose the ingest token through public runtime config or the Nuxt payload. See the [Nitro verification example](/docs/getting-started/headless-nuxt#render-variants-on-the-server). ## Provider boundary Every composable requires `DripPlugin` or an ancestor calling `provideDrip`. Install the plugin once per Vue app, or once per Nuxt request. Component-scoped providers dispose on scope teardown; the plugin disposes when the app unmounts. Request decisions are never stored in module globals. ## Related pages - [Headless Nuxt setup](/docs/getting-started/headless-nuxt) - [React bindings](/docs/sdk/react-bindings) - [SPA and router modes](/docs/sdk/spa-router) - [Runtime API](/docs/sdk/runtime-api) ## API keys and public API v1 > **Simple terms** > > API keys let external servers automate Apex safely for a shop. Lets external systems create and manage experiments, variations, goals, screenshots, webhooks, privacy requests, and harness sessions. ## Routes - `/settings/developers` (the older `/api-keys` address forwards here) - `/api/v1/*` ## Implementation behavior - API keys are created per shop as publishable, test secret, or live secret keys and stored hashed. - Secret keys are shown once, then used as bearer tokens for `/api/v1` requests. - Public route handlers validate the key, resolve the shop, authorize the action, and return JSON responses. - The docs page renders the OpenAPI reference from the app's `openapiSpec` source. ## How to use it - Create a live secret key for production server-to-server automation. - Use test secret keys for staging and automation trials. - Never put secret keys in browser code; use publishable keys only where client exposure is intended. - Use the OpenAPI reference for request and response shapes. ## When not to use it - Do not put live secret keys in browser code, snippets, or public repositories. - Do not use a live key for staging automation. - Do not keep unused keys active after an integration is removed. ## Implementation source - UI lives under `src/app/(dashboard)/api-keys`. - Handlers live under `/api/api-keys` and `/api/v1/*`. - OpenAPI source lives in `src/lib/api/openapi-spec` and schemas live under `src/lib/api`. ## Data and API - Prisma `ApiKey` with hashed secret, type, permissions, expiry, and shop relation. - APIs: `/api/api-keys` and all `/api/v1/*` handlers. - OpenAPI JSON at `/api/v1/openapi.json`. ## Failure modes - 401 or 403: missing bearer token, wrong key type, expired key, disabled key, or wrong shop. - Lost secret: create a new key because full secrets are not recoverable. - Automation mutates wrong shop: verify key belongs to the intended active shop. ## Assets and references > **Simple terms** > > Assets are the files and references Apex can use while building tests, pages, and AI prompts. Stores images, PDFs, creative references, and prompt material used for experiments, landing pages, and Operator instructions. ## Routes - `/assets` ## Implementation behavior - Assets are uploaded under the active shop and tracked with file metadata, usage links, and context. - Asset tiles now display upload/updated dates and inline references to tests that use the asset. - Operator can reference assets when generating copy, layout, or visual changes. - Admin shop settings related to proxy event-delivery can now be reset back to the platform default; check the admin shop settings panel to clear an explicit override. - Usage lookups help determine whether deleting or replacing an asset affects active work. ## How to use it - Upload brand, product, campaign, or research material before generating content-heavy variants. - Attach assets to the relevant idea, experiment, or landing-page workflow when possible. - Check asset usage before removing files. ## When not to use it - Do not upload large unoptimized files when a compressed asset will work. - Do not delete assets before checking where they are used. - Do not expect Operator to use an asset unless it is attached or mentioned in context. ## Implementation source - The page lives under `src/app/(dashboard)/assets`. - Handlers include `/api/assets`, `/api/assets/[id]`, `/api/assets/[id]/file`, and `/api/assets/[id]/usage`. - Asset metadata is shop-scoped and can be referenced by Operator, landing pages, and experiment workflows. ## Data and API - Asset metadata and usage records in Prisma-backed storage. - APIs: `/api/assets`, `/api/assets/[id]`, `/api/assets/[id]/file`, `/api/assets/[id]/usage`. - File delivery depends on the configured storage provider and public/private access mode. ## Failure modes - Broken asset in preview: check URL, permissions, and whether the file was replaced. - Operator ignores asset: make the asset relevant in the prompt or attach it to the active context. - Large files may fail upload or slow preview if not optimized. ## Edge routing and custom domains > **Simple terms** > > Edge routing sends a customer domain through Apex's Cloudflare Worker when a flow needs proxy-level control. Routes customer domains through the Apex Worker path for edge-managed experiences and SSL-for-SaaS style storefront handling. ## Routes - `/settings/developers?tab=edge` - Legacy `/installation/edge-routing` and `/settings/installation/edge-routing` redirect there. ## Implementation behavior - Custom hostnames can CNAME to `edge.drip-apex.com` so traffic enters the Cloudflare Worker. - The Worker resolves the shop/domain, applies routing rules, and proxies or handles configured paths. - Fallback origins and hostname validation prevent unverified traffic from being treated as a trusted shop domain. - Runtime config keeps edge routing separate from normal SDK-only installs. ## How to use it - Use edge routing only when a storefront flow requires Worker mediation rather than SDK-only DOM changes. - Validate DNS and custom hostname status before switching traffic. - Keep origin settings current for each shop domain. - Test preview and production domains separately. ## When not to use it - Do not use edge routing for ordinary DOM-only A/B tests that the SDK can handle. - Do not route production domains before DNS validation and origin testing are complete. - Do not mix Vercel domain ownership and Cloudflare custom hostname routing without checking which edge receives traffic. ## Implementation source - Worker code lives in `packages/worker` and Cloudflare routes are documented in project configuration. - Dashboard install and edge pages use server-side domain-routing and edge-resolution services. - Cloudflare custom hostnames and shop origin fields determine how requests are resolved. ## Data and API - Cloudflare Worker routes, custom hostname records, shop origin fields, and edge-routing verification state. - Dashboard capabilities cover hostname discovery, DNS preflight, custom-hostname configuration, verification, and runtime resolution without exposing internal service paths. - Worker observability is the main debugging source for edge routing. ## Failure modes - Hostname not active: DNS CNAME or validation TXT is missing. - Origin loop or blank response: wrong origin field or Vercel/domain conflict. - SDK works but edge route does not: these are separate deployment paths and must be verified independently. ## Server-rendered variants For a server-rendered storefront behind the customer-deployed Apex Edge Worker (version 1.1.16 or later), the Edge Worker forwards the visitor id and a signed assignment set to the origin so the origin can render variants without a hydration flicker. Let the edge decide and the origin render the variant. This avoids hydration replacing an edge DOM edit. The hosted snippet and Shopify pixel do not provide this origin handoff. On HTML document GET requests, the worker replaces client-supplied copies of these headers: - `x-drip-visitor-id`: the resolved `drip_uid` visitor ID. - `x-drip-assignments`: the exact JSON envelope below. `configRevision` and `assignmentEpoch` are numbers or `null`. Assignments include decisions with no DOM mutations. An attribution-only decision includes `"attributableOnly":true`; retain it for attribution without rendering its treatment. - `x-drip-assignments-signature`: lowercase hex HMAC-SHA-256 of the exact assignment header string, keyed with the worker's `APEX_INGEST_TOKEN`. It is absent when the token is not configured. ```json {"v":1,"configRevision":42,"visitorId":"visitor-id","assignments":[{"experimentId":"experiment-id","variationId":"variation-id","assignmentEpoch":7}]} ``` The assignment header is smaller than 4 KiB. If necessary, the worker sends the first assignments that fit and adds `"truncated":true`. Missing assignments mean render the default, including when the array is empty. The one-shot baseline reload sends an empty assignment set. Unavailable or incompatible config also falls back to an empty set. If header assembly fails, the origin request continues without handoff headers. For a first-time visitor, the worker merges `drip_uid=` into the upstream Cookie header while preserving other cookies. Non-HTML and non-GET requests receive no generated assignment headers. Existing edge rewriting and SDK injection remain active; configure server-rendered components so they do not compete with DOM mutations. **Trust boundary:** an origin reachable without the worker must verify the signature or ignore these headers. Verify the exact received string before parsing it, using the same server-only `APEX_INGEST_TOKEN`. Never put the token in public runtime config. The signature authenticates the envelope, not freshness or a URL, so it is not authorization for prices, access, or other protected actions. Keep origin/CDN HTML caching partitioned by assignments or disabled; the worker's HTML cache separates assignment sets and does not store responses carrying Set-Cookie. ### Verify the handoff 1. Put a temporary header-echo origin path behind your worker on a test domain. Request it with `Accept: text/html`, first without a cookie and then with `Cookie: drip_uid=handoff-test`. 2. Confirm both handoff headers, an upstream visitor cookie on the first request, and `visitorId: "handoff-test"` on the second. Verify the signature against the exact received JSON with HMAC-SHA-256 and the shared token. Do not expose a public echo endpoint containing real visitor data. 3. Send spoofed handoff headers and confirm the worker replaces them. Request JSON or POST a document request and confirm no handoff is generated. 4. Request `GET /_drip/status` on a hostname routed to the worker (use its workers.dev hostname if the storefront route only covers a path). Confirm `workerVersion` is 1.1.16 or later and `originHandoff` is `"enabled"`. See [Render variants on the server](/docs/getting-started/headless-nuxt#render-variants-on-the-server) for a Nitro middleware example. ## Allowlist Apex in your firewall # Allowlist Apex in your firewall Apex preview proxies identify storefront requests with the per-store secret header `X-Apex-Verification: `. Find and copy the token in **Settings → Installation → Developer and diagnostics**. Treat it like a password and do not place it in storefront code or share it publicly. ## Cloudflare WAF Create a custom rule scoped to your store hostnames with this expression: ```text any(http.request.headers["x-apex-verification"][*] eq "") ``` Choose **Skip**, select the relevant WAF protections, and place the rule first. Keep the hostname scope in the same rule so the token cannot authorize unrelated domains. Cloudflare Super Bot Fight Mode cannot be skipped by a custom rule; if it still challenges previews, adjust that feature separately for the store. For Akamai Bot Manager, add a condition where request header `X-Apex-Verification` equals your token to the allowlist or transactional endpoint exception. ## Auxiliary Apex traffic Operator and QA browser loads send the secret header only to your store's own hostnames. If interception is unavailable or disabled, or if other non-preview scanners also need an exception, restrict a secondary rule to the store hostnames and these distinctive User-Agents: - `Apex-Operator/1.0` - `DripApex/OnboardingScan` - `Mozilla/5.0 (compatible; ApexBrandBot/1.0)` - `ApexStorefrontImageBot/1.0` - `DripApex/StorefrontUnlock` - `DripApex/DomainPreflight` ## Store connection, Shopify install, and Web Pixel > **Simple terms** > > Store connection gets Apex onto the storefront and connects Shopify so events, products, orders, and revenue can flow. Connects Apex to a storefront so experiments, analytics, Shopify events, product data, and revenue analysis can work. ## Routes - `/settings/installation` (admin) - `/settings/developers?tab=script` (developer): manual script install and runtime controls - `/settings/developers?tab=edge` (developer): edge routing - Legacy `/installation` paths redirect to `/settings/installation`; the manual, runtime and edge routing sub-pages redirect to their Developers tab. ## Implementation behavior - Shopify OAuth grants app scopes and stores installation metadata for the active shop. - Theme app embed or manual snippet loads Apex Runtime on storefront pages. - The runtime snippet belongs directly in ``, never behind a tag manager or consent gate. Scoped anti-flicker hides treatment elements until mutations resolve, with a 1200ms fail-open cap. - Shopify Web Pixel and event settings send checkout/order-related events that are not available from DOM-only tracking. - Product, inventory, and order sync endpoints populate cost and reconciliation data when scopes are available. - Install verification checks Apex Runtime reachability, snippet size, runtime settings, and recent events. ## How to use it - Use Shopify OAuth when the store is Shopify; use manual snippet for custom storefronts. - Enable app embed or install the Apex Runtime snippet exactly once. - Verify realtime events after installation. - Reconnect Shopify when scopes, store permissions, or app access change. ## When not to use it - Do not install both manual snippet and app embed in a way that loads the SDK twice. - Do not defer the Apex script until consent. Configure consent inside Apex so variation rendering can remain flicker-safe while storage and tracking stay blocked. - Do not rely on Shopify revenue reporting without Web Pixel/event settings enabled. - Do not debug experiments before install verification and realtime events pass. ## Implementation source - Install pages live under `src/app/(dashboard)/settings/installation`. - Old `/installation` routes now redirect to the new `/settings/installation` tree. - Handlers include `/api/shopify/auth`, `/api/shopify/auth/callback`, `/api/shopify/settings`, `/api/shopify/snippet`, `/api/shopify/webhooks`, `/api/onboarding/verify-install`, and `/api/shops/runtime-settings`. - Runtime delivery uses the SDK package, Worker URL, Shopify installation metadata, and Web Pixel configuration. ## Data and API - Models include `Shop`, Shopify installation/session records, `ShopifyEventSetting`, product/order/cost sync records. - APIs under `/api/shopify/auth`, `/api/shopify/auth/callback`, `/api/shopify/settings`, `/api/shopify/snippet`, `/api/shopify/webhooks`, `/api/onboarding/verify-install`, and `/api/shops/runtime-settings`. - Runtime uses `/api/exp`, CDN SDK, Worker URL, and Web Pixel events. ## Failure modes - No storefront events: app embed disabled, snippet missing, blocked script, wrong shop ID, or Worker URL problem. - OAuth failed: domain mismatch, missing scopes, or stale callback state. - Revenue missing: Web Pixel or Shopify event settings are not enabled. ## Related guidance - [SSR framework coexistence](/docs/sdk/framework-coexistence) - [Consent integration](/docs/data-privacy/consent-integration) ## Privacy, export, deletion, and auditability > **Simple terms** > > Privacy endpoints help export or delete user/visitor data and keep records of sensitive changes. Supports compliance workflows for exporting or deleting visitor/user data and auditing sensitive operational actions. ## Routes - `/api/v1/data-export` - `/api/v1/data-deletion` For the merchant-facing privacy workflow, start with [GDPR export and deletion](/docs/data-privacy/gdpr-export-deletion), [Consent and regional compliance](/docs/data-privacy/consent-regional-compliance), and [Developer access and data](/docs/data-privacy/developer-access-data). ## Implementation behavior - Public privacy endpoints accept authenticated server-to-server requests scoped by shop API key. - Export looks up relevant visitor/user data across persisted records and analytics where supported. - Deletion requests remove or anonymize records according to the implemented data path. - Audit logs and operational records preserve changes to experiments and admin-sensitive workflows. ## How to use it - Use data export for access requests and data deletion for erasure workflows. - Pass stable visitor/user identifiers so the request can find the right records. - Keep privacy automation server-side with secret API keys. - Check audit logs when reconstructing who changed experiment or integration state. ## When not to use it - Do not run privacy requests from browser code or public clients. - Do not assume one identifier finds every record if visitors were anonymous or cookies changed. - Do not skip audit review for sensitive admin or experiment changes. ## Implementation source - Public handlers live under `/api/v1/data-export` and `/api/v1/data-deletion`. - Privacy-related handlers use shop-scoped API key authentication. - The implementation spans relational shop data and analytics/event identifiers, with retention limits depending on storage path. ## Data and API - Public APIs: `/api/v1/data-export` and `/api/v1/data-deletion`. - Internal privacy/admin APIs and audit-log models. - Event data, visitor/session identifiers, experiment assignment data, and shop-scoped relational records. ## Failure modes - Request cannot find data: identifier mismatch or events not linked to a user ID. - Partial deletion risk: analytics and relational stores have different retention paths. - Do not run privacy workflows with client-exposed keys. ## Runtime controls, SDK, anti-flicker, and consent > **Simple terms** > > The runtime is the code on the storefront that decides eligibility, applies changes, and sends tracking events. The browser runtime fetches experiment config, assigns visitors, applies variants, tracks goals, respects runtime settings, and batches events. ## Routes - `/installation` - `/settings/integrations` - `/api/exp` - `/api/shops/runtime-settings` ## Implementation behavior - The lowercase `drip` runtime global initializes with shop ID and worker URL, then fetches `/api/exp` for active runtime config. - Assignment uses traffic allocation, variation weights, targeting, and sticky visitor cookies. - The mutation engine applies DOM changes and custom code after eligibility is resolved. - Goal listeners emit pageview, click, custom, revenue, and Shopify-linked events. - Event batching flushes every five seconds or twenty events; anti-flicker can hide content until variants apply. - Consent and runtime settings decide when tracking can run and how conservative the SDK should be. ## How to use it - Install the snippet from Store Connection and verify it in Realtime Analytics. - Use custom tracking calls for non-DOM events. - Tune anti-flicker only around surfaces that can visibly change. - Keep the shop ID and worker URL aligned with the active shop. - Consent provider settings were moved out of `Settings → Installation` into `Settings → Integrations` (see the Integrations page for the full configuration UI). A read-only summary of the consent provider remains on the Installation page where the snippet is kept. {/* Release 7ef24af: Folded Multi-shop into brand Tests; brand/store runtime guidance. */} - The runtime is aware of organization-level brands and operates at the brand and store granularity. UI surfaces may include a brand selector in the sidebar and a store selector under the page title for brands that contain multiple stores; ensure the runtime `shop ID` in the installed snippet corresponds to the currently selected store so that runtime config, assignments, and events map to the expected store context. - When creating tests for a brand, select stores from the store selector so a single test can run in one or more stores. The test page shows store tabs and a single brand-level result alongside per-store results; server-rendered storefronts that rely on forwarded assignments should receive the worker-forwarded `visitorId` and signed assignments so SSR renders the correct variant without flicker. ## When not to use it - Do not bypass consent requirements for analytics tracking. - Do not use heavy anti-flicker on pages where no visible change can happen. - Do not use client-side SDK behavior for server-only experiments or secure backend decisions. ## Implementation source - SDK code lives in `packages/sdk`; Worker ingestion lives in `packages/worker`; Tinybird schemas and pipes live in `packages/tinybird`. - Config is served by `/api/exp` and runtime settings are read or written through `/api/shops/runtime-settings`. - Events are batched by the SDK, sent to the Worker, normalized to NDJSON, and stored in Tinybird. ## Data and API - Packages: `packages/sdk`, `packages/worker`, `packages/tinybird`. - APIs: `/api/exp`, `/api/shops/runtime-settings`, and installation snippet endpoints. - Tinybird receives normalized NDJSON from the Worker. ## Failure modes - Flicker: anti-flicker missing, selector delayed, or heavy page script contention. - Assignments change unexpectedly: cookies cleared, visitor ID reset, or weights/status changed mid-test. - Events missing: consent disabled, network blocked, Worker unavailable, or batching not flushed before navigation. ## GA4 and GTM exposure integration Enable the GA4 dataLayer exposure recipe in Settings → Integrations. Each eligible exposure pushes `apex_experiment_viewed` to the configured dataLayer. Map its fields to GA4 event parameters in GTM: {/* user-note: "Apex no longer wraps your tag manager’s dataLayer.push; the SDK reads dataLayer entries instead, so goals and revenue are read from the dataLayer within a quarter second while the page is visible, and at the latest when the page is hidden or closed." */} | Field | Value | | --- | --- | | `experiment_id`, `variation_id` | Short numeric public IDs, encoded as strings; UUIDs are no longer the primary public identifier | | `experiment_key` | Ticket key (short public key used for reporting); experiment and variation display names are not published to the storefront config | | `project_id` | Shop UUID | | `variation_index` | Zero-based variation index | | `variation_name` | Index-based label computed only from the variation index, such as `Variation B` | | `is_control` | Whether the assigned variation is the control | | `experiment_uuid`, `variation_uuid` | Internal UUIDs retained for backward compatibility and internal consumers; they are not the primary public IDs | With `includeMetadata: false`, only the two ID fields and their two UUID aliases are pushed alongside `event`. Enabled dataLayer destinations receive public IDs regardless of this setting; older configs without public IDs fall back to UUIDs. Enabled queue destinations receive the full metadata payload. **Migration:** `experiment_name` changes from the UUID to the ticket key. Update existing GA4/GTM mappings to use `experiment_uuid` and `variation_uuid` wherever the original UUIDs are required. The two shops already using this destination, Shinie and Men of Mayhem, need this mapping review. By decision, only identifiers are published: experiment titles, variation display names and shop names never enter the public storefront config or the push. `variation_name` is an index-based label such as `Variation B`. ## Team, clients, organization, account, and admin settings > **Simple terms** > > These settings decide who can access Apex, which clients exist, and how workspace-level administration works. Manages who can access work, which clients belong to an agency, account-level preferences, integrations, and organization-level configuration. ## Routes - `/settings/team` (the store team; the workspace team opens with `?scope=workspace`) - `/clients` - `/settings/store` (store identity, workspace settings, brands and archived items) - `/settings/account` - `/settings/integrations` The older `/team` and `/org/settings` addresses still work and forward to these pages. ## Implementation behavior - Store team membership and organization membership are separate so client access can be scoped. - Agency workspaces expose client management and workspace settings beyond single-store company workflows. - Account settings manage personal user profile and account-level state. - Integration settings manage Shopify, profit analysis, and external platform health. ## How to use it - Invite teammates at the store or organization level depending on the access they need. - Use Clients for agency workspace client setup. - Use Settings → Store for workspace naming and defaults. - Use Integrations when Shopify or profit-analysis data needs reconnection. - When inviting people, choose Editor or Viewer on the role cards. A store-level invite applies the role to that store only; an organization-level invite applies the default role to every store in the organization. ### Client roles - **Editor** — For people who actively manage experimentation. Can create, edit, launch and stop tests; work with backlog ideas; manage settings, integrations and installation; invite people; report bugs and request help. Cannot manage billing, remove people or delete the shop. - **Viewer** — For people who need visibility without editing access. Can open tests, results and the pipeline, report bugs and request help, and approve designs when the per-person **Can approve designs** switch is enabled. Cannot change content or see research notes, feature flags or internal tasks. - **Owner** — For the person accountable for the store. Can do everything an Editor can, plus manage billing, remove people, change roles and approve designs. Cannot delete or transfer the shop. DRIP sets the single Owner for each shop. - **Admin client preview (release 874e528):** An admin-only client preview is available at Settings → Team that renders the dashboard as a client viewer or client approver. The preview is gated server-side and read-only, and lets admins confirm the client-facing view without granting client credentials. ### The invite screen The store invite screen shows the roles as cards, not as a dropdown. Each card carries the role name, what the role can do, and what it cannot do, so the choice is explained before it is made. Owner is not on the cards: each store has one Owner, set when the store is created or onboarded. One line above the email field names the store's plan and says who to invite. The plan changes the recommended role and that line, and nothing else: a Viewer on one plan can do exactly what a Viewer on another plan can do. - **Self-Managed** — the client runs everything themselves. Editor is recommended. - **Apex Managed** — the client builds and launches tests while DRIP delivers research and scored ideas. Editor is recommended. - **Drip Managed** — DRIP builds and launches tests. Viewer is recommended; design approval is switched on from the team list once they accept. The Viewer card carries a note instead of a switch: design approval is a per-person right, so it is switched on from the team list once the person has accepted the invitation. A DRIP address (`dripagency.de`, `dripagency.dev`) cannot be invited into a client store: the screen says that DRIP staff get access from Admin, and the send button stays off until the address changes. ### The member list Every member row names the role. Hover, focus or tap the role to open the same card the invite screen shows. People who hold DRIP platform access carry a **DRIP** badge instead of a role, because that access comes from Admin and is not a shop role to change here. The invitation email carries the same card, so an invited person reads what the role can and cannot do before accepting. ## When not to use it - Do not invite users at the organization level when they only need one store. - Do not change ownership or leave a workspace without confirming another admin remains. - Do not use account settings for store or workspace configuration. ## Implementation source - Pages live under `src/app/(dashboard)/team`, `clients`, `org`, `account`, and settings routes. - Handlers include `/api/team/*`, `/api/orgs/*`, `/api/account/profile`, and invitation endpoints. - Membership and role models in Prisma separate organization access from shop access. ## Data and API - Models include `Organization`, `OrgMember`, `Shop`, `ShopMember`, invitations, account/user profile records, and integration records. - APIs under `/api/team`, `/api/orgs`, `/api/account/profile`, and invitation endpoints. - Active shop and active organization state determines what settings are visible. ## Failure modes - User cannot see a shop: they may be in the organization but not assigned to the store. - Wrong client edited: switch active workspace/shop before changing settings. - Ownership transfer or leave-workspace actions should be checked carefully because they affect access. ## Audit of bandit campaign-mode allocation > **Simple terms** > > "Send more traffic to the winning variant while the promo runs" sounds like free money. We simulated over a million short merchant campaigns to measure how much money it actually is, and what it does to the numbers you see afterwards. The money is small. The distortion of the reported result is large, unless the bandit is built with two specific safeguards. ## The setup The audit simulated two-variant campaigns under realistic e-commerce conditions: - Campaign lengths of 3, 7, and 14 days; 2,000 to 50,000 visitors; conversion rates of 1% to 3%; true lifts of 0%, +5%, +10%, and +20% relative; heavy-tailed order values for revenue scenarios. - Realistic frictions: delayed purchases (most purchases do not land in the same session the visitor was assigned in), hour-of-day and weekday seasonality, sale-shaped demand drift, and a scenario where the winner alternates — better on weekends, worse on weekdays. - Allocation policies: a fixed 50/50 split, epsilon-greedy, Thompson sampling, and Thompson sampling with a hard 10% minimum allocation per arm. The statistics reuse Apex's production decision code — the same sequential method, seed derivation, and heavy-tailed order-value model used in [the Apex certification](/docs/statistics/methodology-and-certification). The run covered 233 policy-scenario cells and 1,156,800 replications with fixed seeds; re-running reproduces every number byte-for-byte. ## Result 1 — the money is small The entire prize is bounded by hindsight: routing 100% of traffic to a true +10% winner for the whole campaign is only worth +4.76% over a 50/50 split. A bandit has to spend the early campaign learning, so it captures a fraction of that. | Scenario | Maximum possible gain over 50/50 | Thompson sampling captures | |---|---:|---:| | 7 days, +5% true lift, 10k visitors | 2.44% | 12% | | 7 days, +10% true lift, 10k visitors | 4.76% | 22% | | 7 days, +20% true lift, 10k visitors | 9.09% | 40% | | 7 days, +10% true lift, 50k visitors | 4.76% | 43% | In money: a typical 7-day, 10,000-visitor campaign with a genuine +10% winner earns **+131 EUR on roughly 22,000 EUR** of campaign revenue. The best case tested (50,000 visitors) earns +1,371 EUR on roughly 110,000 EUR. When there is no real winner, the gain is exactly 0.00% — always. If the arms are identical, no allocation can beat any other. The bandit's entire value is conditional on a real, stable difference existing. ## Result 2 — the bandit picks a side even when there is nothing there In an A/A campaign — two identical variants — the allocation still drifts: | Campaign length | Campaigns ending visibly concentrated on one arm (Thompson) | (epsilon-greedy) | |---|---:|---:| | 3 days | 75% | 87%–90% | | 7 days | 80% | 94%–96% | | 14 days | 82% | 96%–98% | The merchant watches the dashboard tilt toward "the winner" all week, and there is nothing there. With a real but small winner (+5%), Thompson ends the campaign pointed at the **losing** arm 28.5% of the time; epsilon-greedy 38.8%. That matters beyond the campaign. A merchant who ends the week with 80% of traffic on variant B concludes B won and ships it permanently. A permanent decision worth tens of thousands of euros per year gets made from an allocation signal that is wrong roughly one time in three at small effect sizes. ## Result 3 — the reported lift more than doubles Adaptive allocation biases the measured lift upward, because traffic concentrates on whichever arm is currently lucky, and that arm's measured rate inherits the selection. | Policy, 7 days, true lift +10% | Reported lift (typical) | |---|---:| | Fixed 50/50 | about 11% | | Thompson sampling, uncorrected | about 24% | | Thompson + 10% floor + corrected estimate | about 13% | Two findings worth stating plainly: - **The obvious fix makes it worse.** Stratifying the estimate by time (hour or day) increases the bias under an unfloored bandit, because it up-weights exactly the periods where the starved arm has almost no data. - **The correct estimator only works with a floor.** Inverse-probability weighting — re-weighting outcomes by the allocation probabilities the platform itself set — is unbiased in principle, but with unfloored allocation its variance explodes and the estimate is useless. With a hard 10% minimum per arm, the corrected estimate lands within about 2 points of the honest 50/50 baseline, at roughly 1.5 times the noise. The minimum-allocation floor is therefore not a safety nicety. It is the precondition that makes an honest lift number computable at all. ## The alternating winner is pure cost When the true winner alternates — variant B better on weekends, slightly worse on weekdays — every adaptive policy earned **zero or negative** money against 50/50, while still ending on the wrong arm 27%–43% of the time and inflating the reported lift by up to 10 points. A bandit chasing a moving winner is pure cost. Promotional weekends are precisely where weekday/weekend interactions are most plausible — which is the same window where campaign-mode features are marketed hardest. ## When adaptive allocation earns anything All of these must hold at once: | Condition | From the data | |---|---| | A real, stable winner exists | 0% lift pays exactly 0.00% | | The effect is large | +20% pays about +3.6%; +5% pays about +0.3% | | Traffic is high | 50k visitors captures 43% of headroom; 2k captures about 10% | | The winner does not alternate | An alternating winner pays zero or negative | | The campaign is long enough to learn | 3-day campaigns leave most of the prize uncollected | ## Questions to ask any platform selling auto-optimization 1. What does the allocation do on an A/A test — does it drift toward a "winner" that is not there, and is that shown to the user as signal? 2. Is there a hard minimum allocation per arm, and what is it? 3. Is the reported lift corrected for adaptive allocation, or is it the raw dashboard number? By how much do the two differ on a simulated known-truth campaign? 4. Can harm on the losing arm still be detected once that arm has been starved of traffic? 5. Can the vendor show a deterministic audit with stated seeds and scenario grid? ## Where Apex stands Apex does not ship adaptive allocation. This audit is the reason, with numbers: for typical campaign sizes the honest expected gain is around 1% of campaign revenue, while an unsafeguarded bandit distorts the ship/no-ship decision that is worth far more than the campaign itself. If a campaign mode ever ships, it ships in the only configuration the data supports — Thompson sampling with a hard allocation floor, slow reallocation, and a corrected lift estimate — and the uncorrected number is never shown. ## Interpretation limits The study covers two-arm campaigns; bandits gain more with many arms, and that shift was not measured. Returning-visitor experience under changing weights was not priced. Hourly traffic follows a fixed seasonality curve rather than being random itself. Results apply to the tested grid of traffic, lift, delay, and seasonality scenarios; the full per-cell output and the simulation code are archived with the audit. ## Audit of Bayesian stopping rules > **Simple terms** > > A “95% chance to beat control” label can look intuitive while answering a narrower question than merchants think. We simulated common published rules to measure what happens when users check them daily. ## The setup The audit reimplemented three rules from public methodology descriptions. It did not run or inspect any vendor’s proprietary production code, and the results should be read as an audit of the published rules and parameter choices—not as a claim about every implementation or product safeguard. - **Chance to beat at least 95%:** a flat-prior posterior probability that the treatment beats control. - **Expected-loss stopping at 1% relative:** stop when the estimated loss from shipping the current leader falls below a conservative threshold. - **Expected-loss stopping at 5% relative:** the same rule with a more permissive threshold. Each rule was evaluated daily for 28 days under realistic small-business traffic and conversion rates. The run included 360,000 A/A experiments plus 60,000 simulations with a real winner, using fixed seeds. In an A/A experiment the two variants are identical. Every declared superior winner is therefore false by construction. ## Results | Rule | False-winner rate in A/A | Median declaration day | |---|---:|---:| | Chance to beat at least 95% | **40%–46%** | day 4–6 | | Expected loss, 1% relative | **57%–99.8%** | day 4–9 | | Expected loss, 5% relative | **about 100%** | day 1–2 | The chance-to-beat rule declared a false winner in nearly half of the A/A experiments within a month of daily peeking, usually during the first week. ## Why chance-to-beat inflates With a flat prior, the probability that B exceeds A behaves similarly to a one-sided significance statistic. Repeatedly checking it and stopping at the first threshold crossing creates an optional-stopping problem. Bayesian arithmetic does not automatically make every stopping rule safe. The posterior calculation can be correct while the user-facing decision procedure fails to preserve the interpretation attached to the threshold. ## Why expected loss eventually fires Expected-loss stopping answers a different question: “How much could we lose by shipping the current leader?” Under a true tie, the expected loss of choosing either arm approaches zero as more data arrives. The rule is therefore designed to conclude, not to prove superiority. Declaring equivalence or indifference can be reasonable; presenting the selected arm as a superior winner with an observed lift creates a false lesson. This also explains the counterintuitive traffic result: the 1% expected-loss rule produced more winner declarations at higher traffic because the estimated loss shrank faster. ## Speed and error together The audit compared a real 10% lift scenario using matched seeds: | Measure | Chance to beat at least 95% | Apex sequential verdict | |---|---:|---:| | Median visitors to detect the winner | about 18,000 | about 86,000 | | Wrong-winner rate | 4.6% | about 0.04% | | False winners in A/A with daily peeking | 40%–46% | 0.2%–3.9% against a 5% budget | The chance-to-beat rule was roughly 4.8 times faster in that scenario, but the error behavior was not comparable. A useful speed claim must always be paired with the risk spent to obtain it. Apex’s own engine has the opposite limitation: it often spends less than its allowed 5% error budget and can take longer than necessary. That finding is public in [the Apex methodology and certification](/docs/statistics/methodology-and-certification). The engineering objective is faster decisions at a disclosed risk level, not a contest between Bayesian and frequentist labels. ## Questions to ask any experimentation platform 1. What false-winner rate does the decision rule produce on A/A tests under daily peeking? 2. Does the dashboard number keep the same meaning when it is inspected repeatedly? 3. Is an early-stopped lift corrected or clearly labeled for winner’s-curse bias? 4. Does an expected-loss stop mean superiority, equivalence, or simply that either decision is cheap? 5. Can the platform show a deterministic audit with stated seeds and parameter choices? ## Interpretation limits These results apply to the documented rules, flat-prior assumptions, tested thresholds, and simulated traffic grid. A production product can add safeguards, different priors, minimum sample rules, or other decision policies that change the outcome. The result is not “Bayesian statistics are bad.” The result is that a posterior probability and a stopping policy are separate things, and the interface must state honestly what decision guarantee the combination provides. ## How Apex statistics work > **Simple terms** > > Apex is built to reach useful business decisions quickly without presenting noise as a winner. We measure whether that promise holds instead of asking customers to trust a label. ## Statistics as risk management A/B testing is not science for its own sake. It is a way to manage the cost of a wrong decision against the cost of waiting. The familiar 95% convention is not a universal quality grade. A reversible copy change, a checkout change, and an automated harm pause carry different consequences. In principle, the correct threshold comes from the economics of the decision: reversibility, downside, traffic cost, and time to learn. Apex currently keeps its public decision contract simple: | Decision | Shipped threshold | Reason | |---|---:|---| | New-experiment winner default | 80% | Out-of-box shop default; configurable per experiment from 80% up to a 99% maximum (global ceiling) | | Guardian harm action | 99% | A false pause is client-facing and should be rare | Lower thresholds can be rational for cheap, reversible decisions, and stricter thresholds can be rational for expensive ones. Apex currently exposes a configurable winner threshold, not automatic risk tiers based on the kind of change. New experiments inherit the shop's testing defaults, whose out-of-box winner threshold is 80%. Any future automatic threshold policy must be measured against the same certification harness before it becomes part of the product contract. Apex enforces a shared 99% significance ceiling across UI, API, shop defaults, and auto-stop; values stored above 99% are clamped at read time. The 5% false-positive budget in the certification results below applies to the **95% winner-threshold configuration used in that registered grid**. It does not turn the shipped 80% default into a 95% contract. ## One-sided decisions A store normally asks a directional question: “Should we ship B because it is better than A?” The matching test is one-sided. A two-sided test asks whether B differs in either direction and spends additional sample on a question that does not match the shipping decision. The direction is part of the disclosed decision rule. It is not free speed: choosing a direction is itself a risk decision, which is why high-stakes safety actions use a stricter threshold. ## Repeated peeking A classical fixed-horizon test assumes one evaluation at its planned sample size. If a dashboard checks that test every day and acts the first time it becomes significant, the false-positive rate no longer matches the label. Apex measured this behavior directly under daily peeking: - At the registered 95% winner threshold, the live sequential winner engine conformed in all 24 certified scenarios, with measured false-positive rates from **0.2% to 3.9%** against a 5% budget. The grid included a 90-day horizon and three-arm experiments. - A classical test evaluated daily violated its budget in all six scenarios, measuring **27.1% to 27.6%** false positives against a 5% label. - The former revenue significance path measured **21.384% to 27.284%** false positives against 5%. Apex gated that inference instead of continuing to display a confidence claim that failed its audit. This is why the live verdict uses an always-valid sequential method and why fixed-horizon statistics must not be presented as safe to inspect continuously. ## Speed is part of validity The certification also found that the live sequential engine is conservative. Across scenarios it often spent less than the available 5% error budget. That protects against false winners, but it also costs power: - At 2,000 visitors per day over 90 days, a real 10% lift on a 3% baseline reached about **81% power**, with a median of roughly 86,000 visitors before a verdict. - A 5% lift on a 1% baseline reached only about **7% power** over the same horizon. Being stricter than promised is not automatically better. It can make merchants wait longer than the disclosed risk requires. Future tuning is therefore evaluated on two axes at once: the false-positive budget must continue to hold, and time to decision should improve. ## Parallel experiments Apex supports multiple experiments on the same traffic. This is a designed-for case. - Each experiment randomizes independently, so assignments from another experiment are distributed across both arms and normally cancel out of the comparison. - Overlap can add variance because another treatment changes the environment for everyone. - Material interactions are possible when two tests affect the same surface or mechanism. Those conflicts should be prevented with targeting and exclusion groups rather than by serializing the entire program. - Sample-ratio checks and trust gating remain active for every experiment independently. Parallel testing preserves learning velocity without assuming that only one experiment can exist per visitor. Apex attribution and analytics are designed to retain multiple experiment assignments. ## Guardian and trust gates Some measurements determine whether Apex should show a verdict at all. - **Guardian harm auto-pause:** all 10 certified cells conformed, measuring **0.05% to 0.17%** false pauses against a 1% budget. - **Sample-ratio mismatch:** the check measured about **0.09%** against its 0.1% nominal boundary and detected simulated 55/45 and 60/40 allocation corruption in every drill, with a median detection time of one day. - **Broken-variation tripwire:** the baseline audit found up to 8.9% false flags under a 90/10 allocation. After adding an expected-conversions floor, the re-measured rate was at most **0.004%**. - **Revenue harm check:** all four cells violated the 1% budget, measuring **1.624% to 3.202%** false pauses. It remains part of the revenue inference remediation. If allocation or measurement integrity is broken, the correct response is to suppress the verdict, not decorate untrustworthy data with a confidence number. ## The certification run The certification grid was registered before the full run and executed through the production decision functions. - More than **3,000,000 simulated experiments** across 68 cells. - Baseline rates from 0.5% to 10%. - Traffic from 100 to 10,000 visitors per day. - Two- and three-arm designs. - 28- and 90-day horizons. - Fixed seeds and deterministic derivation. - Generated reports and machine-computed verdicts rather than hand-entered results. | Decision surface | Cells | Result | Measured outcome | |---|---:|---|---| | Sequential winner verdict at 95% | 24 | **24/24 conform** | 0.2%–3.9% vs 5% | | Classical test under daily peeking | 6 | **6/6 violate** | 27.1%–27.6% vs 5% | | Guardian harm auto-pause | 10 | **10/10 conform** | 0.05%–0.17% vs 1% | | Revenue significance | 4 | **4/4 violate** | 21.384%–27.284% vs 5%; gated | | Revenue harm check | 4 | **4/4 violate** | 1.624%–3.202% vs 1% | | Guardian tripwire | 6 | Descriptive | at most 0.004% after remediation at 90/10 | | Sample-ratio check | 3 budgeted plus 2 drills | Boundary-calibrated | about 0.09% vs 0.1%; all corruption drills detected | | Power and time to decision | 9 | Descriptive | quantifies the cost of conservative decisions | Of the 51 cells with a pass/fail budget, 34 conformed, 14 violated, and 3 were marginal at the sample-ratio boundary. The remaining 17 cells measured descriptive properties such as power, detection speed, and tripwire noise. ## What the certificate means The certificate supports three claims: 1. The live sequential winner verdict kept its false-positive promise in the registered scenarios at the tested 95% configuration. 2. Guardian’s automated harm action was conservative in the registered scenarios. 3. Apex found and disclosed failures in other surfaces instead of treating the audit as marketing. It does not prove that every possible traffic pattern or metric distribution is safe. It is a reproducible baseline and an acceptance harness: when statistical code changes, the affected scenarios must be measured again. See also [the audit of common Bayesian stopping rules](/docs/statistics/bayesian-stopping-audit). ## Statistics > **Simple terms** > > Apex treats statistics as risk management for business decisions. These pages explain the methods, the limits, and the simulations used to verify them. A confidence number is useful only if it keeps its meaning under the way people actually use the product. Apex therefore tests its statistical decision code under repeated peeking, low and high traffic, multiple variants, and safety-trigger scenarios. ## Read the methodology - [How Apex statistics work](/docs/statistics/methodology-and-certification) explains confidence thresholds, one-sided decisions, repeated peeking, parallel experiments, Guardian, sample-ratio checks, and the 68-cell certification run. - [Audit of common Bayesian stopping rules](/docs/statistics/bayesian-stopping-audit) shows what published “chance to beat” and expected-loss rules did in deterministic A/A simulations. - [Audit of bandit campaign-mode allocation](/docs/statistics/bandit-campaign-audit) measures what adaptive traffic allocation earns in 1.15 million simulated merchant campaigns, and what it does to the reported lift. ## What is live today - The live winner verdict uses a sequential method designed for repeated looks at accumulating data. New experiments default to an 80% winner threshold, configurable from 80% up to a 99% global ceiling. Apex enforces a shared 99% significance ceiling across UI, API, shop defaults, and auto-stop; values stored above 99% are clamped at read time. - Apex Guardian uses a stricter 99% harm threshold for client-facing safety actions. - Revenue significance remains gated while its inference path is rebuilt and re-certified. - Sample-ratio mismatch and other trust checks can suppress a verdict when the underlying allocation is not trustworthy. ## Recent changes - Value goals now chart their actual value over time (release `c59ee6d`). This replaces the previous conversion-rate placeholder in value-goal charts and aligns reported timeseries with the recorded value source. (See release delta: `.release-delta.json`.) - The unlinked `/experiments/statistics` dashboard route and its orphaned components were removed in the release at `f2ac995` (owner decision 2026-08-27). This repo change deleted the UI route and related components; the methodology, certification, and audit documentation remain as the canonical public explanation of Apex's statistical approach. (See release delta: `.release-delta.json`.) ## What the evidence covers The Apex certification ran more than three million simulated experiments through the production decision functions. The same fixed seeds and engine checkout reproduce the measured statistical fields byte-for-byte. The public methodology page includes the complete rollup, including the surfaces that failed their budgets.