# Shopify connector Descrybe v2 connects Shopify as a first-class store source alongside WooCommerce. UI: **`/stores`** (hub) → **`/stores/shopify`** (setup). `/shopify` redirects to the setup page. ## What syncs | Data | Direction | Notes | |------|-----------|--------| | Products | Push | Processed Descrybe products → Shopify Admin API create/update (queued / scheduled — **not** store webhooks) | | Orders | Pull | Queued / scheduled pull into `shopify_orders` / `shopify_order_items` (not live webhooks) | | Reviews | — | Not available (Shopify has no first-party product reviews Admin API) | | Inbound webhooks | — | Not received; do not register Shopify webhook URLs at Descrybe until a full HMAC receive path ships (`docs/store-connectors.md`) | ## Setup (recommended: Dev Dashboard client credentials) As of **1 January 2026**, Shopify no longer creates new custom apps in Admin → Develop apps. New apps are created in the [Dev Dashboard](https://dev.shopify.com/dashboard/), installed on the store, then authenticated with the **client credentials** grant (short-lived Admin API tokens, ~24h). 1. Open the Dev Dashboard → **Apps** → **Create app** → **Start from Dev Dashboard** (UI deep-links this). 2. Create a **version**: App URL can be the default API-only home; scopes at least `read_products`, `write_products`, `read_orders` → **Release**. 3. From the app **Home** tab → **Install app** on your store (approve scopes). 4. App **Settings** → copy **Client ID** and **Client secret**. 5. In Descrybe **`/stores/shopify`**: enter shop domain (`your-store` or `your-store.myshopify.com`), paste Client ID + secret, enable sync → **Save** → **Test Connection**. 6. Queue **product sync** and **orders sync**. Run the worker so pending flags are claimed. Descrybe exchanges Client ID + secret for an access token on save and **refreshes automatically** before expiry (no migration; secrets live encrypted in `access_token` + `sync_options`). ### Legacy Admin API token (`shpat_…`) Custom apps created in Shopify Admin **before** January 2026 still expose a permanent Admin API access token. The connect form has a “legacy token” path for those. Prefer Client ID + secret for anything new. ### Still manual (no one-click App Store install) - Creating the Dev Dashboard app, releasing a version, and installing it on the merchant store - Approving scopes in Shopify Admin when scopes change - Collaborator accounts cannot use the Dev Dashboard — the store owner (or staff with Develop permission) must create the app - Client credentials only work when the app and store are in the **same** Shopify organization (see Shopify’s `shop_not_permitted` docs) Shop domain must be the `*.myshopify.com` admin host. Custom storefront domains are rejected (Admin API always uses myshopify.com). SSRF guards block IPs, localhost, and private ranges. ## Environment variables (optional live path) Set in process env or untracked `.env` (never commit secrets): | Variable | Purpose | |----------|---------| | `SHOPIFY_SHOP_DOMAIN` | Shop name or `name.myshopify.com` | | `SHOPIFY_ACCESS_TOKEN` | Admin API access token (legacy / ops scripts) | | `SHOPIFY_API_VERSION` | Default `2024-10` | | `SHOPIFY_DRY_RUN` | When `true`, treat as dry-run (UI also has a Dry run checkbox) | Aliases are not required; credentials saved via the UI are encrypted at rest (`CREDENTIALS_ENCRYPTION_KEY` / `APP_ENCRYPTION_KEY` — see [ops-runtime.md](ops-runtime.md)). ### No live shop? Dry-run / mock - In the UI: enable **Dry run**, or set access token to exactly `dry-run`. - Dry-run skips live HTTP calls: test connection returns a mock shop; product create/update and a sample order page succeed in-process. - Unit tests in `internal/shopify` cover domain SSRF guards, token encryption, client-credentials parsing, and dry-run client behavior (no network). ## APIs | Method | Path | Notes | |--------|------|-------| | GET | `/api/shopify` | Config + sync badges (no secrets). Includes `auth_mode`, `has_client_credentials`. | | PUT | `/api/shopify` | Save shop domain + (`client_id`/`client_secret` **or** `access_token`) + api_version + dry_run | | POST | `/api/shopify/test` | Live or dry-run shop.json (refreshes client-credentials token if needed) | | POST | `/api/shopify/sync` | Queue product push | | POST | `/api/shopify/sync-orders` | Queue orders pull | | GET | `/api/shopify/orders` | List synced orders | ## Worker `cmd/worker` claims pending Shopify jobs after WooCommerce: - `products` → `shopify.Service.SyncCompany` - `orders` → `shopify.Service.SyncOrders` Scheduled enqueue (every 15m ops tick) marks enabled configs whose `last_synced_at` is older than the interval (default 6h). ## Schema Migration `apps/api/sql/schema/017_shopify.sql`: - `shopify_configs` — shop domain, encrypted access token, api version, sync_options JSONB - `shopify_orders` / `shopify_order_items` — pulled order data Client credentials metadata (`auth_mode`, `client_id`, encrypted `client_secret_enc`, `token_expires_at`) is stored in `sync_options` (no extra migration). Apply with `make migrate` (or `scripts/migrate.ps1` on Windows). ## Product matching 1. Cached Shopify product id in `sync_options.product_ids` (keyed by Descrybe processed product UUID). 2. Else GraphQL `productVariants(query: "sku:…")`. 3. Else create a new product with a single variant (SKU/price/barcode) and a `descrybe.product_id` metafield. ## Related - Hub overview: [store-connectors.md](store-connectors.md) - WooCommerce parity: [woocommerce-demo.md](woocommerce-demo.md) - Shopify docs: [Create apps (Dev Dashboard)](https://shopify.dev/docs/apps/build/dev-dashboard/create-apps-using-dev-dashboard), [Get API access tokens](https://shopify.dev/docs/apps/build/dev-dashboard/get-api-access-tokens)