Skip to main content
Shopify is a leading e-commerce platform for creating and managing online stores. Connecting Shopify to Nekt lets you centralize core commerce data — orders, products, customers, inventory, collections, and locations — sourced from the Shopify Admin GraphQL API. With incremental syncs and consistent schemas, you can join Shopify with marketing, finance, and operations data to drive end-to-end analytics like revenue performance, cohort behavior, merchandising impact, inventory health, and fulfillment efficiency.

Configuring Shopify as a Source

In the Sources tab, click on the “Add source” button located on the top right of your screen. Then, select the Shopify option from the list of connectors. Click Next and you’ll be prompted to add your access.

1. Add account access

You’ll need a Shopify Admin API access token and your store identifier.
  • Access Token: The private app access token for authenticating API requests.
  • Store ID: The name of your Shopify store extracted from your store admin URL. Use only the store subdomain: https://[store].myshopify.com/adminStore ID = [store].
  • Start Date: The earliest record date to sync.
Important: As of January 1, 2026, you can no longer create new custom apps directly in the Shopify admin. You must use the Dev Dashboard to create new apps. Legacy custom apps created before this date can still be managed in the Shopify admin.
Follow these steps to create a custom app in Shopify and generate a long-lived (non-expiring) access token for Nekt:
1

Access the Dev Dashboard

  1. Log in to your Shopify store admin at https://[your-store].myshopify.com/admin
  2. Go to Settings > Apps
  3. Click Develop apps
  4. Click Build apps in Dev Dashboard to open the Dev Dashboard
2

Create a new Custom App

  1. In the Dev Dashboard, click Create app
  2. In the “Start from Dev Dashboard” section, enter a name for your app (e.g., “Nekt Data Integration”)
  3. Click Create
3

Configure your app version

  1. In the URLs section:
    • Set your App URL to https://shopify.dev/apps/default-app-home
    • Add https://developers.google.com/oauthplayground as an Allowed redirection URL
  2. Select a Webhooks API Version (use the latest stable version)
  3. In the Access section, enter the following scopes:
    read_customers,read_inventory,read_locations,read_orders,read_products
    
  4. Click Release
  5. Optionally enter a version name (e.g., “v1.0”) and message
  6. Click Release to confirm
4

Get your Client Credentials

  1. In the Dev Dashboard, go to your app’s Settings page
  2. Copy your Client ID and Client Secret
Important: The client secret is sensitive information. Store it securely and never expose it in frontend code or public repositories.
5

Open the Google OAuth Playground

Click the link below to open Google OAuth Playground pre-configured for Shopify:Open Shopify OAuth Playground →
6

Configure the OAuth Playground

In the OAuth Playground, click the gear icon (⚙️) on the right side to open “OAuth 2.0 configuration” and update these values:
  1. Replace SHOP_NAME with your store’s subdomain in both:
    • OAuth authorization endpoint: https://[your-store].myshopify.com/admin/oauth/authorize
    • OAuth token endpoint: https://[your-store].myshopify.com/admin/oauth/access_token
  2. Replace SHOPIFY_CLIENT_ID with your Client ID
  3. Replace SHOPIFY_CLIENT_SECRET with your Client Secret
  4. Click Close
7

Authorize and Get Your Access Token

  1. Click Authorize APIs (Step 1 in the playground)
  2. You’ll be redirected to Shopify - click Install app to grant permissions
  3. After returning to the playground, click Exchange authorization code for tokens (Step 2)
  4. Your access token will appear in the response on the right side
{
  "access_token": "shpat_xxxxxxxxxxxxxxxxxxxxx",
  "scope": "read_customers,read_inventory,read_locations,read_orders,read_products"
}
This token will remain valid indefinitely until you uninstall the app or rotate the client secret.
8

Copy your credentials for Nekt

You now have everything needed to configure Shopify in Nekt:
  • Access Token: The access_token from the response (starts with shpat_)
  • Store ID: Your store’s subdomain from https://[store-id].myshopify.com
If you created a custom app before January 1, 2026, you can still manage it directly from your Shopify admin. These legacy apps provide a simpler experience with non-expiring tokens:
1

Access Legacy Apps

  1. Go to Settings > Apps in your Shopify admin
  2. Find your app in the Legacy custom apps section
  3. Click on the app to manage its settings
