> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaireonai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Shopify

> Install the KaireonAI Personalization app, place storefront offer blocks, and understand how orders and refunds attribute back to decisions.

## Overview

The KaireonAI Personalization app for Shopify shows a personalized, ranked offer on four storefront surfaces — the product page, cart, home page, and the Thank You page — and closes the loop back to KaireonAI's decisioning engine with impressions, clicks, add-to-carts, purchases, and refunds. It requires no code changes to your theme beyond adding app blocks through the theme editor.

<Info>
  This page is the merchant-facing install and architecture guide. If you're self-hosting KaireonAI and wiring up your own Shopify integration, see the [Shopify App API Reference](/api-reference/shopify-app) for the exact endpoint, webhook, environment-variable contract, and the two supported deployment topologies (a dedicated edge service, or a single-service opt-in).
</Info>

### What gets installed

| Component               | Type                           | Where it runs                                                              |
| ----------------------- | ------------------------------ | -------------------------------------------------------------------------- |
| Embedded admin          | Shopify admin app (Polaris UI) | Inside the Shopify admin, under **Apps → KaireonAI Personalization**       |
| Storefront offer blocks | Theme app extension (3 blocks) | Product page, cart page, home/collection page — added via the theme editor |
| Thank You page offer    | Checkout UI extension          | Thank You page, for logged-in customers                                    |
| Checkout web pixel      | Web pixel extension            | Fires alongside checkout, corroborates purchases                           |
| Webhook receiver        | Backend route                  | Listens for order, refund, product, customer, and GDPR compliance events   |

## Setup

Connecting Shopify is per-tenant and self-service: an admin on your KaireonAI tenant connects your own Shopify app's credentials in **Settings → Integrations → Shopify**, then installs it on one or more stores. There's a guided 4-step checklist on that page that tracks your progress automatically.

### 1. Create a custom app

