# Shopify

> Products become catalog items, orders become outcomes, and no customer field is ever read.

Source: https://www.patterna.nl/docs/connector-shopify

Shopify gives Patterna two things: your products as catalog items of kind `product`, and orders as `order_placed` outcomes. There are two ways in. Without any token, the storefront sync reads your shop's public product feed and an orders export gives co-purchase pairs (next section). With a read-only Admin API token, Patterna reads products and orders on a schedule, and webhooks for changes can go straight to Patterna or through your backend (sections 1 and 2). Neither way selects a customer field.

## Without a token: the storefront feed and an orders export

**Products.** Every Shopify store publishes `https://<your-shop>/products.json`. The storefront sync reads it page by page and upserts each product: the Shopify product id as `external_id` (the same key the Admin connector uses, so switching later creates no duplicates), the product type as collection, the shop's tags, the description as text for tagging, and the attributes `price_cents`, `currency`, `compare_at_cents` (on sale), `available` (any variant in stock), `image_url`, `url`, `handle`, `product_type` and `format: "product"`. A product that left the feed is archived, not deleted. The sync runs on a schedule once the connector is configured for your tenant, and you can start one at any time from **Connectors → Shopify storefront → Sync now** (the panel queues it; the worker picks it up within a minute and shows the result: products read, products archived).

The schedule is a tenant setting we set for you: `settings.connectors.shopify_storefront = {"enabled": true, "shop_url": "https://your-shop.example", "every_minutes": 60}`. The worker runs the feed every `every_minutes` (hourly by default) and writes each run under its own source, `shopify_storefront`, so **Connectors** shows the storefront's last run separately from the webhook's. A failed run raises a `connector_failure` alert (email or Slack, from Settings → Alerts) and appears in the notification centre. Without the setting the feed is read only when you press Sync now or when we run it by hand:

```sh
python -m patterna_jobs shopify-storefront --tenant your-tenant --shop-url https://your-shop.example
```

The feed is capped at 10,000 products; a larger shop uses the Admin token.

**Order history.** Export your orders as CSV from Shopify admin (Orders → Export) and hand us the file. The reader keeps three columns (order name, line-item name, SKU), matches line items to products by SKU and then by title, and writes product pairs bought together with their lift, for pairs in at least two orders. Names, emails and addresses in the export are never read, and the file is not copied. The pairs feed each product's neighbours ("bought together"); they say nothing about any one customer.

```sh
python -m patterna_jobs orders-csv --tenant your-tenant --csv orders_export.csv
```

It prints the order count, the multi-product orders, the pairs and the line items it could not match. New orders after the export still need `order_placed` events (section 2) to count in `purchase_rate` and the repurchase window.

## 1. Create an Admin API token

1. In Shopify admin, open **Settings → Apps and sales channels → Develop apps** and create an app named `Patterna`.
2. Under **Configuration → Admin API integration**, select exactly these scopes:

   ```text
   read_products
   read_orders
   ```

3. Install the app and copy the **Admin API access token**. Shopify shows it once.
4. Hand it to us through your secret channel, together with your shop domain (`your-shop.myshopify.com`). Today the token lives in the jobs worker's environment, named in the connector's config (`token_env`); it is not entered in the panel and never stored in the database.

The jobs use the token to read products and orders through the Admin GraphQL API. The order query selects no customer, address, email or note fields.

## 2. Add the webhooks

The endpoint for the two webhooks, both in JSON:

```text
https://patterna-api.orven.am/v1/webhooks/shopify/your-tenant
```

| Topic | What Patterna does |
|---|---|
| `orders/create` | writes an `order_placed` event |
| `products/update` | upserts the product as an item of kind `product` |

You can create them under **Settings → Notifications → Webhooks**, or from the app. Shopify signs each delivery with an `X-Shopify-Hmac-Sha256` header.

The tenant is in the path; there is no Bearer key. Each delivery is authenticated by its HMAC alone, under the `shopify_webhook_secret` an owner sets with `PATCH /settings`. Use the secret Shopify shows for the webhooks you created (admin-made and app-made webhooks have different secrets).

- No secret set: every delivery is refused with `401 webhook_not_configured`.
- Wrong HMAC: `401 bad_signature`.

You can also receive `orders/create` in **your own backend** instead: verify Shopify's HMAC there, map the Shopify customer to your random Patterna id (or leave the user out), and send `order_placed` with `POST /events` and your ingest key. That path keeps the customer mapping on your side. Product updates also come in through the scheduled product read, or your own catalog sync with `PUT /catalog/items`.

## What becomes what

**Products → items.** Kind `product`, with the editorial title, status and attributes such as `price_cents`, `currency`, `categories[]`, `image_url` and `url`. Products are tagged into your taxonomy like any other item.

**Orders → `order_placed`.**

| Field | Value |
|---|---|
| `order_id_hash` | a hash of the Shopify order id, never the id itself |
| `product_ids[]` | the products in the order |
| `value_cents` | order value in cents |
| `currency` | ISO currency code |

An order is linked to a user or session only through the `patterna_uid` or `patterna_sid` cart attribute, or by your backend when it forwards the order. Without one, the order is stored without a user: it still feeds the co-purchase pairs and the popularity prior, not any one person's profile. The ledger computes no revenue figure today; its product metric is `purchase_rate` (an order containing a shown product within 7 days of a tap). See [Proof and the ledger](https://www.patterna.nl/docs/proof-and-the-ledger).

## The shop link parameter

When your app links from a recommendation to a product page in the shop, add one tracking parameter to the link, `ref=<request_id>`: the request id of the slate, so the visit becomes a `product_viewed` event with `product_id` and `request_id`. It carries nothing else.

Never put a user id in the link: not the Patterna id, not an email, an account id or a Shopify customer id. A shop URL is public (shared, logged, cached), and a pseudonymous id in it would travel with it.

Linking an order to a person happens on your side, not in the link: your backend forwards the order with the Patterna id or session id, or your storefront writes the `patterna_uid` or `patterna_sid` cart attribute server-side from its own session, never from a URL parameter. The order reader looks only at those cart attributes. Until one of the two is in place, orders arrive without a user link, and forwarding orders from your backend is the simpler path. See [Identity across connectors](https://www.patterna.nl/docs/identity-across-connectors).

## What is never read

- Customers, emails, names, phone numbers, addresses.
- Order notes and any free text.
- The raw Shopify order id (only its hash is stored).

## Check it works

**Connectors → Shopify** shows the last run, rows in, rows mapped, fields dropped and errors; **Shopify storefront** shows the feed's own runs and the pending Sync now request. Place a test order and confirm one `order_placed` row appears.
