Skip to content
adapters.io

Shopify to BigQuery: how to load store data your team can actually model

9 min read Data engineering The Adapters team

Last updated July 2026

Field mapping auto-plugged · tap a port to rewire

5 sample records ready

To move Shopify data into BigQuery, read orders, line items, refunds, customers, and products through the Admin API by cursor, land each page in a staging table, and MERGE into a target keyed on the Shopify id. Track an updated_at watermark so you catch orders that were edited, refunded, or cancelled after they were created. Store money as NUMERIC, cast every timestamp to UTC, and partition your fact tables by order date.

Key takeaways

  • Orders change after creation. A nightly dump on created_at misses edits, refunds, and cancellations. Use the updated_at watermark instead.
  • Money arrives as strings. Shopify returns amounts as text in both shop and presentment currency, so cast to NUMERIC and pick one currency per column.
  • MERGE on the id and dedupe first. A watermark plus ROW_NUMBER() makes retries idempotent instead of duplicating rows.
  • Partition and cluster. BigQuery bills bytes scanned, so partitioning fact tables by order date keeps most queries cheap.

How do you connect Shopify to BigQuery?

You connect Shopify to BigQuery by reading the Admin API and writing the results into BigQuery tables, because there is no native BigQuery destination inside Shopify. Something has to sit in the middle: a managed connector, a scheduled script against the API, or a custom app. The managed route is the Shopify to BigQuery integration, which owns the pagination, the watermark, the staging table, the MERGE, and the retries.

The Admin API comes in two flavors, GraphQL and REST, and both paginate by cursor rather than by page number. You request a batch, get an opaque cursor back, and pass it to the next call. Throttling is cost based: Shopify runs a leaky bucket, so each query spends points from a bucket that refills at a steady rate, and a GraphQL query that asks for deep nested data costs more than a shallow one. That matters the moment you backfill several years of orders, because you are making thousands of sequential calls and each one can get throttled or time out.

Practically, the connection has three parts. First, an access token from a custom app with read scopes on orders, products, and customers. Second, a BigQuery landing spot: a project, a dataset, and a service account that can create tables and run MERGE. Third, the transfer itself, which pages through each object, lands raw or typed rows in a staging table, and merges into the target. If you also send store data to accounting, the pattern behind Shopify to QuickBooks is the same shape pointed at a different destination.

How do you load Shopify orders into BigQuery incrementally?

Load Shopify orders incrementally by keeping a watermark of the highest updated_at you have seen, requesting only orders changed since then, and MERGE-ing the result into BigQuery keyed on the order id. The updated_at filter is the important choice. Filtering on created_at feels simpler, but it only catches brand new orders and silently skips every order that was edited, refunded, or cancelled after the fact.

This is the trap that bites most first attempts. An order created on Monday can be edited on Tuesday, partially refunded on Wednesday, and cancelled on Friday. A naive nightly dump keyed on creation date grabs Monday's version once and never looks back, so your warehouse slowly drifts away from Shopify. Watching updated_at means every one of those changes bumps the order back into your next pull.

The load itself follows the same shape every time. Land the batch in a staging table, then collapse duplicates before you merge, because a single window can carry several versions of the same order:

Step What runs Why it matters
Dedupe the batch ROW_NUMBER() OVER (PARTITION BY id ORDER BY updated_at DESC), keep rank 1 Stops a multi-version window from creating duplicate order rows
MERGE on id MERGE INTO fct_order t USING staging s ON t.id = s.id Makes retries idempotent, so a re-run after a timeout produces the same state
Advance the watermark Store the max updated_at from the batch Next run resumes exactly where this one stopped

Overlap the window by a few minutes on each run so an order committed out of clock order is not skipped. Because the MERGE is keyed on the id and idempotent, re-reading a handful of orders costs nothing and buys you correctness.

How do Shopify fields map to BigQuery types?

Map Shopify ids to STRING, money strings to NUMERIC, timestamps to TIMESTAMP in UTC, and nested blocks like the shipping address or tax lines to the JSON type so you can query keys without flattening first. Start with the object layout, which splits facts from dimensions, then the field types.

