Table of Contents
- Key Highlights
- Introduction
- What changed in GraphQL Admin API 2026-04
- Technical details and query patterns
- Why Shopify made this change
- Impact on apps, reporting, and integrations
- Migration checklist — what to do and why
- Code patterns and examples
- Edge cases and pitfalls to watch
- Recommended reporting patterns
- Testing strategy
- Real migration scenarios and timelines
- Governance and internal policies
- Performance and rate-limit considerations
- Communication to merchants and partners
- Long-term considerations
- FAQ
Key Highlights
- The InventoryLevel object now includes an isActive boolean and, when requested, the API returns inventory levels that were previously hidden after deactivation.
- Default queries remain unchanged (they continue returning only active levels); apps that need to include inactive levels must opt in with includeInactive and should use isActive to filter and reconcile counts.
- Apps and integrations must audit reporting, aggregation, UI assumptions, and reconciliation logic to avoid double-counting or misreporting when inactive levels are included.
Introduction
Shopify’s GraphQL Admin API update for 2026-04 restores visibility and control over inventory levels that had previously been hidden when deactivated. The InventoryLevel object now exposes an isActive boolean and the API supports an includeInactive argument to return deactivated inventory levels. This change affects how inventory quantities are reported, displayed, and reconciled across apps, analytics pipelines, and fulfillment systems. The update preserves backward compatibility for existing queries but introduces new responsibilities for integrators who choose to include inactive levels.
This article explains the change, why it matters, where it will affect your systems, and how to migrate safely. It includes concrete code examples, recommended practices for reporting and UI, and a migration checklist you can apply to production systems and test environments.
What changed in GraphQL Admin API 2026-04
Prior to 2026-04, when an inventory level was deactivated Shopify cleared its associated quantities and excluded that level from API responses. That behavior created an effective loss of historical inventory data in API responses—deactivated locations simply disappeared from InventoryLevel queries.
The 2026-04 release adjusts two core behaviors:
- Deactivation no longer clears quantities. An inventory level that is deactivated retains its quantity values and can be adjusted programmatically.
- The InventoryLevel object now includes an isActive boolean field. The API can return inactive inventory levels if you explicitly request them via includeInactive. This gives apps the ability to distinguish between locations that were never tracked and locations that were tracked and later deactivated.
Default behavior: If you do nothing, your existing queries continue to return only active inventory levels. The new behavior only affects responses when includeInactive is provided or when you rely on the isActive field in returned nodes.
Technical details and query patterns
The InventoryLevel object now contains an isActive field. The GraphQL API also accepts an includeInactive argument on inventoryLevels and inventoryLevel fields, enabling clients to fetch levels that are inactive.
A typical default query (unchanged) might look like this:
query {
productVariant(id: "gid://shopify/ProductVariant/123456789") {
inventoryLevels(first: 50) {
edges {
node {
id
available
location {
id
name
}
# isActive is present now, but won’t matter unless includeInactive is used
isActive
}
}
}
}
}
Because the API still defaults to active-only levels, the query above will return only active inventory levels unless you request otherwise. To include inactive levels:
query {
productVariant(id: "gid://shopify/ProductVariant/123456789") {
inventoryLevels(first: 100, includeInactive: true) {
edges {
node {
id
available
isActive
location {
id
name
}
}
}
}
}
}
Interpreting the response:
- isActive: true indicates the inventory level is active and being tracked.
- isActive: false indicates the level exists but has been deactivated; its quantities are preserved and can be adjusted.
Filtering on the client side If your app only wants active levels, filter using isActive:
- Option A: Don’t pass includeInactive (server returns only active).
- Option B: Pass includeInactive: true when you need context, but filter client-side for isActive === true when computing totals.
Pagination concerns Including inactive levels can increase the number of returned nodes. Always respect pagination (first/after cursors) and make queries resilient to larger result sets. Avoid requesting extremely large pages; use cursors for safe, incremental retrieval.
Mutation and adjustment possibilities The API behavior change specifically notes that inactive levels can be adjusted just like active ones. That implies mutations that update inventory quantities or perform adjustments should work on inactive levels; ensure your app has permission and logic to handle updates to inactive nodes when appropriate.
Why Shopify made this change
The previous behavior—clearing quantities and hiding deactivated inventory levels—produced two practical problems for merchants and integrators:
- Loss of visibility: Deactivated locations disappeared from API responses, removing important context for historical tracking and reconciliation.
- Confusing reconciliation: Clearing quantities on deactivation meant the act of deactivating changed historical quantity values, complicating audits and reconciliation across systems.
The 2026-04 change preserves quantities on deactivation and exposes the activation state explicitly. That lets apps:
- Differentiate between “never tracked” and “tracked and later deactivated” locations.
- Adjust inactive levels without having to re-create or infer prior quantities.
- Produce more accurate historical reports and inventory reconciliations.
This change improves data integrity and makes integrations less brittle by ensuring deactivation is a non-destructive operation from an API visibility standpoint.
Impact on apps, reporting, and integrations
The update affects many kinds of systems that rely on InventoryLevel responses. Below are the most common places you will see an effect and how to adapt.
- Aggregations and stock totals If your app aggregates available inventory across locations, including inactive levels could change totals if you start querying them. Two patterns emerge:
- Keep totals limited to active levels: Do not request includeInactive or filter out inactive nodes in your aggregation.
- Provide comprehensive reporting: Use includeInactive to surface deactivated-level quantities for audits or reconciliation, but clearly separate those totals from active inventory.
Example: A retailer with three fulfillment locations, one deactivated but holding 50 units. An app that starts including inactive levels in a total stock sum without filtering will add 50 units to the available inventory figure, misleading fulfillment decisions.
- UI and merchant experience Dashboards, location lists, and product pages may now receive nodes with isActive:false. Update interfaces to:
- Display activation status clearly (e.g., a badge or column “Location status: inactive”).
- Separate active stock from inactive stock in summaries, with a clear explanation of what “inactive” implies.
- Provide actions: allow merchants to reactivate levels, inspect adjustments, or archive location-level history.
- Fulfillment and order routing Order routing and fulfillment logic must continue to use only active inventory thresholds when deciding where to fulfill an order—unless your business rules explicitly allow fulfillment from inactive locations. Verify:
- Fulfillment engines filter by isActive where appropriate.
- Pick lists and transfer recommendations exclude inactive locations by default.
- Synchronization with external systems (ERP/POS/WMS) Third-party systems that periodically pull inventory levels must decide whether to sync inactive levels:
- If your external system expects only active locations, continue to request active-only levels.
- Integrate inactive-level data when performing audits, return handling, or historical reconciliation.
- Avoid blindly writing inactive-level quantities into external systems where status mapping doesn’t exist.
- Inventory adjustments and reconciliation workflows Because inactive levels can now be adjusted, workflows that previously reactivated a level to correct quantities are no longer required. However:
- Add checks in adjustment UIs to indicate the level’s isActive status before applying changes.
- Preserve audit trails: record why an adjustment was made on an inactive level and by whom.
- Webhooks and event-driven systems The change does not explicitly alter webhook payloads, but any webhook handler that assumes only active inventory levels will be present should be reviewed. For example:
- Inventory change handlers should check isActive where logic depends on the activation state.
- If you maintain a local cache of inventory levels keyed to locations, consider adding isActive to the cached schema.
- Analytics and business intelligence Data pipelines that ingest InventoryLevel nodes must clearly tag isActive and separate totals for active vs inactive. When building reports:
- Store isActive as a dimension.
- Provide rollsups excluding inactive levels by default, and include them as an optional dimension for audits.
Real-world examples
- Multi-channel retailer: A brand operates storefronts and third-party pick-up locations. One pick-up location was deactivated before physical stock was moved; quantities remained attached. If a reporting app begins including inactive levels, pickup availability could appear where it’s not truly available for customers. The correct behavior: include inactive levels only for inventory audits and mark them as unavailable for customer-facing channel availability.
- Warehouse migration: A fulfillment center gets deactivated when a third-party logistics (3PL) partnership ends, but the warehouse’s inventory wasn't physically shipped out at the time of deactivation. With quantities preserved, the merchant can reconcile and request the 3PL to return remaining units or adjust transfer requests without losing historical counts.
Migration checklist — what to do and why
Adopt this checklist to reduce risk when you opt to include inactive levels or simply to ensure compatibility.
- Inventory your queries and services
- List all GraphQL and REST endpoints that currently fetch InventoryLevel or inventoryLevels.
- Identify services that compute aggregated counts from those queries.
- Decide on a policy for including inactive levels
- Policy A (recommended conservative): Keep default behavior—do not pass includeInactive in day-to-day production queries. Use includeInactive only in reconciliation, audit, or admin contexts.
- Policy B (explicit): If an app requires a complete historical view, always pass includeInactive but explicitly filter or mark isActive in UI and reports.
- Update backend aggregation logic
- When summing quantities, either:
- Filter to isActive: true, or
- Maintain separate aggregates for active and inactive levels.
- Update front-end displays and UX
- Add clear indicators for location activation state.
- Provide user controls to include/exclude inactive counts in summaries.
- Adjust reconciliation and reporting jobs
- Tag historical data with isActive and maintain an audit log of changes to activation state.
- Revise ETL pipelines to store isActive as a dimension.
- Add tests
- Unit tests: Ensure inventory aggregation functions handle both active and inactive nodes.
- Integration tests: Simulate includeInactive:true responses and validate UI behavior and totals.
- End-to-end tests: Confirm fulfillment decisions remain unchanged when only active nodes are considered.
- Verify mutation and permission flows
- Check that your app has permission to adjust inactive inventory levels and that business rules allow adjustments.
- If your app automatically adjusts inventory on events, ensure it doesn’t react inappropriately to adjustments on inactive levels.
- Monitor after release
- Add metric tracking for when includeInactive is used.
- Set alerts for unexpectedly large delta changes in aggregated inventory totals that could indicate logic errors.
Code patterns and examples
Below are safe, practical patterns you can adapt.
JavaScript (Node) — fetch active-only inventory levels (default):
const fetch = require('node-fetch');
async function getActiveInventoryLevels(variantId, pageSize = 50) {
const query = `
query inventory($id: ID!, $first: Int!) {
productVariant(id: $id) {
inventoryLevels(first: $first) {
edges {
node {
id
available
isActive
location {
id
name
}
}
}
}
}
}
`;
const variables = { id: variantId, first: pageSize };
const res = await fetch('https://your-shopify-endpoint/admin/api/2026-04/graphql.json', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Shopify-Access-Token': process.env.SHOPIFY_TOKEN,
},
body: JSON.stringify({ query, variables }),
});
const json = await res.json();
return json.data.productVariant.inventoryLevels.edges.map(e => e.node);
}
JavaScript — include inactive levels and filter:
async function getInventoryLevelsIncludeInactive(variantId, pageSize = 100) {
const query = `
query inventory($id: ID!, $first: Int!) {
productVariant(id: $id) {
inventoryLevels(first: $first, includeInactive: true) {
edges {
node {
id
available
isActive
location {
id
name
}
}
}
}
}
}
`;
// fetch like above...
}
Python (requests) — filtering totals for active levels:
import os, requests
def fetch_inventory_levels(variant_gid, include_inactive=False):
query = """
query ($id: ID!, $first: Int!, $includeInactive: Boolean) {
productVariant(id: $id) {
inventoryLevels(first: $first, includeInactive: $includeInactive) {
edges { node { id available isActive location { id name } } }
}
}
}
"""
variables = {"id": variant_gid, "first": 50, "includeInactive": include_inactive}
headers = {
"Content-Type": "application/json",
"X-Shopify-Access-Token": os.environ["SHOPIFY_TOKEN"]
}
r = requests.post("https://your-shopify-endpoint/admin/api/2026-04/graphql.json", json={"query": query, "variables": variables}, headers=headers)
data = r.json()
nodes = [edge['node'] for edge in data['data']['productVariant']['inventoryLevels']['edges']]
return nodes
def total_active_available(nodes):
return sum(n['available'] for n in nodes if n['isActive'])
GraphQL filtering on server (when needed) If you have a server-side GraphQL wrapper or caching layer, filter on isActive on the server to reduce client complexity. For example, your server could translate a client param includeInactive:false into passing includeInactive omitted to Shopify, or pass includeInactive:true to Shopify but then filter nodes before returning to clients.
Bulk operations and backfills If you intend to backfill local analytics with inactive-level data, follow safe batching. Because includeInactive can return many more nodes, design your ETL to:
- Fetch pages with cursors.
- Persist isActive per record and use update-on-conflict semantics to avoid duplicate writes.
- Tag the ingestion run with timestamp and source to track provenance.
Edge cases and pitfalls to watch
- Double counting across systems: If a third-party service has previously removed deactivated location records and you start ingesting inactive levels, you may now count those quantities twice if your pipeline also ingests a separate historical export.
- Assumptions about zero quantities: Previously deactivation cleared quantities to zero. Now deactivated levels can have non-zero available values. Any logic that treats missing levels as zero must be updated.
- UI assumptions: Merchant-facing UI that displays “total available stock” must clearly indicate whether inactive quantities are included. A sudden change in visible totals could confuse operations teams.
- Concurrency: If a background job adjusts inventory and another job reactivates a level, ensure atomicity in your own systems to avoid race conditions. Use Shopify-provided idempotency or transaction patterns where available.
- Mapping to location status: Shopify's location object has its own lifecycle. Deactivated inventory level does not necessarily mean the Location itself is deleted. Confirm mapping semantics for your use case.
Recommended reporting patterns
Treat inventory reporting with a separation of concerns. Maintain two canonical aggregates:
- Active Inventory Total: Sum of available where isActive === true. This is the operational number used for sales, fulfillment routing, and order acceptance.
- Historical/Inactive Inventory Total: Sum of available where isActive === false. Use for audit, reconciliation, transfers, or inventory recovery efforts.
Reporting examples
- Dashboard: Show Active Inventory prominently. Provide a collapsible section labeled “Inactive inventory (for audit)” that lists totals per product and location.
- Daily reconciliation job: Run includeInactive:true once per day, compare active vs inactive changes, and generate a reconciliation report with discrepancies and suggested actions.
- Alerts: Trigger alerts if inactive inventory exceeds a threshold (e.g., more than 10% of total inventory across locations), as this can indicate bookkeeping errors or forgotten stock.
Visualization and UX tips
- Use distinct colors and icons for active vs inactive locations.
- Provide a tooltip or modal explaining how inactive inventory is counted and when it should be included.
- Allow merchants to filter variant-level reports by activity state.
Testing strategy
Adopt a thorough testing approach before enabling includeInactive in production reports:
- Unit tests: create mock InventoryLevel responses with mixed isActive values. Validate that aggregation, filtering, and UI components behave as expected.
- Integration tests: run an end-to-end test against a staging store where you can create inventory levels, deactivate them, adjust quantities, and assert the API returns expected data.
- Regression tests: ensure behavior for existing queries remains unchanged when includeInactive is not specified. Confirm that active-only totals still match pre-2026-04 outputs.
- Load tests: if you plan to include inactive levels widely, simulate the increased volume and observe API rate limits, network, and database impacts.
Real migration scenarios and timelines
Scenario A: Small app that shows inventory per location to store managers
- Impact: Minimal. Managers typically view active inventory only.
- Action: No immediate changes. Add a “Show inactive locations” toggle that queries with includeInactive when used. Update UI to mark inactive locations.
Scenario B: ERP integration that imports inventory nightly
- Impact: Potential double-counting or mismatched totals if inactive levels are suddenly included.
- Action: Update ETL to include isActive as a dimension. For day-to-day syncs, continue to import only active levels. Add a reconciler job to run weekly with includeInactive to catch any historical discrepancies.
Scenario C: Analytics vendor building historical inventory dashboards for merchants
- Impact: Opportunity to provide richer historical insights; must handle increased data volume.
- Action: Ingest includeInactive data for historical reports. Store activation events (activation and deactivation timestamps) to produce time-series that represent “what the merchant saw” at each point in time.
Recommended timeline for a small-to-medium integration
- Week 1: Inventory existing queries, add tests, and implement isActive-aware filters.
- Week 2: Update UI to present activation state and create admin-only “include inactive” views.
- Week 3: Run reconciliation jobs on staging with includeInactive: true, validate reports against merchant expectations.
- Week 4: Gradually roll out to production with monitoring of inventory totals and merchant support ready.
Governance and internal policies
Large organizations should institute a policy for handling inactive inventory levels:
- Define owners: who decides when to include inactive levels in operational flows.
- Permission model: restrict who can adjust inactive level quantities.
- Audit trail: capture who toggled isActive and when, with reason codes.
- Data retention: archive inactive-level data as part of historical records, with clear retention policies.
Performance and rate-limit considerations
Including inactive levels increases the number of nodes returned. Consider:
- Use pagination, avoid excessively large first: values.
- Cache results where feasible, but include short TTLs for operational flows.
- Monitor API rate usage after enabling includeInactive in production to ensure you don’t exceed rate limits.
- For batch reconciliation, use off-peak windows and stagger jobs across merchants.
Communication to merchants and partners
If you operate a merchant-facing app:
- Prepare release notes describing the change and its default behavior (no change unless includeInactive is used).
- Offer clear guidance on how activation state affects counts, fulfillment, and reporting.
- Provide support scripts or admin tools for merchants to view inactive levels and reassign or remove stock.
If you are a Shopify partner providing apps to other merchants:
- Share a migration guide with customers explaining how to interpret isActive and how to configure your app’s settings to exclude inactive levels by default.
Long-term considerations
The new semantics encourage richer inventory modeling: activation becomes a first-class state distinct from quantity. Use this to:
- Model location-level lifecycle events (e.g., decommissioned warehouse, temporary closure).
- Track and alert on inactive inventory that should be physically recovered or transferred.
- Improve auditability by preserving quantities across activation state changes.
Treat deactivation as an administrative state rather than a destructive operation. This reduces data loss and supports better supply chain management.
FAQ
Q: What exactly does isActive mean? A: isActive is a boolean flag on InventoryLevel indicating whether inventory tracking is currently active for that location-level pair. true means the level is active and used by operational flows; false means the level is recorded but inactive.
Q: Will my existing queries break? A: No. Default GraphQL behavior remains the same: queries without includeInactive continue to return only active inventory levels. Existing logic remains valid. The change matters only if you begin requesting includeInactive or base logic on implicit assumptions about previously hidden levels.
Q: How do I include inactive levels in my query? A: Pass includeInactive: true to inventoryLevels or inventoryLevel fields in GraphQL. The returned InventoryLevel nodes include isActive so you can filter or display the activation state.
Q: Should I include inactive levels in my inventory totals? A: Not for operational totals used by order acceptance or fulfillment decisions. Keep operational totals restricted to active levels. Include inactive levels for audits, reconciliation, or administrative review.
Q: Can inactive levels be adjusted via API? A: Yes. The 2026-04 release indicates inactive inventory levels can be adjusted like active ones. Ensure business rules and permissions accommodate adjustments to inactive nodes where appropriate.
Q: Will this change affect webhooks? A: Webhooks themselves are not reported to change with this update, but handlers that assume only active levels are present should be reviewed. If your webhook processing logic filters or aggregates InventoryLevel data, add checks for isActive.
Q: How should I test my app for this update? A: Add unit, integration, and end-to-end tests that include inventory levels with mixed isActive states. Validate aggregates, UI displays, and fulfillment routing. Run reconciliation jobs with includeInactive to compare historical numbers.
Q: What if inactive levels are a lot of data for my analytics pipeline? A: Fetch includeInactive data only for reconciliation runs or when explicitly requested by a merchant. Use batching and pagination, and consider incremental ingestion to reduce impact.
Q: Can I reactivate an inactive inventory level? A: Yes. Reactivation semantics remain part of location and inventory lifecycle. Reactivating restores the level to active status without reconstructing quantity history.
Q: Where should I display inactive inventory in a merchant-facing UI? A: Use a clearly labeled section for inactive inventory, separate from operational stock. Provide actions such as “reactivate”, “adjust”, or “request return/transfer” with contextual explanations.
Q: Who should be notified when inactive inventory is adjusted? A: That depends on your governance model. At minimum, log adjustments with user, timestamp, and reason. Consider notifying inventory managers or creating a task for physical verification when inactive quantities change.
Q: Is there any difference in the REST Admin API? A: The 2026-04 change specifically references the GraphQL Admin API. If you use the REST Admin API, verify corresponding endpoints for similar semantics or equivalent fields; do not assume parity without checking the REST documentation.
Q: When did this change go into effect? A: The change is part of the 2026-04 GraphQL Admin API release. Existing queries remain backward-compatible, but includeInactive and isActive behavior are available starting in this version.
Q: What are immediate next steps I should take this week? A: Audit your inventory queries, add unit tests handling isActive:true/false, update UI to show isActive, and schedule a staging reconciliation run with includeInactive:true to spot differences early.
Q: Who should I contact if I find unexpected behavior? A: Follow your usual support channels for Shopify Admin API issues and check merchant support routes if merchant-facing behavior is affected. Maintain detailed reproduction steps including GraphQL queries and sample responses to expedite investigation.