# Shopify

Connect Shopify with a custom app token and signed webhooks so ProductLense records paid orders and refunds and attributes each buyer.

ProductLense reads your Shopify customers, paid orders and refunds and attributes each buyer to the channel that first brought them. Shopify orders are one-off purchases, so there are no subscriptions. ProductLense only reads: it never writes to your store, never refunds, cancels, fulfils or changes anything.

## What you need

| Field | Secret | Where to find it |
|---|---|---|
| Shop domain | No | Your myshopify.com domain, for example `acme.myshopify.com`. |
| Admin API access token | Yes | The `shpat_...` token of a custom app with the `read_orders` and `read_customers` scopes. |
| Webhook signing secret | Yes | The key Shopify signs webhooks with: shown in Settings → Notifications → Webhooks when you create webhooks there, or the custom app's client secret when the app registers them through the Admin API. |

Every plan includes one billing connection per site, Free too; Growth and Scale allow any number. See [plans and limits](/docs/account/plans-and-limits).

## Permissions

Give the custom app these Admin API scopes, both read-only:

- `read_orders`
- `read_customers`

ProductLense calls the Admin API (version 2025-01): `GET shop.json`, `customers.json` and `orders.json` (paid orders, any status).

> [!NOTE]
> Shopify returns only the last 60 days of orders to apps without the `read_all_orders` scope. Add `read_all_orders` to the app if you want the full 12-month backfill.

## Set up

1. In Shopify admin, open Settings → Apps and sales channels → Develop apps → Create an app.
2. Under Configuration, give it the Admin API scopes above, install it, and copy the Admin API access token.
3. In ProductLense, open your site's Settings → Billing connections → Shopify, enter the shop domain and token, check the Processor fees estimate, and click Connect and import history.
4. Copy the webhook URL from the connection card.
5. In Shopify admin, open Settings → Notifications → Webhooks and create a JSON webhook for each topic below with that URL. Copy the signing key shown on that page into the connection in ProductLense.
6. Add the ProductLense script to your theme as in the [Shopify install guide](/docs/install/shopify).

## Webhook

URL: `https://api.productlense.app/api/webhooks/shopify/<connection-id>` (ProductLense shows the exact URL after you save).

Topics ProductLense uses:

- `orders/paid`, `orders/updated`
- `refunds/create`
- `customers/create`, `customers/update`

Verification: the `X-Shopify-Hmac-Sha256` header, a base64 HMAC-SHA256 of the raw body. The `X-Shopify-Shop-Domain` header must also equal the connection's shop domain, so one connection never accepts another shop's data. A repeated delivery of the same order or refund is processed once.

## What ProductLense reads

- **Customers**: email and name.
- **Payments**: paid orders, with their note attributes, landing site, referring site, source and tags.
- **Refunds**: order refunds.
- **Subscriptions**: none.
- **History**: the last 12 months on first connect (limited to 60 days without `read_all_orders`), then a nightly reconcile at 03:00 UTC from the last sync minus 48 hours.

## How fees are derived

Shopify Payments fees are exposed only by the Payouts and Balance Transactions API, which ProductLense does not read, so every order arrives with a fee of 0.

A fee estimate can be set under Processor fees on the connection card (percent, fixed amount per payment, and its currency), for example your Shopify Payments rate. It starts at 0 and is applied only to charges that arrive with a zero fee. Estimated fees are flagged, and the dashboard shows the share of fees in a period that is estimated. An estimate is only as accurate as the rate you enter; card type, currency and your Shopify plan change the real fee.

## Match payments to visitors

Theme scripts do not run on Shopify's checkout, so the link is made before checkout. ProductLense reads the user id from the order's note attributes (cart attributes): `pl_user_id`, `user_id`, `userId` or `uid`. A ProductLense visitor id in `pl_visitor_id` or `visitor_id` is also accepted. Without either, ProductLense falls back to the customer email, matched to the `email` trait sent with `identify`, or to a user id that is that email.

```liquid title="layout/theme.liquid, for logged-in customers"
{% if customer %}
  <script>
    fetch('/cart/update.js', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ attributes: { pl_user_id: '{{ customer.id }}' } }),
    });
  </script>
{% endif %}
```

Call `identify` with the same `customer.id` (see the [Shopify install guide](/docs/install/shopify)), so the cart attribute and the visitor share one id. See [how payments are attributed](/docs/revenue/attribution).
