Big picture: what are "Shopify backend integration layers"?
If you've integrated with any platform before, the mental model here is familiar. A Shopify store is a hosted system that owns your commerce data โ products, inventory, orders, customers, fulfillment. Your job, as an integration engineer, is to connect that system to the rest of your world: an ERP, a warehouse system, a CRM, a data warehouse, custom pricing logic, and so on.
"Integration layers" is just the set of surfaces where data and control cross the boundary between Shopify and everything else. There are only a few of them, and they each answer a different question:
- Admin API (GraphQL & REST) โ "How does my backend read and write Shopify data?" You call Shopify on a schedule or on demand.
- Webhooks โ "How does Shopify tell me the moment something changes?" Shopify calls you when events happen.
- Shopify Functions โ "How do I customize Shopify's own decisions?" Small logic that runs inside Shopify, right in critical flows like checkout.
- Data & analytics / ShopifyQL โ "How do I get clean data out for reporting and BI?" Querying and exporting for analytics rather than transactions.
A useful way to hold all four in your head: the first two are about moving data across the boundary (pull vs push), the third is about injecting logic into Shopify, and the fourth is about getting data out for analysis without abusing the transactional APIs.
One more framing that pays off immediately: in Shopify, almost everything you build attaches to an "app." Even a private integration that only your company uses is technically a custom app. The app is the thing that holds your credentials, your permission scopes, your webhook subscriptions, and your deployed Functions. Keep that in mind as you read โ "create an app" keeps showing up because it's the container for all of it.
Admin API โ the core backend API
The Admin API is your main read/write channel into a store. When your backend needs to know about or change something in Shopify, this is almost always how it happens. Typical jobs:
- Sync products and prices from an ERP/PIM into Shopify.
- Pull orders into a data warehouse or accounting system.
- Push inventory levels from a WMS when stock changes.
- Sync customers out to a CRM, or enrich Shopify customers from one.
- Create fulfillments, issue refunds, manage discounts, and so on.
GraphQL vs REST โ and the important 2024 change
The Admin API comes in two flavors, but they are no longer equal footing, and this is the single most important thing to internalize as a newcomer:
- GraphQL Admin API โ the modern, primary API. You ask for exactly the fields you want in one request, which avoids over-fetching and reduces round-trips. Shopify is investing all new capabilities here (including the larger product/variant limits).
- REST Admin API โ the older, familiar resource-per-endpoint style. As of October 1, 2024 it is officially "legacy." Since April 1, 2025, new public apps must be built on GraphQL. Existing/custom apps can still call REST for now, but it receives no new features and Shopify is steering everyone off it.
A tiny GraphQL request looks like this โ note you name the fields you want back:
# POST /admin/api/2025-10/graphql.json
query {
orders(first: 10, query: "financial_status:paid") {
edges { node {
id
name
createdAt
totalPriceSet { shopMoney { amount currencyCode } }
} }
}
}
Authenticated HTTPS calls from your backend or integration platform โ a GraphQL query/mutation (or a REST request), carrying an access token in the header.
JSON containing Shopify data โ products, orders, customers, inventory, fulfillments, etc. With GraphQL, the response shape mirrors exactly what you asked for.
โ A Shopify app (a custom app is fine for internal use). โก An access token plus the right scopes (e.g. read_orders, write_inventory) โ request only what you need. โข Awareness of rate limits so you don't get throttled.
GraphQL uses a calculated-cost rate limit (each query has a point cost against a refilling bucket); REST uses a simpler request-count limit. Either way: pace yourself and back off when told to.
When polling is fine vs when you want events
The Admin API is a pull mechanism, so you decide how often to pull. Simple scheduled polling ("sync every 5โ15 minutes") is perfectly acceptable when near-real-time is good enough โ nightly catalog syncs, periodic reconciliation, batch exports. But polling for things that need to feel instant (e.g. reacting the second an order is placed) is wasteful and laggy. That's exactly what webhooks are for. A very common, healthy pattern is webhooks for "react now" + a periodic poll as a safety net to catch anything missed.
Must-doNon-negotiable
- Handle rate limits. Detect throttling and back off (exponential backoff + retry). Never hammer through a
429. - Store credentials securely. Access tokens are secrets โ vault/secret manager, never in code or logs.
- Don't treat it as a reporting engine. No giant analytical scans on the transactional API. Use the analytics path instead (ยง5).
- Pin an API version and track Shopify's quarterly releases so you migrate deliberately, not by surprise.
OptionalNegotiable
- REST vs GraphQL โ though for new work, GraphQL is the clear default.
- How real-time your sync is โ every few minutes vs near-instant via webhooks.
- Where the integration runs โ your own service vs an iPaaS/integration platform.
- Bulk operations for very large reads/writes โ adopt when volume demands it.
GraphQL in depth โ how it actually works
Every GraphQL request โ query or mutation โ is a single POST to one endpoint:
POST https://{shop}.myshopify.com/admin/api/2026-04/graphql.json
X-Shopify-Access-Token: {your_token}
Content-Type: application/json
The body carries a query string and optionally a variables map. Shopify resolves only the fields you asked for and returns them under a data key. Any server-side problems land in an errors array โ the HTTP status can still be 200, so always check errors in the response body, not just the status code.
Queries vs mutations vs subscriptions
Read-only fetch. Ask for exactly the fields you need โ nothing more. Example: fetch an order with its line items and the shopMoney amounts only.
Write operation (create / update / delete). Returns the modified resource or a userErrors array on validation failure. Example: fulfillmentCreate, inventoryAdjustQuantities, orderClose.
Not used in server-to-server flows. Webhooks (ยง03) are the event mechanism for backend integrations.
Pagination โ cursor-based
Shopify GraphQL uses cursor-based pagination. Every list field wraps results in a connection with edges / node and a pageInfo block:
query {
orders(first: 50, after: "cursor_from_prev_page", query: "financial_status:paid") {
edges { node { id name createdAt } }
pageInfo { hasNextPage endCursor }
}
}
When hasNextPage is true, pass endCursor as the next after argument. Never use offset-based counting โ results are not stable across pages that way.
Mutations and userErrors
mutation FulfillOrder($fulfillment: FulfillmentInput!) {
fulfillmentCreate(fulfillment: $fulfillment) {
fulfillment { id status trackingInfo { number url } }
userErrors { field message }
}
}
userErrors is non-null when Shopify accepts the HTTP request but rejects the business logic (e.g. order already fulfilled, invalid location). Always check it before treating a mutation as successful.
Cost-based rate limiting (calculated query cost)
Unlike REST's request-count bucket, GraphQL uses a point-cost system. Each field and connection depth adds to the cost of a query. The response extensions tell you exactly how much you spent:
// extensions block in every GraphQL response
"extensions": {
"cost": {
"requestedQueryCost": 42,
"actualQueryCost": 38,
"throttleStatus": {
"maximumAvailable": 2000,
"currentlyAvailable": 1962,
"restoreRate": 100
}
}
}
The bucket refills at 100 points/second up to a maximum of 2 000. If currentlyAvailable drops to zero, Shopify returns a THROTTLED error. Back off and retry โ do not hammer. In HotWax, the Moqui service that calls GraphQL should read extensions.cost.throttleStatus and insert a sleep when headroom is low.
Bulk operations โ for large data pulls
When syncing a full catalog or large order history, individual queries hit cost limits fast. Shopify's Bulk Operations API is the right tool: you kick off an async bulkOperationRunQuery mutation, Shopify processes it server-side, and you download the result as a .jsonl file from a signed URL once the job finishes. HotWax uses this pattern for initial catalog imports from Shopify.
mutation {
bulkOperationRunQuery(
query: """
{ products { edges { node { id title variants { edges { node { sku price } } } } } }
"""
) {
bulkOperation { id status }
userErrors { field message }
}
}
Poll with { currentBulkOperation { id status url } } until status is COMPLETED, then fetch the url. Each line of the .jsonl is one GraphQL node โ parse with a streaming reader to avoid memory pressure.
Key namespaces HotWax uses via GraphQL
orders query + order by ID. Mutations: orderClose, orderCancel, draftOrderComplete. Fields to watch: totalPriceSet.shopMoney vs presentmentMoney, lineItems.edges.node.id (the orderItemExternalId stored in HotWax), fulfillmentOrders.
inventoryLevel / inventoryLevels, inventoryItem. Mutations: inventoryAdjustQuantities, inventorySetOnHandQuantities. HotWax pushes ATP back to Shopify via these mutations after brokering.
fulfillmentCreate mutation (with tracking numbers), fulfillmentOrderAcceptCancellationRequest. Used when HotWax marks a shipment as shipped and needs to update Shopify.
products / product query, productVariant. Used for catalog sync โ pulling title, description, images, variant SKUs, prices, and metafields into the HotWax MDM/PIM.
Webhooks โ the event layer
Webhooks flip the direction. Instead of you asking Shopify "anything new?", Shopify proactively sends an HTTP POST to a URL you control the moment something happens โ an order is created, a product is updated, an order is paid or cancelled, inventory changes, and so on.
You subscribe to specific event topics (like orders/create or products/update) and give Shopify a destination. When the event fires, your endpoint receives a JSON payload describing what changed. A payload skeleton for an order looks roughly like:
// POST from Shopify โ https://api.yourco.com/hooks/shopify
{
"id": 820982911946154500,
"name": "#1001",
"financial_status": "paid",
"line_items": [ { "sku": "ABC-01", "quantity": 2 } ],
"customer": { "id": 115310627314723950 }
}
// Header: X-Shopify-Hmac-SHA256: <signature you MUST verify>
Your configuration: which topics you care about + a reachable HTTPS URL (or an integration platform / event bus endpoint). You can also stream to managed destinations like a pub/sub queue.
JSON payloads delivered to your endpoint describing the changed resource. Your service responds 200 quickly to acknowledge receipt.
โ A public HTTPS endpoint Shopify can reach. โก The ability to verify the HMAC signature on each request. โข A plan for retries & duplicates โ Shopify retries on failure, so the same event can arrive more than once.
Webhooks vs polling
Freshness and efficiency. You hear about changes within seconds and you don't burn API calls asking "anything new?" thousands of times a day.
Reliability engineering. You own signature verification, retries, deduplication, and an always-available endpoint. Miss a delivery and you may need a reconciliation poll to recover.
Polling is conceptually simpler and stateless-feeling, but it's less real-time and less efficient at scale. Most mature integrations use both: webhooks for immediacy, plus a low-frequency poll to heal any gaps.
Must-doNon-negotiable
- Verify the HMAC signature on every webhook. Unsigned/invalid requests are rejected โ this is your authenticity check.
- Make handlers idempotent. Assume any event can arrive twice; processing it again must be safe (dedupe on event/resource id).
- Don't do heavy work in the request. Acknowledge fast, then offload to a background job/queue. Slow handlers cause timeouts and retries.
- Return 2xx quickly on success; let failures trigger Shopify's retry rather than hanging.
OptionalNegotiable
- Which topics you subscribe to first โ start with a small, high-value set (e.g.
orders/create) and grow. - Whether you keep a safety-net poll alongside webhooks (recommended, but a judgment call).
- Delivery target โ your own endpoint vs a managed queue/event-bus destination.
Webhooks in depth โ subscription methods & HotWax integration
Two ways to register subscriptions
Declarative. Subscriptions live in shopify.app.toml under a [webhooks] block and are applied to every store that installs the app. Requires Shopify CLI โฅ 3.63. Best for standard, app-wide topics.
[webhooks]
api_version = "2026-04"
[[webhooks.subscriptions]]
topics = ["orders/create", "orders/updated", "orders/cancelled"]
uri = "https://api.yourco.com/hooks/shopify"
[[webhooks.subscriptions]]
topics = ["inventory_levels/update"]
uri = "pubsub://your-project:your-topic"
Imperative, shop-specific. Use the webhookSubscriptionCreate mutation when you need per-shop control or topics not available in the config file.
mutation webhookSubscriptionCreate(
$topic: WebhookSubscriptionTopic!,
$webhookSubscription: WebhookSubscriptionInput!
) {
webhookSubscriptionCreate(
topic: $topic,
webhookSubscription: $webhookSubscription
) {
webhookSubscription { id topic format uri }
userErrors { field message }
}
}
Payload filtering & field trimming
Two customization tools Shopify provides that reduce noise in your handlers:
A rule expression that gates delivery โ Shopify only sends the webhook if the resource matches. Example: fire products/update only for active products in a specific product type. Configured via filter in the TOML subscription block.
[[webhooks.subscriptions]]
topics = ["products/update"]
uri = "https://api.yourco.com/hooks/product-update"
filter = "status:active AND product_type:Apparel"
Trims the payload to only the fields you declare. Reduces payload size and avoids processing data you'll discard anyway. Example: for customer deletes, you only need id and email.
[[webhooks.subscriptions]]
topics = ["customers/delete"]
uri = "https://api.yourco.com/hooks/customer-delete"
include_fields = ["id", "email"]
Key topics HotWax subscribes to
orders/createPrimary trigger for order ingestion into HotWax OMS. The payload carries line items, customer, shipping address, financial_status, and sales_channel. HotWax maps this to a new OrderHeader + OrderItem + OrderItemShipGroup.
orders/updatedFires on any field change to an order โ edits, partial cancellations, address updates. HotWax re-fetches the order via GraphQL on receipt rather than trusting the potentially-stale webhook payload.
orders/cancelledTriggers cancellation flow in HotWax โ releases reserved inventory and updates OrderStatus to CANCELLED.
products/create / products/updateKeeps the HotWax product catalog in sync with Shopify. On update, HotWax re-syncs variant-level data (price, SKU, inventory policy) via the GraphQL product query.
inventory_levels/updateUsed in scenarios where Shopify is the inventory source of truth (less common when HotWax owns ATP). Typically disabled or filtered to specific locations in HotWax-managed setups to avoid circular updates.
fulfillments/createFires when a fulfillment (with optional tracking) is created on Shopify's side. HotWax listens here if a 3PL updates Shopify directly, to reconcile Shipment and ItemIssuance records.
Delivery reliability & retries
Shopify retries failed deliveries (non-2xx or timeout) with exponential backoff for up to 48 hours. This means your handler will see duplicate events. The standard pattern in HotWax's Moqui services is:
- Acknowledge
200 OKimmediately (within 5 seconds). - Write the raw payload to a queue /
WebhookEvententity. - Abackground service picks it up, checks if the
idwas already processed (X-Shopify-Webhook-Idheader), and skips if so. - Otherwise, run the business logic (order import, catalog update, etc.).
HMAC verification โ mandatory
Every Shopify webhook carries an X-Shopify-Hmac-SHA256 header โ a base64-encoded HMAC-SHA256 of the raw request body, keyed with your app's client secret. Verify it before processing:
// Groovy / Moqui equivalent logic
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
def rawBody = request.inputStream.bytes
def secret = "your_client_secret"
def mac = Mac.getInstance("HmacSHA256")
mac.init(new SecretKeySpec(secret.bytes, "HmacSHA256"))
def computed = mac.doFinal(rawBody).encodeBase64().toString()
def shopifyHdr = request.getHeader("X-Shopify-Hmac-SHA256")
if (computed != shopifyHdr) return [status: "error", message: "Invalid signature"]
200. The actual OMS work (creating orders, updating inventory) happens asynchronously in a Moqui service job โ keeping the webhook handler fast and decoupled from OMS transaction time.
Shopify Functions โ business logic inside Shopify
Functions are different in kind from the first two surfaces. Instead of moving data across the boundary, you're shipping a small piece of your logic to run inside Shopify, right in the middle of critical flows where calling out to an external service would be too slow or too risky.
They're how you customize Shopify's own decisions: discount calculations, shipping/delivery option logic, payment method ordering, cart and checkout validations, and similar. Shopify hands your Function a defined input, your code runs in milliseconds, and it returns a decision.
A defined, schema-bound input Shopify passes you โ data about the cart, line items, products, customer, etc. You declare exactly which fields you need via an input query.
A structured decision โ e.g. a discount amount, which delivery/payment options are allowed or reordered, whether to block checkout with a validation error.
โ A Shopify app that defines and deploys the Function. โก Familiarity with that Function type's schema and basic programming โ Functions compile to WebAssembly (Rust is the recommended language; JS/others are supported).
Extremely fast and deterministic, with tight CPU/memory/time limits. It runs as part of Shopify's flow, not as a long-running service.
What they're great at โ and what they're not
Fast, reliable, and right next to checkout. Perfect for decisions that must happen in-flow: pricing rules, discount logic, shipping/payment customization, validations.
Not for workflows or heavy integration. By default a Function cannot make arbitrary network calls or do long-running work โ it must be self-contained and quick.
fetch-then-run pattern rather than free-form HTTP. Treat "no external calls" as your default mental model. If you find yourself wanting one, first ask whether the data could instead be pre-loaded into a metafield the Function can read โ that's usually the cleaner answer.
The healthy division of labor: keep complex/heavy logic in your external systems (kept in sync via the Admin API and webhooks), and push only the fast, in-flow decision into a Function. Don't try to make a Function the brain of your integration.
Must-doNon-negotiable
- Don't rely on Functions to call external systems. Design them to be self-contained by default.
- Keep logic simple and deterministic. Same input โ same output, well within the time/CPU budget.
- Use metafields to feed in data the Function needs, rather than reaching out at runtime.
OptionalNegotiable
- How much logic you move into Functions vs keep external and sync via API.
- Language choice (Rust recommended for performance; others compile to Wasm too).
- Whether enterprise network access is worth pursuing for a specific edge case.
Data & analytics / ShopifyQL โ getting data out
Reporting and BI are a separate concern from your transactional integrations, and conflating them is a classic early mistake. Shopify gives you purpose-built ways to query and export data for analysis, so you don't end up scanning the transactional Admin API to build dashboards.
The main paths:
- ShopifyQL โ Shopify's commerce-focused, SQL-like query language for analytics (sales, orders, products, customers, sessions), with built-in time-series and period-comparison support. Historically it lived in the admin's report editor; as of October 2025 you can also run ShopifyQL programmatically via a
shopifyqlQueryfield on the GraphQL Admin API (and there's a Python SDK for analytics workflows). - Built-in reports & exports โ the admin's own analytics, plus CSV/data exports of orders, customers, products for ad-hoc needs.
- A data warehouse โ for anything serious, you move Shopify data (via Admin API / bulk operations / webhooks) into a warehouse and treat Shopify as one source among many.
-- ShopifyQL: monthly sales this year
FROM sales
SHOW total_sales
GROUP BY month
SINCE startOfYear(0y)
ORDER BY month
Queries (ShopifyQL) or export/extract requests. Programmatic ShopifyQL goes through the GraphQL Admin API.
Structured tabular data โ rows/columns of orders, customers, products, sales metrics โ ready for reports, dashboards, or a warehouse load.
A working understanding of Shopify's core data objects โ orders, line items, customers, products, inventory โ and how they relate. (Plan availability can affect some analytics features.)
Built-in reporting vs your own stack
You need standard commerce reports for a single store, day-to-day operational decisions, and don't need to blend Shopify with other data sources. Start here.
You need to join Shopify with ERP/CRM/marketing data, keep long history, do custom modeling, or serve a BI team. Then Shopify becomes one input to a proper pipeline.
Must-doNon-negotiable
- Don't hammer transactional APIs for analytics workloads. Use the analytics/export paths designed for it.
- Respect rate limits even on ShopifyQL-over-GraphQL โ it shares the same budget.
OptionalNegotiable
- Whether to invest early in a warehouse โ or start with built-in reports/exports and graduate later.
- Which BI tooling sits on top, and how often you refresh.
Putting it together: starter patterns
Two concrete architectures a new team can actually ship. Both are intentionally modest โ you can layer more on later.
Pattern A โ Small brand, one external system (ERP / inventory)
Goal: keep catalog and inventory aligned, and capture orders, without overbuilding.
periodic push/pull โ
orders/create โ
Who owns what: the Admin API handles scheduled product/inventory sync (every few minutes is fine). A small set of webhooks (orders/create, maybe orders/paid) gives near-real-time order capture so fulfillment isn't waiting on the next poll. A nightly reconciliation poll heals any missed webhook. No Functions, no warehouse yet โ built-in reports cover analytics.
Pattern B โ Brand that wants smarter pricing/discount logic
Goal: custom discount rules at checkout, while keeping external systems in sync.
+ webhooks โ
Who owns what: a Shopify Function makes the fast, in-flow discount decision โ fed by metafields your backend populates ahead of time (not live calls). The Admin API keeps products and rule metafields current; webhooks keep your backend informed of orders and product changes. The heavy "how should pricing work" logic lives in your backend; only the lightweight decision lives in the Function.
Summary โ negotiables vs non-negotiables
Must-doTrue for almost any serious integration
- Respect Admin API rate limits and handle retries with backoff.
- Verify every webhook (HMAC) and make handlers idempotent.
- Never block checkout or critical flows on slow external calls โ keep in-flow logic inside Shopify (Functions) and everything else asynchronous.
- Store secrets safely (API keys, access tokens) in a real secret manager.
- Keep analytics off the transactional APIs โ use ShopifyQL/exports/a warehouse.
- Pin and track API versions so quarterly changes never surprise you.
OptionalDecide later as you mature
- REST vs GraphQL โ but default to GraphQL for anything new; REST is legacy.
- How real-time your sync needs to be (minutes vs seconds).
- How heavily you use Functions vs external logic synced via API.
- Whether and when to build a dedicated analytics pipeline.
- Own service vs integration platform for running the glue.
- Bulk operations โ adopt when data volume justifies it.
If you remember nothing else: pull with the Admin API, get pushed to via webhooks, inject in-flow decisions with Functions, and pull analytics out separately. Get the four non-negotiables right (rate limits, webhook verification + idempotency, never block checkout, secure secrets) and you'll have a foundation that scales cleanly from your first sync to a full commerce platform integration.