In the [Shopify Partner Dashboard](https://partners.shopify.com), create a custom app for your store and request exactly these four scopes — no more, no less:

```
read_products,read_orders,read_inventory,read_customers
```

The app never writes to your product catalog, orders, or customers with these scopes. Product edits you make inside KaireonAI's embedded admin only affect KaireonAI's own copy of your catalog (curation fields like activation, per-surface placement, and per-offer weekly caps), never your Shopify store. `read_customers` powers the customer sync that personalizes recommendations — see [Customer sync and personalization](#customer-sync-and-personalization) below.

<Info>
  **Also request Protected Customer Data access.** In the Partner Dashboard, go to **your app → API access → Protected customer data** and request it — this is a separate approval step from granting the `read_customers` scope above. Without it, Shopify redacts `email`, first/last name, `tags`, and address fields to `null` on every `customers/create`/`customers/update` webhook and every customer GraphQL call KaireonAI makes (backfill and per-order spend sync alike) — only the customer id and the non-PII purchase aggregates (`amountSpent`/`numberOfOrders`) still come through. **Personalization keeps working without it:** the spend/recency/frequency tiers KaireonAI scores on (see [Customer sync and personalization](#customer-sync-and-personalization) below) are derived entirely from those aggregates, never from a PII field. What you lose without approval is the `tags`-based signal and the identity fields (name, email, address) KaireonAI would otherwise store for you.
</Info>

<Warning>
  **Already installed before `read_customers` existed?** Shopify does not retroactively widen an already-granted scope set. If your store was installed under an older 3-scope app (`read_products,read_orders,read_inventory` only), you must **reinstall** the app from that store's Shopify admin (uninstall, then reinstall) to re-consent to the new scope — until you do, `customers/create`/`customers/update` webhooks are never delivered and recommendations for that store keep scoring on priors only (no per-customer personalization). Reinstalling goes through the same path as any uninstall (see [Uninstalling one store vs. disconnecting the integration](#uninstalling-one-store-vs-disconnecting-the-integration)): your imported offers and creatives are archived, not deleted, but they are **not** automatically reactivated on reinstall — revisit the Offers tab afterward and reactivate what you want live again.
</Warning>

### 2. Enter your credentials

Paste the app's **Client ID** and **Client Secret** from the Partner Dashboard into the Settings → Integrations → Shopify checklist. The Client Secret is **write-only**: KaireonAI encrypts it at rest and never displays it back to you (or any admin) once saved — the credentials card afterward shows only "Secret set ✓". Rotating later works the same way: enter a new secret to replace it, or leave it blank to rename only the Client ID.

### 3. Download the deploy kit

In **Settings → Integrations → Shopify**, click **Download deploy kit**. This downloads a zip containing a ready-to-run Shopify CLI project: `shopify.app.toml` pre-filled with your app's Client ID and this deployment's own public URLs, every storefront extension's source, and a `deploy.sh` script — no manual editing needed.

The pre-filled `shopify.app.toml`'s **Application URL** deliberately points at the embedded admin path (`/shopify-admin`), not your deployment's bare root — that's the page Shopify actually iframes inside `admin.shopify.com` when a merchant opens the app. The other 4 URL fields (install redirect, both webhook subscriptions, the App Proxy) keep their own API-route paths under the same host. None of this needs manual editing; it's called out here only so you recognize it's correct if you inspect the unzipped file.

Unzip it into an **empty** folder first (the zip has no wrapping top-level folder, so unzipping into a non-empty directory mixes its files in with whatever's already there), then run:

```bash theme={null}
bash deploy.sh
```

This installs the Shopify CLI and extension dependencies, then pushes the app configuration and every extension (theme app extension, checkout UI extension, web pixel) to your Partner Dashboard app. Sign in with your Partner Dashboard account when the CLI asks.

**One-time: allow network access in checkout UI extensions.** In the Partner Dashboard, go to **your app → API access → "Allow network access in checkout UI extensions."** This is auto-approved the moment you click it and only needs doing once per app — it doesn't block the deploy above, but the checkout extension won't receive live network calls until it's approved.

<Warning>
  There is no `shopify app config link` step anywhere in this flow, and you should not run it against this app: it overwrites the deploy kit's pre-filled `shopify.app.toml` with your app's near-empty remote configuration from the Partner Dashboard. If you've done this by accident, re-download the deploy kit from Settings → Integrations → Shopify rather than trying to hand-restore the file — the kit is regenerated fresh from your saved credentials every time.
</Warning>

<Note>
  **Self-hosting a single-service deployment?** KaireonAI can serve the embedded admin and the Shopify surface from the same deployment as the rest of the platform (`SHOPIFY_SERVE_ON_CORE=1`) — see [Deployment topology](/api-reference/shopify-app#deployment-topology) in the API reference for the full setup, including the `KAIREON_PUBLIC_URL` environment variable the deploy kit above uses to fill in its URLs.
</Note>

### 4. Install on your store

In the Partner Dashboard: **Apps → your app → Test your app / Select store** → install on your development store (or a production store, once you're ready). **Provisioning happens automatically** on first launch of the embedded admin — KaireonAI creates four storefront channels for that store (`shopify_product_page`, `shopify_cart`, `shopify_home`, `shopify_thank_you`), the outcome types the app records (impression, click, add to cart, purchase, dismiss, refund), a default frequency-cap contact policy (max 3 offers/customer/day), and a Naive-Bayes-scored decision flow (`shopify-nba-flow`) that's published and active immediately — there is no separate "publish your flow" step for the Shopify surface, and no manual step to trigger provisioning. Provisioning also kicks off a background sync of your existing customer catalog (see [Customer sync and personalization](#customer-sync-and-personalization) below) — this doesn't block or delay the install.

The Settings → Integrations → Shopify page **auto-detects** the install: while your credentials are saved but no store has installed yet, it polls every 10 seconds and shows a "Store connected" confirmation the moment your store finishes installing — no manual refresh needed.

### 5. Import your catalog

Open the embedded admin's **Settings** tab and click **Run import now**. New products are created as KaireonAI offers in **draft** status so you can review them before they go live on any storefront surface (see [Curation and defaults](#curation-and-defaults) below). Each imported offer also gets a **creative for every surface** — this is what actually lets it render as a card once you activate it; an offer with no creative for a surface can never appear there.

### 6. Curate and activate offers

In the **Offers** tab, activate the offers you want recommended, set an optional **per-offer weekly cap**, and choose **which of the four surfaces** each offer appears on. The surfaces you check control which storefront channels have an active creative for that offer; the weekly cap becomes a real per-offer frequency policy the decisioning engine enforces.

### 7. Add the storefront blocks

In your theme editor (Online Store → Themes → Customize):

* **Product page** — add the "Personalized offer" block to your product template.
* **Cart page** — add the "Personalized offer" block to your cart template.
* **Home / collection page** — add the "Personalized offer" block to your home page or a collection template.

Each block is a self-contained app block (no configuration needed) that fetches a recommendation on page load and renders it once the card resolves — nothing renders (and no error is shown) if there's no eligible offer for that visitor.

### 8. Confirm the Thank You page and checkout pixel are active

These are checkout/post-purchase extensions configured separately from theme blocks — confirm they're enabled for your store's checkout after running the deploy kit (Step 3 above).

## Multiple stores under one tenant

One KaireonAI tenant can connect more than one Shopify store to the **same** app credentials — install on a second dev or production store the same way (Step 4 above), and it appears as a new row in the Stores table on Settings → Integrations → Shopify once its install completes. You only do Steps 1–3 once per tenant; Steps 4 onward repeat per store.

Each store gets its **own** four storefront channels, so two stores' offers, creatives, and recommendations never cross-contaminate — an offer imported from one store can never surface on another store's storefront, even if both stores carry the same product under the same Shopify product ID. The bandit-scoring decision flow, outcome types, and frequency-cap contact policy are shared across every store in the tenant (provisioned once per tenant, not once per store).

<Note>
  The Surfaces tab still breaks results out per store (grouped, so a two-store tenant sees both). The Offers and Performance tabs scope to whichever store's embedded admin you're viewing — open the app from store A's Shopify admin and you see only store A's offers, curation, and performance numbers; store B's own embedded admin shows only store B's. An offer imported from store A is never visible or editable from store B's Offers tab, even though both stores share the same tenant's decision flow and scoring model. Offers imported before this per-store identity stamp existed may not appear in either store's Offers/Performance tabs until a re-import or the next `products/update` webhook re-stamps them — this is self-healing, not a data loss.
</Note>

### Uninstalling one store vs. disconnecting the integration

These are two different actions:

* **Uninstalling a single store** (from that store's own Shopify admin → Settings → Apps → KaireonAI Personalization → Delete) stops personalization for **that store only** — its credential is revoked and its offers/creatives are archived. Sibling stores under the same tenant are unaffected and keep running normally.
* **Disconnecting the integration** (Settings → Integrations → Shopify → Disconnect, inside KaireonAI) removes your saved Client ID/Secret and **suspends every store** connected to that tenant at once — a tenant-wide kill switch, not a per-store action. Reconnecting requires re-entering credentials and re-installing each store.

### Data erasure across multiple stores

Shopify sends a `shop/redact` compliance request roughly 48 hours after a store is uninstalled, asking KaireonAI to erase everything held for that store. In a multi-store tenant this respects store boundaries: a customer who only ever interacted with the uninstalled store is fully erased, but a customer who **also** interacted with a sibling store you own (the same shopper browsing two of your stores) keeps their data for that sibling store — only the uninstalled store's own interaction history is removed for that shared customer. A customer who never interacted with the uninstalled store at all is untouched. This also removes every **synced customer profile** (see [Customer sync and personalization](#customer-sync-and-personalization)) attributed to the uninstalled store, independent of whether that customer ever triggered a recommendation.

## Curation and defaults

* Newly imported or newly synced products default to **draft** — nothing appears on your storefront until you activate an offer.
* **Every imported offer gets one creative per surface**, all active by default, so a freshly-activated offer is immediately eligible on all four surfaces. Provisioning also happens for shops installed before this behavior existed — the first embedded-admin launch backfills creatives for your already-imported catalog.
* **Surfaces are a real on/off switch.** Un-checking a surface for an offer flips that surface's creative to inactive (it's never deleted, so re-checking it is instant); the offer then stops appearing there. An offer with every surface un-checked won't render anywhere, even while active.
* **The weekly cap is enforced, not cosmetic.** Setting a per-offer weekly cap creates a frequency policy that limits how many times that offer can be shown to a given customer per week; clearing it to `0` removes the cap.
* **There is no discount field.** The app has read-only access to your catalog and holds no discount-writing scope, so it can't apply a discount at checkout — a display-only percentage that never reached the cart would mislead shoppers, so it isn't offered. Run discounts through Shopify's own Discounts, and the imported offer's price stays in sync.
* The **"Automatically create newly-synced products as drafts"** toggle (Settings tab) is persisted per tenant, but as of this writing **it is not yet consumed anywhere** — every import, regardless of this toggle's value, currently creates new offers as drafts. Existing behavior (draft-first) matches what the toggle implies when left on (its default), so nothing is silently different today; a future release will make this toggle actually gate the behavior when turned off.
* Editing a product's price, image, or category directly in Shopify re-syncs to the matching KaireonAI offer automatically — **unless** you've edited that field inside KaireonAI's Offers tab, in which case your edit wins and future Shopify syncs stop overwriting that specific field.

## Customer sync and personalization

KaireonAI syncs your Shopify customer profiles into its own decisioning data store so recommendations can be personalized by purchase behavior, not just by offer priority.

### What's synced

| Category            | Fields                                                                                                                                                                                                  |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Identity            | Name, email, tags, city, country, marketing-consent flag, verified-email flag, Shopify account creation date                                                                                            |
| Purchase aggregates | Total spent, order count, average order value, first/last order date, days since last order                                                                                                             |
| Derived tiers       | **Spend tier** (`new`/`low`/`mid`/`high`/`vip`), **recency** (`active`/`lapsing`/`dormant`), **purchase frequency** (`none`/`one_time`/`repeat`/`loyal`) — recomputed from the aggregates on every sync |
| Affinity            | Recently purchased product/variant ids and their categories (used for content-based signal, capped to the most recent 20)                                                                               |

Every field listed above is synced regardless of a customer's marketing-consent choice — `accepts_marketing` is stored as a queryable field but never gates whether a profile is synced or used for scoring, since it reflects *marketing* consent, not *personalization* consent.

<Note>
  **Protected Customer Data gates the Identity row, not the rest.** The Identity fields (name, email, tags, city, country) only populate once your Shopify app has Protected Customer Data access (see [step 1](#1-create-a-custom-app) above) — without it, Shopify sends them redacted to `null` and KaireonAI stores exactly what it's given. Purchase aggregates and the derived tiers below are unaffected: Shopify never gates `amountSpent`/`numberOfOrders`-sourced data behind Protected Customer Data, so spend/recency/frequency-based personalization works from day one regardless of approval status.
</Note>

### How it stays fresh

* **At install (and every reinstall/reprovision):** a background sync pulls your full customer catalog plus the last 90 days of orders, so personalization has real signal from day one instead of waiting for new activity.
* **On an ongoing basis:** `customers/create` and `customers/update` webhooks keep profiles current, and every `orders/create` webhook refreshes that customer's recency and affinity.
* **Every order also re-syncs authoritative spend, directly.** Shopify does not reliably send a `customers/update` webhook with the recomputed lifetime total after every order — most notably, orders placed from the Shopify admin (manual or draft orders) can update a customer's real spend without ever firing that webhook. So immediately after recording an order, KaireonAI fetches that customer's own current `amountSpent`/`numberOfOrders` straight from Shopify's Admin API and writes them as an absolute value, never an increment, so a retried or duplicate webhook delivery can't double-count. This means **`spend_tier` and `frequency_bucket` reflect Shopify's own authoritative totals after any order**, whether or not Shopify also happens to send a `customers/update` webhook for it.

### How it personalizes recommendations

`shopify-nba-flow`'s Enrich step looks up the customer profile above by their Shopify customer id and hands their spend tier, recency, purchase frequency, tags, and category affinity to the scoring model (see [How offers are ranked](#how-offers-are-ranked)) alongside the candidate offer's own category. A visitor Shopify hasn't given you an identity for yet (an anonymous storefront visitor, or a logged-in customer KaireonAI hasn't synced a profile for) still gets recommendations — the lookup simply finds nothing, and the model scores that visitor on its priors alone. Nothing throws and no card is withheld for lack of a synced profile.

<Note>
  **Multiple stores, one customer id:** Shopify customer ids are unique per store, not globally. If the very same numeric id happens to belong to different real people across two of your stores under one tenant, the most recently synced profile wins (there's one row per customer id, not one per store). This is a narrow edge case — it only matters if you operate multiple stores under one tenant and haven't verified your id ranges don't overlap.
</Note>

## How offers are ranked

Every Shopify tenant gets one auto-provisioned decision flow (`shopify-nba-flow`) that scores candidate offers with a **Naive Bayes propensity model** (`shopify-nba-model`) by default. The model starts with uniform priors — every offer scores around 50% for every customer until real outcomes accumulate — and its predictors are exactly the customer signal [customer sync](#customer-sync-and-personalization) provides (spend tier, recency, purchase frequency, tags, category affinity) plus the candidate offer's own category. Two customers with different purchase profiles can therefore get a different score for the same offer once the model has learned from enough outcomes; a brand-new tenant with no `/respond` history yet scores every candidate the same, which is expected cold-start behavior, not a bug.

10% of traffic is automatically held out into a no-offer control group (`shopify-nba-flow`'s built-in experiment) so you always have a clean baseline to measure the model's lift against.

A **multi-armed bandit** (`shopify-bandit`, Thompson sampling) model is also provisioned for every tenant as a ready-to-use alternative — it just isn't wired into the flow's score step by default. If you'd rather rank by real-time exploration/exploitation instead of the learned propensity model, open `shopify-nba-flow` in **Studio → Decision Flows** and repoint the Score node at `shopify-bandit` (or any other model you train). The flow is protected only against accidental **deletion**, not editing — you can change its scoring, qualification, or ranking configuration like any other decision flow and publish a new version.

## Storefront placements

| Surface           | Channel key            | Where it appears                                       |
| ----------------- | ---------------------- | ------------------------------------------------------ |
| Product page      | `shopify_product_page` | Below product details (via the theme app block)        |
| Cart              | `shopify_cart`         | On the cart page (via the theme app block)             |
| Home / collection | `shopify_home`         | Wherever you place the block in the theme editor       |
| Thank You page    | `shopify_thank_you`    | After checkout completes, for logged-in customers only |

<Note>
  The Thank You page offer only renders for **logged-in customers** — checkout extensions run in a sandbox with no access to the anonymous visitor cookie the theme blocks use, so a guest checkout sees no personalized card there. Guest purchases are still fully attributed via the order webhook (see below); only the Thank You page's own upsell card is unavailable to guests.
</Note>

The Thank You page card records its own impression (once per rendered card) and click outcomes, the same two lightest-weight signals the other three surfaces record — these feed the same bandit/model learning loop described in [How attribution works](#how-attribution-works) below, just over a session-token-authenticated endpoint instead of the App Proxy the theme blocks use (checkout extensions can't reach the storefront's App Proxy path).

## How attribution works

KaireonAI tracks three tiers of signal for every recommended offer, from lightest-weight to authoritative:

1. **Impressions, clicks, and add-to-carts** are recorded the instant they happen, straight from the storefront block (via Shopify's App Proxy) — these drive the bandit's learning in near real time.
2. **A checkout web pixel** fires a lightweight corroboration event when checkout starts and completes. This never carries revenue on its own (it always records \$0) — it exists purely to give the model an early signal before the authoritative order data has arrived.
3. **The `orders/create` webhook is the authoritative revenue signal.** When a customer completes an order containing an item that was added to cart via a KaireonAI offer, the webhook reads the order's real line-item price, quantity, and any line-level discount, and records a "purchase" outcome with that exact revenue — this is what drives revenue reporting in the Performance tab and what the bandit learns from as a true positive/negative signal.

### Refunds

A `refunds/create` webhook records a matching **negative** outcome (a "refund") with the refunded amount, netting the original purchase's revenue back out of your reporting and correcting the model's learning signal. A refund on an item KaireonAI never recorded a purchase for (e.g., it predates the app, or the original webhook delivery was missed) has nothing to net against and is logged for the reconciliation sweep to catch instead.

### Reconciliation

Webhooks are fast but best-effort — an occasional missed delivery is expected in any webhook-based integration. Every 6 hours, KaireonAI runs a reconciliation sweep per installed store that:

* Re-derives any purchase or refund outcome that a missed or failed webhook never recorded, from Shopify's own order history.
* Catches any product created directly in Shopify that never triggered an import (there is no live `products/create` webhook — new products are picked up by this sweep, or by manually re-running the Settings tab's import).

This means a brief webhook outage never permanently loses revenue attribution or a new product — it's caught within a few hours at most.

## Performance reporting

The embedded admin's **Performance** tab shows, per offer, impressions, an "Engagements" count (clicks plus other positive interactions like add-to-cart, blended into one number), attributed revenue, and a conversion rate, over a 7/30/90-day window you choose.

## Uninstalling

Uninstalling the app from a store's Shopify admin immediately stops storefront personalization for that store and revokes the credential KaireonAI's edge service uses to call your tenant's decisioning API on its behalf — no further recommend/respond traffic is possible for that store after uninstall. If your tenant has other stores connected, they are unaffected — see [Multiple stores under one tenant](#multiple-stores-under-one-tenant) for the full uninstall-vs-disconnect distinction. Shopify's GDPR compliance webhooks (customer data erasure/export requests, and a full-store data erasure roughly 48 hours after uninstall) are honored automatically for the uninstalled store; no action is required on your part.

## Privacy and data handling

* **Attribution** (impressions, clicks, purchases, refunds) is keyed on Shopify's own numeric customer id (for logged-in customers) or an anonymous, cookie-based token (for guests) — neither is personally identifying on its own.
* **Customer sync** (see [Customer sync and personalization](#customer-sync-and-personalization) above) does store customer name, email, city, and country, alongside purchase-history aggregates and the derived tiers computed from them — this is what powers per-customer personalization, and only exists once you grant the `read_customers` scope. The app never gains write access to your customer records; it only reads what Shopify sends.
* Product catalog data synced into KaireonAI (name, price, image, category) is used only to power your own storefront's recommendations.
* Customer data erasure and export requests initiated through Shopify (e.g., a customer requesting their data, or a merchant requesting erasure) are handled automatically via Shopify's mandatory GDPR compliance webhooks, and cover the synced customer profile (including tiers and affinity) the same way they cover every other KaireonAI record for that customer.

## Troubleshooting

| Symptom                                         | Likely cause                                                                                                                                                                                                                                                                                                                                    |
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| No offer appears on a storefront block          | No offers are activated yet, or none qualify for that surface/customer under your contact policy caps — check the Offers tab. This is expected behavior (the block hides itself), not an error.                                                                                                                                                 |
| A product is missing from the Offers tab        | Run **Import** again in Settings, or wait for the 6-hourly reconciliation sweep.                                                                                                                                                                                                                                                                |
| A purchase isn't showing revenue in Performance | Check that the storefront block's "Add to cart" button (not a separate/custom add-to-cart control) was used — attribution is tied to a cart-line property the block sets. A purchase added through a different flow (e.g., a direct product-page "Buy now" button your theme adds separately) can't be attributed to a specific recommendation. |
| Thank You page shows nothing                    | Expected for guest checkouts (see [Storefront placements](#storefront-placements) above) — confirm with a logged-in test customer.                                                                                                                                                                                                              |