2

View or Regenerate Access Token

  1. Go to the API credentials tab
  2. Click Reveal token once to view your access token
  3. Copy the token immediately - it’s only shown once
  4. If you need a new token, uninstall and reinstall the app
Legacy custom apps have non-expiring access tokens that are shown directly in the admin UI, making them simpler to use than new Dev Dashboard apps.
Once you’re done, click Next.

2. Select streams

Choose which data streams you want to sync — you can select all streams or pick specific ones that matter most to you. For faster extractions, select only the streams relevant to your analysis.
Tip: The stream can be found more easily by typing its name.
Select the streams and click Next.

3. Configure data streams

Customize how you want your data to appear in your catalog. Select the desired layer where the data will be placed, a folder to organize it inside the layer, a name for each table (which will effectively contain the fetched data) and the type of sync.
  • Layer: Choose between the existing layers on your catalog. This is where you will find your new extracted tables as the extraction runs successfully.
  • Folder: A folder can be created inside the selected layer to group all tables being created from this new data source.
  • Table name: We suggest a name, but feel free to customize it. You have the option to add a prefix to all tables at once and make this process faster!
  • Sync Type: You can choose between INCREMENTAL and FULL_TABLE.
    • Incremental: Every time the extraction happens, we’ll get only the new data — which is good if, for example, you want to keep every record ever fetched.
    • Full table: Every time the extraction happens, we’ll get the current state of the data — which is good if, for example, you don’t want to have deleted data in your catalog.
Once you are done configuring, click Next.

4. Configure data source

Describe your data source for easy identification within your organization, not exceeding 140 characters. To define your Trigger, consider how often you want data to be extracted from this source. This decision usually depends on how frequently you need the new table data updated (every day, once a week, or only at specific times). Optionally, you can determine when to execute a full sync. This will complement the incremental data extractions, ensuring that your data is completely synchronized with your source every once in a while. Once you are ready, click Next to finalize the setup.

5. Check your new source

You can view your new source on the Sources page. If needed, manually trigger the source extraction by clicking on the arrow button. Once executed, your data will appear in your Catalog.
For you to be able to see it on your Catalog, you need at least one successful source run.

Streams and Fields

