<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: FlareCanary</title>
    <description>The latest articles on DEV Community by FlareCanary (@flarecanary).</description>
    <link>https://dev.to/flarecanary</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834499%2F8c191c74-2040-4cd1-beaa-4ca99b664ca9.png</url>
      <title>DEV Community: FlareCanary</title>
      <link>https://dev.to/flarecanary</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/flarecanary"/>
    <language>en</language>
    <item>
      <title>Shopify retires API version 2025-07 on July 16 — pinned apps don't error, they silently fall forward to 2025-10</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sun, 05 Jul 2026 04:01:32 +0000</pubDate>
      <link>https://dev.to/flarecanary/shopify-retires-api-version-2025-07-on-july-16-pinned-apps-dont-error-they-silently-fall-5bjl</link>
      <guid>https://dev.to/flarecanary/shopify-retires-api-version-2025-07-on-july-16-pinned-apps-dont-error-they-silently-fall-5bjl</guid>
      <description>&lt;p&gt;On &lt;strong&gt;July 16, 2026 at 15:00 UTC&lt;/strong&gt;, Shopify stops serving API version &lt;code&gt;2025-07&lt;/code&gt;. If your app, integration, or script still pins that version, here is the part that catches people off guard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your requests do not start failing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;400&lt;/code&gt;, no &lt;code&gt;406&lt;/code&gt;, no "unsupported version" error. Shopify's documented behavior is to &lt;em&gt;fall forward&lt;/em&gt;: a request for an inaccessible version is served using the &lt;strong&gt;oldest accessible stable version&lt;/strong&gt; instead. After July 16, that's &lt;code&gt;2025-10&lt;/code&gt;. So a call that says &lt;code&gt;X-Shopify-Api-Version: 2025-07&lt;/code&gt; quietly gets answered by &lt;code&gt;2025-10&lt;/code&gt; — a schema you never tested against, on a date you didn't pick.&lt;/p&gt;

&lt;p&gt;Same endpoint. Same &lt;code&gt;200 OK&lt;/code&gt;. Different response shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the dangerous kind of breaking change
&lt;/h2&gt;

&lt;p&gt;Most deprecation posts treat a version sunset like a model retirement: there's a date, calls stop working, you migrate before it. Loud, scheduled, hard to miss.&lt;/p&gt;

&lt;p&gt;Shopify version sunsets are the opposite. The whole point of fall-forward is that &lt;strong&gt;nothing breaks loudly&lt;/strong&gt; — Shopify keeps answering you so your store doesn't go dark. The cost of that graceful behavior is that the moment your pinned version disappears, your client silently starts consuming a newer contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fields deprecated-then-removed between &lt;code&gt;2025-07&lt;/code&gt; and &lt;code&gt;2025-10&lt;/code&gt; come back &lt;strong&gt;&lt;code&gt;null&lt;/code&gt;&lt;/strong&gt; or vanish from the payload.&lt;/li&gt;
&lt;li&gt;Types that &lt;em&gt;widened&lt;/em&gt; now return shapes your parsing code never expected.&lt;/li&gt;
&lt;li&gt;Enum values, default behaviors, and pagination semantics shift to the newer version's rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it raises an exception on Shopify's side. The break happens &lt;strong&gt;inside your code&lt;/strong&gt;, when it reads a field that moved and gets back something it wasn't written for. That's a margin report three weeks later, not a page at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only signal is a response header
&lt;/h2&gt;

&lt;p&gt;Shopify does tell you which version actually served the request — in the &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; response header. After fall-forward, that header reads &lt;code&gt;2025-10&lt;/code&gt; even though you asked for &lt;code&gt;2025-07&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the catch: the signal is on &lt;strong&gt;every response&lt;/strong&gt;, but it's on the part of the response almost nobody inspects. Most clients read the JSON body and ignore the headers entirely. There is no Sunset header on the body, no error field, no deprecation block in the payload. If you're not diffing &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; against the version you sent, the fall-forward is invisible until behavior drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three concrete shifts a 2025-07 app inherits on July 16
&lt;/h2&gt;

&lt;p&gt;This isn't hypothetical. Here are real &lt;code&gt;2025-07&lt;/code&gt; → &lt;code&gt;2025-10&lt;/code&gt; Admin API changes your pinned client silently adopts the instant it falls forward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;StoreCreditAccount.owner&lt;/code&gt; widened from one type to a union.&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;2025-07&lt;/code&gt;, a store credit account's &lt;code&gt;owner&lt;/code&gt; is always a &lt;code&gt;Customer&lt;/code&gt;. In &lt;code&gt;2025-10&lt;/code&gt;, the owner can be a &lt;code&gt;Customer&lt;/code&gt; &lt;strong&gt;or&lt;/strong&gt; a &lt;code&gt;CompanyLocation&lt;/code&gt; (B2B). If your code reads &lt;code&gt;owner&lt;/code&gt; assuming it's a customer — an inline fragment that only handles &lt;code&gt;... on Customer&lt;/code&gt;, or a parser that expects &lt;code&gt;owner.firstName&lt;/code&gt; — B2B-owned accounts now resolve to a shape you don't handle. No error: just an empty or null branch where a value used to be. Store-credit balances silently stop reconciling for your B2B accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ProductVariant.taxCode&lt;/code&gt; deprecated (Avalara AvaTax sunset).&lt;/strong&gt;&lt;br&gt;
As of &lt;code&gt;2025-10&lt;/code&gt;, &lt;code&gt;ProductVariant.taxCode&lt;/code&gt; is deprecated as part of Shopify retiring the legacy AvaTax integration path. A 2025-07 client that reads &lt;code&gt;taxCode&lt;/code&gt; to drive tax classification doesn't get an exception when it falls forward — it gets a field that's on its way out, with values that may no longer mean what they did. Tax logic keyed on that field drifts without a single failed request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;InventoryItem.variant&lt;/code&gt; → &lt;code&gt;InventoryItem.variants&lt;/code&gt; (singular replaced by a connection).&lt;/strong&gt;&lt;br&gt;
The singular &lt;code&gt;InventoryItem.variant&lt;/code&gt; field was deprecated in favor of an &lt;code&gt;InventoryItem.variants&lt;/code&gt; connection. Code written for &lt;code&gt;2025-07&lt;/code&gt; that walks &lt;code&gt;inventoryItem.variant.id&lt;/code&gt; is reading a field on the deprecation path; once it's removed in a later version your fall-forward eventually reaches, that read returns &lt;code&gt;null&lt;/code&gt; and your inventory sync quietly maps to nothing.&lt;/p&gt;

&lt;p&gt;These three are just what's visible one quarter forward. The deeper problem: fall-forward doesn't move you one version — it moves you to &lt;em&gt;whatever the oldest supported version happens to be on the day yours expires.&lt;/em&gt; The longer a pin sits unmaintained, the bigger the silent jump when it finally lapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do before July 16
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every place you pin &lt;code&gt;2025-07&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;X-Shopify-Api-Version&lt;/code&gt; headers, GraphQL/REST URL paths (&lt;code&gt;/admin/api/2025-07/...&lt;/code&gt;), SDK config, webhook subscription versions, and any vendored client library's default. Webhooks have their own version setting; don't assume the API pin covers them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bump to a supported version and test against its schema&lt;/strong&gt;, not just "does it still return 200." Move to &lt;code&gt;2025-10&lt;/code&gt; or newer deliberately, read the release notes for the versions you're skipping, and fix the field-level changes on &lt;em&gt;your&lt;/em&gt; schedule instead of inheriting them on Shopify's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert on &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; in your client.&lt;/strong&gt; Log it, and alarm if the served version ever differs from the version you requested. That one check turns a silent fall-forward into a loud, fixable signal — which is exactly the difference between finding this in CI and finding it in a chargeback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The version sunset itself is well-documented. The trap is that "well-documented" and "fails loudly" are not the same thing. Shopify keeps your store running by quietly handing you a newer contract — and a &lt;code&gt;200&lt;/code&gt; with the wrong shape is harder to catch than an honest error.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; watches your API responses for exactly this: a field that changes type, a value that goes null, a response shape that drifts — including the silent version fall-forwards that pinned clients can't see. If the contract you depend on changes, you hear about it from us before your customers do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>api</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Shopify retires API version 2025-07 on July 16 — pinned apps don't error, they silently fall forward to 2025-10</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sat, 04 Jul 2026 05:00:37 +0000</pubDate>
      <link>https://dev.to/flarecanary/shopify-retires-api-version-2025-07-on-july-16-pinned-apps-dont-error-they-silently-fall-2hfi</link>
      <guid>https://dev.to/flarecanary/shopify-retires-api-version-2025-07-on-july-16-pinned-apps-dont-error-they-silently-fall-2hfi</guid>
      <description>&lt;p&gt;On &lt;strong&gt;July 16, 2026 at 15:00 UTC&lt;/strong&gt;, Shopify stops serving API version &lt;code&gt;2025-07&lt;/code&gt;. If your app, integration, or script still pins that version, here is the part that catches people off guard:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your requests do not start failing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;400&lt;/code&gt;, no &lt;code&gt;406&lt;/code&gt;, no "unsupported version" error. Shopify's documented behavior is to &lt;em&gt;fall forward&lt;/em&gt;: a request for an inaccessible version is served using the &lt;strong&gt;oldest accessible stable version&lt;/strong&gt; instead. After July 16, that's &lt;code&gt;2025-10&lt;/code&gt;. So a call that says &lt;code&gt;X-Shopify-Api-Version: 2025-07&lt;/code&gt; quietly gets answered by &lt;code&gt;2025-10&lt;/code&gt; — a schema you never tested against, on a date you didn't pick.&lt;/p&gt;

&lt;p&gt;Same endpoint. Same &lt;code&gt;200 OK&lt;/code&gt;. Different response shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is the dangerous kind of breaking change
&lt;/h2&gt;

&lt;p&gt;Most deprecation posts treat a version sunset like a model retirement: there's a date, calls stop working, you migrate before it. Loud, scheduled, hard to miss.&lt;/p&gt;

&lt;p&gt;Shopify version sunsets are the opposite. The whole point of fall-forward is that &lt;strong&gt;nothing breaks loudly&lt;/strong&gt; — Shopify keeps answering you so your store doesn't go dark. The cost of that graceful behavior is that the moment your pinned version disappears, your client silently starts consuming a newer contract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fields deprecated-then-removed between &lt;code&gt;2025-07&lt;/code&gt; and &lt;code&gt;2025-10&lt;/code&gt; come back &lt;strong&gt;&lt;code&gt;null&lt;/code&gt;&lt;/strong&gt; or vanish from the payload.&lt;/li&gt;
&lt;li&gt;Types that &lt;em&gt;widened&lt;/em&gt; now return shapes your parsing code never expected.&lt;/li&gt;
&lt;li&gt;Enum values, default behaviors, and pagination semantics shift to the newer version's rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of it raises an exception on Shopify's side. The break happens &lt;strong&gt;inside your code&lt;/strong&gt;, when it reads a field that moved and gets back something it wasn't written for. That's a margin report three weeks later, not a page at 3 a.m.&lt;/p&gt;

&lt;h2&gt;
  
  
  The only signal is a response header
&lt;/h2&gt;

&lt;p&gt;Shopify does tell you which version actually served the request — in the &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; response header. After fall-forward, that header reads &lt;code&gt;2025-10&lt;/code&gt; even though you asked for &lt;code&gt;2025-07&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the catch: the signal is on &lt;strong&gt;every response&lt;/strong&gt;, but it's on the part of the response almost nobody inspects. Most clients read the JSON body and ignore the headers entirely. There is no Sunset header on the body, no error field, no deprecation block in the payload. If you're not diffing &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; against the version you sent, the fall-forward is invisible until behavior drifts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three concrete shifts a 2025-07 app inherits on July 16
&lt;/h2&gt;

&lt;p&gt;This isn't hypothetical. Here are real &lt;code&gt;2025-07&lt;/code&gt; → &lt;code&gt;2025-10&lt;/code&gt; Admin API changes your pinned client silently adopts the instant it falls forward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;StoreCreditAccount.owner&lt;/code&gt; widened from one type to a union.&lt;/strong&gt;&lt;br&gt;
In &lt;code&gt;2025-07&lt;/code&gt;, a store credit account's &lt;code&gt;owner&lt;/code&gt; is always a &lt;code&gt;Customer&lt;/code&gt;. In &lt;code&gt;2025-10&lt;/code&gt;, the owner can be a &lt;code&gt;Customer&lt;/code&gt; &lt;strong&gt;or&lt;/strong&gt; a &lt;code&gt;CompanyLocation&lt;/code&gt; (B2B). If your code reads &lt;code&gt;owner&lt;/code&gt; assuming it's a customer — an inline fragment that only handles &lt;code&gt;... on Customer&lt;/code&gt;, or a parser that expects &lt;code&gt;owner.firstName&lt;/code&gt; — B2B-owned accounts now resolve to a shape you don't handle. No error: just an empty or null branch where a value used to be. Store-credit balances silently stop reconciling for your B2B accounts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ProductVariant.taxCode&lt;/code&gt; deprecated (Avalara AvaTax sunset).&lt;/strong&gt;&lt;br&gt;
As of &lt;code&gt;2025-10&lt;/code&gt;, &lt;code&gt;ProductVariant.taxCode&lt;/code&gt; is deprecated as part of Shopify retiring the legacy AvaTax integration path. A 2025-07 client that reads &lt;code&gt;taxCode&lt;/code&gt; to drive tax classification doesn't get an exception when it falls forward — it gets a field that's on its way out, with values that may no longer mean what they did. Tax logic keyed on that field drifts without a single failed request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;InventoryItem.variant&lt;/code&gt; → &lt;code&gt;InventoryItem.variants&lt;/code&gt; (singular replaced by a connection).&lt;/strong&gt;&lt;br&gt;
The singular &lt;code&gt;InventoryItem.variant&lt;/code&gt; field was deprecated in favor of an &lt;code&gt;InventoryItem.variants&lt;/code&gt; connection. Code written for &lt;code&gt;2025-07&lt;/code&gt; that walks &lt;code&gt;inventoryItem.variant.id&lt;/code&gt; is reading a field on the deprecation path; once it's removed in a later version your fall-forward eventually reaches, that read returns &lt;code&gt;null&lt;/code&gt; and your inventory sync quietly maps to nothing.&lt;/p&gt;

&lt;p&gt;These three are just what's visible one quarter forward. The deeper problem: fall-forward doesn't move you one version — it moves you to &lt;em&gt;whatever the oldest supported version happens to be on the day yours expires.&lt;/em&gt; The longer a pin sits unmaintained, the bigger the silent jump when it finally lapses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do before July 16
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find every place you pin &lt;code&gt;2025-07&lt;/code&gt;&lt;/strong&gt; — &lt;code&gt;X-Shopify-Api-Version&lt;/code&gt; headers, GraphQL/REST URL paths (&lt;code&gt;/admin/api/2025-07/...&lt;/code&gt;), SDK config, webhook subscription versions, and any vendored client library's default. Webhooks have their own version setting; don't assume the API pin covers them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bump to a supported version and test against its schema&lt;/strong&gt;, not just "does it still return 200." Move to &lt;code&gt;2025-10&lt;/code&gt; or newer deliberately, read the release notes for the versions you're skipping, and fix the field-level changes on &lt;em&gt;your&lt;/em&gt; schedule instead of inheriting them on Shopify's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assert on &lt;code&gt;X-Shopify-API-Version&lt;/code&gt; in your client.&lt;/strong&gt; Log it, and alarm if the served version ever differs from the version you requested. That one check turns a silent fall-forward into a loud, fixable signal — which is exactly the difference between finding this in CI and finding it in a chargeback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The version sunset itself is well-documented. The trap is that "well-documented" and "fails loudly" are not the same thing. Shopify keeps your store running by quietly handing you a newer contract — and a &lt;code&gt;200&lt;/code&gt; with the wrong shape is harder to catch than an honest error.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; watches your API responses for exactly this: a field that changes type, a value that goes null, a response shape that drifts — including the silent version fall-forwards that pinned clients can't see. If the contract you depend on changes, you hear about it from us before your customers do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>api</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>The Google Merchant API migration has a silent mispricing trap (Content API shuts down Aug 18, 2026)</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Wed, 01 Jul 2026 05:01:04 +0000</pubDate>
      <link>https://dev.to/flarecanary/the-google-merchant-api-migration-has-a-silent-mispricing-trap-content-api-shuts-down-aug-18-2026-2gkn</link>
      <guid>https://dev.to/flarecanary/the-google-merchant-api-migration-has-a-silent-mispricing-trap-content-api-shuts-down-aug-18-2026-2gkn</guid>
      <description>&lt;p&gt;If you push product data to Google Shopping through a custom integration — a feed builder, a PIM sync job, an internal Node/Python service that calls &lt;code&gt;shoppingcontent.googleapis.com&lt;/code&gt; — you are on a clock. On &lt;strong&gt;August 18, 2026&lt;/strong&gt;, Google permanently shuts down the Content API for Shopping. Every &lt;code&gt;products.insert&lt;/code&gt;, every &lt;code&gt;products.list&lt;/code&gt;, every inventory and price update against the v2.1 endpoints stops returning data and starts returning errors. There is no soft cutoff and no grace period after that date.&lt;/p&gt;