Shopify object BigQuery table Notes
orders fct_order One row per order. Partition by order date, cluster by customer and status
line_items fct_order_line Belongs to an order. Grain is one row per product per order
refunds fct_refund Separate objects, not a flag on the order. Store as negative money
customers dim_customer Dimension. Join key for margin, cohorts, and LTV
products dim_product Dimension. Attach COGS here to make margin possible
Shopify field / type BigQuery type Notes
total_price (string, shop currency) NUMERIC Cast the text to NUMERIC. Never store money as FLOAT64
presentment amount (string) NUMERIC What the buyer paid in their currency. Keep it in its own column, do not mix with shop currency
currency / presentment_currency STRING ISO codes. They tell a reader which amount column applies
created_at / updated_at TIMESTAMP Store as UTC. updated_at is your incremental watermark
id (order, customer, product) STRING The natural MERGE key. Keep it as text even though it looks numeric
shipping_address / tax_lines JSON Nested objects. Store raw and query keys without a reload
financial_status / fulfillment_status STRING New values appear over time, so validate downstream rather than constraining

The money field deserves a second look, because it is a real correctness bug and not a style preference. Shopify returns amounts as strings, and it returns them twice: once in your shop currency and once in the buyer's presentment currency. If you cast the presentment amount into the same column as the shop amount, a report that sums that column adds euros to dollars to yen and produces a number that means nothing. Pick one currency per column, cast to NUMERIC, and keep the other in its own labeled field.

How do you calculate true margin and LTV from Shopify in BigQuery?

Calculate true margin by blending order revenue with cost of goods sold, payment fees, and ad spend, then subtracting refunds to get net. Shopify tells you what a customer paid, but it does not know what the product cost you to buy, what your processor kept, or what you spent on ads to win the sale. Those three numbers live outside Shopify, and margin is meaningless until you join them in.

Attach COGS at the dim_product grain so every line item can look up its unit cost. Bring payment fees in from your processor: if your store runs on Stripe, the fee and net settle on the balance transaction, and the mapping is spelled out in Stripe to BigQuery, which lands the fee column right next to your order revenue. Ad spend joins by date and channel from your marketing platforms. Gross revenue minus COGS minus fees minus refunds, allocated back to the order, is contribution margin you can actually trust.

LTV is a cohort roll-up on top of that same model. Stamp each customer with their first order date, group customers into monthly cohorts, and sum net margin per cohort over the months that follow. Because refunds reduce net and margin already nets out costs, the LTV curve reflects money you kept rather than money that briefly touched the account. That is the number worth spending against.

How do you handle refunds and cancelled orders in Shopify data?

Handle refunds by loading them as their own rows in fct_refund with negative amounts, and handle cancellations by trusting the order status you already refreshed through the updated_at watermark. Refunds are separate objects in Shopify, not a single field on the order, so a refund can be partial, can arrive days after the sale, and can stack (two partial refunds against one order). Modeling them as facts lets you sum the negatives against gross revenue for a clean net.

Cancellations are quieter but just as important. When an order is cancelled its updated_at moves, so an incremental load keyed on that field pulls the cancelled version and your MERGE overwrites the stale row. Read cancelled_at and financial_status in your reporting layer so a cancelled or fully refunded order stops counting as revenue. This is also where store operations meet the books: if your bookkeeper needs those payouts in accounting, you can convert a transactions CSV into a QBO file for import so the refunds and cancellations reconcile on both sides.

How much does it cost to sync Shopify to BigQuery?

BigQuery bills on two axes: storage for the bytes you keep, and query cost, which on-demand pricing charges by the bytes each query scans (or by slots if you buy dedicated capacity). Shopify volumes are usually modest, so storage is close to a rounding error. The cost that grows is query scan, and it grows fastest when dashboards read whole fact tables.

The fix is layout. Partition fct_order and fct_order_line by order date so a query for last month reads one month of data instead of all history, and cluster by customer and status so filtered reads prune further. A dashboard that scans a single partition costs a tiny fraction of one that scans the table, for the same answer. If your query patterns are steady and heavy, flat-rate slots can beat on-demand, but most stores do fine on-demand once partitioning is in place.

On the pipeline side, our pricing is flat by plan (Starter $49, Growth $149, Scale $399, Enterprise custom) rather than metered per row, so a big sales month does not produce a surprise invoice. See pricing for the full breakdown. Between a flat pipeline bill and partitioned tables that keep scans small, the total cost of moving Shopify into BigQuery stays predictable even as your order volume climbs.

Sync Shopify into BigQuery with orders, refunds, and margin ready to model

Cursor reads, watermark loads, dedupe, MERGE, and retries. Map the pair in the browser and pay a flat price from $49 a month.

Try the live demo

No credit card required.