DEV Community

FlareCanary
FlareCanary

Posted on

Your Shopify discount is in the admin but missing from the API — the 2026-07 market-eligibility trap

Shopify shipped market eligibility for discounts in the 2026-07 Admin API (announced on the changelog as "Assign discounts to specific markets," May 7, 2026). Merchants can now scope a code or automatic discount to specific markets — region, B2B company location, retail location.

That's the feature. Here's the part that doesn't show up in a release-note skim:

If you query discounts on an API version prior to 2026-07, any discount that has market eligibility set is filtered out of the response — because older versions can't represent it. This applies to both list queries and fetching a specific discount by ID.

No error. No userErrors. No deprecation header. The discount is simply not in the payload. And unlike most breaking changes, this one has no countdown — it is already live for any merchant who has touched the new market-eligibility selector in their admin (it's available on Basic plans and up). If your app is pinned to 2025-10 or 2026-01, you may be returning incomplete discount data to your users right now.

Here is the silent-fail surface we keep seeing.

1. A clean 200 with the discount missing

The app queries discounts on its pinned version:

# App pinned to API version 2026-01
query {
  discountNodes(first: 100) {
    nodes {
      id
      discount {
        ... on DiscountCodeBasic { title status }
        ... on DiscountAutomaticBasic { title status }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The merchant has 12 active discounts. Three of them are scoped to the "North America" market. The query returns 9 nodes, HTTP 200, no userErrors, no extensions warning. Nothing in the response indicates three discounts were withheld.

There is no schema diff to catch at code review. The query is valid on both versions. The field set is identical. The only difference is which rows come back — and that's data, not schema, so static analysis and SDK type-checking won't flag it.

2. Fetching by ID returns null — which reads as "deleted"

This is the dangerous one.

query {
  discountNode(id: "gid://shopify/DiscountAutomaticNode/1099876") {
    id
  }
}
Enter fullscreen mode Exit fullscreen mode

If discount 1099876 has market eligibility set and you're on a pre-2026-07 version, this returns null. Same as a discount that was deleted. Same as an ID that never existed.

A lot of integration code treats "I asked for this ID and got null" as a tombstone:

node = client.get_discount(local_record.shopify_gid)
if node is None:
    local_record.delete()   # discount removed upstream — clean up
Enter fullscreen mode Exit fullscreen mode

That code now deletes a live discount from the app's own store the first time the merchant adds a market restriction to it. The discount is still working in the merchant's storefront. The app just garbage-collected its own copy because a version-filtered read looked like a deletion. This is silent data loss inside the integration, not just a missing read.

3. Sync, audit, and reconciliation jobs undercount with no signal

Anything that enumerates discounts is now working from a partial set on old versions:

  • Loyalty / promo-management apps that mirror discounts
  • "Export all active discounts" / reporting jobs
  • Discount-conflict checkers ("does this new code overlap an existing one?")
  • Reconciliation jobs that compare app state to Shopify state

They all get a 200 and a short list. The conflict checker clears a code that actually collides. The reconciliation job "heals" a discount it can't see by recreating or deleting it. The audit dashboard shows 9 active promos when there are 12. Every one of these fails toward looks correct — the worst kind, because no alert fires.

The merchant's symptom is the giveaway, and it's the exact phrase people are about to start Googling: "my discount shows in the Shopify admin but is missing from the API." It's not a bug in your sync. It's the version filter.

4. Bulk operations and anything sharing the query layer inherit the gap

bulkOperationRunQuery executes a GraphQL query at your app's configured API version. A nightly bulk export over discountNodes is exactly the "list everything" job most likely to drive downstream reporting — and it silently drops the same market-eligible discounts. Because bulk results land as a JSONL file you parse later, there's even less chance anyone notices the count is short.

If you have a webhook-driven discount cache, sanity-check what your subscription's API version actually represents before trusting it as complete. The reliable mental model: on a pre-2026-07 version, any read path that could return a market-eligible discount returns it filtered out instead.

5. After you upgrade, the inheritance rules are their own trap

Moving to 2026-07 fixes the disappearing-rows problem but introduces a correctness one if you write migration code that maps discounts to markets:

  • Discounts do not inherit across market types. A discount assigned to a regional market does not automatically cover a B2B company location or a retail location — those are separate market types.
  • Within a type, a regional assignment does cascade to sub-markets: assign to "North America" and it applies to "Canada" automatically.

Migration scripts that "assign this discount to all markets" by enumerating leaf markets and looping will either over-apply (re-adding sub-markets that already inherit) or under-apply (missing other market types entirely). Model the assignment against the type/inheritance rules, not a flat list of market IDs.

The fix

  1. Upgrade your Admin API version to 2026-07 (or later) before treating any discount read as complete. This is the only real fix — there is no flag to opt the old version into representing market eligibility.
  2. Until you've upgraded, treat discount reads from older versions as potentially incomplete, not authoritative. Specifically: do not drive deletes, tombstoning, or reconciliation off a null/missing discount from a pre-2026-07 version. A "not found" on an old version is ambiguous — it could be a market-eligible discount, not a deletion.
  3. Detect your exposure with a version diff. Run the same discountNodes count against the same shop on your current version and on 2026-07. Any delta is the set of market-eligible discounts you were blind to. That number is also the size of your reconciliation-job error.
  4. On 2026-07, filter market intentionally. Use the market context / market_ids argument on discountNodes rather than assuming the unfiltered list is global, and encode the type/sub-market inheritance rules before mapping discounts to markets.

The reason this one is worth acting on now rather than at a cutoff date: there is no cutoff. The filter is active the moment a merchant uses a feature Shopify has already shipped to every Basic-and-up store. The gap between "discount visible in admin" and "discount absent from API" opens silently, on the merchant's schedule, not yours — and the only signal is a row count that looks plausible.


FlareCanary watches your third-party APIs and SDKs for breaking changes like this one — including version-gated response filtering that returns a clean 200 with rows quietly removed — and surfaces them before they hit production. Free tier monitors 5 endpoints.

Top comments (0)