&lt;p&gt;That part is loud. You'll know within minutes because the calls 404, the feed job alerts, and the catalog stops updating.&lt;/p&gt;

&lt;p&gt;The part that bites quietly is the migration itself. The &lt;a href="https://developers.google.com/merchant/api/guides/compatibility/overview" rel="noopener noreferrer"&gt;Merchant API&lt;/a&gt; is not a renamed Content API — it's an architectural rebuild with new resource shapes, a new identifier scheme, and a new money type. A migration done as a search-and-replace of the base URL will compile, authenticate, return &lt;code&gt;200&lt;/code&gt;, and silently write wrong data to your live catalog. Here are the three surfaces where that happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Content API for Shopping&lt;/th&gt;
&lt;th&gt;Merchant API v1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price amount&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;price.value&lt;/code&gt; — decimal &lt;strong&gt;string&lt;/strong&gt; (&lt;code&gt;"15.99"&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;price.amountMicros&lt;/code&gt; — &lt;strong&gt;int64&lt;/strong&gt; (&lt;code&gt;15990000&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price currency&lt;/td&gt;
&lt;td&gt;&lt;code&gt;price.currency&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;price.currencyCode&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource identity&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;merchantId&lt;/code&gt; + &lt;code&gt;productId&lt;/code&gt; params&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;name&lt;/code&gt;: &lt;code&gt;accounts/{account}/products/{product}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Product ID format&lt;/td&gt;
&lt;td&gt;&lt;code&gt;channel:contentLanguage:feedLabel:offerId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;contentLanguage~feedLabel~offerId&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID delimiter&lt;/td&gt;
&lt;td&gt;colon (&lt;code&gt;:&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;tilde (&lt;code&gt;~&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;channel&lt;/code&gt; in ID&lt;/td&gt;
&lt;td&gt;present (&lt;code&gt;online&lt;/code&gt; / &lt;code&gt;local&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;removed&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OAuth scope&lt;/td&gt;
&lt;td&gt;&lt;code&gt;https://www.googleapis.com/auth/content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-sub-API scopes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four of those rows produce no error when you get them wrong. They produce &lt;em&gt;wrong data that the API happily accepts&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The &lt;code&gt;amountMicros&lt;/code&gt; trap silently misprices your entire catalog
&lt;/h2&gt;

&lt;p&gt;This is the one to fix before you touch anything else.&lt;/p&gt;

&lt;p&gt;In the Content API, a price is a decimal string with a sibling currency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15.99"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the Merchant API, the amount is an &lt;strong&gt;int64 count of micros&lt;/strong&gt;, where one million micros equals one unit of currency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"amountMicros"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"15990000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currencyCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;$15.99&lt;/code&gt; is &lt;code&gt;15990000&lt;/code&gt;. The conversion is &lt;code&gt;Math.round(parseFloat(value) * 1_000_000)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now look at what a field-renaming migration does. The natural instinct, when a guide says &lt;em&gt;"the amount field name changed from &lt;code&gt;value&lt;/code&gt; to &lt;code&gt;amountMicros&lt;/code&gt;"&lt;/em&gt;, is to map the old field onto the new name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WRONG — renames the field, skips the unit conversion&lt;/span&gt;
&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;amountMicros&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;oldProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// "15.99"&lt;/span&gt;
  &lt;span class="na"&gt;currencyCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;oldProduct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;amountMicros&lt;/code&gt; is typed as int64, so the string &lt;code&gt;"15.99"&lt;/code&gt; either gets coerced to &lt;code&gt;15&lt;/code&gt; (truncated) or rejected depending on your client library — and &lt;code&gt;15&lt;/code&gt; micros is &lt;strong&gt;$0.000015&lt;/strong&gt;. Every product in the feed is now listed at effectively zero. Google accepts the write. The products stay "active." Your Shopping ads start serving at a price that doesn't match your landing page, which trips Google's price-mismatch checks &lt;em&gt;days later&lt;/em&gt; — long after the deploy, with no stack trace pointing back at the migration.&lt;/p&gt;

&lt;p&gt;The inverse mistake is just as quiet and more expensive: a team that knows about micros but applies the multiply twice (once in a mapping layer, once in a helper) ships &lt;code&gt;15990000000000&lt;/code&gt; and lists a $16 product at $16 million. It won't sell, but it also won't error — it just silently stops converting.&lt;/p&gt;

&lt;p&gt;There is no error path here. The only way to catch it is to diff the prices that landed in Merchant Center against the prices you intended to send.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The product ID delimiter flips from &lt;code&gt;:&lt;/code&gt; to &lt;code&gt;~&lt;/code&gt; — and drops &lt;code&gt;channel&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Content API product IDs are colon-delimited and lead with the channel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;online:en:US:SKU12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merchant API product IDs are tilde-delimited and &lt;strong&gt;have no channel segment&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;en~US~SKU12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any code that builds or parses product IDs by string manipulation breaks silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// builds a malformed ID against the new API&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;feedLabel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// still using ':' and channel&lt;/span&gt;

&lt;span class="c1"&gt;// parsing an inbound Merchant API id by the old delimiter&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offer&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// no ':' present → channel = whole string, rest undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The build path creates a product the API treats as &lt;em&gt;a different offer&lt;/em&gt; than the one you meant to update — so instead of updating SKU12345 you insert a duplicate, and the original goes stale until it expires. The parse path silently mis-shards your records, and because &lt;code&gt;offer&lt;/code&gt; comes back &lt;code&gt;undefined&lt;/code&gt;, downstream lookups quietly miss.&lt;/p&gt;

&lt;p&gt;The dropped &lt;code&gt;channel&lt;/code&gt; segment is its own trap: in the old format &lt;code&gt;online:...&lt;/code&gt; and &lt;code&gt;local:...&lt;/code&gt; were distinct products. The Merchant API moves the online/local distinction out of the identifier entirely. If your reconciliation logic keyed products by the full colon-string, your online and local variants now collide on the same key.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;id&lt;/code&gt; becomes &lt;code&gt;name&lt;/code&gt;, and scripts reading &lt;code&gt;.id&lt;/code&gt; get &lt;code&gt;undefined&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The Content API returned a flat &lt;code&gt;id&lt;/code&gt; on every product. The Merchant API follows Google's &lt;a href="https://google.aip.dev/122" rel="noopener noreferrer"&gt;resource-name convention&lt;/a&gt; — the unique identifier is &lt;code&gt;name&lt;/code&gt;, formatted &lt;code&gt;accounts/{account}/products/{product}&lt;/code&gt;. There is no top-level &lt;code&gt;id&lt;/code&gt; field on the new product resource.&lt;/p&gt;

&lt;p&gt;Every script that does &lt;code&gt;product.id&lt;/code&gt;, every database column populated from &lt;code&gt;response.id&lt;/code&gt;, every log line keyed on the product ID reads &lt;code&gt;undefined&lt;/code&gt; after the cutover. Nothing throws — JavaScript hands you &lt;code&gt;undefined&lt;/code&gt;, Python hands you a &lt;code&gt;KeyError&lt;/code&gt; only if you used &lt;code&gt;[]&lt;/code&gt; instead of &lt;code&gt;.get()&lt;/code&gt;, and most feed code uses &lt;code&gt;.get()&lt;/code&gt; defensively precisely so a missing field doesn't crash the run. So the field goes missing and the run keeps going, writing &lt;code&gt;null&lt;/code&gt; IDs into your sync table.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to grep for before you cut over
&lt;/h2&gt;

&lt;p&gt;A focused audit on every service that talks to &lt;code&gt;shoppingcontent.googleapis.com&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "shoppingcontent.googleapis.com\|content/v2" .&lt;/code&gt; — every match is an endpoint that 404s on August 18, 2026.&lt;/li&gt;
&lt;li&gt;Search your price-mapping layer for the move to &lt;code&gt;amountMicros&lt;/code&gt;. Confirm there is exactly &lt;strong&gt;one&lt;/strong&gt; &lt;code&gt;* 1_000_000&lt;/code&gt; (or &lt;code&gt;* 1e6&lt;/code&gt;) on the path from your stored price to the request body — not zero, not two.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "\.split(':')\|:\${" .&lt;/code&gt; near product-ID construction — colon-delimited ID logic that needs to become tilde-delimited and channel-free.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "\.id\b" .&lt;/code&gt; in your product sync code — reads that now need to be &lt;code&gt;.name&lt;/code&gt;, and any DB column fed from the old flat &lt;code&gt;id&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check your OAuth flow: the blanket &lt;code&gt;auth/content&lt;/code&gt; scope is replaced by per-sub-API scopes. A token minted with only the old scope authorizes nothing on the new endpoints — and if you request a partial set, the calls that lack scope fail individually while the rest succeed, so a half-migrated scope grant looks like an intermittent outage rather than a config error.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The August 18 shutdown will get attention because it's a date on a calendar with a hard error attached. The migration that everyone does in the weeks before it is where the silent damage lives: a catalog that's fully "active" in Merchant Center, priced at $0.00 or $16 million, with duplicate SKUs and null IDs in your sync table — and not one line in the logs to say so.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FlareCanary monitors API responses for schema drift, silent removals, and behavior changes across upstream providers. If you sync product data to Google Shopping, Shopify, or anywhere else that ships breaking changes through migration guides, &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;flarecanary.com&lt;/a&gt; catches the drift before your customers do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ecommerce</category>
      <category>api</category>
      <category>googleshopping</category>
      <category>integrations</category>
    </item>
    <item>
      <title>HighLevel webhooks flip X-WH-Signature X-GHL-Signature on July 1, 2026 — three silent failure modes</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sun, 28 Jun 2026 05:00:43 +0000</pubDate>
      <link>https://dev.to/flarecanary/highlevel-webhooks-flip-x-wh-signature-x-ghl-signature-on-july-1-2026-three-silent-failure-do2</link>
      <guid>https://dev.to/flarecanary/highlevel-webhooks-flip-x-wh-signature-x-ghl-signature-on-july-1-2026-three-silent-failure-do2</guid>
      <description>&lt;p&gt;If you run a HighLevel integration — an n8n flow, a custom CRM bridge, a Make scenario, an internal Node service that listens for "Opportunity stage changed" — you have five weeks. On &lt;strong&gt;July 1, 2026&lt;/strong&gt;, HighLevel deprecates the legacy &lt;code&gt;X-WH-Signature&lt;/code&gt; (RSA) webhook signature header. After that date, every webhook is signed only with &lt;code&gt;X-GHL-Signature&lt;/code&gt; using &lt;strong&gt;Ed25519&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;From the &lt;a href="https://marketplace.gohighlevel.com/docs/webhook/WebhookIntegrationGuide/index.html" rel="noopener noreferrer"&gt;HighLevel Webhook Integration Guide&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"The legacy header **X-WH-Signature&lt;/em&gt;* will be &lt;strong&gt;deprecated on July 1, 2026&lt;/strong&gt;. After that date, webhooks will be signed only with &lt;strong&gt;X-GHL-Signature&lt;/strong&gt;."*&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The loud failure mode is the one everyone plans around. Your &lt;code&gt;crypto.verify(...)&lt;/code&gt; call returns &lt;code&gt;false&lt;/code&gt;, HighLevel sees a non-2xx from your endpoint, retries exhaust, the automation chain dies. You notice within an hour because the "new lead created" Slack notifier stops firing.&lt;/p&gt;

&lt;p&gt;The failure modes worth grepping for &lt;em&gt;now&lt;/em&gt; are the ones that don't surface as a 401. Three of them are common enough that they'll bite a meaningful chunk of the agency-built integrations running on HighLevel today.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changes
&lt;/h2&gt;

&lt;p&gt;Two things change simultaneously, and that's part of the trap:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Surface&lt;/th&gt;
&lt;th&gt;Before Jul 1, 2026&lt;/th&gt;
&lt;th&gt;After Jul 1, 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Signature header&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X-WH-Signature&lt;/code&gt; (and &lt;code&gt;X-GHL-Signature&lt;/code&gt; when present)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;X-GHL-Signature&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Algorithm&lt;/td&gt;
&lt;td&gt;RSA (SHA-256)&lt;/td&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public key&lt;/td&gt;
&lt;td&gt;RSA public key endpoint&lt;/td&gt;
&lt;td&gt;Ed25519 public key endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Signature length&lt;/td&gt;
&lt;td&gt;256 bytes (2048-bit RSA)&lt;/td&gt;
&lt;td&gt;64 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verifier API (Node)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.createVerify('SHA256').update(body).verify(pubkey, sig)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;crypto.verify(null, body, pubkey, sig)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two headers. Two algorithms. Two public keys. Two completely different verifier call shapes. A migration that's &lt;em&gt;only&lt;/em&gt; a string replace from &lt;code&gt;x-wh-signature&lt;/code&gt; to &lt;code&gt;x-ghl-signature&lt;/code&gt; is silently broken on at least three of those rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The "skip if signature missing" branch becomes an open endpoint
&lt;/h2&gt;

&lt;p&gt;This is the one to grep for first. Pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/webhook/highlevel&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-wh-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// dev/test environments don't sign — skip verification&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handlePayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyRSA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RSA_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bad sig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;handlePayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;if (!sig)&lt;/code&gt; early-return exists in roughly every webhook handler I've seen in agency-shipped HighLevel code. It's there because somebody wanted to test locally without setting up signature verification, or because an older version of HighLevel really did skip signing on certain event types.&lt;/p&gt;

&lt;p&gt;After July 1, 2026, &lt;strong&gt;every&lt;/strong&gt; production webhook from HighLevel arrives with &lt;code&gt;X-WH-Signature&lt;/code&gt; absent. The handler reads &lt;code&gt;req.headers['x-wh-signature']&lt;/code&gt;, gets &lt;code&gt;undefined&lt;/code&gt;, takes the skip-verify branch, and processes the payload. Your endpoint is now unauthenticated. Anyone who knows the URL can POST arbitrary JSON to it — fake "stage changed to Won" events that trigger your billing automation, fake "new contact" events that pollute your CRM.&lt;/p&gt;

&lt;p&gt;The fix is one line — switch the header name and remove the skip-verify branch — but the open-endpoint window between now and the fix is exactly the kind of thing that doesn't show up in logs because &lt;em&gt;the requests succeed&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. RSA verifier passed an Ed25519 signature returns false (silently)
&lt;/h2&gt;

&lt;p&gt;The migration guide says to use &lt;code&gt;X-GHL-Signature&lt;/code&gt; and provides Ed25519 verification snippets. What it doesn't emphasize is that &lt;strong&gt;you cannot mix and match the verifier call with the new header&lt;/strong&gt;. Code that updates the header but keeps the RSA verifier looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;x-ghl-signature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;           &lt;span class="c1"&gt;// updated&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createVerify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// not updated&lt;/span&gt;
&lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;RSA_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ok === false, because sig is a 64-byte Ed25519 sig&lt;/span&gt;
&lt;span class="c1"&gt;// and verifier is checking RSA-PSS / RSA-PKCS1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two outcomes, both bad:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If your handler returns 401 on &lt;code&gt;!ok&lt;/code&gt;, HighLevel retries with exponential backoff, gives up after the retry window, and the event is &lt;strong&gt;lost&lt;/strong&gt;. No alert fires — HighLevel doesn't push failed-delivery dashboards into the agency dashboard, and most agency setups don't subscribe to delivery-failure events.&lt;/li&gt;
&lt;li&gt;If your handler logs but still processes (an "alert on verify failure but proceed" pattern that some teams use during cutover), you're back to unauthenticated processing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, the symptom is the same the entire production stack from July 1 onward: deliveries look 200-green from HighLevel's side until you check the actual content delivered to your downstream system, and they look fine from your side until you realize the automations that should have fired never did.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;body-parser&lt;/code&gt; + RSA-verifier stack ported to Ed25519 also needs to handle the body differently — &lt;code&gt;crypto.createVerify&lt;/code&gt; takes streamed updates; &lt;code&gt;crypto.verify&lt;/code&gt; with &lt;code&gt;null&lt;/code&gt; algorithm wants the full buffer. If your middleware already consumed the stream into a JSON object, you need the &lt;strong&gt;raw body&lt;/strong&gt; preserved (&lt;code&gt;bodyParser.raw&lt;/code&gt; or &lt;code&gt;express.raw&lt;/code&gt; with a &lt;code&gt;verify&lt;/code&gt; callback to stash &lt;code&gt;req.rawBody&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cached public keys work right up until the cutoff
&lt;/h2&gt;

&lt;p&gt;A lot of agency setups load the HighLevel RSA public key into an environment variable at deploy time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HIGHLEVEL_PUBKEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-----BEGIN&lt;/span&gt; PUBLIC KEY-----&lt;span class="se"&gt;\n&lt;/span&gt;MIIBIjANBgkqhkiG9w0...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That key keeps working through June 30. On July 1, every signature is generated against a &lt;strong&gt;different keypair&lt;/strong&gt; — the Ed25519 keypair. Your env var is now matched against signatures it has never seen and cannot validate by definition.&lt;/p&gt;

&lt;p&gt;If you also took the time to switch verifiers (so issue #2 doesn't apply), you'll still fail because the public key is wrong. The verifier returns false. The signatures look "well-formed but invalid," which is indistinguishable in your logs from a real attack — and that's the worst place to be on July 1, because the on-call response to "all webhook signatures are suddenly invalid" is exactly the same as "we're being attacked": tighten the rate limit, page the security team, panic.&lt;/p&gt;

&lt;p&gt;The fix is to fetch the Ed25519 public key from HighLevel's published JWKS-equivalent endpoint &lt;strong&gt;at runtime&lt;/strong&gt; (with caching), not bake it into config. Even setting aside the July 1 cutoff, baking long-lived public keys into env vars is the pattern that makes every future key rotation a deploy event.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to grep for this week
&lt;/h2&gt;

&lt;p&gt;A 30-minute audit on every HighLevel webhook handler in your fleet:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "x-wh-signature\|X-WH-Signature" .&lt;/code&gt; — every match is a code path that breaks on July 1.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "createVerify" .&lt;/code&gt; near webhook handlers — RSA verifier calls that need to become &lt;code&gt;crypto.verify(null, ...)&lt;/code&gt; Ed25519 calls.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;grep -rn "if (!sig\|if (!signature\|sig === undefined" .&lt;/code&gt; — skip-verify branches that turn into open endpoints.&lt;/li&gt;
&lt;li&gt;Search your env vars and secret stores for &lt;code&gt;HIGHLEVEL_PUBKEY&lt;/code&gt; or similar — anything pinned to the RSA key needs to be re-pointed at the Ed25519 key, ideally fetched at runtime.&lt;/li&gt;
&lt;li&gt;Confirm your webhook handler preserves the &lt;strong&gt;raw body&lt;/strong&gt; for Ed25519 verification — &lt;code&gt;body-parser&lt;/code&gt;'s default JSON middleware consumes the stream and loses the byte-exact payload that the signature covers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The cutoff is hard. HighLevel is not running a parallel-signed period where both headers ship — once &lt;code&gt;X-WH-Signature&lt;/code&gt; is deprecated, it's gone. The thirty minutes spent grepping now is the difference between a quiet July 2 and explaining to a client why the "new lead → SMS notification" pipeline stopped firing five days into the month.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FlareCanary monitors API responses for schema drift, silent removals, and behavior changes across upstream providers. If you run integrations against HighLevel, Stripe, Shopify, GitHub, or anywhere else that ships breaking changes through changelog posts, &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;flarecanary.com&lt;/a&gt; catches the drift before your customers do.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webhooks</category>
      <category>security</category>
      <category>integrations</category>
      <category>api</category>
    </item>
    <item>
      <title>GitHub GraphQL drops Team.viewerSubscription + 17 audit-log entries on July 1 — and partial errors are the silent surface</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Mon, 22 Jun 2026 05:00:50 +0000</pubDate>
      <link>https://dev.to/flarecanary/github-graphql-drops-teamviewersubscription-17-audit-log-entries-on-july-1-and-partial-errors-5a9k</link>
      <guid>https://dev.to/flarecanary/github-graphql-drops-teamviewersubscription-17-audit-log-entries-on-july-1-and-partial-errors-5a9k</guid>
      <description>&lt;p&gt;If you use the GitHub GraphQL API for team notification dashboards, audit-log exports, or any kind of organization compliance reporting, you have nine days when this lands. On &lt;strong&gt;July 1, 2026&lt;/strong&gt;, GitHub deletes two fields from the &lt;code&gt;Team&lt;/code&gt; type and the fields from seventeen audit-log entry types.&lt;/p&gt;

&lt;p&gt;The fields are listed in GitHub's own &lt;a href="https://docs.github.com/en/graphql/overview/breaking-changes" rel="noopener noreferrer"&gt;breaking changes log&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Team.viewerSubscription&lt;/code&gt; — removed. Reason: "Team notifications subscriptions are being deprecated."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Team.viewerCanSubscribe&lt;/code&gt; — removed. Same reason.&lt;/li&gt;
&lt;li&gt;All scalar fields on &lt;code&gt;TeamAddMemberAuditEntry&lt;/code&gt;, &lt;code&gt;TeamAddRepositoryAuditEntry&lt;/code&gt;, &lt;code&gt;TeamChangeParentTeamAuditEntry&lt;/code&gt;, &lt;code&gt;TeamRemoveMemberAuditEntry&lt;/code&gt;, &lt;code&gt;TeamRemoveRepositoryAuditEntry&lt;/code&gt;, and twelve sibling repository/audit entry types — removed. Reason: "The GraphQL audit-log is deprecated. Please use the REST API instead."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The loud failure mode is the one most teams plan around. A query that selects a field GraphQL no longer recognizes fails server-side validation. You get a clear error, you regenerate types, you redeploy.&lt;/p&gt;

&lt;p&gt;That's not the failure mode this article is about.&lt;/p&gt;

&lt;p&gt;The failure mode worth thinking about is GraphQL's partial-error semantics — specifically, what happens to clients that don't treat the &lt;code&gt;errors&lt;/code&gt; array as fatal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "removed" actually returns
&lt;/h2&gt;

&lt;p&gt;When GitHub removes &lt;code&gt;Team.viewerSubscription&lt;/code&gt;, queries that select it don't fail with a 4xx HTTP status. The HTTP layer returns 200 OK with a body like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"organization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"team"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"viewerSubscription"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Field 'viewerSubscription' doesn't exist on type 'Team'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"locations"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"column"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"viewerSubscription"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact shape depends on whether GitHub leaves the field validation-rejected (no &lt;code&gt;data&lt;/code&gt; for the team at all) or treats it as a removed-but-tolerated field returning null. In practice, large-vendor GraphQL APIs deliberately bias toward the second shape during deprecation periods so that clients with partially-stale schemas don't go fully dark.&lt;/p&gt;

&lt;p&gt;Either way, the client side of this is where it gets quietly bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Apollo / urql / Relay "log and continue" pattern
&lt;/h2&gt;

&lt;p&gt;Most production GraphQL client setups have an error link or middleware that logs &lt;code&gt;errors[]&lt;/code&gt; to a telemetry sink and continues. The reasoning is sound — partial responses are useful, and a network-level failure should not be confused with a single-field schema mismatch.&lt;/p&gt;

&lt;p&gt;The result is that a compliance dashboard that does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;GET_TEAM_SUBS&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscribed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;team&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;viewerSubscription&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SUBSCRIBED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…silently flips &lt;code&gt;subscribed&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt; on July 1. The Apollo error link logged the schema error to Sentry, where it joins the existing six hundred GraphQL errors nobody triages.&lt;/p&gt;

&lt;p&gt;If the dashboard's job is to show "who on the platform team is muted on PR notifications," the dashboard reports "everybody is muted." If the job is to enforce "all leads must be subscribed to security-channel notifications," every lead's subscription check returns false and either the enforcement skips them (if the rule is "alert when subscribed == false") or alerts on everybody (if the rule is "alert when subscribed != true"). Most enforcement rules are written the second way, which means July 1 produces a wall of false-positive alerts and a tendency to mute the rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Persisted queries and codegen don't help
&lt;/h2&gt;

&lt;p&gt;The standard answer to GraphQL drift — codegen with &lt;code&gt;graphql-codegen&lt;/code&gt; or Relay's compiler, against an up-to-date schema — only helps if you're regenerating before July 1. The codegen does not know that a field that exists in your local schema today will be gone on a future date.&lt;/p&gt;

&lt;p&gt;The deeper problem is persisted queries. A persisted-query setup hashes the query at build time and submits the hash at runtime. Server-side, GitHub validates against its live schema. The build that shipped last quarter persisted a query selecting &lt;code&gt;Team.viewerSubscription&lt;/code&gt;. The build doesn't change between now and July 1. On July 1, the hashed query starts returning the partial-error shape above, and the client app never sees a code change to prompt regeneration.&lt;/p&gt;

&lt;p&gt;If you have a persisted-query setup pointed at GitHub's GraphQL endpoint, every persisted hash that references the removed fields needs to be rebuilt and republished before July 1 — not after a deploy, after July 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The audit-log surface is a different problem
&lt;/h2&gt;

&lt;p&gt;GitHub is not just removing two &lt;code&gt;Team&lt;/code&gt; fields. It is deprecating the entire GraphQL audit-log subsystem and pointing users at the REST endpoint instead.&lt;/p&gt;

&lt;p&gt;The seventeen entry types that get gutted on July 1 cover the common team and repository administrative actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;TeamAddMemberAuditEntry&lt;/code&gt;, &lt;code&gt;TeamRemoveMemberAuditEntry&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TeamAddRepositoryAuditEntry&lt;/code&gt;, &lt;code&gt;TeamRemoveRepositoryAuditEntry&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TeamChangeParentTeamAuditEntry&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The repository-side equivalents for visibility, archive, transfer, and access changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical security pipeline does something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Audit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;!,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$org&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;auditLog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;edges&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="n"&gt;__typename&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TeamAddMemberAuditEntry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;actorLogin&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;login&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;pageInfo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endCursor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;hasNextPage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After July 1, the &lt;code&gt;node&lt;/code&gt; still has &lt;code&gt;__typename&lt;/code&gt;, but every field inside the &lt;code&gt;... on TeamAddMemberAuditEntry&lt;/code&gt; fragment returns null or schema-errors out. The pipeline still iterates pages, still increments its "events processed" counter, still writes rows to the warehouse. The warehouse rows are tombstones — typename and timestamps if you're lucky, nothing if you're not.&lt;/p&gt;

&lt;p&gt;The REST migration is non-trivial. The REST audit-log API uses cursor pagination via a &lt;code&gt;phrase=&lt;/code&gt; query parameter rather than GraphQL connection cursors, returns flattened event objects rather than nested &lt;code&gt;team { }&lt;/code&gt; / &lt;code&gt;user { }&lt;/code&gt; shapes, and has different field names (&lt;code&gt;actor&lt;/code&gt; vs &lt;code&gt;actorLogin&lt;/code&gt;, &lt;code&gt;team&lt;/code&gt; as a string vs an object). It's a rewrite, not a remap.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Insights/Analytics integrations don't surface this
&lt;/h2&gt;

&lt;p&gt;The third-party analytics tools layered on top of GitHub's GraphQL API — &lt;code&gt;lowdefy&lt;/code&gt;, Sourcegraph batch changes that read audit data, GitHub-Actions-based compliance bots — typically use the GitHub Apps OAuth flow and inherit GraphQL access. None of them publish a "we use these fields" manifest. The only way to know whether one of them will break is to query its error logs after July 1, or to read its source.&lt;/p&gt;

&lt;p&gt;The default posture is to assume any integration that touches Teams or audit data is affected unless its changelog explicitly says otherwise. The vendors with a published changelog (Octokit, gh CLI) will have already shipped a fix. The bespoke "we wrote a small dashboard for our compliance team" surface is where the drift lands.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find what's affected before July 1
&lt;/h2&gt;

&lt;p&gt;Three commands worth running this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Grep your queries:&lt;/strong&gt; &lt;code&gt;grep -rE 'viewerSubscription|viewerCanSubscribe|TeamAddMemberAuditEntry|TeamRemoveMemberAuditEntry|TeamAddRepositoryAuditEntry|TeamRemoveRepositoryAuditEntry|TeamChangeParentTeamAuditEntry' src/ queries/&lt;/code&gt; catches direct references. Add the twelve other audit-entry types if your reporting covers repository visibility/transfer changes. GitHub's &lt;a href="https://docs.github.com/en/graphql/overview/breaking-changes" rel="noopener noreferrer"&gt;breaking-changes page&lt;/a&gt; has the full list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory persisted queries:&lt;/strong&gt; any GraphQL setup using Apollo's Automatic Persisted Queries or Relay's &lt;code&gt;--persist-output&lt;/code&gt; flag has a JSON map of query hashes to query bodies. &lt;code&gt;grep&lt;/code&gt; that map for the field names. The hashes themselves are content-addressed, so a hash referencing &lt;code&gt;viewerSubscription&lt;/code&gt; won't change until the query body changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replay last week's audit-log queries with the deprecation banner:&lt;/strong&gt; GitHub returns an &lt;code&gt;X-GitHub-Media-Type&lt;/code&gt; header and includes deprecation warnings in the &lt;code&gt;errors[]&lt;/code&gt; array for fields scheduled for removal. If your client logs GraphQL errors anywhere — Sentry, Datadog, CloudWatch — filter for the message &lt;code&gt;"Team notifications subscriptions are being deprecated"&lt;/code&gt; or &lt;code&gt;"The GraphQL audit-log is deprecated"&lt;/code&gt; over the past 30 days. Anything that fires today will silently fail on July 1.&lt;/p&gt;

&lt;p&gt;The published deprecation table is here: &lt;a href="https://docs.github.com/en/graphql/overview/breaking-changes" rel="noopener noreferrer"&gt;GitHub GraphQL API Breaking changes&lt;/a&gt;. The REST replacement for the audit log is documented under &lt;a href="https://docs.github.com/en/rest/orgs/audit-log" rel="noopener noreferrer"&gt;REST API endpoints for audit log&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The pattern this lands in is the one GraphQL APIs hit most often during big deprecation cycles. The schema validator doesn't care about wall-clock dates — until July 1, the field exists and queries succeed. On July 1, the field returns null with a partial error that most production clients are configured to swallow. The dashboards stop showing data, the compliance enforcers stop enforcing, and the alarms don't fire because the request never failed.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FlareCanary watches the response shapes of API endpoints you depend on and tells you when a field that used to be present starts coming back null. GraphQL partial-error responses are exactly the kind of drift that survives an Apollo error link and lands in a dashboard tile that's quietly wrong. &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;flarecanary.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>graphql</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Azure SQL Database 2014-04-01 APIs retire June 30 — your tools auto-upgrade, the response shapes change</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Fri, 19 Jun 2026 05:00:59 +0000</pubDate>
      <link>https://dev.to/flarecanary/azure-sql-database-2014-04-01-apis-retire-june-30-your-tools-auto-upgrade-the-response-shapes-lj2</link>
      <guid>https://dev.to/flarecanary/azure-sql-database-2014-04-01-apis-retire-june-30-your-tools-auto-upgrade-the-response-shapes-lj2</guid>
      <description>&lt;p&gt;If you manage Azure SQL Database resources through ARM templates, the Azure SQL REST API, the Az.Sql PowerShell module on an old version, the Azure CLI shipped with an older deployment image, or any third-party tooling that wraps the Azure Resource Manager surface, you have eight days of runway when this lands. Microsoft is retiring the entire &lt;code&gt;2014-04-01&lt;/code&gt; API version of the Azure SQL Database control plane on &lt;strong&gt;June 30, 2026&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The original retirement date was October 31, 2025. Microsoft extended it by eight months "based on customer feedback," which means a lot of estate still has 2014-04-01 baked into it.&lt;/p&gt;

&lt;p&gt;The loud failure mode is straightforward: ARM templates that pin &lt;code&gt;"apiVersion": "2014-04-01"&lt;/code&gt; start failing at deploy time. You get a clear error, you bump the version, you redeploy. That's not the failure mode this article is about.&lt;/p&gt;

&lt;p&gt;The interesting failure mode is what happens when your tooling auto-upgrades to the newer stable version (&lt;code&gt;2021-11-01&lt;/code&gt;) without you noticing. Microsoft itself describes this as the benign path:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The retirement of the 2014 API will not affect Azure PaaS or SaaS products that are kept up to date, as the API will be updated automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What the announcement does not spell out is that the upgrade from 2014-04-01 to 2021-11-01 is not a transparent version bump. It's seven years of API redesigns crammed into a single rename. Endpoints disappear without a replacement. Resource types are renamed. Audit policies move from one storage backend to another. The most common automation patterns built against 2014-04-01 will continue to "succeed" against 2021-11-01 — they just stop reading the thing they think they're reading.&lt;/p&gt;

&lt;p&gt;The mappings are spelled out in Microsoft's own &lt;a href="https://learn.microsoft.com/en-us/rest/api/sql/retirement" rel="noopener noreferrer"&gt;retirement notice&lt;/a&gt;. Here are the five silent surfaces that fall out of the differences between the two versions, with what each one does to the kind of script you probably have running.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Eight endpoint groups have no replacement at all
&lt;/h2&gt;

&lt;p&gt;The retirement table lists thirty-odd 2014-04-01 endpoint groups mapped to their 2021-11-01 successors. Eight of them have a footnote that reads: &lt;em&gt;"APIs marked as deprecated with no newer/stable versions, are permanently discontinued from the service."&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database Connection Policies&lt;/li&gt;
&lt;li&gt;Elastic Pool Activities&lt;/li&gt;
&lt;li&gt;Elastic Pool Database Activities&lt;/li&gt;
&lt;li&gt;Queries&lt;/li&gt;
&lt;li&gt;Query Statistics&lt;/li&gt;
&lt;li&gt;Query Texts&lt;/li&gt;
&lt;li&gt;Recommended Elastic Pools&lt;/li&gt;
&lt;li&gt;Service Tier Advisors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have a script that polls &lt;code&gt;/queries&lt;/code&gt;, &lt;code&gt;/queryStatistics&lt;/code&gt;, or &lt;code&gt;/queryTexts&lt;/code&gt; against the 2014-04-01 API to surface query performance data outside of the portal — the SDK or tooling layer above you cannot transparently retry against 2021-11-01, because the endpoint isn't there. Whatever your tooling does when an endpoint disappears, that's what it does on July 1. Most SDKs raise. Most schedulers log the raise and continue. The dashboard the script was feeding goes flat. Nobody notices because the job ran "successfully" — it raised, logged, and exited zero, exactly as it was written to.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Recommended Elastic Pools&lt;/code&gt; is the sneakiest of the eight, because the recommendation it surfaced was advisory. If your pool-sizing job was using it to decide whether to scale or merge pools, the job continues to make sizing decisions — based on an empty recommendation set. Everything looks "stable" because there's nothing to act on.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Table-based auditing silently maps to blob-based auditing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Database Table Auditing Policies&lt;/code&gt; (2014-04-01) maps to &lt;code&gt;Database Blob Auditing Policies&lt;/code&gt; (2021-11-01). Same for the server-level variant.&lt;/p&gt;

&lt;p&gt;These are not the same audit subsystem. Table auditing wrote audit rows to Azure Table Storage in the same storage account. Blob auditing writes audit logs to Blob Storage with a different filename convention, different retention semantics, and a different schema. The legacy table-auditing destination has been deprecated for years, but the &lt;em&gt;API surface&lt;/em&gt; persisted in 2014-04-01 as a way to enable, disable, and read the policy.&lt;/p&gt;

&lt;p&gt;Tooling that reads "is auditing enabled" against the new endpoint gets a different answer for the same logical question. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A compliance script that did &lt;code&gt;GET .../auditingPolicies/Default?api-version=2014-04-01&lt;/code&gt; and asserted on &lt;code&gt;properties.auditingState == "Enabled"&lt;/code&gt; is now hitting &lt;code&gt;.../auditingSettings/default?api-version=2021-11-01&lt;/code&gt;. The property layout is different. If the tool was tolerant — e.g. checked &lt;code&gt;if (auditing.properties.state == 'Enabled')&lt;/code&gt; against a value that doesn't appear in the new shape — it returns &lt;code&gt;undefined&lt;/code&gt;, the check evaluates falsy, and you report "auditing not enabled" for databases that very much have blob auditing enabled.&lt;/li&gt;
&lt;li&gt;A provisioner that &lt;em&gt;wrote&lt;/em&gt; table-auditing config via 2014-04-01 (&lt;code&gt;PUT .../auditingPolicies/Default&lt;/code&gt;) is now writing blob-auditing config (&lt;code&gt;PUT .../auditingSettings/default&lt;/code&gt;). The request shape is similar enough that a JSON template with the wrong keys can still 200, but the resulting policy is partially configured, pointed at the wrong storage container, or missing the retention period. Your IaC layer reports a successful apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, the audit you thought you had is not the audit you have, and the surface that would tell you is the surface that just changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Threat Detection Policies were renamed to Advanced Threat Protection Settings
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Database Threat Detection Policies&lt;/code&gt; (2014-04-01) maps to &lt;code&gt;Database Advanced Threat Protection Settings&lt;/code&gt; (2021-11-01).&lt;/p&gt;

&lt;p&gt;The 2014 endpoint exposed properties like &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;emailAddresses&lt;/code&gt;, &lt;code&gt;emailAccountAdmins&lt;/code&gt;, and &lt;code&gt;disabledAlerts&lt;/code&gt;. The 2021 endpoint replaces the alert/email config with a simpler &lt;code&gt;state&lt;/code&gt; toggle and pushes the alert configuration into Defender for Cloud at the subscription tier.&lt;/p&gt;

&lt;p&gt;A security posture script that reads the old endpoint's &lt;code&gt;disabledAlerts&lt;/code&gt; array — say, to ensure SQL injection alerts haven't been muted — has nothing analogous to read on the new endpoint. The migration broke the very signal the script existed to check. The script still runs. It still produces a report. The report just doesn't include the field it was built around, and depending on how it was written, either renders the column empty or silently treats "no muted alerts" as the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Disaster Recovery Configurations became Failover Groups
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Disaster Recovery Configurations&lt;/code&gt; (2014-04-01) → &lt;code&gt;Failover Groups&lt;/code&gt; (2021-11-01).&lt;/p&gt;

&lt;p&gt;This isn't a rename for cosmetic reasons. Failover Groups are the modern, multi-database DR primitive. Disaster Recovery Configurations were a per-database active geo-replication wrapper with a different lifecycle, different role semantics ("Primary"/"Secondary"/"Source"/"Target" depending on which way you looked at it), and a different relationship to the underlying replicas.&lt;/p&gt;

&lt;p&gt;If you have a DR posture monitor that does something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;for each server, list &lt;code&gt;disasterRecoveryConfiguration&lt;/code&gt; children, count where role == "Primary," compare against expected_count, alert if delta&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;…you are reading an empty list on the new endpoint, because nothing creates DR configurations on a 2021-vintage server. Everything created in the last few years is a failover group, and failover groups live at a different URL with a different resource type. The script returns zero. The script's invariant is "zero is bad." But the script doesn't fire, because the script's invariant was actually "zero compared to last week's count is bad" — and last week's count was also zero, since the script has been wrong since the auto-upgrade happened. It looks healthy by virtue of being uniformly broken.&lt;/p&gt;

&lt;p&gt;The unambiguous version of this problem is the IaC tool that, on next plan, decides to &lt;em&gt;delete&lt;/em&gt; the &lt;code&gt;disasterRecoveryConfiguration&lt;/code&gt; child because "it's not in the template anymore." It isn't in the template anymore because the template now references &lt;code&gt;failoverGroups&lt;/code&gt;. The DR resource itself doesn't get deleted (the underlying replica's data lives elsewhere), but the management plane object pointing at it does, and reattaching from scratch is the kind of thing you find out about during a real outage.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Restorable Dropped Databases now points at managed instances
&lt;/h2&gt;

&lt;p&gt;The most quietly dangerous entry in the migration table:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Restorable Dropped Databases&lt;/code&gt; (2014-04-01) → &lt;code&gt;Restorable Dropped Managed Databases&lt;/code&gt; (2021-11-01).&lt;/p&gt;

&lt;p&gt;Read that carefully. The replacement is the &lt;em&gt;managed instance&lt;/em&gt; variant, not the logical-database variant. If you had a self-service "undelete a dropped database" tool that called the 2014 endpoint against your standard Azure SQL Database (not managed instance), and that tool got auto-upgraded by an SDK bump or a wrapper-library bump, it is now asking the API for restorable dropped &lt;em&gt;managed&lt;/em&gt; databases. For a standard Azure SQL server, the list is empty.&lt;/p&gt;

&lt;p&gt;The runtime behavior:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User drops a database, panics, opens the self-service restore tool.&lt;/li&gt;
&lt;li&gt;The tool calls the upgraded endpoint, asks for restorable dropped databases on the managed-instance resource path.&lt;/li&gt;
&lt;li&gt;The managed-instance resource doesn't exist on a standard logical server, or the list returns empty for the correct managed instance.&lt;/li&gt;
&lt;li&gt;The tool reports "no restorable databases found."&lt;/li&gt;
&lt;li&gt;The user assumes the drop was permanent and moves on.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The actual restore endpoint for a standard logical server is &lt;code&gt;restorableDroppedDatabases&lt;/code&gt; under &lt;code&gt;Microsoft.Sql/servers/restorableDroppedDatabases&lt;/code&gt; — which &lt;em&gt;still exists&lt;/em&gt; in &lt;code&gt;2021-11-01&lt;/code&gt;, just not in this row of the migration table. Microsoft's own retirement-mapping doc only lists the managed-instance variant under the 2014 row, because the standard variant has been promoted to the server-scoped resource model. If your tool didn't notice the model shift, the migration documentation actively misleads.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find what's affected before June 30
&lt;/h2&gt;

&lt;p&gt;Three commands to run in your environment this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory ARM templates and Bicep:&lt;/strong&gt; &lt;code&gt;grep -rE '"apiVersion"\s*:\s*"2014-04-01"' templates/ infra/&lt;/code&gt; (or the moral equivalent in Bicep) catches the loud cases. Use the &lt;a href="https://learn.microsoft.com/en-us/azure/governance/resource-graph/overview" rel="noopener noreferrer"&gt;Azure Resource Graph&lt;/a&gt; to see &lt;em&gt;deployed&lt;/em&gt; resources by API version if you have it enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory SDK and CLI versions:&lt;/strong&gt; the Azure SQL SDKs (&lt;code&gt;Azure.ResourceManager.Sql&lt;/code&gt; in .NET, &lt;code&gt;azure-mgmt-sql&lt;/code&gt; in Python) shipped major versions between the 2014 and 2021 API generations. A &lt;code&gt;pip list | grep azure-mgmt-sql&lt;/code&gt; or its NuGet/npm equivalent will show whether you're on a version that pins old APIs. The Az.Sql PowerShell module has the same property — &lt;code&gt;Get-Module Az.Sql | Format-List Version&lt;/code&gt; tells you whether your runners are still on a pre-&lt;code&gt;5.x&lt;/code&gt; build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inventory custom REST callers:&lt;/strong&gt; if anything in your environment talks directly to &lt;code&gt;management.azure.com/subscriptions/.../providers/Microsoft.Sql/...?api-version=2014-04-01&lt;/code&gt;, that's the highest-confidence place to look first. Network egress logs, an &lt;code&gt;az monitor activity-log list&lt;/code&gt; over the past 30 days, or just a grep across your repos for &lt;code&gt;Microsoft.Sql/&lt;/code&gt; paths will surface them.&lt;/p&gt;

&lt;p&gt;For each find, the migration is mechanical when the endpoint maps cleanly (most &lt;code&gt;databases&lt;/code&gt;, &lt;code&gt;servers&lt;/code&gt;, &lt;code&gt;firewallRules&lt;/code&gt; calls), and a real design decision when it lands on one of the five surfaces above. Make the design decision now while you have a fallback period, not after July 1.&lt;/p&gt;

&lt;p&gt;The published retirement notice is here: &lt;a href="https://learn.microsoft.com/en-us/rest/api/sql/retirement" rel="noopener noreferrer"&gt;Azure SQL Database REST API 2014-04-01 Retirement Notice&lt;/a&gt;. The full 2021-11-01 surface is documented in the same place — read it side-by-side with whatever your tooling is currently using.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;FlareCanary watches the response shapes of API endpoints you depend on and tells you when they drift. The Azure SQL control plane is exactly the kind of surface where renamed properties and silently-substituted endpoints land in production before a deployment-time error ever fires. &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;flarecanary.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>cloud</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>SharePoint silently retired the EnableAzureADB2BIntegration setting in May — and your old guest links break in July</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Wed, 17 Jun 2026 04:03:10 +0000</pubDate>
      <link>https://dev.to/flarecanary/sharepoint-silently-retired-the-enableazureadb2bintegration-setting-in-may-and-your-old-guest-4jnd</link>
      <guid>https://dev.to/flarecanary/sharepoint-silently-retired-the-enableazureadb2bintegration-setting-in-may-and-your-old-guest-4jnd</guid>
      <description>&lt;p&gt;If you administer a SharePoint Online tenant, the &lt;code&gt;EnableAzureADB2BIntegration&lt;/code&gt; setting is the thing you used to flip to choose between the legacy SharePoint One-Time Passcode (SPO OTP) experience for external guests and the modern Entra B2B Invitation Manager flow. The cmdlet has been around for years. Compliance scripts read it. Tenant baselines assert on it. Some org policies still document it as the toggle.&lt;/p&gt;

&lt;p&gt;Starting May 2026, that setting no longer does anything.&lt;/p&gt;

&lt;p&gt;Microsoft is rolling Entra B2B integration out to every SharePoint tenant automatically. The rollout windows are not announced per-tenant — "tenants are selected automatically by our rollout systems. You cannot choose a specific date." Once your tenant flips, &lt;code&gt;Set-SPOTenant -EnableAzureADB2BIntegration $false&lt;/code&gt; becomes a no-op. The cmdlet returns without error. The next &lt;code&gt;Get-SPOTenant&lt;/code&gt; may still show a value. External sharing has already moved to B2B regardless.&lt;/p&gt;

&lt;p&gt;And in &lt;strong&gt;July 2026&lt;/strong&gt;, the other shoe drops. Every external guest who was authenticated through SPO OTP — and never got upgraded to a real Entra B2B guest account in your directory — silently loses access to every link you ever shared with them. The link doesn't 404. The user gets a flat "This organization updated its guest access settings" page. Your audit log says the sharing event was successful months ago. Your sharing reports still list the file as shared. The recipient just can't open it anymore.&lt;/p&gt;

&lt;p&gt;That's two stacked silent surfaces with a hard July cliff in between. Here's what each one actually does, and how to find what's affected before the cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The setting is gone but the cmdlet still answers
&lt;/h2&gt;

&lt;p&gt;Read the Microsoft docs carefully:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Starting May 2026, Microsoft enables SharePoint and OneDrive integration with Microsoft Entra B2B for all tenants, &lt;strong&gt;regardless of the tenant's setting&lt;/strong&gt; for &lt;code&gt;EnableAzureADB2BIntegration&lt;/code&gt;. Once rolled out, &lt;strong&gt;this setting has no effect on sharing behavior&lt;/strong&gt;, and the ability to disable the integration is removed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The phrase that gets glossed over is "regardless of the tenant's setting." The setting field is not being deleted. The PowerShell cmdlet is not being removed. The property still exists, still returns a value, and &lt;code&gt;Set-SPOTenant -EnableAzureADB2BIntegration $false&lt;/code&gt; still parses and executes. It just no longer controls the behavior it used to control.&lt;/p&gt;

&lt;p&gt;This is the textbook silent surface: an API endpoint that continues to accept the same request and return the same response, but the underlying behavior changed. Anything in your environment that reads the setting to &lt;em&gt;infer&lt;/em&gt; sharing behavior is now wrong.&lt;/p&gt;

&lt;p&gt;Concrete examples of what breaks in the dark:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compliance baselines.&lt;/strong&gt; If your CIS / NIST / internal policy baseline asserts that &lt;code&gt;EnableAzureADB2BIntegration&lt;/code&gt; is &lt;code&gt;$true&lt;/code&gt;, that check still passes after rollout, but it's measuring nothing — the answer is &lt;code&gt;True&lt;/code&gt; for everyone whether they configured it or not. If your baseline asserts it's &lt;code&gt;$false&lt;/code&gt; (some orgs deliberately kept legacy OTP), the check passes and the assertion is a lie. External sharing is using B2B regardless.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant-state monitors.&lt;/strong&gt; Scripts that snapshot tenant settings nightly and diff against a known-good baseline see no diff. The setting didn't change. The world around the setting did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-rollout audits.&lt;/strong&gt; If you ran a "we'll handle this when we're ready" plan and used the setting as your gate, you can't gate on it anymore. The rollout flips your tenant when the rollout flips your tenant.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent provisioning.&lt;/strong&gt; Terraform, Azure Bicep, DSC, or whatever provisions tenant settings continues to "successfully" set the value. The plan and apply both succeed. The value the next plan sees is unchanged. Nothing in the tooling reports that you provisioned a setting that does nothing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've ever depended on this setting to reason about whether sharing goes through OTP or B2B, you can't anymore. The only reliable source of truth after May is "the rollout has happened to this tenant" — which Microsoft does not surface as a single boolean. You have to look at the actual share events.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Auto-rollout means you can't time your migration
&lt;/h2&gt;

&lt;p&gt;When a Microsoft 365 change has an admin-controlled flag, you can plan: turn it on in a test tenant, run validation, schedule the production flip. When the change is forced on by central rollout, you lose that.&lt;/p&gt;

&lt;p&gt;The FAQ is explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I choose when this integration is enabled for my tenant? &lt;strong&gt;No.&lt;/strong&gt; Tenants are selected automatically by our rollout systems. You cannot choose a specific date.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I opt out of this change? &lt;strong&gt;No.&lt;/strong&gt; This change applies to all tenants and aligns with Microsoft's strategy of using one centralized identity provider across all Microsoft 365 applications.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So between May and the end of the rollout, your tenant flips one day with no notice on your end. The day it flips, every new external share creates an Entra B2B guest in your directory instead of an SPO OTP entry. Existing OTP guests are not auto-upgraded. The two populations now coexist in your tenant.&lt;/p&gt;

&lt;p&gt;That coexistence is the trap. Until July, &lt;strong&gt;both populations can still open files.&lt;/strong&gt; B2B guests authenticate via Entra; OTP guests authenticate via the SPO OTP code flow. Everything looks fine. Sharing reports still light up green. Your helpdesk doesn't see tickets, because nobody's actually broken yet.&lt;/p&gt;

&lt;p&gt;So you assume the rollout was a non-event. You move on. Then in July, the OTP cohort starts hitting access-denied, and you have no list of who they are because nobody told you the cohort even existed.&lt;/p&gt;

&lt;p&gt;The only way to get ahead of this is to enumerate, &lt;em&gt;before July&lt;/em&gt;, every external user who is authenticated through OTP and does not yet have a corresponding B2B guest account in your directory. Microsoft surfaces this at site level via the external sharing report — pull the report, look at the &lt;code&gt;User E-mail&lt;/code&gt; column, cross-reference against Entra B2B guests. The mismatch is your at-risk cohort. The tenant-level admin center does not give you this list.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. July's access-denied is silent from the admin side
&lt;/h2&gt;

&lt;p&gt;Here's the precise behavior Microsoft documents:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do my guest users retain access to files shared before the integration? &lt;strong&gt;Yes, but only if they have a Microsoft Entra B2B guest account in your directory.&lt;/strong&gt; If they do not have a Microsoft Entra B2B guest account, external collaborators see access denied starting July 2026.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What the external user sees, after rollout: a page that says "This organization updated its guest access settings." Not "link expired." Not "file not found." Not 404. A specific, generic-sounding error that most users will read as "the link broke" and either re-ping the sender or quietly give up.&lt;/p&gt;

&lt;p&gt;What the admin sees: nothing, until someone files a ticket. The audit log entry for the original share is still there, intact, months old. The file is still shared — &lt;code&gt;Get-SPOUser&lt;/code&gt; still lists the guest. The sharing report still shows the link. The only thing different is that the guest's authentication path no longer works because the OTP authentication backend isn't where SharePoint looks anymore.&lt;/p&gt;

&lt;p&gt;This is the asymmetric failure that makes it hard to test: the share-side state stays valid, the auth-side state stops being honored. Anything you build to monitor "is this share still active" by inspecting share state returns "yes" all the way through the cliff.&lt;/p&gt;

&lt;p&gt;The Microsoft-recommended fix is to either pre-create B2B guest accounts for at-risk users (so July is uneventful), or rely on a workflow where an internal user reshares one file — that creates the B2B guest, which then retroactively grants access to every link previously shared with that email. The second option works but requires a user to notice access is broken and request a reshare, which means the broken-access state is part of the user experience.&lt;/p&gt;

&lt;p&gt;If your SharePoint estate has guests from any tenant provisioned &lt;strong&gt;before June 2023&lt;/strong&gt;, assume you have OTP-only guests. Tenants provisioned after June 2023 have B2B integration on by default and aren't impacted by this specific population, but they still get the setting-no-op behavior from section 1.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Anyone-links and Anonymous-links are unaffected — which matters because people will assume otherwise
&lt;/h2&gt;

&lt;p&gt;One thing that's explicitly &lt;em&gt;not&lt;/em&gt; changing is anonymous/Anyone links:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is there any impact on Anyone/Anonymous links? &lt;strong&gt;No.&lt;/strong&gt; SPO OTP retirement and use of Entra B2B for external sharing does not impact Anyone/Anonymous links.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Surface this in your internal comms. The natural reaction to "external sharing is changing in July" is to assume every external link is at risk and to either re-share everything or panic-disable external sharing. Anonymous links never used OTP — they use the link's own access controls — so they sail through this entirely.&lt;/p&gt;

&lt;p&gt;The cohort that's at risk is the narrow one: links shared with a &lt;em&gt;specific person's email address&lt;/em&gt;, where that recipient authenticated via SPO OTP, and where that recipient has not yet been promoted to a B2B guest in your directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Audit-log event types: the sharing event itself looks different post-rollout
&lt;/h2&gt;

&lt;p&gt;The other piece nobody is talking about is that the audit-log shape of an external share event changes once your tenant is on Entra B2B integration. SPO OTP shares emitted SharePoint-namespaced events with the OTP authentication flow. Entra B2B Invitation Manager emits Microsoft Entra audit events (&lt;code&gt;Add user&lt;/code&gt;, &lt;code&gt;Invite external user&lt;/code&gt;) followed by the SharePoint share event.&lt;/p&gt;

&lt;p&gt;If you have SIEM filters keyed on the SPO OTP event signature — for example, alerting on "external share to a high-risk domain via OTP" — those filters quietly stop matching the new shares. The new shares still happen; they just no longer trip your old query. Same kind of silent surface as the setting in section 1: the data is there, your filter just doesn't see it.&lt;/p&gt;

&lt;p&gt;For Microsoft Sentinel, Defender, Purview Audit, or any third-party SIEM ingesting Microsoft 365 audit data, audit the queries that depend on share-event shape and make sure they also match the Entra B2B Invitation Manager flow. Microsoft Graph Data Connect (MGDC) is the higher-fidelity source if you need both populations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;End of April 2026&lt;/strong&gt; — Last window to manually run &lt;code&gt;Set-SPOTenant -EnableAzureADB2BIntegration $true&lt;/code&gt; on your own schedule and validate in a controlled way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 2026&lt;/strong&gt; — Auto-rollout begins. Tenants flip on Microsoft's schedule, not yours. The setting becomes a no-op. New external shares use Entra B2B.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;July 2026&lt;/strong&gt; — OTP-only guests start seeing access-denied on previously shared links. The hard cliff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August 31, 2026&lt;/strong&gt; — Full SPO OTP retirement complete.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this looks like on a monitoring dashboard
&lt;/h2&gt;

&lt;p&gt;The whole point of writing this up is that none of these surfaces are visible in a normal "is my API working" check. The PowerShell endpoint answers. The audit log writes events. The sharing report renders. The compliance baseline reports green. The user sees a generic error page. The admin sees nothing.&lt;/p&gt;

&lt;p&gt;This is the same shape as the schema-drift incidents we've been tracking elsewhere — Twilio dropping &lt;code&gt;transit_callerid&lt;/code&gt;, Stripe flipping &lt;code&gt;unit_amount_decimal&lt;/code&gt; from string to Decimal, GitHub silently removing &lt;code&gt;payload.commits&lt;/code&gt; from PushEvent: the endpoint stays alive, the response stays parseable, and the &lt;em&gt;meaning&lt;/em&gt; of what it returns has changed under the integration. The tooling that doesn't notice is the tooling that asserts on shape, not on semantics.&lt;/p&gt;

&lt;p&gt;For SharePoint specifically, the assertions that actually catch this are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Setting + behavior coherence:&lt;/strong&gt; read &lt;code&gt;EnableAzureADB2BIntegration&lt;/code&gt; and &lt;em&gt;also&lt;/em&gt; sample a recent external share event; assert the auth path in the event matches the setting. After rollout, the setting can claim &lt;code&gt;$false&lt;/code&gt; and the share will be via B2B — that mismatch is your signal that the rollout flipped you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guest population audit:&lt;/strong&gt; weekly diff of "external email addresses with active shares" vs. "Entra B2B guest accounts." Any email in the first set not in the second is at risk in July.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Share-event shape monitoring:&lt;/strong&gt; alert on a change in the dominant audit event type for external shares — when SPO-OTP-shaped events drop to zero and B2B-Invitation-Manager-shaped events become the norm, your tenant has been flipped, and your SIEM filters need re-tuning.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you maintain any of those today, this rollout is manageable. If you don't, July is going to be a helpdesk surprise rather than a planned change.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/sharepoint/sharepoint-azureb2b-integration" rel="noopener noreferrer"&gt;Microsoft Entra B2B integration for SharePoint &amp;amp; OneDrive&lt;/a&gt; — the canonical doc, updated May 2026.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/sharepoint/faqs-odspintegrationwithentrab2b" rel="noopener noreferrer"&gt;FAQ: Improvements to external sharing in OneDrive and SharePoint&lt;/a&gt; — the source of the "no opt-out, no scheduling, July access-denied" specifics.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/sharepoint/sharing-reports" rel="noopener noreferrer"&gt;Sharing reports in SharePoint&lt;/a&gt; — site-level external sharing report, the only place to enumerate OTP-only guests.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>microsoft365</category>
      <category>sharepoint</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Shopify Scripts stop executing June 30 — and the failure is silent: checkout completes at full price, no error</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sat, 13 Jun 2026 05:00:46 +0000</pubDate>
      <link>https://dev.to/flarecanary/shopify-scripts-stop-executing-june-30-and-the-failure-is-silent-checkout-completes-at-full-pmb</link>
      <guid>https://dev.to/flarecanary/shopify-scripts-stop-executing-june-30-and-the-failure-is-silent-checkout-completes-at-full-pmb</guid>
      <description>&lt;p&gt;On &lt;strong&gt;June 30, 2026&lt;/strong&gt;, every Shopify Script stops executing. This is the final deadline — it has already been pushed twice (August 2024 → August 2025 → June 2026), and Shopify has stated this one is firm. Editing and publishing new Scripts was already turned off on April 15, 2026.&lt;/p&gt;

&lt;p&gt;Most "deadline" posts about this frame it as a migration-project problem: audit your Scripts, rebuild them as Functions, ship before the date. That's true. But it buries the part that actually bites.&lt;/p&gt;

&lt;p&gt;Here is the part that makes this dangerous.&lt;/p&gt;

&lt;p&gt;When a model gets retired — Imagen, an OpenAI snapshot, a Grok slug — the failure is &lt;strong&gt;loud&lt;/strong&gt;. The call returns an error, your pipeline throws, someone gets paged. Loud failures get fixed.&lt;/p&gt;

&lt;p&gt;A Shopify Script ceasing to execute is &lt;strong&gt;not loud&lt;/strong&gt;. There is no Script to call and fail. The checkout simply runs without it. The customer reaches the thank-you page. Shopify returns a completed order. Your logs show a successful checkout. Everything is &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The only thing missing is the logic the Script was doing — and nothing in the system says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The cutoff itself fails silent — checkout just stops applying your rules
&lt;/h2&gt;

&lt;p&gt;Think about what Shopify Scripts actually do. They are Ruby scripts that run inside checkout to modify three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Line item discounts&lt;/strong&gt; — "20% off for wholesale-tagged customers," "buy 3 get 1 free," tiered volume pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping rates&lt;/strong&gt; — hide express shipping for PO boxes, rename rates, discount freight for VIPs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment methods&lt;/strong&gt; — hide credit card for high-risk carts, hide cash-on-delivery above a cart threshold.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On July 1, none of that runs. And checkout does not error — it has nothing to error &lt;em&gt;about&lt;/em&gt;. It just renders the cart without the Script's modifications:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The wholesale customer who should see 20% off &lt;strong&gt;pays full price.&lt;/strong&gt; No error. Order completes.&lt;/li&gt;
&lt;li&gt;The express shipping you hid for PO boxes &lt;strong&gt;shows up again&lt;/strong&gt;, gets selected, and now you owe a carrier for a route you can't service.&lt;/li&gt;
&lt;li&gt;The payment method you hid on high-risk carts &lt;strong&gt;comes back&lt;/strong&gt;, and the fraud you were screening out walks straight through.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is a &lt;code&gt;200&lt;/code&gt;, a completed order, a happy-looking checkout funnel. You will not find this in an error dashboard, because there is no error. You find it in a margin report three weeks later, or in a customer support ticket, or in a chargeback.&lt;/p&gt;

&lt;p&gt;That is the worst kind of failure: revenue logic that silently switches off while the system around it reports success. If you have a Script live today, &lt;strong&gt;June 30 is not a migration deadline — it is the day a piece of your checkout silently changes behavior.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A deployed Function does nothing until a discount node exists in the Admin
&lt;/h2&gt;

&lt;p&gt;So you migrate. You rebuild the Script as a Shopify Function — WebAssembly, the official replacement. You write the logic, &lt;code&gt;npm run deploy&lt;/code&gt;, the Function shows up in your Partner Dashboard. Done?&lt;/p&gt;

&lt;p&gt;No. And this is the trap that catches teams who think a Function is a drop-in for a Script.&lt;/p&gt;

&lt;p&gt;A Script ran the moment it was saved in the Script Editor. A &lt;strong&gt;Function does not run just because it is deployed.&lt;/strong&gt; A discount Function only executes when a &lt;em&gt;discount&lt;/em&gt; is attached to it — a &lt;code&gt;DiscountAutomaticApp&lt;/code&gt; or &lt;code&gt;DiscountCodeApp&lt;/code&gt; node, created in the Shopify Admin (or via the &lt;code&gt;discountAutomaticAppCreate&lt;/code&gt; GraphQL mutation) and set to &lt;strong&gt;Active&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Deploy the Function, forget to create the discount node — or create it and leave it in Draft, or with a future start date — and the result is exactly the same as section 1: checkout runs, completes, charges full price. The Function exists. It is just never invoked. There is no error, because nothing called anything.&lt;/p&gt;

&lt;p&gt;This is the half-migration failure mode. The Script is gone, the Function is "deployed," the dashboard looks green, and discounts are silently off. Confirm the discount node exists, is &lt;strong&gt;Active&lt;/strong&gt;, and has a start date in the past. The Function is only half the wiring.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The input query: a field you forget to request comes back &lt;code&gt;null&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Scripts and Functions receive cart data completely differently, and this is where logic silently no-ops.&lt;/p&gt;

&lt;p&gt;A Ruby Script got the whole cart handed to it — &lt;code&gt;Input.cart.line_items&lt;/code&gt;, customer, tags, everything in scope. A Function gets only what its &lt;strong&gt;input query&lt;/strong&gt; explicitly asks for. The input query is a GraphQL document you write; Shopify runs it and passes the result to your Function as JSON.&lt;/p&gt;

&lt;p&gt;The rule that bites: &lt;strong&gt;a field you do not request in the input query is &lt;code&gt;null&lt;/code&gt; in your Function.&lt;/strong&gt; Not an error. Not a warning. &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So you migrate a Script that gives a discount to customers tagged &lt;code&gt;wholesale&lt;/code&gt;. In the Function you write the discount logic correctly — but your input query doesn't request &lt;code&gt;cart.buyerIdentity.customer.hasTags&lt;/code&gt; (or the metafield, or the product tags, or the line-item &lt;code&gt;sellingPlanAllocation&lt;/code&gt;, whatever your rule keys on). The Function runs. It reads the customer tags. They are &lt;code&gt;null&lt;/code&gt;. The &lt;code&gt;null&lt;/code&gt; doesn't match &lt;code&gt;wholesale&lt;/code&gt;. It returns "no discount."&lt;/p&gt;

&lt;p&gt;&lt;code&gt;200 OK&lt;/code&gt;. Order completes. Full price. The Function ran &lt;em&gt;successfully&lt;/em&gt; — it just made its decision on data that was silently absent. Every conditional discount you migrate has this exposure: the condition you branch on must be in the input query, or your branch quietly takes the wrong path.&lt;/p&gt;

&lt;p&gt;When you migrate a Script, write down every field its logic reads, and check each one is in the input query. The dangerous field is the one your logic depends on and your query forgot.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;combinesWith&lt;/code&gt; and discount strategy: a Script that always applied may now lose
&lt;/h2&gt;

&lt;p&gt;A Script applied its discount &lt;strong&gt;unconditionally&lt;/strong&gt; — the Ruby ran, the discount landed, end of story. A Function discount does not get that guarantee. It lives under two layers of Shopify-side arbitration that Scripts never had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;combinesWith&lt;/code&gt;&lt;/strong&gt; is configured on the discount &lt;em&gt;node&lt;/em&gt;, not in your Function code. It declares whether this discount can stack with order / product / shipping discounts. If you had a Script that gave &lt;em&gt;both&lt;/em&gt; a line-item discount &lt;em&gt;and&lt;/em&gt; an order-level discount, and you rebuild it as two Functions whose discount nodes are not configured to combine with each other — only one applies. The other is silently dropped. Your Function code is correct; the node configuration is the bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;discountApplicationStrategy&lt;/code&gt;&lt;/strong&gt; decides which discount wins per line item — &lt;code&gt;FIRST&lt;/code&gt;, &lt;code&gt;MAXIMUM&lt;/code&gt;, or &lt;code&gt;ALL&lt;/code&gt;. A Script that always stacked its discount on top may become a Function that only applies when it is the &lt;em&gt;best&lt;/em&gt; discount on that line. On a cart that already has a better discount, yours silently does not apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this errors. The checkout completes, an order is created, &lt;em&gt;a&lt;/em&gt; discount may even show — just not the combination your Script produced. If your Script's behavior depended on stacking, your Function's behavior now depends on the node config and the strategy matching it. Verify both against a real multi-discount cart.&lt;/p&gt;

&lt;p&gt;One more: if you are following an older tutorial, make sure you build against the unified &lt;strong&gt;Discount Function API&lt;/strong&gt;, not the legacy split &lt;em&gt;Order Discount&lt;/em&gt; / &lt;em&gt;Product Discount&lt;/em&gt; Function APIs — those are themselves deprecated. Migrating onto an already-deprecated target is a migration you get to do twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inventory your Scripts now.&lt;/strong&gt; In the Shopify admin, open the Script Editor (Apps → Script Editor) and the &lt;strong&gt;Shopify Scripts customizations report&lt;/strong&gt;. List every live Script and exactly what it modifies — discount, shipping, or payment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For each Script, decide the replacement explicitly.&lt;/strong&gt; A Shopify Function, a public app from the App Store, or &lt;em&gt;consciously dropping it&lt;/em&gt;. The danger is the Script nobody owns — it just stops on June 30 and no one is watching the metric it moved.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat "deployed" and "live" as two different states.&lt;/strong&gt; For every discount Function: confirm the discount node exists, is &lt;strong&gt;Active&lt;/strong&gt;, has a past start date, and has &lt;code&gt;combinesWith&lt;/code&gt; set to match the stacking your Script did.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Diff the input query against the Script's logic.&lt;/strong&gt; Every field the Ruby read must be in the Function's input query. Anything missing is &lt;code&gt;null&lt;/code&gt;, and &lt;code&gt;null&lt;/code&gt; silently changes which branch your discount logic takes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Test on real carts before June 30, not after.&lt;/strong&gt; Build the carts your Scripts actually targeted — the wholesale customer, the PO box address, the high-risk payment cart, the multi-discount cart — and run a full checkout. Confirm the final price, the shipping options, and the payment methods match what the Script produced. This is the only check that catches a silent no-op.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch the margin metric across the cutover.&lt;/strong&gt; Average discount per order, blended shipping cost, payment-method mix. If a Script silently stops, these move before any human notices. A dashboard line is cheaper than a chargeback.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model retirements get headlines because they fail loud. Shopify Scripts will fail quiet — checkout will keep completing, orders will keep flowing, and the only signal that something broke is a number in a report moving the wrong way. Plan for the quiet failure.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; watches your third-party APIs and SDKs for breaking changes like this one — deprecations, response-shape changes, and silently-dropped behavior — and surfaces them before they reach production. Free tier monitors 5 endpoints.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>ecommerce</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>Google retires every Imagen model on June 24 — and the Gemini image migration fails silently in 4 places</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Wed, 10 Jun 2026 05:00:36 +0000</pubDate>
      <link>https://dev.to/flarecanary/google-retires-every-imagen-model-on-june-24-and-the-gemini-image-migration-fails-silently-in-4-paf</link>
      <guid>https://dev.to/flarecanary/google-retires-every-imagen-model-on-june-24-and-the-gemini-image-migration-fails-silently-in-4-paf</guid>
      <description>&lt;p&gt;On &lt;strong&gt;June 24, 2026&lt;/strong&gt;, Google shuts down every remaining Imagen model on the Gemini API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;imagen-4.0-ultra-generate-001&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;imagen-4.0-fast-generate-001&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(&lt;code&gt;imagen-3.0-generate-002&lt;/code&gt; already went dark on November 10, 2025 — if you somehow survived that one, you were already on borrowed time.)&lt;/p&gt;

&lt;p&gt;The recommended replacement is &lt;code&gt;gemini-2.5-flash-image&lt;/code&gt; — the model Google markets as "Nano Banana" — or &lt;code&gt;gemini-3-pro-image-preview&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the part that makes this dangerous. The &lt;strong&gt;retirement itself is loud&lt;/strong&gt;: after June 24, a request to &lt;code&gt;imagen-4.0-generate-001&lt;/code&gt; returns an error, your pipeline throws, someone gets paged. That's fine. Loud failures get fixed.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;migration is not loud&lt;/strong&gt;. Imagen and Gemini's image generation are not the same API with a different model string — they are two different request shapes, two different endpoints, and two different parameter vocabularies. And Gemini's image endpoint will happily accept a request full of Imagen-era parameters, ignore the ones it doesn't recognize, and return &lt;code&gt;200 OK&lt;/code&gt; with an image. Not the image you asked for. &lt;em&gt;An&lt;/em&gt; image.&lt;/p&gt;

&lt;p&gt;That's the trap. Teams will migrate under deadline pressure, see images come back, see green status codes, and ship. The defects land downstream, weeks later, with no error attached.&lt;/p&gt;

&lt;p&gt;Here's the silent-fail surface.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The endpoint and response shape changed — and a half-migrated parser fails quiet
&lt;/h2&gt;

&lt;p&gt;Imagen runs on the &lt;strong&gt;&lt;code&gt;:predict&lt;/code&gt;&lt;/strong&gt; endpoint. You send &lt;code&gt;instances&lt;/code&gt; and &lt;code&gt;parameters&lt;/code&gt;, and you get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"predictions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bytesBase64Encoded"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gemini image generation runs on &lt;strong&gt;&lt;code&gt;:generateContent&lt;/code&gt;&lt;/strong&gt;. You send &lt;code&gt;contents&lt;/code&gt; with &lt;code&gt;parts&lt;/code&gt;, and the image comes back at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"candidates"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"parts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inlineData"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mimeType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"image/png"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you point your code at the new model but keep calling &lt;code&gt;:predict&lt;/code&gt;, you get a loud error — good. But the failure that actually ships is the &lt;em&gt;half&lt;/em&gt; migration: you move to &lt;code&gt;:generateContent&lt;/code&gt;, and a downstream helper still reaches for &lt;code&gt;response.predictions[0].bytesBase64Encoded&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That path doesn't exist on a Gemini response. In JavaScript it evaluates to &lt;code&gt;undefined&lt;/code&gt;. In Python it's a &lt;code&gt;KeyError&lt;/code&gt; only if you index hard — and most image-handling code doesn't, because it was written defensively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;predictions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[{}])[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytesBase64Encoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;predictions&lt;/code&gt; is missing, so &lt;code&gt;img&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, so the &lt;code&gt;if&lt;/code&gt; is skipped. No exception. No image written. &lt;code&gt;200 OK&lt;/code&gt; logged. The function returns cleanly. You find out when someone notices the asset bucket stopped filling — and by then you can't tell which day it started.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;sampleCount&lt;/code&gt; is gone — your batch silently collapses to one image per call
&lt;/h2&gt;

&lt;p&gt;This is the sharpest one.&lt;/p&gt;

&lt;p&gt;Imagen's &lt;code&gt;:predict&lt;/code&gt; request takes a &lt;code&gt;sampleCount&lt;/code&gt; parameter: ask for up to 4 images in a single call (&lt;code&gt;imagen-4.0-generate-001&lt;/code&gt;), or 1 at a time on the Ultra tier. Plenty of pipelines lean on this — generate 4 candidates per prompt, score them, keep the best. The &lt;code&gt;sampleCount: 4&lt;/code&gt; is load-bearing.&lt;/p&gt;

&lt;p&gt;Gemini's image API has no &lt;code&gt;sampleCount&lt;/code&gt;. It generates &lt;strong&gt;one image per call&lt;/strong&gt;, and Google's own documentation is blunt about it: &lt;em&gt;"The model won't always follow the exact number of image outputs that the user explicitly asks for."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So when your migrated request carries &lt;code&gt;sampleCount: 4&lt;/code&gt; — or you ask conversationally for "four variations" — Gemini doesn't error. It returns one image, &lt;code&gt;200 OK&lt;/code&gt;. Your candidate pool just dropped from 4 to 1. The "pick the best of 4" step still runs; it's now picking the best of 1. Output quality quietly degrades, throughput math is off by 4×, and nothing in the response says "you asked for four and got one."&lt;/p&gt;

&lt;p&gt;If any part of your system reasons about &lt;em&gt;how many&lt;/em&gt; images it gets back, that assumption is now wrong, and it's wrong silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Aspect ratio moved namespaces — and became a suggestion
&lt;/h2&gt;

&lt;p&gt;Imagen takes &lt;code&gt;aspectRatio&lt;/code&gt; as a top-level entry in &lt;code&gt;parameters&lt;/code&gt;, and it's a hard constraint: ask for &lt;code&gt;16:9&lt;/code&gt;, get &lt;code&gt;16:9&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Gemini image generation, aspect ratio is &lt;strong&gt;not&lt;/strong&gt; a top-level parameter. It lives nested inside an image-config block on the generation config. Carry the Imagen-style top-level &lt;code&gt;aspectRatio&lt;/code&gt; field straight over and it lands nowhere — it's an unrecognized key, silently dropped, and Gemini falls back to its default (square, or whatever it infers from the prompt).&lt;/p&gt;

&lt;p&gt;Result: &lt;code&gt;200 OK&lt;/code&gt;, a perfectly valid image, in the wrong dimensions. Every downstream consumer that assumed a fixed aspect ratio — a CSS grid, a video frame, a thumbnail cropper, a print layout — now gets a shape it wasn't built for. Best case it looks wrong. Worst case the cropper "fixes" it by cutting the subject's head off, automatically, with no error.&lt;/p&gt;

&lt;p&gt;And even when you &lt;em&gt;do&lt;/em&gt; nest the field correctly, treat it as a strong hint rather than a guarantee. There are documented reports of Gemini image models returning a 1:1 square for an explicit &lt;code&gt;16:9&lt;/code&gt; request. Validate the dimensions of what comes back; don't trust that you got what you asked for.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;personGeneration&lt;/code&gt; has no clean equivalent — your safety posture shifts without a diff
&lt;/h2&gt;

&lt;p&gt;Imagen exposes a &lt;code&gt;personGeneration&lt;/code&gt; parameter: &lt;code&gt;dont_allow&lt;/code&gt;, &lt;code&gt;allow_adult&lt;/code&gt;, &lt;code&gt;allow_all&lt;/code&gt;. Teams set this deliberately — a kids' education product pins &lt;code&gt;dont_allow&lt;/code&gt;; an internal tool runs &lt;code&gt;allow_all&lt;/code&gt;. It's a compliance decision, often written down in a review somewhere.&lt;/p&gt;

&lt;p&gt;Gemini's image API doesn't have a &lt;code&gt;personGeneration&lt;/code&gt; knob in that shape. It folds people-generation behavior into the general model safety stack. So &lt;code&gt;personGeneration&lt;/code&gt; on a migrated request is — you'll notice the pattern by now — an unrecognized key, silently dropped.&lt;/p&gt;

&lt;p&gt;What you're left with is Gemini's default people-generation behavior, which is &lt;strong&gt;not&lt;/strong&gt; guaranteed to match the Imagen setting you carefully chose. A pipeline pinned to &lt;code&gt;dont_allow&lt;/code&gt; may start returning images with people in them. A pipeline that relied on &lt;code&gt;allow_all&lt;/code&gt; may start refusing prompts it used to honor. Either way, &lt;code&gt;200 OK&lt;/code&gt;, no warning, and a safety/compliance posture that changed without anyone editing the line that used to control it. This is the surface a security review would actually care about, and it has no error and no schema diff to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The two things to also budget for
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Prompt rewriting.&lt;/strong&gt; Imagen runs an LLM-based prompt rewriter by default — it silently expands your terse prompt before generating. Gemini handles prompts natively and differently. Migrate, send the &lt;em&gt;byte-identical&lt;/em&gt; prompt, and the image style, composition, and detail level shift — because the prewriting layer your results were implicitly tuned against is gone. Nothing breaks; your outputs just drift. If you have a brand or style baseline, re-approve it after migrating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mask-based editing has no replacement.&lt;/strong&gt; This is the migration some teams can't actually make. Imagen's capability/editing model does precise, programmatic mask-based inpainting and outpainting — you supply a reference image plus a mask, and edits land exactly inside the mask. Gemini 2.5 Flash Image does &lt;em&gt;conversational&lt;/em&gt; editing: you describe the change in words. There is no pixel-precise mask parameter. If you have a pipeline that does deterministic mask-driven edits — product photography, automated retouching, compositing — there is no drop-in path. Find this out now, in June, not in a sprint planning meeting in July.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to actually do
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grep for the dead model IDs across everything&lt;/strong&gt; — app code, IaC, notebooks, prompt configs, batch-job definitions:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-nE&lt;/span&gt; &lt;span class="s2"&gt;"imagen-(4&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;0-(generate|ultra-generate|fast-generate)-001|3&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;0)"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Treat this as a rewrite, not a string swap.&lt;/strong&gt; The endpoint, request shape, and response shape all change. Budget for it like the API migration it is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit every Imagen-era parameter against the new API.&lt;/strong&gt; Make a literal checklist: &lt;code&gt;sampleCount&lt;/code&gt;, &lt;code&gt;aspectRatio&lt;/code&gt;, &lt;code&gt;personGeneration&lt;/code&gt;, prompt-rewrite behavior. For each, confirm it either has a real equivalent you've wired up, or accept — explicitly, in writing — that you're losing it. The danger is the parameter you neither migrate nor consciously drop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Assert on the response, don't assume it.&lt;/strong&gt; After migrating, check the &lt;em&gt;count&lt;/em&gt; of images returned and the &lt;em&gt;dimensions&lt;/em&gt; of each one against what you requested. Both can silently differ. A three-line assertion catches sections 2 and 3 of this post.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Re-approve your output baseline.&lt;/strong&gt; Prompt rewriting is gone and the underlying model is different. Whatever "looks right" meant for your product, re-establish it against real Gemini output before June 24 — not after.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model retirement is the loud part, and the loud part will get handled. The migration is the quiet part. Plan for the quiet part.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; watches your third-party APIs and SDKs for breaking changes like this one — model retirements, response-shape changes, and silently-dropped parameters — and surfaces them before they reach production. Free tier monitors 5 endpoints.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>google</category>
      <category>gemini</category>
      <category>imagen</category>
      <category>ai</category>
    </item>
    <item>
      <title>Atlassian Admin v1 APIs Go Dark on June 30 — Your User-Provisioning Script Probably Hits Eleven of Them</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sun, 07 Jun 2026 05:00:42 +0000</pubDate>
      <link>https://dev.to/flarecanary/atlassian-admin-v1-apis-go-dark-on-june-30-your-user-provisioning-script-probably-hits-eleven-of-l0n</link>
      <guid>https://dev.to/flarecanary/atlassian-admin-v1-apis-go-dark-on-june-30-your-user-provisioning-script-probably-hits-eleven-of-l0n</guid>
      <description>&lt;p&gt;If your team has any custom Atlassian Cloud admin automation — IdP sync, SCIM glue, lifecycle scripts, group provisioning — it almost certainly hits at least one of the eleven endpoints under &lt;code&gt;/admin/v1/orgs/{orgId}/...&lt;/code&gt;. After &lt;strong&gt;June 30, 2026&lt;/strong&gt;, those endpoints are gone.&lt;/p&gt;

&lt;p&gt;Atlassian announced the v1 sunset alongside the new v2 Directory/Users/Groups APIs. The migration is documented; the part that isn't documented loudly is that v2 isn't a drop-in path swap. The shape of the URL changed, the auth model picked up an extra resolution step, and several common automation patterns become two-call sequences instead of one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The eleven endpoints to grep for
&lt;/h2&gt;

&lt;p&gt;User-management endpoints going away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/users/search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/users/{accountId}/suspend-access&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/users/{accountId}/restore-access&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /admin/v1/orgs/{orgId}/directory/users/{accountId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Group-management endpoints going away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/groups/search&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/groups&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /admin/v1/orgs/{orgId}/directory/groups/{groupId}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/groups/{groupId}/roles/assign&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/groups/{groupId}/roles/revoke&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /admin/v1/orgs/{orgId}/directory/groups/{groupId}/memberships&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /admin/v1/orgs/{orgId}/directory/groups/{groupId}/memberships/{accountId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus &lt;code&gt;POST /admin/v1/orgs/{orgId}/users/invite&lt;/code&gt;, which Atlassian deprecated on January 13, 2026 — same June 30 sunset date.&lt;/p&gt;

&lt;p&gt;A literal grep on your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"admin/v1/orgs"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.{py,js,ts,go,rb,sh}"&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything comes back, it dies on June 30.&lt;/p&gt;

&lt;p&gt;This affects any tenant on &lt;strong&gt;centralized user management&lt;/strong&gt; — which, since 2025, has been the default for new orgs and the migration target for existing ones. If you don't know which user management mode your org is on, the v1 sunset still applies once the migration completes, so treat it as in-scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v2 actually looks like
&lt;/h2&gt;

&lt;p&gt;The v1 path was organization-rooted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/admin/v1/orgs/{orgId}/directory/users/{accountId}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The v2 path is directory-rooted under an organization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/admin/v2/orgs/{orgId}/directories/{directoryId}/users/{accountId}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;directoryId&lt;/code&gt; is not optional, and Atlassian's v1 callers never had to know it. Every script migrating off v1 needs an extra step to discover the directory before it can act on a user or group inside that directory.&lt;/p&gt;

&lt;p&gt;So a one-call workflow becomes a two-call workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. List directories for the org → pick a directoryId
2. Call the v2 endpoint with {orgId}/directories/{directoryId}/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For orgs with one directory, this is mostly mechanical. For orgs with multiple directories (Identity Manager, Google, Microsoft, plus a local directory), every script now has to disambiguate, and "the right one" depends on which IdP the user came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  OAuth scopes — one easy miss
&lt;/h2&gt;

&lt;p&gt;The Admin scope set didn't get a new "v2 scope." But scripts that previously only operated on users-by-account-id never had to declare &lt;code&gt;read:directories:admin&lt;/code&gt;. Now they do, because every v2 call passes through a directory resolution.&lt;/p&gt;

&lt;p&gt;Apps that don't add &lt;code&gt;read:directories:admin&lt;/code&gt; to their token request will get scope errors on the new endpoints — without the v1 endpoint being there to fall back to. The scope error happens at request time, not at auth time, so a token issued before the migration looks fine until the first call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failures will look like
&lt;/h2&gt;

&lt;p&gt;Atlassian's announcement says v1 endpoints "will remain available until 30 June 2026. After this date, they will be fully deprecated." The exact response shape post-sunset isn't documented for &lt;code&gt;/admin/v1/orgs/...&lt;/code&gt;, but Atlassian's pattern on other removed REST APIs (Jira &lt;code&gt;/rest/api/3/search&lt;/code&gt; and friends) has been &lt;strong&gt;HTTP 410 Gone&lt;/strong&gt; with a JSON error along the lines of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;410&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The requested API has been removed. Please use the newer endpoints."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the expected fingerprint, not a guarantee. If your client treats 410 as terminal (most retry libraries do), the failed call will not be retried, and the operation — suspend, remove, group-add — silently won't happen. The script returns "successful" on the calls that didn't go through the dead endpoint and "failed" on the ones that did, and a half-completed offboarding looks identical to a successful one in the audit log.&lt;/p&gt;

&lt;h2&gt;
  
  
  The use cases that quietly break
&lt;/h2&gt;

&lt;p&gt;The Atlassian announcement explicitly calls out a handful of automation patterns that depend on the v1 endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offboarding scripts.&lt;/strong&gt; Calling &lt;code&gt;suspend-access&lt;/code&gt; and then &lt;code&gt;directory/users/{accountId}&lt;/code&gt; DELETE in sequence is the canonical Atlassian-side termination flow. Both endpoints are on the list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SCIM-adjacent IdP sync.&lt;/strong&gt; Many teams wrote custom sync because Atlassian's SCIM connector didn't cover their IdP, or didn't handle group memberships the way they wanted. Custom sync scripts overwhelmingly use the v1 group memberships endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bulk invite onboarding.&lt;/strong&gt; &lt;code&gt;POST /admin/v1/orgs/{orgId}/users/invite&lt;/code&gt; was the canonical way to invite a list of users with a default group set. Already deprecated since January.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group provisioning from CI.&lt;/strong&gt; Terraform-style "groups defined in code" pipelines that create/delete groups based on YAML — the create and delete endpoints are both on the list.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The offboarding case is the one that goes wrong silently. A user gets removed from your IdP, the sync script runs, the v2 path fails because the script wasn't migrated, and the user keeps their Atlassian access until someone notices on the next access review. That's the nightmare scenario for security teams who track "removed in IdP within X hours" as a compliance metric.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do this quarter
&lt;/h2&gt;

&lt;p&gt;Three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory.&lt;/strong&gt; Grep &lt;code&gt;admin/v1/orgs&lt;/code&gt; across all internal repos, CI configs, and scripts on admin/ops machines. Capture which endpoints are in use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolve directory IDs.&lt;/strong&gt; Build (or buy) a small helper that lists directories on the org and returns the directory ID for a given user — every v2 call needs this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the &lt;code&gt;read:directories:admin&lt;/code&gt; scope to your token requests&lt;/strong&gt; before you migrate the calls. The scope addition is forward-compatible with v1 calls, so you can add it without breaking anything before you cut over.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your custom sync also uses Jira's &lt;code&gt;/rest/api/3/search&lt;/code&gt; or any other v3 Jira API marked for removal, batch them — same orgs, same teams, same maintenance window.&lt;/p&gt;

&lt;p&gt;The pattern here is the one we keep seeing across providers: a deprecation notice goes out, the SDK or admin script gets an "available until" date that everyone marks on a sticky note, and on the day the API returns 410 the team finds out which scripts they actually shipped to production years ago. We built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; to flag the response-shape and field-semantic changes that don't even show up as a deprecation notice — but on this one, the calendar entry for &lt;strong&gt;June 30, 2026&lt;/strong&gt; is the one to set.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your offboarding flow uses any v1 endpoint, treat it as the highest-priority migration. Half-completed user removals are an audit problem long before they're a security problem.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>atlassian</category>
      <category>api</category>
      <category>devops</category>
      <category>security</category>
    </item>
    <item>
      <title>Auth0 removes enabled_clients from connection reads July 13 — Terraform/Pulumi will silently see every client as disabled</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Thu, 04 Jun 2026 05:00:42 +0000</pubDate>
      <link>https://dev.to/flarecanary/auth0-removes-enabledclients-from-connection-reads-july-13-terraformpulumi-will-silently-see-51hj</link>
      <guid>https://dev.to/flarecanary/auth0-removes-enabledclients-from-connection-reads-july-13-terraformpulumi-will-silently-see-51hj</guid>
      <description>&lt;p&gt;If you manage Auth0 connections through Terraform, Pulumi, or any in-house multi-tenant provisioning script, there's a quiet failure mode landing on &lt;strong&gt;July 13, 2026&lt;/strong&gt;. On that date, Auth0 removes the &lt;code&gt;enabled_clients&lt;/code&gt; field from &lt;code&gt;GET /api/v2/connections&lt;/code&gt; and &lt;code&gt;GET /api/v2/connections/{id}&lt;/code&gt; responses, and stops accepting &lt;code&gt;enabled_clients&lt;/code&gt; in &lt;code&gt;PATCH /api/v2/connections/{id}&lt;/code&gt;. The field was deprecated January 13, 2026; July 13 is end-of-life.&lt;/p&gt;

&lt;p&gt;Nothing returns an error. Calls still get a &lt;code&gt;200&lt;/code&gt;. The connection object comes back, with all the same fields you've always read — minus one. The code that reads &lt;code&gt;connection.enabled_clients&lt;/code&gt; to figure out which apps are wired to a connection gets either an empty array or &lt;code&gt;undefined&lt;/code&gt;, depending on how Auth0 serializes the absence. Either way, your IaC plan, your drift-detection job, or your client-provisioning script reads "no clients enabled" and reacts accordingly. That reaction is usually one of two failure modes, both bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Silent Failure Modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mode 1: Drift-detection wipe
&lt;/h3&gt;

&lt;p&gt;Terraform's &lt;code&gt;auth0_connection&lt;/code&gt; resource (and the Pulumi equivalent) include &lt;code&gt;enabled_clients&lt;/code&gt; as a managed attribute. On every &lt;code&gt;plan&lt;/code&gt;, the provider calls &lt;code&gt;GET /api/v2/connections/{id}&lt;/code&gt;, reads the current &lt;code&gt;enabled_clients&lt;/code&gt;, and compares it to what's in your state file. After July 13, the read returns empty, the state file still lists the configured set, and the provider sees a delta. Then &lt;code&gt;apply&lt;/code&gt; calls &lt;code&gt;PATCH&lt;/code&gt; to "fix" it — sending the configured &lt;code&gt;enabled_clients&lt;/code&gt; list back. The PATCH ignores the field (it's deprecated for input too), the connection's client associations stay whatever they actually are, and the next &lt;code&gt;plan&lt;/code&gt; shows the same drift again.&lt;/p&gt;

&lt;p&gt;The dangerous variant: if you've ever managed Auth0 partly through Terraform and partly through the dashboard or a separate provisioning tool, your state file lists &lt;em&gt;only the clients Terraform knows about&lt;/em&gt;. A drift-detection run that decides "the source of truth is my state file" — common in CI pipelines that auto-apply — will silently issue a corrective change. Until July 13, that worked: PATCH with the desired &lt;code&gt;enabled_clients&lt;/code&gt; reconciled the difference. After July 13, the PATCH silently no-ops, so Terraform-managed connections look right in the state but actually still have the out-of-band associations intact. If you then &lt;em&gt;also&lt;/em&gt; run the new endpoint-based code to "clean up disabled clients," you can wipe the out-of-band ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mode 2: Multi-tenant provisioning over-restriction
&lt;/h3&gt;

&lt;p&gt;A common multi-tenant pattern in SaaS apps that white-label Auth0: when a new tenant signs up, your control plane creates an Auth0 client for them and enables existing connections for that client. The reverse — enumerating which connections each client is enabled on — is usually done by walking all connections, reading &lt;code&gt;enabled_clients&lt;/code&gt;, and filtering by the tenant's client_id.&lt;/p&gt;

&lt;p&gt;After July 13, that walk returns empty &lt;code&gt;enabled_clients&lt;/code&gt; for every connection. Code that interprets the empty list as "this connection has no enabled clients" and then "remediates" by re-enabling everything from a desired-state list hits either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A no-op (the PATCH ignores the field, the actual associations don't change, the tenant works fine in production but every audit report says they're disabled).&lt;/li&gt;
&lt;li&gt;A retry storm (if the script keeps trying to re-enable until &lt;code&gt;GET&lt;/code&gt; confirms the change, which never happens because the field is gone).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either way, observability dashboards keyed on &lt;code&gt;enabled_clients&lt;/code&gt; go dark or flatline. SCIM-style sync jobs that reconcile Auth0 against an external IdP source-of-truth start over-eagerly trying to fix a discrepancy that isn't real.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;p&gt;The new endpoints, available now:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;GET /api/v2/connections/{id}/clients&lt;/code&gt;&lt;/strong&gt; — returns the enabled clients for a connection, paginated.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"opaque_cursor"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query params: &lt;code&gt;take&lt;/code&gt; (1–1000, default 50), &lt;code&gt;from&lt;/code&gt; (cursor; omit on first call). When &lt;code&gt;next&lt;/code&gt; is absent in the response, you've walked the full set. Required scope: &lt;code&gt;read:connections&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;PATCH /api/v2/connections/{id}/clients&lt;/code&gt;&lt;/strong&gt; — toggle enabled status for specific clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"client_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def456..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Returns &lt;code&gt;204 No Content&lt;/code&gt; on success. Required scope: &lt;code&gt;update:connections&lt;/code&gt;. The hard constraint that bites bulk operations: &lt;strong&gt;maximum 50 clients per request&lt;/strong&gt;. If your provisioning code enables a connection for all 200 clients in a tenant batch via one &lt;code&gt;enabled_clients&lt;/code&gt; PATCH today, the equivalent through the new endpoint is four sequential calls. Naive ports that don't chunk silently truncate at 50 (or, depending on the API, return &lt;code&gt;400&lt;/code&gt; — the docs don't pin this down).&lt;/p&gt;

&lt;p&gt;The new PATCH is also &lt;em&gt;selective&lt;/em&gt;, not &lt;em&gt;replacing&lt;/em&gt;. The old &lt;code&gt;enabled_clients&lt;/code&gt; was a full-set replacement: PATCH with &lt;code&gt;["client_a", "client_b"]&lt;/code&gt; made those exactly the enabled set. The new endpoint only toggles the clients you list. Anything not in your PATCH array keeps its current status. Tools that assumed PATCH = full replacement will now leave stale enables in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Slips Through Normal Detection
&lt;/h2&gt;

&lt;p&gt;Walk the standard checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP status check&lt;/strong&gt; — passes. &lt;code&gt;GET /api/v2/connections/{id}&lt;/code&gt; still returns 200. The connection object is still there, just without the &lt;code&gt;enabled_clients&lt;/code&gt; key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema validation&lt;/strong&gt; — passes. &lt;code&gt;enabled_clients&lt;/code&gt; was always an optional field for connections that hadn't been wired to any client yet. A response without it is structurally valid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform plan&lt;/strong&gt; — &lt;em&gt;thinks&lt;/em&gt; it caught it. The provider sees the configured &lt;code&gt;enabled_clients&lt;/code&gt; is no longer present in the remote state and shows a diff. The diff says "will add three clients." &lt;code&gt;apply&lt;/code&gt; runs, the PATCH succeeds (&lt;code&gt;200&lt;/code&gt;, the deprecated field is silently dropped), and the next &lt;code&gt;plan&lt;/code&gt; shows the &lt;em&gt;same&lt;/em&gt; diff. To an operator, this looks like Terraform fighting with another process — not like a deprecated field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI / unit tests&lt;/strong&gt; — pass if they mock Auth0. Anyone who tests against a recorded fixture from before July 13 sees the same field they always saw. Tests don't notice until they hit real Auth0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration audits&lt;/strong&gt; — only catch it if you specifically look for &lt;code&gt;enabled_clients&lt;/code&gt; in your codebase. Most audits look for endpoint URL changes, not field-level removals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Terraform Auth0 provider will probably ship a release that switches &lt;code&gt;enabled_clients&lt;/code&gt; to use the new endpoints under the hood — but the release calendar isn't Auth0's, it's HashiCorp's (or the community maintainers'). Pinning to an older provider version with explicit &lt;code&gt;enabled_clients&lt;/code&gt; config delays the migration into the deprecation window without addressing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Detect It Now
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Grep for &lt;code&gt;enabled_clients&lt;/code&gt; across your infra repos.&lt;/strong&gt; Every match is a migration site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform &lt;code&gt;auth0_connection&lt;/code&gt; resources with &lt;code&gt;enabled_clients = [...]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi &lt;code&gt;auth0.Connection&lt;/code&gt; resources with &lt;code&gt;enabledClients: [...]&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Direct API calls — anything reading &lt;code&gt;.enabled_clients&lt;/code&gt; off a connection response, or sending &lt;code&gt;enabled_clients&lt;/code&gt; in a PATCH body&lt;/li&gt;
&lt;li&gt;SDK calls: &lt;code&gt;managementClient.connections.update(..., { enabled_clients: ... })&lt;/code&gt; in node-auth0 / auth0-python / auth0.net&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Verify the new endpoints work for your scopes.&lt;/strong&gt; The new &lt;code&gt;GET /api/v2/connections/{id}/clients&lt;/code&gt; requires &lt;code&gt;read:connections&lt;/code&gt;, which most existing M2M tokens already have. The PATCH requires &lt;code&gt;update:connections&lt;/code&gt;. If you've used a scoped-down token in CI that only had &lt;code&gt;read:client_grants&lt;/code&gt; or similar, you may need to expand it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Chunk PATCHes by 50.&lt;/strong&gt; Wherever you currently send a full &lt;code&gt;enabled_clients&lt;/code&gt; array, the replacement code has to enumerate the desired client_ids, diff against the current set (paginated via the new GET), and issue &lt;code&gt;PATCH /clients&lt;/code&gt; calls in chunks of 50 with &lt;code&gt;status: true&lt;/code&gt; / &lt;code&gt;status: false&lt;/code&gt; per client. Bulk operations that don't chunk break.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Treat missing &lt;code&gt;enabled_clients&lt;/code&gt; as a sentinel during migration.&lt;/strong&gt; If your code path can run before or after July 13 (a slow CI matrix, a paused tenant, a long-lived M2M token still hitting an older runtime), add an explicit check: if &lt;code&gt;enabled_clients&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt; on the connection response, fall back to the new endpoint instead of treating it as "no clients enabled." That single guard makes the cutover safe regardless of which side of July 13 the call lands on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;A vendor moves a piece of state from "inlined on a parent resource" to "dedicated sub-resource endpoints." The parent endpoint keeps working. The field just stops being there. Code that assumed &lt;em&gt;presence&lt;/em&gt; of the field as "this connection has clients" and &lt;em&gt;absence&lt;/em&gt; as "this connection has no clients" reads the new absence as the old "no clients" — and acts on it.&lt;/p&gt;

&lt;p&gt;The same shape shows up across cloud providers and SaaS APIs every few months. The mitigation isn't to track every deprecation memo; it's to treat field absence as ambiguous (was it removed? was it always empty? was it filtered out by a scope?) and require an explicit signal before reacting. After July 13, "absent" on Auth0 connection responses means "go ask the new endpoint" — not "no clients enabled."&lt;/p&gt;

&lt;p&gt;If you run Auth0 through IaC, the cheapest move this month is to grep for &lt;code&gt;enabled_clients&lt;/code&gt;, write the new-endpoint reads alongside the old-field reads, and gate the cutover on a feature flag you can flip per-environment. Eight weeks of runway is enough to do it carefully; the alternative is debugging silent drift on a Monday in July.&lt;/p&gt;

</description>
      <category>auth0</category>
      <category>api</category>
      <category>terraform</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>Shopify Changed heldBy From a String to an Object in 2025-01 — Your Query Still Returns 200 OK</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Mon, 01 Jun 2026 05:00:36 +0000</pubDate>
      <link>https://dev.to/flarecanary/shopify-changed-heldby-from-a-string-to-an-object-in-2025-01-your-query-still-returns-200-ok-1bc0</link>
      <guid>https://dev.to/flarecanary/shopify-changed-heldby-from-a-string-to-an-object-in-2025-01-your-query-still-returns-200-ok-1bc0</guid>
      <description>&lt;p&gt;Shopify ships a new GraphQL Admin API version every quarter. Most releases are additive. A few are not. The 2025-01 release is one of the "are not": it removed a handful of fields that thousands of apps were reading, and it quietly changed the type of one of the most common fulfillment fields from a string to an object.&lt;/p&gt;

&lt;p&gt;The endpoint still returns &lt;code&gt;200 OK&lt;/code&gt;. The query still parses. The field your code reads just isn't there anymore — or it's there under a new name with a completely different shape.&lt;/p&gt;

&lt;p&gt;If you upgraded a Shopify app from 2024-x to 2025-01 without reading the full release notes and you're seeing "this field is empty now" in production, this is probably why.&lt;/p&gt;

&lt;h2&gt;
  
  
  The heldBy → heldByApp Change
&lt;/h2&gt;

&lt;p&gt;On a &lt;code&gt;FulfillmentHold&lt;/code&gt; object, the 2024-x GraphQL schema returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fulfillmentHold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;heldBy&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;# String! — "PrepSupport" or "ShopifyFulfillmentNetwork" or an app name&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As of 2025-01, &lt;code&gt;heldBy&lt;/code&gt; is gone. In its place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;fulfillmentHold&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;heldByApp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c"&gt;# String! — "PrepSupport"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c"&gt;# ID!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://shopify.dev/docs/api/release-notes/2025-01" rel="noopener noreferrer"&gt;2025-01 release notes&lt;/a&gt; put it like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you currently query &lt;code&gt;fulfillmentHold.heldBy&lt;/code&gt;, then transition to querying &lt;code&gt;fulfillmentHold.heldByApp.title&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important thing is what happens if you don't. GraphQL in this version will happily return &lt;code&gt;null&lt;/code&gt; for a field it no longer recognizes (depending on your client's error handling), or it will throw a field-level error that your resolver swallows. Your downstream code — the logic that routes held orders to a human, the Slack notification that names the blocking app, the dashboard tile that shows "held by X" — starts quietly producing empty strings or "Unknown."&lt;/p&gt;

&lt;p&gt;Type systems don't save you here, either. Most Shopify SDKs are either hand-rolled TypeScript types generated from the schema at some pinned version, or they're &lt;code&gt;any&lt;/code&gt;-shaped because the developer didn't codegen against the current version. If your schema snapshot is from 2024-10, &lt;code&gt;heldBy&lt;/code&gt; is still in your types, your IDE still autocompletes it, and your unit tests (mocking your own types) pass. The mismatch only shows up at runtime, against Shopify's live API.&lt;/p&gt;

&lt;h2&gt;
  
  
  PrivateMetafield Is Just Gone
&lt;/h2&gt;

&lt;p&gt;The second big removal in 2025-01:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;PrivateMetafield&lt;/code&gt; is removed from the public GraphQL Admin API. Use app-data metafields instead.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is no field rename, no transitional alias, no "deprecated for N versions" wind-down. Every query, mutation, and resolver that referenced &lt;code&gt;PrivateMetafield&lt;/code&gt;, &lt;code&gt;privateMetafields&lt;/code&gt;, &lt;code&gt;privateMetafieldUpsert&lt;/code&gt;, or the &lt;code&gt;MetafieldStorefrontVisibility&lt;/code&gt; object stops working.&lt;/p&gt;

&lt;p&gt;If your app was using private metafields to store per-shop configuration — credentials, feature flags, per-merchant routing rules — that configuration didn't move anywhere. You have to migrate it to app-data metafields yourself, re-grant access via the new app-reserved namespace scheme, and replace every read and write. And until you do, the queries come back empty.&lt;/p&gt;

&lt;p&gt;The reason this is a silent-failure story and not a loud one is that GraphQL errors on unknown types usually manifest as empty result sets plus a &lt;code&gt;"errors": [...]&lt;/code&gt; array in the response body. Plenty of client libraries treat empty results as a valid zero-length response and log the error array at debug level. Your code reads &lt;code&gt;data.app.privateMetafields.edges&lt;/code&gt;, gets &lt;code&gt;[]&lt;/code&gt;, and moves on. The merchant's configuration just looks like it was never set.&lt;/p&gt;

&lt;h2&gt;
  
  
  accountNumber and routingNumber, Also Gone
&lt;/h2&gt;

&lt;p&gt;A smaller but spicier one, buried in the same release:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Removed &lt;code&gt;accountNumber&lt;/code&gt; and &lt;code&gt;routingNumber&lt;/code&gt; fields from &lt;code&gt;ShopifyPaymentsBankAccount&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your app was reading bank account details from Shopify Payments — for displaying masked account info, for reconciling payouts against external bookkeeping, for compliance paperwork — those two fields no longer exist. The field resolver returns &lt;code&gt;null&lt;/code&gt;. Whatever UI or report depended on them now shows blanks.&lt;/p&gt;

&lt;p&gt;The reason Shopify gave is reasonable (PCI / financial-data exposure narrowing). The reason apps break anyway is that nothing in the upgrade path forces you to notice. You bump your API version header from &lt;code&gt;2024-10&lt;/code&gt; to &lt;code&gt;2025-01&lt;/code&gt;, you ship, and the fields that used to come back populated just come back &lt;code&gt;null&lt;/code&gt;. Everything else is identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why CI Tests and Type Systems Don't Catch This
&lt;/h2&gt;

&lt;p&gt;Every time I write up one of these stories — the &lt;a href="https://dev.to/flarecanary/github-pushevent-silently-stopped-returning-commits-heres-what-we-learned-44cb"&gt;GitHub PushEvent commits field&lt;/a&gt;, Stripe's &lt;a href="https://dev.to/flarecanary/stripe-basil-current-period-end-undefined-2fjm"&gt;Basil &lt;code&gt;current_period_end&lt;/code&gt; move&lt;/a&gt;, now Shopify 2025-01 — the pattern is the same:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The upstream API version is under the upstream's control, not yours.&lt;/li&gt;
&lt;li&gt;The breaking change is a field removal or a type reshape, not a new 4xx status.&lt;/li&gt;
&lt;li&gt;Responses still parse. HTTP still returns success.&lt;/li&gt;
&lt;li&gt;Your unit tests pass because you mocked the response shape yourself.&lt;/li&gt;
&lt;li&gt;Your integration tests pass because your sandbox data was seeded before the change.&lt;/li&gt;
&lt;li&gt;Your type checker passes because your types were generated against the old schema.&lt;/li&gt;
&lt;li&gt;The break shows up in production, against real data, after a deploy that looked clean.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There is no test you can write inside your codebase that catches a response-shape change introduced by code you don't own. The contract is between two systems and you only control one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Catches It
&lt;/h2&gt;

&lt;p&gt;Three things can catch a silent upstream schema change, in order of cost:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Version pinning with forced upgrade cadence.&lt;/strong&gt; Shopify lets you pin your API version via the &lt;code&gt;Shopify-Api-Version&lt;/code&gt; header. Pin an explicit version and don't let it float. Then schedule the upgrade (2024-10 → 2025-01, etc.) as a deliberate project with a review of the full release notes, a grep of your codebase for removed/renamed fields, and a regression test against a non-production shop. This is what every Shopify app developer should already be doing. Most aren't, which is why forum posts like &lt;a href="https://community.shopify.dev/t/submit-for-review-button-is-disabled-due-to-deprecated-api-warning-after-upgrading-to-2025-04/32546" rel="noopener noreferrer"&gt;"Submit for review button is disabled after upgrading to 2025-04"&lt;/a&gt; keep showing up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Deprecation-warning monitoring.&lt;/strong&gt; Shopify exposes deprecated API call counts in the Partner Dashboard and will block app submissions if you have any in the 3-day window before review. Read the warnings, fix the calls. This requires that you bothered to upgrade in the first place, though — it doesn't catch drift on a version you've been sitting on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Runtime shape monitoring.&lt;/strong&gt; Poll the endpoints your app actually hits on a schedule, record the response shape, and alert when a field disappears, a type shifts, or nullability changes. This is the one catch for APIs that change shape out from under you without you bumping any versions — and it's the one that works for every third-party API, not just the ones that publish a deprecation dashboard. (&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; does this; so do a few others. The point is to have &lt;em&gt;something&lt;/em&gt; doing it, not specifically us.)&lt;/p&gt;

&lt;p&gt;The economics matter. The cost of an undetected Shopify field rename isn't "an exception in the logs." It's usually something worse — wrong data on a merchant-facing dashboard, a fulfillment routing rule that silently misfires, a compliance report with a missing column. The business cost of one of those is larger than the annual cost of any monitor. And it only takes one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next Shopify Release Is Already Doing This Again
&lt;/h2&gt;

&lt;p&gt;Shopify's 2026-07 release (still months away as of this writing) is dropping &lt;code&gt;grams&lt;/code&gt; from &lt;code&gt;DraftOrderLineItem&lt;/code&gt; in favor of &lt;code&gt;weight&lt;/code&gt;. &lt;code&gt;customerPaymentMethodRemoteCreditCardCreate&lt;/code&gt; is fully removed after January 2026. These are the ones listed in the changelog; history says there will be 5–10 more smaller ones by the time the version actually ships.&lt;/p&gt;

&lt;p&gt;If you integrate with Shopify, now is a good time to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Grep your codebase for &lt;code&gt;heldBy&lt;/code&gt; (without the &lt;code&gt;App&lt;/code&gt;), &lt;code&gt;privateMetafield&lt;/code&gt;, &lt;code&gt;accountNumber&lt;/code&gt;, &lt;code&gt;routingNumber&lt;/code&gt;, &lt;code&gt;grams&lt;/code&gt;, &lt;code&gt;multiLocation&lt;/code&gt;, &lt;code&gt;customerPaymentMethodRemoteCreditCardCreate&lt;/code&gt;, &lt;code&gt;metafieldDelete&lt;/code&gt;, &lt;code&gt;visibleToStorefrontApi&lt;/code&gt;. Anything that comes back is a candidate break.&lt;/li&gt;
&lt;li&gt;Pin your &lt;code&gt;Shopify-Api-Version&lt;/code&gt; header explicitly if you aren't already.&lt;/li&gt;
&lt;li&gt;Subscribe to the &lt;a href="https://shopify.dev/changelog" rel="noopener noreferrer"&gt;Shopify changelog&lt;/a&gt; and actually read it.&lt;/li&gt;
&lt;li&gt;Set up shape monitoring on your most critical queries — fulfillment, orders, metafields, payments. Whether you do it yourself, use an observability tool, or use a schema-drift monitor, the cost of not doing it compounds every quarter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upstream API breaking changes are not a problem you can solve once. They're a recurring cost of running a product that depends on APIs you don't own. The apps that survive long-term aren't the ones that never get hit — they're the ones that notice within minutes instead of weeks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;Wilson&lt;/a&gt; — I write about how third-party API schemas break, quietly, against apps that depend on them. If you integrate with Shopify, Stripe, GitHub, Twilio, or any of a dozen others, &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; polls your critical endpoints and alerts on shape changes before your customers notice. Free tier. No card required.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>api</category>
      <category>graphql</category>
      <category>monitoring</category>
    </item>
  </channel>
</rss>