Below you’ll find all available data streams from Shopify and their corresponding fields:
Stream containing orders, transactions and line items. Supports incremental sync using updatedAt.Key Fields:
  • id - Unique identifier for the order
  • name - The order name (e.g., #1001)
  • legacyResourceId - Legacy numeric ID
  • email, phone - Contact information
  • processedAt, createdAt, updatedAt, cancelledAt, closedAt - Timestamps
  • confirmed - Whether the order is confirmed
  • currencyCode, presentmentCurrencyCode - Currency codes
Transactions:
  • transactions[].id, kind, status, gateway, authorizationCode, test, paymentId
  • transactions[].createdAt, processedAt
  • transactions[].amountSet.presentmentMoney.amount, currencyCode
  • transactions[].amountSet.shopMoney.amount, currencyCode
  • transactions[].parentTransaction.id
Totals:
  • totalPriceSet - Total price (shopMoney + presentmentMoney)
  • subtotalPriceSet - Subtotal price
  • totalShippingPriceSet - Total shipping
  • totalDiscountsSet - Total discounts
  • totalTaxSet - Total tax
  • totalWeight, taxesIncluded, taxExempt
Statuses and Flags:
  • displayFinancialStatus - Financial status (PAID, PENDING, REFUNDED, etc.)
  • displayFulfillmentStatus - Fulfillment status (FULFILLED, UNFULFILLED, PARTIAL, etc.)
  • fullyPaid, fulfillable, restockable, test, customerAcceptsMarketing
  • sourceName, sourceIdentifier, currentSubtotalLineItemsQuantity
  • tags, note, clientIp
Customer and Addresses:
  • customer.id, firstName, lastName, email
  • shippingAddress.* - Full shipping address
  • billingAddress.* - Full billing address
Discounts and Metafields:
  • discountCode, discountCodes[]
  • metafields[].id, namespace, key, value, type, description, createdAt, updatedAt
Sales Channel:
  • channelInformation.channelDefinition.handle
Line Items:
  • lineItems[].id, title, quantity, sku, vendor, variantTitle
  • lineItems[].customAttributes[].key, value
  • lineItems[].discountedTotalSet.*, originalTotalSet.*
  • lineItems[].discountAllocations[].allocatedAmount.amount, currencyCode
  • lineItems[].discountAllocations[].discountApplication.targetType, allocationMethod, targetSelection, title, description, code
  • lineItems[].variant.id, title, sku, product.id, product.title
Stream containing products, variants, images and metafields. Supports incremental sync using updatedAt.Key Fields:
  • id - Unique product identifier
  • title - Product title
  • descriptionHtml - HTML description
  • vendor - Vendor name
  • productType - Product type/category
  • handle - URL-friendly slug
  • createdAt, updatedAt, publishedAt - Timestamps
  • status - Product status (ACTIVE, ARCHIVED, DRAFT)
  • tags - Product tags
  • templateSuffix - Liquid template suffix
Options:
  • options[].id, name, position, values[]
Variants:
  • variants[].id, title, sku, price, position
  • variants[].inventoryPolicy - Inventory policy (DENY, CONTINUE)
  • variants[].compareAtPrice - Original price before discount
  • variants[].createdAt, updatedAt, taxable, barcode
  • variants[].inventoryItem.id - Link to inventory item
Images:
  • images[].id, altText, originalSrc
  • featuredImage.id, altText, originalSrc
Metafields:
  • metafields[].id, namespace, key, value, type, description
Stream containing inventory items, levels, and costing information. Supports incremental sync using updatedAt.Key Fields:
  • id - Unique inventory item ID
  • legacyResourceId - Legacy numeric ID
  • sku - Stock keeping unit
  • tracked - Whether inventory is tracked
  • trackedEditable.reason - Why tracked field is editable
  • requiresShipping - Whether item requires shipping
  • duplicateSkuCount - Count of duplicate SKUs
  • inventoryHistoryUrl - URL to inventory history
  • createdAt, updatedAt - Timestamps
Origin & Customs:
  • countryCodeOfOrigin, provinceCodeOfOrigin
  • harmonizedSystemCode
  • countryHarmonizedSystemCodes[].countryCode, harmonizedSystemCode
Inventory Levels (per location):
  • inventoryLevels[].id - Inventory level ID
  • inventoryLevels[].location.id, location.name - Location info
  • inventoryLevels[].item.id, item.sku - Item info
  • inventoryLevels[].quantities[].id, name, quantity, updatedAt - Quantity by type (available, committed, incoming, etc.)
Cost and Measurements:
  • unitCost.amount, unitCost.currencyCode
  • measurement.weight.unit, measurement.weight.value
  • locationsCount.count
Variant & Product:
  • variant.id, variant.title, variant.sku
  • variant.product.id, variant.product.title
Stream containing customer profiles. Supports incremental sync using updatedAt.Key Fields:
  • id - Unique customer ID
  • firstName, lastName
  • createdAt, updatedAt
  • verifiedEmail, taxExempt, tags, state
  • numberOfOrders - Total order count
Spend and Contacts:
  • amountSpent.amount, amountSpent.currencyCode
  • defaultEmailAddress.emailAddress, defaultEmailAddress.marketingState
  • defaultPhoneNumber.phoneNumber, defaultPhoneNumber.marketingState, defaultPhoneNumber.marketingCollectedFrom
Addresses:
  • addresses[].id, firstName, lastName, address1, city, province, country, zip, phone, name, provinceCode, countryCodeV2
  • defaultAddress.* - Same structure as addresses
Metafields:
  • metafields[].id, namespace, key, value, type, description
Stream containing product collections and included products. Supports incremental sync using updatedAt.Key Fields:
  • id, title, handle, description
  • publishedAt, updatedAt
Image:
  • image.altText, image.originalSrc
Products:
  • products[].id, title, handle, createdAt, updatedAt
  • products[].productType, vendor, tags, totalInventory, status, price, inventoryQuantity
  • products[].variants[].id, title, sku, price, inventoryQuantity
Sorting and Template:
  • sortOrder, sortType, sortValue, templateSuffix, url
Metafields:
  • metafields[].id, namespace, key, value, type, description
Stream containing store and warehouse locations. Uses full table sync.Key Fields:
  • id, name
  • active, activatable, deactivatable, deletable
  • createdAt, updatedAt, deactivatedAt
  • addressVerified, isFulfillmentService
Fulfillment Service:
  • fulfillmentService.id, fulfillmentService.serviceName
Address:
  • address.address1, address2, city, country, countryCode, province, provinceCode, zip
Metafields:
  • metafields[].id, namespace, key, value, type, description
Stream containing abandoned checkouts. Uses full table sync.Key Fields:
  • id - Unique identifier for the abandoned checkout
  • abandonedCheckoutUrl - URL for the abandoned checkout
  • createdAt, updatedAt, completedAt - Timestamps
  • taxesIncluded - Whether taxes are included
Price Sets:
  • subtotalPriceSet.presentmentMoney.amount, currencyCode
  • totalPriceSet.presentmentMoney.amount, currencyCode
  • totalTaxSet.presentmentMoney.amount, currencyCode
  • totalDiscountSet.presentmentMoney.amount, currencyCode
Customer:
  • customer.id, firstName, lastName, email, tags, state, note, createdAt, updatedAt
Addresses:
  • billingAddress.* - Address lines, city, province, country, zip, phone
  • shippingAddress.* - Address lines, city, province, country, zip, phone
Line Items:
  • lineItems[].id, title, sku, quantity
  • lineItems[].variant.id, variant.price
Taxes and Discounts:
  • discountCodes[]
  • taxLines[].rate, title, priceSet.presentmentMoney.amount, currencyCode

Data Model

The following diagram illustrates the relationships between the core data streams in Shopify.

Use Cases for Data Analysis

This guide outlines valuable business intelligence use cases focused on inventory intelligence, helping you optimize stock levels, identify sales velocity, and prevent lost revenue from stockouts or overstocking. Run these queries on Explorer.

1. Inventory Health Dashboard

Get a comprehensive view of your inventory health by product, including stock levels, cost valuation, and risk classification across all locations. Business Value:
  • Identify products at risk of stockouts before they happen
  • Spot overstocked items tying up capital
  • Understand inventory distribution across locations
WITH inventory_by_product AS (
  SELECT
    ii.variant.product.id AS product_id,
    ii.variant.product.title AS product_title,
    ii.sku,
    ii.variant.title AS variant_title,
    CAST(ii.unitCost.amount AS DOUBLE) AS unit_cost,
    ii.unitCost.currencyCode AS currency,
    SUM(
      CASE WHEN q.name = 'available' THEN q.quantity ELSE 0 END
    ) AS available_qty,
    SUM(
      CASE WHEN q.name = 'committed' THEN q.quantity ELSE 0 END
    ) AS committed_qty,
    SUM(
      CASE WHEN q.name = 'incoming' THEN q.quantity ELSE 0 END
    ) AS incoming_qty,
    COUNT(DISTINCT il.location.id) AS location_count
  FROM
    nekt_raw.shopify_inventory_items ii
    CROSS JOIN UNNEST(ii.inventoryLevels) AS t(il)
    CROSS JOIN UNNEST(il.quantities) AS t2(q)
  WHERE
    ii.tracked = true
  GROUP BY
    ii.variant.product.id,
    ii.variant.product.title,
    ii.sku,
    ii.variant.title,
    ii.unitCost.amount,
    ii.unitCost.currencyCode
)
SELECT
  product_title,
  sku,
  variant_title,
  available_qty,
  committed_qty,
  incoming_qty,
  (available_qty + incoming_qty) AS total_incoming_available,
  location_count,
  ROUND(unit_cost * available_qty, 2) AS inventory_value,
  currency,
  CASE
    WHEN available_qty <= 0 THEN 'OUT_OF_STOCK'
    WHEN available_qty <= 5 THEN 'LOW_STOCK'
    WHEN available_qty <= 20 THEN 'NORMAL'
    WHEN available_qty > 100 THEN 'OVERSTOCK'
    ELSE 'HEALTHY'
  END AS stock_status
FROM
  inventory_by_product
ORDER BY
  available_qty ASC
product_titleskuvariant_titleavailable_qtycommitted_qtyincoming_qtytotal_incoming_availablelocation_countinventory_valuecurrencystock_status
Classic White TeeCWT-SSmall03505020.00USDOUT_OF_STOCK
Denim JacketDJ-MMedium2802189.98USDLOW_STOCK
Canvas SneakersCS-42EU 424120243159.96USDLOW_STOCK
Leather BeltLB-LLarge1520152224.85USDNORMAL
Summer DressSD-MMedium47504721,551.00USDHEALTHY
Basic Socks PackBSP-OSOne Size3421220054231,710.00USDOVERSTOCK
Inventory Status Distribution:
StatusSKU Count% of CatalogAction
Healthy11245%No action needed
Normal6225%Monitor regularly
Low Stock3815%Plan reorder
Out of Stock208%Urgent reorder
Overstock187%Consider markdowns

2. Sales Velocity & Days of Stock Remaining

Combine order data with inventory levels to understand how fast each product sells and estimate days of stock remaining — the core metric for preventing stockouts. Business Value:
  • Forecast when products will run out of stock
  • Prioritize reordering based on sales velocity
  • Identify fast-moving products that need safety stock buffers
WITH daily_sales AS (
  SELECT
    li.variant.product.id AS product_id,
    li.variant.product.title AS product_title,
    li.sku,
    SUM(li.quantity) AS units_sold,
    COUNT(DISTINCT o.id) AS order_count,
    SUM(CAST(li.discountedTotalSet.shopMoney.amount AS DOUBLE)) AS revenue
  FROM
    nekt_raw.shopify_orders o
    CROSS JOIN UNNEST(o.lineItems) AS t(li)
  WHERE
    o.displayFinancialStatus IN ('PAID', 'PARTIALLY_REFUNDED')
    AND o.test = false
    AND o.processedAt >= CURRENT_DATE - INTERVAL '30' DAY
  GROUP BY
    li.variant.product.id,
    li.variant.product.title,
    li.sku
),
current_stock AS (
  SELECT
    ii.variant.product.id AS product_id,
    ii.sku,
    SUM(
      CASE WHEN q.name = 'available' THEN q.quantity ELSE 0 END
    ) AS available_qty
  FROM
    nekt_raw.shopify_inventory_items ii
    CROSS JOIN UNNEST(ii.inventoryLevels) AS t(il)
    CROSS JOIN UNNEST(il.quantities) AS t2(q)
  WHERE
    ii.tracked = true
  GROUP BY
    ii.variant.product.id,
    ii.sku
)
SELECT
  ds.product_title,
  ds.sku,
  ds.units_sold AS units_sold_30d,
  ds.order_count AS orders_30d,
  ROUND(ds.revenue, 2) AS revenue_30d,
  ROUND(ds.units_sold / 30.0, 2) AS daily_velocity,
  cs.available_qty,
  CASE
    WHEN ds.units_sold / 30.0 > 0
    THEN CAST(ROUND(cs.available_qty / (ds.units_sold / 30.0), 0) AS INTEGER)
    ELSE NULL
  END AS days_of_stock,
  CASE
    WHEN cs.available_qty <= 0 THEN 'STOCKOUT'
    WHEN ds.units_sold / 30.0 > 0 AND cs.available_qty / (ds.units_sold / 30.0) <= 7 THEN 'CRITICAL'
    WHEN ds.units_sold / 30.0 > 0 AND cs.available_qty / (ds.units_sold / 30.0) <= 14 THEN 'REORDER_NOW'
    WHEN ds.units_sold / 30.0 > 0 AND cs.available_qty / (ds.units_sold / 30.0) <= 30 THEN 'MONITOR'
    ELSE 'HEALTHY'
  END AS reorder_urgency
FROM
  daily_sales ds
  LEFT JOIN current_stock cs ON ds.product_id = cs.product_id AND ds.sku = cs.sku
ORDER BY
  days_of_stock ASC NULLS LAST
product_titleskuunits_sold_30dorders_30drevenue_30ddaily_velocityavailable_qtydays_of_stockreorder_urgency
Classic White TeeCWT-S89762,225.002.9700STOCKOUT
Denim JacketDJ-M42383,318.001.4021CRITICAL
Canvas SneakersCS-4235302,794.651.1743CRITICAL
Leather BeltLB-L2825839.720.931516MONITOR
Summer DressSD-M18151,079.820.604778HEALTHY
Reorder Urgency Summary:
UrgencySKU CountAvg Days of StockRevenue at Risk (30d)
STOCKOUT30$6,750.00
CRITICAL (< 7 days)53$8,420.00
REORDER NOW (7-14 days)811$4,230.00
MONITOR (14-30 days)1222$3,100.00
HEALTHY (> 30 days)7265
Products with STOCKOUT and CRITICAL urgency represent potential lost revenue. The “Revenue at Risk” column estimates how much revenue these products generated in the last 30 days — revenue you may lose if stock isn’t replenished.

3. Inventory Distribution by Location

Analyze how your inventory is distributed across fulfillment locations to optimize warehouse allocation and reduce shipping times. Business Value:
  • Balance inventory across warehouses to reduce shipping costs
  • Identify locations running low on key products
  • Optimize fulfillment routing
WITH location_inventory AS (
  SELECT
    il.location.id AS location_id,
    il.location.name AS location_name,
    ii.variant.product.title AS product_title,
    ii.sku,
    SUM(
      CASE WHEN q.name = 'available' THEN q.quantity ELSE 0 END
    ) AS available_qty,
    SUM(
      CASE WHEN q.name = 'committed' THEN q.quantity ELSE 0 END
    ) AS committed_qty,
    CAST(ii.unitCost.amount AS DOUBLE) AS unit_cost
  FROM
    nekt_raw.shopify_inventory_items ii
    CROSS JOIN UNNEST(ii.inventoryLevels) AS t(il)
    CROSS JOIN UNNEST(il.quantities) AS t2(q)
  WHERE
    ii.tracked = true
  GROUP BY
    il.location.id,
    il.location.name,
    ii.variant.product.title,
    ii.sku,
    ii.unitCost.amount
)
SELECT
  location_name,
  COUNT(DISTINCT sku) AS unique_skus,
  SUM(available_qty) AS total_available,
  SUM(committed_qty) AS total_committed,
  ROUND(SUM(unit_cost * available_qty), 2) AS total_inventory_value,
  SUM(CASE WHEN available_qty <= 0 THEN 1 ELSE 0 END) AS out_of_stock_skus,
  SUM(CASE WHEN available_qty BETWEEN 1 AND 5 THEN 1 ELSE 0 END) AS low_stock_skus,
  ROUND(
    SUM(CASE WHEN available_qty <= 0 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(DISTINCT sku), 0),
    1
  ) AS stockout_rate_pct
FROM
  location_inventory
GROUP BY
  location_name
ORDER BY
  total_inventory_value DESC
location_nameunique_skustotal_availabletotal_committedtotal_inventory_valueout_of_stock_skuslow_stock_skusstockout_rate_pct
Main Warehouse - NYC24812,450892187,250.008143.2
West Coast Fulfillment - LA1958,320645124,800.0012226.2
Pop-Up Store - Miami861,2407818,600.005185.8
Location Inventory Share:
LocationInventory Value ShareAvailable Units ShareStockout Rate
Main Warehouse - NYC56.6%56.5%3.2%
West Coast Fulfillment - LA37.7%37.8%6.2%
Pop-Up Store - Miami5.6%5.6%5.8%
The West Coast Fulfillment location has the highest stockout rate at 6.2%. Consider redistributing stock from the Main Warehouse or increasing replenishment frequency for this location.

4. Inventory Turnover & Dead Stock Analysis

Identify products that aren’t selling (dead stock) versus those with healthy turnover, helping you make markdown, promotion, or discontinuation decisions. Business Value:
  • Free up capital tied in dead stock through markdowns
  • Identify candidates for clearance sales or bundles
  • Optimize purchasing decisions based on actual turnover rates
WITH product_sales_90d AS (
  SELECT
    li.variant.product.id AS product_id,
    li.variant.product.title AS product_title,
    li.sku,
    SUM(li.quantity) AS units_sold_90d,
    SUM(CAST(li.discountedTotalSet.shopMoney.amount AS DOUBLE)) AS revenue_90d,
    MIN(o.processedAt) AS first_sale,
    MAX(o.processedAt) AS last_sale
  FROM
    nekt_raw.shopify_orders o
    CROSS JOIN UNNEST(o.lineItems) AS t(li)
  WHERE
    o.displayFinancialStatus IN ('PAID', 'PARTIALLY_REFUNDED')
    AND o.test = false
    AND o.processedAt >= CURRENT_DATE - INTERVAL '90' DAY
  GROUP BY
    li.variant.product.id,
    li.variant.product.title,
    li.sku
),
current_stock AS (
  SELECT
    ii.variant.product.id AS product_id,
    ii.sku,
    SUM(
      CASE WHEN q.name = 'available' THEN q.quantity ELSE 0 END
    ) AS available_qty,
    CAST(ii.unitCost.amount AS DOUBLE) AS unit_cost
  FROM
    nekt_raw.shopify_inventory_items ii
    CROSS JOIN UNNEST(ii.inventoryLevels) AS t(il)
    CROSS JOIN UNNEST(il.quantities) AS t2(q)
  WHERE
    ii.tracked = true
  GROUP BY
    ii.variant.product.id,
    ii.sku,
    ii.unitCost.amount
)
SELECT
  COALESCE(cs.product_id, ps.product_id) AS product_id,
  COALESCE(ps.product_title, 'Unknown') AS product_title,
  COALESCE(cs.sku, ps.sku) AS sku,
  COALESCE(ps.units_sold_90d, 0) AS units_sold_90d,
  ROUND(COALESCE(ps.revenue_90d, 0), 2) AS revenue_90d,
  cs.available_qty,
  ROUND(cs.unit_cost * cs.available_qty, 2) AS stock_value_at_cost,
  CASE
    WHEN COALESCE(ps.units_sold_90d, 0) = 0 AND cs.available_qty > 0 THEN 'DEAD_STOCK'
    WHEN COALESCE(ps.units_sold_90d, 0) > 0 AND cs.available_qty > 0
      THEN ROUND(ps.units_sold_90d * 4.0 / cs.available_qty, 2)
    ELSE NULL
  END AS annual_turnover_rate,
  CASE
    WHEN COALESCE(ps.units_sold_90d, 0) = 0 AND cs.available_qty > 0 THEN 'Dead Stock'
    WHEN COALESCE(ps.units_sold_90d, 0) > 0 AND cs.available_qty > 0
      AND (ps.units_sold_90d * 4.0 / cs.available_qty) < 2 THEN 'Slow Mover'
    WHEN COALESCE(ps.units_sold_90d, 0) > 0 AND cs.available_qty > 0
      AND (ps.units_sold_90d * 4.0 / cs.available_qty) >= 2
      AND (ps.units_sold_90d * 4.0 / cs.available_qty) < 6 THEN 'Moderate'
    WHEN COALESCE(ps.units_sold_90d, 0) > 0 AND cs.available_qty > 0
      AND (ps.units_sold_90d * 4.0 / cs.available_qty) >= 6 THEN 'Fast Mover'
    ELSE 'No Stock'
  END AS turnover_category
FROM
  current_stock cs
  LEFT JOIN product_sales_90d ps ON cs.product_id = ps.product_id AND cs.sku = ps.sku
WHERE
  cs.available_qty > 0
ORDER BY
  annual_turnover_rate ASC NULLS FIRST
product_titleskuunits_sold_90drevenue_90davailable_qtystock_value_at_costannual_turnover_rateturnover_category
Vintage ScarfVS-BLU00.00851,275.00Dead Stock
Wool GlovesWG-M389.9767670.000.18Slow Mover
Leather BeltLB-L28839.7215224.857.47Moderate
Classic White TeeCWT-M1563,900.0022220.0028.36Fast Mover
Canvas SneakersCS-42352,794.654159.9635.00Fast Mover
Stock Value by Turnover Category:
Turnover CategorySKU CountStock Value ($)% of Total ValueRecommended Action
Fast Movers (6x+/yr)2412,400.0015%Increase safety stock
Moderate (2-6x/yr)4824,800.0030%Maintain current levels
Slow Movers (< 2x/yr)3520,650.0025%Reduce reorder quantities
Dead Stock (0 sales)1824,750.0030%Markdown / bundle / discontinue
$24,750 in dead stock represents 30% of your total inventory value. Consider running clearance promotions, creating product bundles, or discontinuing these items to free up capital.

5. Abandoned Cart Recovery Intelligence

Analyze abandoned checkout data to understand lost revenue potential and identify high-value recovery opportunities. Business Value:
  • Quantify revenue lost from abandoned carts
  • Identify the most abandoned products to address pricing or UX issues
  • Prioritize recovery campaigns by cart value
WITH abandoned_analysis AS (
  SELECT
    ac.id,
    ac.createdAt,
    CAST(ac.totalPriceSet.presentmentMoney.amount AS DOUBLE) AS cart_value,
    ac.totalPriceSet.presentmentMoney.currencyCode AS currency,
    ac.customer.email AS customer_email,
    ac.completedAt,
    CASE WHEN ac.completedAt IS NOT NULL THEN 'RECOVERED' ELSE 'ABANDONED' END AS status,
    CARDINALITY(ac.lineItems) AS item_count
  FROM
    nekt_raw.shopify_abandoned_checkouts ac
  WHERE
    ac.createdAt >= CURRENT_DATE - INTERVAL '30' DAY
)
SELECT
  DATE(createdAt) AS abandon_date,
  COUNT(*) AS total_abandoned,
  COUNT(CASE WHEN status = 'RECOVERED' THEN 1 END) AS recovered,
  ROUND(COUNT(CASE WHEN status = 'RECOVERED' THEN 1 END) * 100.0 / COUNT(*), 1) AS recovery_rate_pct,
  ROUND(SUM(cart_value), 2) AS total_cart_value,
  ROUND(SUM(CASE WHEN status = 'ABANDONED' THEN cart_value ELSE 0 END), 2) AS lost_revenue,
  ROUND(SUM(CASE WHEN status = 'RECOVERED' THEN cart_value ELSE 0 END), 2) AS recovered_revenue,
  ROUND(AVG(cart_value), 2) AS avg_cart_value,
  ROUND(AVG(item_count), 1) AS avg_items_per_cart
FROM
  abandoned_analysis
GROUP BY
  DATE(createdAt)
ORDER BY
  abandon_date DESC
abandon_datetotal_abandonedrecoveredrecovery_rate_pcttotal_cart_valuelost_revenuerecovered_revenueavg_cart_valueavg_items_per_cart
2026-01-2734617.64,892.504,032.00860.50143.902.4
2026-01-2641819.55,740.804,618.301,122.50140.022.1
2026-01-2528414.33,980.203,410.00570.20142.152.6
2026-01-24521121.27,280.005,726.401,553.60140.002.3
2026-01-2338718.45,320.604,342.10978.50140.022.2
5-Day Abandonment Summary:
MetricValue
Total Abandoned Carts193
Total Cart Value$27,214.10
Lost Revenue$22,128.80
Recovered Revenue$5,085.30
Average Recovery Rate18.6%
Average Cart Value$141.01
At the current 18.6% recovery rate, improving recovery by just 5 percentage points (to ~24%) would capture an additional $2,800+ per week in revenue. Consider optimizing abandoned cart email timing, offering targeted discounts, or implementing SMS recovery flows.

Implementation Notes

GraphQL API

  • This connector uses the Shopify Admin GraphQL API for efficient data extraction
  • All monetary values are returned as strings (e.g., "29.99") — cast them to numeric types in your queries
  • The connector uses cursor-based pagination for optimal API performance

Incremental Sync

  • Orders, Products, Inventory Items, Customers, and Collections support incremental sync using updatedAt
  • Locations and Abandoned Checkouts use full table sync
  • Use incremental sync for large stores to minimize API calls and extraction time

Nested Data Structures

  • Line items, transactions, variants, metafields, and inventory levels are stored as nested arrays
  • Use UNNEST (GCP) or CROSS JOIN UNNEST (AWS) to flatten these arrays in your queries
  • The connector automatically flattens GraphQL connection structures (edges/node) into clean arrays

Inventory Quantities

  • The inventoryLevels[].quantities[] array contains multiple quantity types per location:
    • available — Units available for sale
    • committed — Units reserved for unfulfilled orders
    • incoming — Units expected from incoming transfers
    • on_hand — Total physical units at the location
  • Filter by name to get the quantity type you need

Price Sets

  • Shopify provides monetary values in both shop currency (your store’s base currency) and presentment currency (the customer’s local currency)
  • Use shopMoney for internal reporting and presentmentMoney for customer-facing analysis

API Scopes Required

The Shopify connector requires the following API scopes:
  • read_orders — Orders and transactions
  • read_products — Products, variants, and collections
  • read_customers — Customer profiles
  • read_inventory — Inventory items and levels
  • read_locations — Store locations