<?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>Forem: FlareCanary</title>
    <description>The latest articles on Forem by FlareCanary (@flarecanary).</description>
    <link>https://forem.com/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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3834499%2F8c191c74-2040-4cd1-beaa-4ca99b664ca9.png</url>
      <title>Forem: FlareCanary</title>
      <link>https://forem.com/flarecanary</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/flarecanary"/>
    <language>en</language>
    <item>
      <title>GitHub Just Retired Seven Org Security Fields — Your 'New Repo Hardening' Script Is Now A No-Op</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Tue, 28 Apr 2026 04:01:30 +0000</pubDate>
      <link>https://forem.com/flarecanary/github-just-retired-seven-org-security-fields-your-new-repo-hardening-script-is-now-a-no-op-3id7</link>
      <guid>https://forem.com/flarecanary/github-just-retired-seven-org-security-fields-your-new-repo-hardening-script-is-now-a-no-op-3id7</guid>
      <description>&lt;p&gt;GitHub announced on April 21 that seven security-defaults fields on the org REST endpoint are now retired:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;advanced_security_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependabot_alerts_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependabot_security_updates_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dependency_graph_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret_scanning_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret_scanning_push_protection_enabled_for_new_repositories&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;secret_scanning_push_protection_custom_link_enabled&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These appeared on &lt;code&gt;GET /orgs/{org}&lt;/code&gt; and were writable on &lt;code&gt;PATCH /orgs/{org}&lt;/code&gt;. The retirement isn't a 410 or a schema-validation error. The fields just stop appearing on reads. The PATCH still returns 200 OK whether you send them or not — it just no longer applies them.&lt;/p&gt;

&lt;p&gt;Replacement: &lt;a href="https://docs.github.com/en/rest/code-security/configurations" rel="noopener noreferrer"&gt;Code Security Configurations API&lt;/a&gt; (&lt;code&gt;/orgs/{org}/code-security/configurations&lt;/code&gt;). Different endpoint, different shape, different concept — configurations are objects you attach to repositories, not booleans on the org.&lt;/p&gt;

&lt;p&gt;This is the deepest silent-fail vector we've seen all month. It's not a billing column or a string field. It's the one-line setup script every platform-engineering team wrote to harden new repos by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The script that quietly stopped working
&lt;/h2&gt;

&lt;p&gt;The classic version:&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgs&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="na"&gt;org&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;acme-platform&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;advanced_security_enabled_for_new_repositories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;dependabot_alerts_enabled_for_new_repositories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secret_scanning_enabled_for_new_repositories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;secret_scanning_push_protection_enabled_for_new_repositories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;Pre-2026-04-21: that PATCH applied org-wide defaults. Every new repo created in &lt;code&gt;acme-platform&lt;/code&gt; after this call had Dependabot, advanced security, and secret scanning on at creation.&lt;/p&gt;

&lt;p&gt;Post-retirement: that PATCH returns 200. Octokit doesn't throw. The audit log records the call. The fields are simply ignored.&lt;/p&gt;

&lt;p&gt;A new repo created tomorrow has none of those defaults applied. The compliance script ran. The dashboard says "secret scanning enabled org-wide." A &lt;code&gt;git push&lt;/code&gt; with an AWS access key in it goes through unchallenged.&lt;/p&gt;

&lt;p&gt;The terraform-github-provider equivalent (&lt;a href="https://registry.terraform.io/providers/integrations/github/latest/docs/resources/organization_settings" rel="noopener noreferrer"&gt;&lt;code&gt;github_organization_settings&lt;/code&gt;&lt;/a&gt;) wraps the same fields and has the same behavior. &lt;code&gt;terraform apply&lt;/code&gt; says no changes. The state file thinks it's converged. The org isn't enforcing what the state file claims.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why reads break differently from writes
&lt;/h2&gt;

&lt;p&gt;GitHub's REST API doesn't versioned-deprecate fields the way it does for &lt;code&gt;merge_commit_sha&lt;/code&gt;. There's no &lt;code&gt;X-GitHub-Api-Version: 2022-11-28&lt;/code&gt; you can set to keep the old field alive. The fields are just gone from &lt;code&gt;GET /orgs/{org}&lt;/code&gt; everywhere now.&lt;/p&gt;

&lt;p&gt;Reading code that does 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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;org&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;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgs&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="nx"&gt;org&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="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;secret_scanning_enabled_for_new_repositories&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…now hits &lt;code&gt;undefined&lt;/code&gt;. JavaScript's truthiness rules turn that into the false branch. Compliance dashboards that read these fields to display "✓ Secret scanning on" suddenly read "✗" and a security engineer pages someone, OR — worse — the dashboard caches the last known value from a week ago and keeps showing green for months.&lt;/p&gt;

&lt;p&gt;Writing code is the more dangerous shape. The PATCH endpoint accepts arbitrary unknown keys without erroring. There's no "did this field actually apply" affordance in the response. Your script can include 47 retired-or-renamed properties and the API still returns 200 with the org object that has none of them on it. Comparing the response back is the only check, and almost nobody writes that compare.&lt;/p&gt;

&lt;h2&gt;
  
  
  The replacement is not a one-line swap
&lt;/h2&gt;

&lt;p&gt;The Code Security Configurations API is conceptually different. Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;PATCH /orgs/{org}  { secret_scanning_enabled_for_new_repositories: true }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…you now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;POST /orgs/{org}/code-security/configurations&lt;/code&gt; to create a &lt;em&gt;configuration object&lt;/em&gt; (with secret scanning, Dependabot, advanced security, push protection settings as nested objects).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POST /orgs/{org}/code-security/configurations/{config_id}/defaults&lt;/code&gt; to attach it as the default for new repos. The body specifies which repo types it applies to (&lt;code&gt;new_repos&lt;/code&gt;, &lt;code&gt;private_repos&lt;/code&gt;, &lt;code&gt;public_repos&lt;/code&gt;, &lt;code&gt;all&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Optionally, &lt;code&gt;POST /orgs/{org}/code-security/configurations/{config_id}/attach&lt;/code&gt; to retroactively apply it to existing repos.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The shape of the configuration object isn't a 1:1 mapping. &lt;code&gt;advanced_security&lt;/code&gt; becomes part of a nested struct. &lt;code&gt;secret_scanning_push_protection_custom_link&lt;/code&gt; becomes a sub-field of secret scanning. Some legacy combinations aren't expressible. Some new combinations are.&lt;/p&gt;

&lt;p&gt;Migration is a real engineering task, not a search-and-replace. And the deprecation notice doesn't have a hard cutoff date — the fields are listed as "Retired" but reads have already broken, so there's no grace window left.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;The pattern by now is familiar. Three things have to be true to catch this in CI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your fixture for &lt;code&gt;GET /orgs/{org}&lt;/code&gt; was regenerated against the live API after April 21 (it wasn't).&lt;/li&gt;
&lt;li&gt;Your test asserts presence (&lt;code&gt;expect(org).toHaveProperty('secret_scanning_enabled_for_new_repositories')&lt;/code&gt;) rather than truthiness on a possibly-undefined field (most don't).&lt;/li&gt;
&lt;li&gt;Your terraform plan/apply tests run against a live GitHub org and diff the result, not against a mocked provider (almost nobody does this).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most platform teams test their org-hardening script by spinning up a sandbox org once, validating the fields land, and never re-validating. The sandbox org's settings still look right because they were set before the retirement. The script that "still works" hasn't actually applied anything for any new repo created after April 21.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Where tests missed it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GitHub PushEvent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;commits&lt;/code&gt; field silently dropped&lt;/td&gt;
&lt;td&gt;Tests didn't assert field presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Stripe Basil&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;current_period_end&lt;/code&gt; moved to &lt;code&gt;items&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests used Checkout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Shopify 2025-01&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fulfillmentHold&lt;/code&gt; type change&lt;/td&gt;
&lt;td&gt;Tests mocked the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;OpenAI Responses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;input_text&lt;/code&gt; removed for assistants&lt;/td&gt;
&lt;td&gt;Tests covered request role=user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Twilio regional&lt;/td&gt;
&lt;td&gt;Regional domains stop resolving&lt;/td&gt;
&lt;td&gt;Tests don't hit prod DNS paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;HubSpot Contacts v1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-memberships&lt;/code&gt; returns empty&lt;/td&gt;
&lt;td&gt;Tests asserted against sandbox fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;GitHub merge_commit_sha&lt;/td&gt;
&lt;td&gt;Field removed from PR responses&lt;/td&gt;
&lt;td&gt;Tests used pre-rollout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;GitHub org security fields&lt;/td&gt;
&lt;td&gt;7 fields retired from /orgs/{org}&lt;/td&gt;
&lt;td&gt;Org-hardening script tested once, never re-run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eight in a row. The breaking change is always in a field a test isn't asserting against, in a layer (transitive SDK upgrade, version-pinned header, configuration drift) the test isn't exercising, or in a script that runs once during onboarding and is never re-validated.&lt;/p&gt;

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

&lt;p&gt;Three actions, in priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep your platform-eng repos for the seven field names.&lt;/strong&gt; Anywhere they appear in a PATCH body or a read assertion is a candidate failure. Org-hardening Octokit calls, Terraform configs, compliance dashboards, audit-log queries — all suspect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Audit any new repository created in your org after April 21, 2026.&lt;/strong&gt; Check whether secret scanning, Dependabot alerts, and advanced security are actually enabled on each one. If your org-default script broke silently, every new repo since then has weaker security than the dashboard claims.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Migrate to Code Security Configurations.&lt;/strong&gt; Create the configuration, attach it as a default for new repos, and — separately — attach it retroactively to repos created during the silent-fail window. The retroactive attach is the cleanup step most migrations forget.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The PATCH that returns 200 OK and does nothing is the worst kind of API regression. There's no error to alert on. The audit log shows you ran the script. Compliance reports green. And every new repo in your org for the past week has its security defaults in whatever state GitHub decided to leave them in — which, based on testing, is "not what your script set."&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; for exactly this layer. Snapshot the response shape on a schedule, diff it, alert when fields disappear or accept-but-ignore semantics shift. The GitHub org security retirement is the textbook case: no error, no version header, no migration warning, just seven fields that used to enforce things and now silently don't.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If your platform team's onboarding script touched any of these seven org-level fields and hasn't been audited since April 21, your org is shipping new repos with whatever default security GitHub decided on — not what your runbook says. Worth a Tuesday afternoon.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>security</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>GitHub Just Removed merge_commit_sha From Pull Request Responses — Your Release Bot Is Probably Tagging null</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Mon, 27 Apr 2026 04:02:24 +0000</pubDate>
      <link>https://forem.com/flarecanary/github-just-removed-mergecommitsha-from-pull-request-responses-your-release-bot-is-probably-156d</link>
      <guid>https://forem.com/flarecanary/github-just-removed-mergecommitsha-from-pull-request-responses-your-release-bot-is-probably-156d</guid>
      <description>&lt;p&gt;GitHub's &lt;a href="https://docs.github.com/en/rest/about-the-rest-api/breaking-changes?apiVersion=2026-03-10" rel="noopener noreferrer"&gt;2026-03-10 REST API version&lt;/a&gt; ships a quiet but consequential breaking change: the &lt;code&gt;merge_commit_sha&lt;/code&gt; property is gone from pull request responses. 21 endpoints affected. There is no error, no 410, no migration warning header — the field just stops appearing in the JSON.&lt;/p&gt;

&lt;p&gt;For CI/CD code that does 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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pr&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;octokit&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pulls&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="nx"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pull_number&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;tagDeploymentArtifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;merge_commit_sha&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now tag with &lt;code&gt;undefined&lt;/code&gt;. The deployment still ships. The artifact registry still accepts the upload. Six weeks later, somebody asks "which commit produced this build?" and the answer is &lt;code&gt;undefined-1747291204&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is incident #7 in our silent-breakage series (&lt;a href="https://dev.to/flarecanary"&gt;GitHub PushEvent&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Stripe Basil&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Shopify 2025-01&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;OpenAI Responses&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Twilio regional&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;HubSpot Contacts v1&lt;/a&gt;). The pattern keeps repeating because removing a field from a JSON response is the cheapest possible breaking change for the API provider and the most invisible possible breaking change for the consumer.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's removed and where
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;merge_commit_sha&lt;/code&gt; disappears from 21 PR-bearing endpoints, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /repos/{owner}/{repo}/pulls&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /repos/{owner}/{repo}/pulls/{pull_number}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /repos/{owner}/{repo}/pulls&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PATCH /repos/{owner}/{repo}/pulls/{pull_number}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /repos/{owner}/{repo}/issues/events&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /repos/{owner}/{repo}/issues/{issue_number}/events&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Search results that embed PR objects&lt;/li&gt;
&lt;li&gt;Project card payloads that link to PRs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same release also removes the singular &lt;code&gt;assignee&lt;/code&gt; field from 31 Issue and PR endpoints. From the changelog:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The singular &lt;code&gt;assignee&lt;/code&gt; field has been marked as 'closing down' for years and duplicates information available in the &lt;code&gt;assignees&lt;/code&gt; array.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;True — and the migration is mechanical. Read from &lt;code&gt;assignees[0]&lt;/code&gt; instead of &lt;code&gt;assignee&lt;/code&gt;. Write &lt;code&gt;assignees: [login]&lt;/code&gt; instead of &lt;code&gt;assignee: login&lt;/code&gt;. The footgun is that &lt;code&gt;assignee&lt;/code&gt; returning &lt;code&gt;undefined&lt;/code&gt; looks identical to "this PR has no assignee," and a lot of routing logic gates on exactly that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why you don't get an error
&lt;/h2&gt;

&lt;p&gt;The REST API breaking-change rollout works by version pinning. If you send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-GitHub-Api-Version: 2022-11-28
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…you keep the old field. If you send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;X-GitHub-Api-Version: 2026-03-10
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…or no version header at all and your client SDK has updated its default — the field is gone.&lt;/p&gt;

&lt;p&gt;Octokit, PyGithub, go-github, and the various community SDKs upgrade their defaults on their own schedules. Your &lt;code&gt;package.json&lt;/code&gt; says &lt;code&gt;^21.0.0&lt;/code&gt;, the maintainer ships &lt;code&gt;21.4.7&lt;/code&gt; next month with the new default version header, your &lt;code&gt;npm install&lt;/code&gt; picks it up on the next CI run, and now your release bot is tagging &lt;code&gt;undefined&lt;/code&gt;. Nothing in your repo changed. Nothing in the API changed for clients still pinning the old version. The change rides in on a transitive bump.&lt;/p&gt;

&lt;h2&gt;
  
  
  The non-obvious replacement
&lt;/h2&gt;

&lt;p&gt;The official guidance points to the &lt;code&gt;pull_request.merge_commit_sha&lt;/code&gt; field on the &lt;strong&gt;webhook payload&lt;/strong&gt; (not the API response) and to the merge commit's SHA reachable via:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;GET /repos/&lt;span class="o"&gt;{&lt;/span&gt;owner&lt;span class="o"&gt;}&lt;/span&gt;/&lt;span class="o"&gt;{&lt;/span&gt;repo&lt;span class="o"&gt;}&lt;/span&gt;/commits/&lt;span class="o"&gt;{&lt;/span&gt;ref&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="c"&gt;# where ref is the head of the default branch immediately after merge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither is a one-line swap.&lt;/p&gt;

&lt;p&gt;Webhook payloads still carry &lt;code&gt;merge_commit_sha&lt;/code&gt; because GitHub versions webhooks separately from the REST API. If your release bot is webhook-driven, you're fine — the field is in the &lt;code&gt;pull_request.closed&lt;/code&gt; event payload. If your release bot is poll-driven (CI runs nightly, asks "what merged today, tag those artifacts"), you have to reconstruct the merge commit by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reading the PR's &lt;code&gt;base.ref&lt;/code&gt; (the target branch)&lt;/li&gt;
&lt;li&gt;Pulling commit history on that branch&lt;/li&gt;
&lt;li&gt;Finding the merge commit by the PR number in the commit message (&lt;code&gt;Merge pull request #N&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Or — for squash/rebase merges — there is no single merge SHA, and you have to use &lt;code&gt;head.sha&lt;/code&gt; of the PR plus tracking metadata your bot wrote earlier&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The squash-merge case is the one most release tooling gets wrong on the rewrite. There never was a &lt;code&gt;merge_commit_sha&lt;/code&gt; for squash merges in the strict sense — GitHub returned the SHA of the squashed-in commit on the base branch — but consumers treated it as canonical. Without that field, the PR head SHA and the resulting commit on &lt;code&gt;main&lt;/code&gt; are different SHAs with no GitHub-provided link between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;Your CI pipeline tests hit a fixture PR JSON. The fixture has &lt;code&gt;merge_commit_sha&lt;/code&gt;. The test asserts the tag string is well-formed.&lt;/p&gt;

&lt;p&gt;Three things have to be true for that test to fail before the rollout:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your fixtures were regenerated against the new API version (they weren't)&lt;/li&gt;
&lt;li&gt;Your test runner sends the new &lt;code&gt;X-GitHub-Api-Version&lt;/code&gt; header (it doesn't, unless you wrote it)&lt;/li&gt;
&lt;li&gt;Your assertion checks &lt;code&gt;typeof sha === 'string' &amp;amp;&amp;amp; sha.length === 40&lt;/code&gt; and not just &lt;code&gt;sha != null&lt;/code&gt; (most don't)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the test stays green. The same pattern as every other silent drift incident:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Where tests missed it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GitHub PushEvent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;commits&lt;/code&gt; field silently dropped&lt;/td&gt;
&lt;td&gt;Tests didn't assert field presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Stripe Basil&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;current_period_end&lt;/code&gt; moved to &lt;code&gt;items&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests used Checkout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Shopify 2025-01&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fulfillmentHold&lt;/code&gt; type change&lt;/td&gt;
&lt;td&gt;Tests mocked the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;OpenAI Responses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;input_text&lt;/code&gt; removed for assistants&lt;/td&gt;
&lt;td&gt;Tests covered request role=user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Twilio regional&lt;/td&gt;
&lt;td&gt;Regional domains stop resolving&lt;/td&gt;
&lt;td&gt;Tests don't hit prod DNS paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;HubSpot Contacts v1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-memberships&lt;/code&gt; returns empty&lt;/td&gt;
&lt;td&gt;Tests asserted against sandbox fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;GitHub merge_commit_sha&lt;/td&gt;
&lt;td&gt;Field removed from PR responses&lt;/td&gt;
&lt;td&gt;Tests used pre-rollout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pattern holds. The breaking change is always in a field a test isn't asserting against — or in a layer (transitive SDK upgrade, version-pinned header) the test isn't exercising.&lt;/p&gt;

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

&lt;p&gt;Three actions, in priority order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep your codebase for &lt;code&gt;merge_commit_sha&lt;/code&gt;.&lt;/strong&gt; Every read site is a candidate failure. Tag every assignment to a deployment artifact, release name, or git tag as a critical path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep for &lt;code&gt;pr.assignee&lt;/code&gt; or &lt;code&gt;issue.assignee&lt;/code&gt; (singular).&lt;/strong&gt; Routing/notification code keyed on this field is the next-largest blast radius after release tagging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pin &lt;code&gt;X-GitHub-Api-Version: 2022-11-28&lt;/code&gt; as a stopgap if you can't fix all the read sites this week.&lt;/strong&gt; GitHub supports old versions for ~24 months. This buys time, not a fix — schedule the migration before the version sunsets.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your release bot is silently tagging &lt;code&gt;undefined&lt;/code&gt;, you usually find out 90 days later when somebody is paged at 3 AM and can't trace the deploy. Worth fixing on a Tuesday afternoon.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; for this layer: poll third-party APIs on a schedule, watch the response shape, page when a field stops appearing. The GitHub merge SHA removal is the textbook incident — no error, no warning, just a field that used to be there and isn't. APIs break this way constantly. We catch it before the 3 AM page.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We track API drift incidents in real time. If your release tooling reads &lt;code&gt;merge_commit_sha&lt;/code&gt; and you haven't audited it for the 2026-03-10 API version, that null is already in your build pipeline somewhere.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>api</category>
      <category>cicd</category>
      <category>devops</category>
    </item>
    <item>
      <title>Auth0 is about to start returning handshake_failure — how to tell if you're affected</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sun, 26 Apr 2026 04:01:59 +0000</pubDate>
      <link>https://forem.com/flarecanary/auth0-is-about-to-start-returning-handshakefailure-how-to-tell-if-youre-affected-dfb</link>
      <guid>https://forem.com/flarecanary/auth0-is-about-to-start-returning-handshakefailure-how-to-tell-if-youre-affected-dfb</guid>
      <description>&lt;p&gt;Auth0 has 14 cipher suites scheduled for removal at the end of H1 2026. If any of your clients — your backend, a reverse proxy, an older mobile binary — negotiates one of them today, your next Auth0 call after the cutoff will fail with &lt;code&gt;handshake_failure&lt;/code&gt; and no further explanation.&lt;/p&gt;

&lt;p&gt;There's no JSON error body. No HTTP status code. No entry in the Auth0 tenant log, because the connection never gets far enough to hit the application layer. The client just gets a TLS alert from the edge and a stack trace that points at your HTTP library, not at Auth0.&lt;/p&gt;

&lt;p&gt;That's a bad failure mode to debug under pressure, so it's worth checking now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exact suites being removed
&lt;/h2&gt;

&lt;p&gt;Auth0's support article lists 14 CBC-mode and RSA-keyed suites as deprecated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two flavors to notice: all CBC-mode AES suites regardless of key exchange, plus every static-RSA suite even when it's using GCM. GCM alone isn't enough — the RSA key exchange is the problem, because it provides no forward secrecy.&lt;/p&gt;

&lt;p&gt;The Canadian region (&lt;code&gt;CA-1&lt;/code&gt;) already removed these suites. If you have traffic split across regions and something works in US/EU but not CA, this is a strong lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failure looks like in your stack
&lt;/h2&gt;

&lt;p&gt;The observable symptom depends on your HTTP client. The common shapes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node (undici / node-fetch):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Error: write EPROTO ... SSL alert number 40 ... handshake failure
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Python (requests / urllib3):&lt;/strong&gt;&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;SSLError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;SSL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SSLV3_ALERT_HANDSHAKE_FAILURE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;sslv3&lt;/span&gt; &lt;span class="n"&gt;alert&lt;/span&gt; &lt;span class="n"&gt;handshake&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Go (net/http):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;remote&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tls&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;handshake&lt;/span&gt; &lt;span class="n"&gt;failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Java (Apache HttpClient):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;javax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;net&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSLHandshakeException&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Received&lt;/span&gt; &lt;span class="n"&gt;fatal&lt;/span&gt; &lt;span class="nl"&gt;alert:&lt;/span&gt; &lt;span class="n"&gt;handshake_failure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of those mention Auth0. None include a cipher name. None suggest a fix. A developer Googling the exact error string mostly lands on Stack Overflow answers about self-signed certificates and clock skew, which is the wrong diagnosis.&lt;/p&gt;

&lt;p&gt;The actually-useful signal is &lt;em&gt;which endpoint&lt;/em&gt; fails. If every call to &lt;code&gt;*.auth0.com&lt;/code&gt; or your tenant's &lt;code&gt;login.&lt;/code&gt; custom domain starts alerting at the same moment, TLS negotiation is the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to check whether you're affected
&lt;/h2&gt;

&lt;p&gt;The handshake is a client-side concern, so you check at the client. Three approaches, cheapest first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ask OpenSSL directly
&lt;/h3&gt;

&lt;p&gt;Pick one of the deprecated suites and force the handshake against your tenant. If the handshake succeeds, that client can still reach Auth0 today, and will break at the cutoff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openssl s_client &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-connect&lt;/span&gt; YOUR-TENANT.auth0.com:443 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-tls1_2&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-cipher&lt;/span&gt; &lt;span class="s1"&gt;'ECDHE-RSA-AES128-SHA'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;lt; /dev/null 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'Cipher|Verify'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see &lt;code&gt;Cipher    : ECDHE-RSA-AES128-SHA&lt;/code&gt; in the output, your OpenSSL build negotiates a deprecated suite and nothing stopped it. Run the same probe from the environment your code actually runs in — a container, a serverless function, a legacy VM — not just your laptop.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Check what your runtime offers
&lt;/h3&gt;

&lt;p&gt;The handshake is a negotiation between what your client offers and what Auth0 accepts. If your client's offered list is a superset of modern suites, you're fine; the removal only bites when the client has &lt;em&gt;only&lt;/em&gt; deprecated options on the table.&lt;/p&gt;

&lt;p&gt;Old Android devices, Java 8 builds without recent TLS patches, Alpine containers with stripped OpenSSL, and vendored binaries from 2019-era SDKs are the usual culprits.&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="c"&gt;# Node&lt;/span&gt;
node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'console.log(require("tls").DEFAULT_CIPHERS)'&lt;/span&gt;

&lt;span class="c"&gt;# Python&lt;/span&gt;
python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import ssl; ctx=ssl.create_default_context(); print(ctx.get_ciphers())"&lt;/span&gt;

&lt;span class="c"&gt;# Java&lt;/span&gt;
keytool &lt;span class="nt"&gt;-list&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-storepass&lt;/span&gt; changeit 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for whether modern suites — &lt;code&gt;TLS_AES_128_GCM_SHA256&lt;/code&gt;, &lt;code&gt;TLS_AES_256_GCM_SHA384&lt;/code&gt;, &lt;code&gt;TLS_CHACHA20_POLY1305_SHA256&lt;/code&gt;, &lt;code&gt;ECDHE-ECDSA-AES128-GCM-SHA256&lt;/code&gt;, &lt;code&gt;ECDHE-RSA-AES128-GCM-SHA256&lt;/code&gt; — are in the list. If they are, you keep working after the cutoff. If only the deprecated suites are present, you break.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Point staging at the Canadian region
&lt;/h3&gt;

&lt;p&gt;Auth0's &lt;code&gt;CA-1&lt;/code&gt; region has already removed the weak suites. If you can temporarily route staging traffic at a Canadian tenant (or any service that's enforced the same restrictions), anything that fails there is what would have failed post-cutoff in your primary region. This is the closest thing to a dress rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to fix
&lt;/h2&gt;

&lt;p&gt;For most callers the fix is a runtime upgrade, not a code change.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node 18+, Python 3.10+, Go 1.17+, Java 11+&lt;/strong&gt; all ship with acceptable defaults. If you're on those, you're probably fine — verify with the probes above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSSL 1.1.1+&lt;/strong&gt; on Linux, &lt;strong&gt;LibreSSL on macOS 12+&lt;/strong&gt;, or &lt;strong&gt;Schannel on Windows Server 2022+&lt;/strong&gt; are the modern baselines. Older TLS stacks need rebuilding or replacing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-managed certificate deployments&lt;/strong&gt; (Auth0's &lt;a href="https://auth0.com/docs/customize/custom-domains/self-managed-certificates" rel="noopener noreferrer"&gt;custom domain setup with your own cert&lt;/a&gt;) are the most exposed — you control the termination layer and therefore the negotiated cipher. Anything in front of that path (nginx, Envoy, a CDN) is also yours to audit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse proxies&lt;/strong&gt; in your path must also negotiate modern suites. A proxy with TLS 1.2 and old suites hard-coded will break exactly the same way, even if the process behind it is on a current runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't upgrade — there's always a legacy binary someone can't rebuild — the fallback is to pin an explicit cipher list that includes at least one GCM or ChaCha20 suite both sides support. That buys you time, not permanence: anything RSA-keyed will lose forward secrecy as a floor requirement eventually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader pattern
&lt;/h2&gt;

&lt;p&gt;Vendor-side TLS changes are a specific case of the general schema-drift problem: the vendor updates their endpoint, your code doesn't change, and the contract between them silently shifts.&lt;/p&gt;

&lt;p&gt;The ways this usually bites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No status code to assert on.&lt;/strong&gt; Integration tests that check &lt;code&gt;response.status === 200&lt;/code&gt; can't fire for a handshake that never completes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not in the vendor's tenant log.&lt;/strong&gt; Auth0's dashboard shows nothing because the request never reached the application layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not in your APM.&lt;/strong&gt; Most APMs start the span at the HTTP request; a TLS-layer failure looks like a one-line network error at best.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The way to catch this before your users do is to make a small, boring background check against each third-party endpoint you depend on — not just that it returns 200, but that the connection itself is healthy from your runtime. A synthetic handshake from inside your infrastructure is cheap, runs on a schedule, and produces an alert that points at the right vendor instead of at a generic network error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum-viable fix for today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Run the OpenSSL probe above against your Auth0 tenant from every environment your code runs in — local, staging, prod, each serverless region, any legacy VM that still has outbound Auth0 traffic&lt;/li&gt;
&lt;li&gt;Grep your Auth0 callers for any forced cipher list: &lt;code&gt;git grep -E 'ciphers|ssl_ciphers|TLS_RSA_WITH|ECDHE_RSA_WITH_AES.*CBC'&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;For any binary you can't rebuild, confirm at least one modern GCM or ChaCha20 suite is in its offered list&lt;/li&gt;
&lt;li&gt;Route staging traffic at the &lt;code&gt;CA-1&lt;/code&gt; region for a day and watch for &lt;code&gt;handshake_failure&lt;/code&gt; alerts — this is the pre-cutoff preview&lt;/li&gt;
&lt;li&gt;Add a scheduled synthetic check that connects to your Auth0 tenant from production and fails loudly on a TLS error, not just on HTTP non-2xx&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If none of the above produce a finding, you're clear. If any of them do, you have roughly six weeks.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors REST APIs — including Auth0 tenants — for schema drift and connection-layer changes. Free tier covers 5 endpoints with daily checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>auth0</category>
      <category>security</category>
      <category>tls</category>
      <category>api</category>
    </item>
    <item>
      <title>shopify.metafields returns undefined in my checkout extension after 2026-04 — here's why</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sat, 25 Apr 2026 04:02:31 +0000</pubDate>
      <link>https://forem.com/flarecanary/shopifymetafields-returns-undefined-in-my-checkout-extension-after-2026-04-heres-why-3ph1</link>
      <guid>https://forem.com/flarecanary/shopifymetafields-returns-undefined-in-my-checkout-extension-after-2026-04-heres-why-3ph1</guid>
      <description>&lt;p&gt;If you just bumped your checkout UI extension to Shopify API version 2026-04 and your &lt;code&gt;shopify.metafields&lt;/code&gt; reads started returning &lt;code&gt;undefined&lt;/code&gt;, you've hit the biggest breaking change in this release.&lt;/p&gt;

&lt;p&gt;Checkout metafields in the metafields API are gone. They've been replaced by order metafields in the &lt;code&gt;appMetafields&lt;/code&gt; API. The rename isn't cosmetic — it's a different data source, a different access pattern, and a different set of timing semantics. The old code doesn't error; it just silently returns nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failure looks like
&lt;/h2&gt;

&lt;p&gt;The code that worked on 2026-01:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;extension&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@shopify/ui-extensions/checkout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;purchase.checkout.block.render&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;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;api&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;metafields&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&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;myField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;metafields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivery_window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// myField.value used to be "2026-05-01T09:00:00"&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myField&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the 2026-04 upgrade, &lt;code&gt;myField&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;. No deprecation warning. No network error. The extension renders &lt;code&gt;'unknown'&lt;/code&gt; in production and the fallback path becomes the default behavior. If the ternary hid a real problem — a missing delivery window, a missing subscription flag, a missing audit tag — your orders flow through without it.&lt;/p&gt;

&lt;p&gt;The symptom in Shopify's developer console, if you're lucky enough to be looking there at the right moment, is either a missing metafield in the &lt;code&gt;api.metafields.current&lt;/code&gt; array or the array itself being empty. Neither is an error condition, just a change in what's served.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it breaks silently
&lt;/h2&gt;

&lt;p&gt;Shopify's UI extensions surface APIs as plain JS properties. When the platform removes a property at a given API version, the consuming code reads &lt;code&gt;undefined&lt;/code&gt; — the same as if the field weren't set for this buyer. There's no distinction between "metafield doesn't exist on this order" and "this API doesn't expose metafields anymore," because both are the same runtime shape.&lt;/p&gt;

&lt;p&gt;The 2026-04 release moves this data to a different surface: &lt;code&gt;shopify.appMetafields&lt;/code&gt;. The old call site keeps compiling because &lt;code&gt;metafields&lt;/code&gt; is still a property on &lt;code&gt;api&lt;/code&gt; — it's just the thing it references has shrunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exact migration
&lt;/h2&gt;

&lt;p&gt;The replacement property is &lt;code&gt;appMetafields&lt;/code&gt;, read from the global &lt;code&gt;shopify&lt;/code&gt; object rather than a callback parameter:&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="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@shopify/ui-extensions/preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;preact&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="k"&gt;async &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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Extension&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&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="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Extension&lt;/span&gt;&lt;span class="p"&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;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;shopify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appMetafields&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myField&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metafield&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;namespace&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;custom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;metafield&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delivery_window&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;myField&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;metafield&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/s-text&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three differences worth internalizing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;appMetafields&lt;/code&gt; is an app-scoped surface.&lt;/strong&gt; Your extension only sees metafields written under your app's reserved namespace. Anything your app wrote to a &lt;code&gt;custom.*&lt;/code&gt; namespace before is visible on the compatibility shim today, but new writes should use the app-reserved namespace going forward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entries are wrapped, not bare.&lt;/strong&gt; Each entry has &lt;code&gt;{ target, metafield }&lt;/code&gt;. The &lt;code&gt;target&lt;/code&gt; is the resource the metafield is attached to (&lt;code&gt;order&lt;/code&gt;, &lt;code&gt;customer&lt;/code&gt;, etc.). The old flat-array shape is gone — filter on &lt;code&gt;target.type&lt;/code&gt; before filtering on namespace/key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's a reactive value, not a snapshot.&lt;/strong&gt; &lt;code&gt;shopify.appMetafields&lt;/code&gt; is a Preact-signals-style reactive value. Reading &lt;code&gt;.value&lt;/code&gt; gives the current array. Inside a component it re-renders when the underlying entries change, which matches the new 2026-01 UI model (Preact web components replacing React).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where the old pattern is hiding in your code
&lt;/h2&gt;

&lt;p&gt;This migration hits more than the obvious metafield calls. Places to grep:&lt;br&gt;
&lt;/p&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;-n&lt;/span&gt; &lt;span class="s2"&gt;"api&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;metafields"&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"useMetafields"&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"shopify&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;metafields"&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"ExtensionPoint.*Checkout"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;useMetafields&lt;/code&gt; React hook is an older API that also routed through the deprecated surface. Anywhere you called it, the replacement is the &lt;code&gt;shopify.appMetafields.value&lt;/code&gt; read above.&lt;/p&gt;

&lt;p&gt;Server-side writers don't need to change — the Admin GraphQL API's &lt;code&gt;metafieldsSet&lt;/code&gt; mutation still works. It's only the read path from checkout extensions that moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cart-metafields path, if you're writing fresh code
&lt;/h2&gt;

&lt;p&gt;Separately from the appMetafields migration, Shopify is steering new code toward cart metafields as the canonical place for per-buyer data that needs to survive checkout. As of 2026-04, cart metafields automatically copy to the resulting order at checkout completion, which closes a long-standing gap where checkout-time state quietly didn't make it to fulfillment.&lt;/p&gt;

&lt;p&gt;If you're building something new today, the decision tree is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-buyer data that needs to outlive checkout&lt;/strong&gt; → cart metafields. They'll auto-copy to the order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-app configuration that needs to be read at checkout&lt;/strong&gt; → app-reserved namespace metafields, read via &lt;code&gt;shopify.appMetafields&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Legacy &lt;code&gt;custom.*&lt;/code&gt;-namespace metafields your app has historically written&lt;/strong&gt; → still readable on the compatibility shim, but plan a migration to your app-reserved namespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Detecting the break before your QA does
&lt;/h2&gt;

&lt;p&gt;The obvious version check:&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;-r&lt;/span&gt; &lt;span class="s2"&gt;"api_version"&lt;/span&gt; extensions/ | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; node_modules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If anything returns 2026-04 or later and the extension still reads &lt;code&gt;api.metafields&lt;/code&gt;, that extension is broken.&lt;/p&gt;

&lt;p&gt;The less-obvious version: extensions can be bumped independently, and an app with multiple extensions can have one on 2026-04 and another on 2026-01. The one on 2026-04 silently fails; the one on 2026-01 works. You get half your data on the order and half missing — exactly the confusing support ticket that's hard to reproduce.&lt;/p&gt;

&lt;p&gt;A small synthetic order run in your staging shop is the cheapest catch. Place an order that exercises each extension, pull the resulting order via the Admin API, and diff the metafields array against what you expect. Any missing key is a lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The broader pattern
&lt;/h2&gt;

&lt;p&gt;What's happening here is a specific case of a general problem: the SDK you depend on changes the shape of the data it returns, not the shape of its function signatures. Types still line up. The code still compiles. The values have just moved, and the old read path becomes &lt;code&gt;undefined&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ways this usually bites:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript doesn't catch it.&lt;/strong&gt; The types got updated on the SDK, your code got updated with &lt;code&gt;@shopify/ui-extensions&lt;/code&gt; 2026-04, and &lt;code&gt;api.metafields&lt;/code&gt; is still typed — it's just a narrower universe of things now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unit tests don't catch it.&lt;/strong&gt; Mocked responses in a test fixture don't know the real platform shape changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI doesn't catch it.&lt;/strong&gt; Nothing in CI is hitting Shopify's edge. The break only shows up in a real checkout.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The way to catch this kind of drift before buyers do is a scheduled synthetic check against the real vendor surface — place an order in a staging shop, read it back, diff the metafields array against a known baseline. Same pattern as monitoring any other third-party API contract, applied to the specific surface each platform exposes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum-viable fix for today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;git grep -E "api_version.*2026-04"&lt;/code&gt; across every extension and confirm which ones bumped&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git grep -n "api\.metafields\|useMetafields"&lt;/code&gt; in every bumped extension — each hit is a broken read&lt;/li&gt;
&lt;li&gt;Migrate those call sites to &lt;code&gt;shopify.appMetafields.value&lt;/code&gt; with &lt;code&gt;.target.type === 'order'&lt;/code&gt; filtering&lt;/li&gt;
&lt;li&gt;Move any app-owned metafield writes to your app-reserved namespace&lt;/li&gt;
&lt;li&gt;Place a synthetic test order in a staging shop and verify the expected metafield keys land on the resulting order&lt;/li&gt;
&lt;li&gt;If you have multiple extensions on mixed API versions, align them or at minimum document which are on which version&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the migration reveals that checkout metafields were load-bearing for fulfillment, consider whether cart metafields — now auto-copied to the order at checkout completion — are the better home going forward.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors REST and GraphQL APIs — including Shopify Admin and Storefront endpoints — for schema drift. Free tier covers 5 endpoints with daily checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>api</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>DALL·E shuts down May 12 — the gpt-image-1 migration isn't the drop-in swap it looks like</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sat, 25 Apr 2026 04:02:03 +0000</pubDate>
      <link>https://forem.com/flarecanary/dalle-shuts-down-may-12-the-gpt-image-1-migration-isnt-the-drop-in-swap-it-looks-like-3p02</link>
      <guid>https://forem.com/flarecanary/dalle-shuts-down-may-12-the-gpt-image-1-migration-isnt-the-drop-in-swap-it-looks-like-3p02</guid>
      <description>&lt;p&gt;OpenAI is shutting down &lt;code&gt;dall-e-2&lt;/code&gt; and &lt;code&gt;dall-e-3&lt;/code&gt; on &lt;strong&gt;May 12, 2026&lt;/strong&gt;. After that date, requests to &lt;code&gt;/v1/images/generations&lt;/code&gt; with either model string will stop working. The recommended replacements are &lt;code&gt;gpt-image-1&lt;/code&gt; and &lt;code&gt;gpt-image-1-mini&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On paper this is a one-line change: swap the model name, ship. In practice, the request and response shapes are different enough that a naive swap breaks clients that worked against DALL·E for the last two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the shutdown looks like
&lt;/h2&gt;

&lt;p&gt;OpenAI's deprecation language: &lt;em&gt;"deprecated models will no longer be accessible"&lt;/em&gt; after the shutdown date. Translated: your &lt;code&gt;POST /v1/images/generations&lt;/code&gt; with &lt;code&gt;"model": "dall-e-3"&lt;/code&gt; will return an error, not a fallback to the new model.&lt;/p&gt;

&lt;p&gt;Expected response shape:&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;"error"&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;"The model `dall-e-3` has been deprecated. Learn more: https://platform.openai.com/docs/deprecations"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invalid_request_error"&lt;/span&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="s2"&gt;"model_not_found"&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;No grace period, no auto-upgrade. The endpoint itself still exists — &lt;code&gt;/v1/images/generations&lt;/code&gt; is alive and serves &lt;code&gt;gpt-image-1&lt;/code&gt;. Only the old model IDs are gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where &lt;code&gt;dall-e-3&lt;/code&gt; is probably pinned in your code
&lt;/h2&gt;

&lt;p&gt;The model string is usually spread across more surfaces than you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables&lt;/strong&gt;: &lt;code&gt;OPENAI_IMAGE_MODEL&lt;/code&gt;, &lt;code&gt;DALLE_MODEL&lt;/code&gt;, &lt;code&gt;IMAGE_MODEL&lt;/code&gt;. Check every environment, not just prod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK wrappers&lt;/strong&gt;: the Python &lt;code&gt;openai&lt;/code&gt; SDK's &lt;code&gt;client.images.generate(model=...)&lt;/code&gt; call. LangChain's &lt;code&gt;DallEAPIWrapper&lt;/code&gt;. Vercel AI SDK image helpers. LiteLLM routers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded defaults&lt;/strong&gt;: it's common to see &lt;code&gt;model or "dall-e-3"&lt;/code&gt; as a fallback when the caller doesn't pass one. That fallback becomes a runtime error on May 12.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tests and fixtures&lt;/strong&gt;: VCR cassettes, recorded responses, snapshot tests. These pass locally but the behavior they capture is about to change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation and onboarding code&lt;/strong&gt;: if your docs show &lt;code&gt;"model": "dall-e-3"&lt;/code&gt; as an example, update them before users copy it into fresh projects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The five-minute audit:&lt;br&gt;
&lt;/p&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;-n&lt;/span&gt; &lt;span class="s2"&gt;"dall-e-3&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;dall-e-2&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;dalle-3&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;dalle-2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The request shape changed
&lt;/h2&gt;

&lt;p&gt;A DALL·E 3 call looks like this:&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dall-e-3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prompt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a watercolor fox"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1024x1024"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vivid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url"&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;code&gt;gpt-image-1&lt;/code&gt; equivalent doesn't accept three of those fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;response_format&lt;/code&gt; is gone.&lt;/strong&gt; DALL·E returned hosted image URLs by default. &lt;code&gt;gpt-image-1&lt;/code&gt; returns &lt;strong&gt;base64-encoded PNG bytes in &lt;code&gt;b64_json&lt;/code&gt;&lt;/strong&gt;, always. If your client reads &lt;code&gt;response.data[0].url&lt;/code&gt;, it will be &lt;code&gt;None&lt;/code&gt;. You need to decode the bytes and either upload them to your own storage or serve them inline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;style&lt;/code&gt; is gone.&lt;/strong&gt; The &lt;code&gt;vivid&lt;/code&gt; / &lt;code&gt;natural&lt;/code&gt; distinction doesn't exist. Prompt-engineer the style into the text instead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;quality&lt;/code&gt; values changed.&lt;/strong&gt; DALL·E 3 used &lt;code&gt;standard&lt;/code&gt; / &lt;code&gt;hd&lt;/code&gt;. &lt;code&gt;gpt-image-1&lt;/code&gt; uses &lt;code&gt;low&lt;/code&gt; / &lt;code&gt;medium&lt;/code&gt; / &lt;code&gt;high&lt;/code&gt; / &lt;code&gt;auto&lt;/code&gt;. A literal &lt;code&gt;"hd"&lt;/code&gt; in the request will be rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;size&lt;/code&gt; values changed.&lt;/strong&gt; DALL·E 3 supported &lt;code&gt;1024x1024 | 1792x1024 | 1024x1792&lt;/code&gt;. &lt;code&gt;gpt-image-1&lt;/code&gt; supports &lt;code&gt;1024x1024 | 1024x1536 | 1536x1024 | auto&lt;/code&gt;. Landscape and portrait are 1536×1024 and 1024×1536, not 1792×1024 and 1024×1792.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;New parameters you probably want to set explicitly: &lt;code&gt;output_format&lt;/code&gt; (&lt;code&gt;png&lt;/code&gt; / &lt;code&gt;jpeg&lt;/code&gt; / &lt;code&gt;webp&lt;/code&gt;), &lt;code&gt;output_compression&lt;/code&gt; (for jpeg/webp), and &lt;code&gt;moderation&lt;/code&gt; (&lt;code&gt;low&lt;/code&gt; / &lt;code&gt;auto&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The cost model flipped
&lt;/h2&gt;

&lt;p&gt;This is the migration gotcha nobody flags in the deprecation notice.&lt;/p&gt;

&lt;p&gt;DALL·E 3 was billed &lt;strong&gt;per image&lt;/strong&gt;: $0.040 for standard 1024×1024, $0.080 for HD. You could forecast cost by counting images.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gpt-image-1&lt;/code&gt; is billed &lt;strong&gt;per token&lt;/strong&gt; — input text tokens, input image tokens (for edits), and output image tokens. A single &lt;code&gt;medium&lt;/code&gt; quality 1024×1024 generation is roughly ~1,000 output image tokens at $40 per million, so around $0.04. &lt;code&gt;high&lt;/code&gt; quality is several times that. &lt;code&gt;gpt-image-1-mini&lt;/code&gt; is cheaper but still token-billed.&lt;/p&gt;

&lt;p&gt;If your current cost forecast is &lt;code&gt;images_per_month × $0.04&lt;/code&gt;, that forecast is wrong after May 12 in ways that depend on your prompt length and quality setting. Re-model before you switch, not after the first invoice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern this fits
&lt;/h2&gt;

&lt;p&gt;OpenAI's deprecation cadence over the last year:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;October 2024&lt;/strong&gt; — &lt;code&gt;gpt-3.5-turbo-0301&lt;/code&gt;, &lt;code&gt;gpt-3.5-turbo-0613&lt;/code&gt; retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;June 2025&lt;/strong&gt; — &lt;code&gt;gpt-4-0314&lt;/code&gt;, &lt;code&gt;gpt-4-32k-0314&lt;/code&gt; retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;January 2026&lt;/strong&gt; — &lt;code&gt;text-moderation-007&lt;/code&gt; moved to legacy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May 12, 2026&lt;/strong&gt; — &lt;code&gt;dall-e-2&lt;/code&gt;, &lt;code&gt;dall-e-3&lt;/code&gt; retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Announced, no firm date&lt;/strong&gt; — Assistants API sunset August 26, 2026&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these was announced with at least 60 days of notice. Every one of them broke production for teams that didn't see the notice. The pattern isn't a surprise attack — it's that nobody instruments the provider's public surface (models list, deprecation page, error shapes) as a monitored contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually catches this
&lt;/h2&gt;

&lt;p&gt;A dependency on a third-party API is a silent coupling. The provider changes the shape of what they return, or stops accepting a model ID, and your code — unchanged — starts failing. CI doesn't catch it because CI runs against fixtures or mocks. Unit tests don't catch it because the SDK still type-checks.&lt;/p&gt;

&lt;p&gt;Two things catch it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Integration tests that hit the real API&lt;/strong&gt; on a schedule (nightly is enough), against every model ID and request shape you rely on. When the shape drifts, the test turns red.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring the provider's models list and documented error shapes&lt;/strong&gt; as a diffable contract. &lt;code&gt;GET https://api.openai.com/v1/models&lt;/code&gt; tells you which IDs are live. When &lt;code&gt;dall-e-3&lt;/code&gt; stops appearing there, you want the alert before the next deploy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Either approach works. What doesn't work is hoping the deprecation email lands in an inbox someone reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum-viable fix for this one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;git grep&lt;/code&gt; for every DALL·E model ID across every repo that talks to OpenAI.&lt;/li&gt;
&lt;li&gt;Replace with &lt;code&gt;gpt-image-1&lt;/code&gt; (or &lt;code&gt;gpt-image-1-mini&lt;/code&gt; if cost-sensitive).&lt;/li&gt;
&lt;li&gt;Remove &lt;code&gt;response_format&lt;/code&gt;, &lt;code&gt;style&lt;/code&gt;, and any &lt;code&gt;quality: "hd"&lt;/code&gt; / &lt;code&gt;quality: "standard"&lt;/code&gt; from the request body.&lt;/li&gt;
&lt;li&gt;Update response-handling code to decode &lt;code&gt;b64_json&lt;/code&gt; instead of fetching &lt;code&gt;url&lt;/code&gt;. Add your own storage step if your product needed hosted URLs.&lt;/li&gt;
&lt;li&gt;Re-forecast cost against the token-billed model. Run a sample batch through &lt;code&gt;gpt-image-1&lt;/code&gt; at your expected quality setting and multiply out.&lt;/li&gt;
&lt;li&gt;Check that the old model IDs aren't still in staging, demo apps, or onboarding example code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If this is the third or fourth time a provider change has broken production without warning, the problem isn't this specific deprecation. It's that you're finding out from alerts instead of ahead of time.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors REST APIs and MCP servers for schema drift — including models-list and error-shape changes from AI providers. Free tier covers 5 endpoints with daily checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>openai</category>
      <category>ai</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>HubSpot Contacts v1 Goes Dark on April 30 — And the Worst Part Is the Endpoints That Keep Working</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Sat, 25 Apr 2026 04:01:36 +0000</pubDate>
      <link>https://forem.com/flarecanary/hubspot-contacts-v1-goes-dark-on-april-30-and-the-worst-part-is-the-endpoints-that-keep-working-5edh</link>
      <guid>https://forem.com/flarecanary/hubspot-contacts-v1-goes-dark-on-april-30-and-the-worst-part-is-the-endpoints-that-keep-working-5edh</guid>
      <description>&lt;p&gt;On &lt;strong&gt;April 30, 2026&lt;/strong&gt;, HubSpot sunsets the Contact Lists v1 API. The headline is simple: most &lt;code&gt;/contacts/v1/lists/*&lt;/code&gt; endpoints start returning HTTP 404. The dangerous part is everything that &lt;em&gt;doesn't&lt;/em&gt; 404.&lt;/p&gt;

&lt;p&gt;Straight from HubSpot's &lt;a href="https://developers.hubspot.com/changelog/extension-contact-lists-api-v1-sunset-moved-to-april-30-2026" rel="noopener noreferrer"&gt;sunset announcement&lt;/a&gt;, six Contacts v1 read endpoints will:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;continue to function but will no longer return list memberships&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a 200 response with the &lt;code&gt;list-memberships&lt;/code&gt; array silently gone. Code that does &lt;code&gt;contact["list-memberships"]&lt;/code&gt; gets a &lt;code&gt;KeyError&lt;/code&gt;. Code that does &lt;code&gt;contact.get("list-memberships", [])&lt;/code&gt; swallows the empty list and now thinks the contact is in zero lists. Both are broken. One of them looks broken.&lt;/p&gt;

&lt;p&gt;This is incident #6 in our silent-breakage series (&lt;a href="https://dev.to/flarecanary"&gt;GitHub PushEvent&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Stripe Basil&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Shopify 2025-01&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;OpenAI Responses &lt;code&gt;input_text&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Twilio regional domains&lt;/a&gt;). Same pattern: the breaking change lives in a field you weren't asserting against.&lt;/p&gt;

&lt;h2&gt;
  
  
  What returns 404 on April 30
&lt;/h2&gt;

&lt;p&gt;The full Contact Lists v1 surface goes away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/:list_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /contacts/v1/lists&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /contacts/v1/lists/:list_id/add&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /contacts/v1/lists/:list_id/remove&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /contacts/v1/lists/:list_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/static&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/dynamic&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/:list_id/contacts/all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/:list_id/contacts/recent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plus everything else under that path. On April 30 they return:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&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;"Resource not found"&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;Three Lists endpoints survive but lose membership context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/all/contacts/all&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/all/contacts/recent&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/lists/recently_updated/contacts/recent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These return contact records, but each contact's &lt;code&gt;list-memberships&lt;/code&gt; array stops being populated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The endpoints that bite — they keep returning 200
&lt;/h2&gt;

&lt;p&gt;Here's the list every audit script needs to grep for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/vid/:vid/profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/vids/batch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/email/:contact_email/profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/emails/batch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/utk/:contact_utk/profile&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /contacts/v1/contact/byUtk/batch&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These all keep returning HTTP 200 on May 1. The difference is in the body.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before April 30:&lt;/strong&gt;&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;"vid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"..."&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="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;"list-memberships"&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;"static-list-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"internal-list-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1714492800000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"is-member"&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;"static-list-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"internal-list-id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1714493000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"vid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"is-member"&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="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;&lt;strong&gt;After April 30:&lt;/strong&gt;&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;"vid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&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;"..."&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="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;"list-memberships"&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;Status code: 200. JSON parses cleanly. Field is present. It's just empty.&lt;/p&gt;

&lt;p&gt;If your sync job uses these endpoints to figure out which CRM lists a contact belongs to — Marketo, Salesforce, ActiveCampaign, custom data warehouse, anything — every contact looks like it belongs to nothing. Conditional sends fire (or don't) based on phantom list membership. Audience exclusion lists don't exclude.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix path: v3 Lists API
&lt;/h2&gt;

&lt;p&gt;The replacement is the &lt;a href="https://developers.hubspot.com/docs/api-reference/crm-lists-v3/v1-migration-guide" rel="noopener noreferrer"&gt;Lists v3 API&lt;/a&gt;. For membership lookups, the new shape is per-list, not per-contact:&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="c"&gt;# v1 (going away or going empty)&lt;/span&gt;
GET /contacts/v1/contact/vid/12345/profile
&lt;span class="c"&gt;# → list-memberships array on the contact object&lt;/span&gt;

&lt;span class="c"&gt;# v3 (the replacement)&lt;/span&gt;
GET /crm/v3/lists/&lt;span class="o"&gt;{&lt;/span&gt;listId&lt;span class="o"&gt;}&lt;/span&gt;/memberships
&lt;span class="c"&gt;# → paginated array of {recordId, membershipTimestamp}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The semantic flip matters. v1 answered "what lists is this contact in?" — efficient when you have a contact and want their lists. v3 answers "who is in this list?" — efficient when you have a list and want its contacts. If your code's hot path is the contact-to-lists direction, you have to either:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cache list memberships on your side, refreshed on a schedule&lt;/li&gt;
&lt;li&gt;Hit &lt;code&gt;/crm/v3/lists/memberships/contacts/{contactId}&lt;/code&gt; (yes, this exists, but it's a different endpoint with different rate limits)&lt;/li&gt;
&lt;li&gt;Subscribe to membership-change webhooks and maintain the inverse index yourself&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pick before April 30. The default — "we'll figure it out when something breaks" — picks option 4: silently shipping a sync job that thinks every contact is in zero lists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;Your integration tests hit a HubSpot sandbox. The contact fixture in the sandbox is in three lists. The test asserts &lt;code&gt;len(contact["list-memberships"]) == 3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Three things have to be true for that test to fail before May 1:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your sandbox is the production HubSpot environment (it isn't)&lt;/li&gt;
&lt;li&gt;HubSpot deprecates the field in the sandbox before prod (they don't)&lt;/li&gt;
&lt;li&gt;The test runs after April 30 with no caching (sometimes)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the test stays green. The same pattern as every other silent drift incident:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Where tests missed it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GitHub PushEvent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;commits&lt;/code&gt; field silently dropped&lt;/td&gt;
&lt;td&gt;Tests didn't assert field presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Stripe Basil&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;current_period_end&lt;/code&gt; moved to &lt;code&gt;items&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests used Checkout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Shopify 2025-01&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fulfillmentHold&lt;/code&gt; type change&lt;/td&gt;
&lt;td&gt;Tests mocked the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;OpenAI Responses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;input_text&lt;/code&gt; removed for assistants&lt;/td&gt;
&lt;td&gt;Tests covered request role=user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Twilio regional&lt;/td&gt;
&lt;td&gt;Regional domains stop resolving&lt;/td&gt;
&lt;td&gt;Tests don't hit prod DNS paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;HubSpot Contacts v1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;list-memberships&lt;/code&gt; returns empty&lt;/td&gt;
&lt;td&gt;Tests asserted against sandbox fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The breaking change always lives in a layer the test suite isn't watching. For HubSpot, that layer is "field that used to be populated and now isn't, while the request and response otherwise look identical."&lt;/p&gt;

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

&lt;p&gt;April 30 is seven days out. Three actions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep your codebase for &lt;code&gt;contacts/v1/&lt;/code&gt;.&lt;/strong&gt; Every match is a potential failure. Endpoints in the 404 group break loudly. Endpoints in the 200-but-empty group break quietly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grep for &lt;code&gt;list-memberships&lt;/code&gt; and &lt;code&gt;list_memberships&lt;/code&gt;.&lt;/strong&gt; Every read against this field is downstream-affected even if you can't change the v1 call site this week.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decide between cache-side and webhook-side.&lt;/strong&gt; v3's per-list shape forces an architectural choice. Picking it under deadline pressure is worse than picking it on Monday.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your platform integrations team is on PTO, the failure mode on May 1 is "every CRM sync job runs to completion with empty audiences." That's a quiet kind of broken — one your monitoring probably doesn't catch.&lt;/p&gt;

&lt;p&gt;We built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; for exactly this layer: poll third-party APIs on a schedule, watch the response, page when a field that used to populate stops populating. The HubSpot deprecation is the textbook case — same status code, same JSON shape, semantically broken. The APIs change whether you're watching or not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We track API drift incidents in real time. If your stack syncs HubSpot list memberships and you haven't audited the read paths yet, April 30 is a hard date — and the dangerous part is the endpoints that don't return 404.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>hubspot</category>
      <category>crm</category>
      <category>devops</category>
    </item>
    <item>
      <title>Twilio Is Killing api.de1.twilio.com on April 28 — And the Regional Domains Never Actually Routed Regionally</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Fri, 24 Apr 2026 13:01:23 +0000</pubDate>
      <link>https://forem.com/flarecanary/twilio-is-killing-apide1twiliocom-on-april-28-and-the-regional-domains-never-actually-routed-1hdc</link>
      <guid>https://forem.com/flarecanary/twilio-is-killing-apide1twiliocom-on-april-28-and-the-regional-domains-never-actually-routed-1hdc</guid>
      <description>&lt;p&gt;On &lt;strong&gt;April 28, 2026&lt;/strong&gt;, seven Twilio API domains stop working:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;api.ie1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.au1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.br1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.de1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.jp1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.sg1.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;api.us2.twilio.com&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your code, SDK config, firewall allowlist, or security group references any of those hostnames, the calls will start failing. There is no SDK version bump that masks this — the DNS records go away.&lt;/p&gt;

&lt;p&gt;Here's the awkward part, straight from Twilio's &lt;a href="https://www.twilio.com/docs/global-infrastructure/api-domain-migration-guide" rel="noopener noreferrer"&gt;API Domain Migration Guide&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;these domains process all requests in US1 regardless of the region code&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everyone who set &lt;code&gt;region: 'de1'&lt;/code&gt; thinking they were keeping data in Germany for residency reasons — they weren't. The request hit a domain that routed everything to US1 anyway. The region code in the hostname was cosmetic.&lt;/p&gt;

&lt;p&gt;This is incident #5 in our silent-breakage series (&lt;a href="https://dev.to/flarecanary/github-pushevent-commits-field-disappeared-nothing-changed-but-everything-broke-1ke6"&gt;GitHub PushEvent&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Stripe Basil&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;Shopify 2025-01&lt;/a&gt;, &lt;a href="https://dev.to/flarecanary"&gt;OpenAI Responses &lt;code&gt;input_text&lt;/code&gt;&lt;/a&gt;). The pattern keeps repeating and the fix keeps looking the same.&lt;/p&gt;

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

&lt;p&gt;If you call Twilio like this today:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;twilio.rest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;de1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# SDK resolves to api.de1.twilio.com
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;+49...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;+49...&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On April 28, that &lt;code&gt;api.de1.twilio.com&lt;/code&gt; DNS name stops resolving. The SDK bubbles up a connection error — &lt;code&gt;ConnectionError&lt;/code&gt; in Python, &lt;code&gt;ECONNREFUSED&lt;/code&gt; / &lt;code&gt;ENOTFOUND&lt;/code&gt; in Node, depending on how the resolver fails. Your retry logic retries. Your logs fill up. Nothing recovers on its own.&lt;/p&gt;

&lt;p&gt;Same thing for raw HTTP clients:&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="c"&gt;# This stops working on April 28, 2026&lt;/span&gt;
curl &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SID&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.de1.twilio.com/2010-04-01/Accounts/&lt;span class="nv"&gt;$SID&lt;/span&gt;/Messages.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The nastier failure mode isn't the SDK — it's your &lt;strong&gt;infrastructure&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Firewall rules that allow outbound &lt;code&gt;443&lt;/code&gt; only to &lt;code&gt;api.de1.twilio.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Security groups that whitelist the regional domain&lt;/li&gt;
&lt;li&gt;Load balancers with the domain hardcoded in health checks&lt;/li&gt;
&lt;li&gt;Proxies that route Twilio traffic through a specific egress&lt;/li&gt;
&lt;li&gt;Secret store entries that embed the full URL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;SDK configs are easy to grep for. Terraform modules and Helm charts referencing &lt;code&gt;api.de1.twilio.com&lt;/code&gt; in a &lt;code&gt;cidr_blocks&lt;/code&gt; or &lt;code&gt;allowed_hosts&lt;/code&gt; block — those are the ones that bite on April 28.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two fix paths
&lt;/h2&gt;

&lt;p&gt;Twilio gives you two options, and you need to pick based on what you were &lt;em&gt;actually&lt;/em&gt; doing, not what you &lt;em&gt;thought&lt;/em&gt; you were doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 1: You don't actually need regional processing
&lt;/h3&gt;

&lt;p&gt;This is the majority case, because — again — the regional domains weren't processing regionally anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code change:&lt;/strong&gt;&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="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;de1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account_sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auth_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Resolves to api.twilio.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Raw HTTP:&lt;/strong&gt;&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="c"&gt;# Before&lt;/span&gt;
curl https://api.de1.twilio.com/...

&lt;span class="c"&gt;# After&lt;/span&gt;
curl https://api.twilio.com/...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Firewall / security group:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;api.twilio.com&lt;/code&gt; (resolves to the US1 edge). Remove the regional hostname entries. If your infra does IP-based rules, you need Twilio's US1 IP list — do not attempt to resolve the domain and pin the IPs, Twilio's edge IPs rotate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Path 2: You actually need regional processing
&lt;/h3&gt;

&lt;p&gt;If you have a real regional processing requirement (data residency, latency, compliance), you need the new localized domains &lt;em&gt;plus&lt;/em&gt; the &lt;code&gt;edge&lt;/code&gt; parameter &lt;em&gt;plus&lt;/em&gt; region-scoped credentials:&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;regional_account_sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;regional_auth_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;edge&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dublin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ie1&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="c1"&gt;# Resolves to api.dublin.ie1.twilio.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things had to change, not one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hostname:&lt;/strong&gt; &lt;code&gt;api.dublin.ie1.twilio.com&lt;/code&gt; (new localized pattern)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SDK params:&lt;/strong&gt; both &lt;code&gt;edge&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt;, together&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credentials:&lt;/strong&gt; regional SID and token, not your global ones&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you only add the &lt;code&gt;edge&lt;/code&gt; parameter and keep the old global credentials, you'll get auth errors. If you keep only &lt;code&gt;region&lt;/code&gt; and drop &lt;code&gt;edge&lt;/code&gt;, the SDK may still resolve to the deprecated domain depending on your SDK version.&lt;/p&gt;

&lt;p&gt;Twilio's migration guide includes a warning: &lt;em&gt;"Older SDK versions may not properly support the &lt;code&gt;edge&lt;/code&gt; parameter or may have bugs related to regional routing."&lt;/em&gt; Update the SDK first. This is a second source of drift — you think you're migrated, but an old SDK silently rewrites your request back to the deprecated hostname.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your tests didn't catch it
&lt;/h2&gt;

&lt;p&gt;You have integration tests. They hit a Twilio sandbox. They pass. The April 28 deprecation still ships a broken prod.&lt;/p&gt;

&lt;p&gt;The reason is the same reason every other silent drift story ships a broken prod:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your tests exercise the API's response shape. They don't exercise the infrastructure around the API.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A test that checks &lt;code&gt;response.status == 'queued'&lt;/code&gt; will continue passing after April 28 — because your test environment probably uses &lt;code&gt;api.twilio.com&lt;/code&gt; already, or it mocks Twilio entirely. The failure mode is DNS resolution against a specific hostname buried in your prod firewall rule. No unit test hits that path.&lt;/p&gt;

&lt;p&gt;The same story played out with the &lt;a href="https://dev.to/flarecanary"&gt;OpenAI &lt;code&gt;input_text&lt;/code&gt; removal&lt;/a&gt;: tests used &lt;code&gt;output_text&lt;/code&gt;, the change was in the &lt;em&gt;assistant role message format&lt;/em&gt;, and it only failed on request construction, not response parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern across APIs
&lt;/h2&gt;

&lt;p&gt;Five incidents, same shape:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;th&gt;Where tests missed it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;GitHub PushEvent&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;commits&lt;/code&gt; field silently dropped&lt;/td&gt;
&lt;td&gt;Tests didn't assert field presence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Stripe Basil&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;current_period_end&lt;/code&gt; moved to &lt;code&gt;items&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Tests used Checkout fixtures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Shopify 2025-01&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;fulfillmentHold&lt;/code&gt; type change&lt;/td&gt;
&lt;td&gt;Tests mocked the response&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;OpenAI Responses&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;input_text&lt;/code&gt; removed for assistants&lt;/td&gt;
&lt;td&gt;Tests covered request role=user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Twilio regional&lt;/td&gt;
&lt;td&gt;Regional domains stop resolving&lt;/td&gt;
&lt;td&gt;Tests don't hit prod DNS paths&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The consistent pattern: the breaking change lives in a layer your test suite isn't watching. Request construction, infrastructure config, DNS, header shape, optional field presence. Response-shape assertions catch ~20% of drift cases at best.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to not be surprised by the next one
&lt;/h2&gt;

&lt;p&gt;Three things actually help:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subscribe to every vendor's changelog.&lt;/strong&gt; Not just the ones you think you use — the ones the SDKs in your &lt;code&gt;package.json&lt;/code&gt; use. Most companies announce breaking changes. You have to be reading.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Monitor the raw response, not just &lt;code&gt;status == 'queued'&lt;/code&gt;.&lt;/strong&gt; If Twilio's response drops a field next year, you want an alert, not a silent parse failure downstream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch the infrastructure layer.&lt;/strong&gt; Firewall rules, security groups, and Terraform modules should reference hostnames, not cached IP addresses — and those hostnames need to survive vendor deprecation windows.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; to do #2 — hit third-party APIs on a schedule, watch the response, page you when the shape changes. It's not magic; it's the poll-and-diff loop you would write yourself if you had the time. The APIs break whether you're watching or not.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;We track API drift incidents in real time. If your stack touches Twilio and you haven't audited your hostname config yet, April 28 is a hard date.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>twilio</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>claude-3-haiku-20240307 just started returning errors — here's what happened</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Wed, 22 Apr 2026 13:18:11 +0000</pubDate>
      <link>https://forem.com/flarecanary/claude-3-haiku-20240307-just-started-returning-errors-heres-what-happened-57he</link>
      <guid>https://forem.com/flarecanary/claude-3-haiku-20240307-just-started-returning-errors-heres-what-happened-57he</guid>
      <description>&lt;p&gt;If your code hits &lt;code&gt;claude-3-haiku-20240307&lt;/code&gt; and started failing yesterday, you're not alone. Anthropic retired Claude Haiku 3 on April 20, 2026. Retired, not deprecated — requests to the old model ID no longer complete. They return an error.&lt;/p&gt;

&lt;p&gt;This is a real deprecation cutoff that was announced 60 days ago and landed without much fanfare. If you missed the email, you're finding out now — probably from a production alert, or worse, from a user telling you the product is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failure looks like
&lt;/h2&gt;

&lt;p&gt;Anthropic's deprecation policy is explicit: &lt;em&gt;retired models return errors, not redirects&lt;/em&gt;. There is no automatic fallback to Haiku 4.5. The model ID just stops resolving.&lt;/p&gt;

&lt;p&gt;In practice that means a 4XX response from the Messages API — &lt;code&gt;not_found_error&lt;/code&gt; if the router decides the model identifier is gone, or &lt;code&gt;invalid_request_error&lt;/code&gt; if it's treated as a malformed request. Either way, the body looks roughly like this:&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&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;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"not_found_error"&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 resource could not be found."&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;"request_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;"req_..."&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;What makes this nasty is that nothing about your code changed. The request body is identical to what worked last week. The SDK version is the same. Your API key is the same. The only thing that changed is whether Anthropic's routing layer still recognizes &lt;code&gt;claude-3-haiku-20240307&lt;/code&gt; — and as of April 20, it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the stale model ID is probably hiding
&lt;/h2&gt;

&lt;p&gt;Most teams have more than one reference to a model name. Places to look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Environment variables and config files&lt;/strong&gt; — &lt;code&gt;ANTHROPIC_MODEL&lt;/code&gt;, &lt;code&gt;CLAUDE_MODEL&lt;/code&gt;, &lt;code&gt;DEFAULT_MODEL&lt;/code&gt;. Check staging and prod, not just the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework wrappers&lt;/strong&gt; — LangChain, LlamaIndex, Vercel AI SDK, LiteLLM. Older versions may have &lt;code&gt;claude-3-haiku-20240307&lt;/code&gt; as a default or in documented examples. Check what your package pins to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded test fixtures&lt;/strong&gt; — VCR cassettes, snapshot tests, and mocked responses often capture the model string.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-grained routing logic&lt;/strong&gt; — anywhere you pick a model based on task type ("use the cheap one for classification"), check which model the "cheap" branch resolves to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs and monitoring dashboards&lt;/strong&gt; — not a code issue, but if you filter traces by model ID, those filters break silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The five-minute grep:&lt;br&gt;
&lt;/p&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;-n&lt;/span&gt; &lt;span class="s2"&gt;"claude-3-haiku-20240307"&lt;/span&gt;
git &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"claude-3-haiku"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If either returns hits, you have work to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to replace it with
&lt;/h2&gt;

&lt;p&gt;Anthropic's recommended replacement is &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;. On paper it's a straight swap. In practice, Haiku 4.5 is a Claude 4-generation model, and the Claude 4 generation introduced a handful of breaking API changes that trip up code written for Haiku 3:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;top_p&lt;/code&gt; can no longer both be set.&lt;/strong&gt; Haiku 3 accepted both. Haiku 4.5 rejects the combination. If your code sets both defensively, remove one before switching.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New stop reasons.&lt;/strong&gt; Claude 4 adds stop reasons that Haiku 3 didn't emit. If you have a &lt;code&gt;switch&lt;/code&gt; or &lt;code&gt;match&lt;/code&gt; on &lt;code&gt;stop_reason&lt;/code&gt; that throws on unknown values, it will start throwing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trailing newlines in tool parameters are preserved.&lt;/strong&gt; Previously stripped. If your downstream logic trims on output, fine; if it does exact-match validation, you may get spurious mismatches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limits are tracked separately per model.&lt;/strong&gt; Your old Haiku 3 quota doesn't transfer. If you were running near the ceiling, plan for headroom at the new model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing.&lt;/strong&gt; Haiku 4.5 is $1.00 input / $5.00 output per million tokens, up from $0.25 / $1.25. That's a 4× increase. Cost-sensitive workloads need to re-forecast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration isn't a one-line change, and pretending it is is how bugs ship.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern this fits
&lt;/h2&gt;

&lt;p&gt;Anthropic's deprecation cadence is now predictable enough to plan against. The recent timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;October 28, 2025&lt;/strong&gt; — Claude Sonnet 3.5 retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;January 5, 2026&lt;/strong&gt; — Claude Opus 3 retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;February 19, 2026&lt;/strong&gt; — Claude 3.5 Haiku and Claude Sonnet 3.7 retired&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;April 20, 2026&lt;/strong&gt; — Claude Haiku 3 retired (yesterday)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;June 15, 2026&lt;/strong&gt; — Claude Opus 4 and Claude Sonnet 4 retire (announced April 14, 2026)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your code pins a Claude 4 model ID today, that pin has a two-month clock on it. The 2024- and early-2025- generation model IDs are all on a one-year glidepath to retirement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring model deprecation as an API change
&lt;/h2&gt;

&lt;p&gt;What's happening here is a narrow case of a broader problem: third-party APIs change between your deploys, and the change is invisible until something breaks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Anthropic deprecation page updates.&lt;/li&gt;
&lt;li&gt;The model ID in &lt;code&gt;GET /v1/models&lt;/code&gt; disappears.&lt;/li&gt;
&lt;li&gt;The error response from &lt;code&gt;POST /v1/messages&lt;/code&gt; changes shape for the old ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of those events trigger your CI. None of them change your code. All of them change whether your code works.&lt;/p&gt;

&lt;p&gt;The way to catch this before your users do is to treat the provider's API surface — the models list, the error shapes, the documented schemas — as a contract you continuously verify. Poll, diff against a known baseline, alert on breaking diffs. Same pattern as monitoring any other third-party API, applied to the specific surface each vendor exposes.&lt;/p&gt;

&lt;p&gt;For Anthropic specifically: &lt;code&gt;GET https://api.anthropic.com/v1/models&lt;/code&gt; returns the currently-available model IDs. If &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt; disappears from that list, you want to know before your next deploy, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum-viable fix for today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;git grep claude-3-haiku-20240307&lt;/code&gt; across every repo that talks to Anthropic&lt;/li&gt;
&lt;li&gt;Replace with &lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remove any call site that sets both &lt;code&gt;temperature&lt;/code&gt; and &lt;code&gt;top_p&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Re-run integration tests against the new model — not just unit tests with mocked responses&lt;/li&gt;
&lt;li&gt;Forecast cost impact of the 4× price change before rolling to production&lt;/li&gt;
&lt;li&gt;Check that your rate-limit dashboards pull the new model's quotas, not the retired one's&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then, if this is the second or third time a provider API change has broken your build without warning, consider whether catching these earlier is worth instrumenting.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors REST APIs and MCP servers for schema drift — including model availability endpoints from AI providers. Free tier covers 5 endpoints with daily checks.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>anthropic</category>
      <category>claude</category>
      <category>api</category>
    </item>
    <item>
      <title>CI Tests Won't Save You from MCP Schema Drift</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Tue, 21 Apr 2026 04:01:14 +0000</pubDate>
      <link>https://forem.com/flarecanary/ci-tests-wont-save-you-from-mcp-schema-drift-2o01</link>
      <guid>https://forem.com/flarecanary/ci-tests-wont-save-you-from-mcp-schema-drift-2o01</guid>
      <description>&lt;p&gt;There's a growing category of tools that validate MCP server schemas in CI/CD pipelines. Run them on pull requests, catch schema mismatches before you deploy.&lt;/p&gt;

&lt;p&gt;This is genuinely useful. But it solves the wrong half of the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP drift problem has two halves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Half 1: Your code drifts from the server.&lt;/strong&gt; You change your agent code, but the MCP server's tool schemas haven't changed. CI testing catches this — run tests, verify your code still matches the tool definitions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Half 2: The server drifts from your code.&lt;/strong&gt; The MCP server updates its tool schemas, but you haven't deployed anything. CI doesn't run because &lt;em&gt;you&lt;/em&gt; didn't change anything. Your agent keeps calling tools with the old parameter names, and the LLM silently adapts (or silently fails).&lt;/p&gt;

&lt;p&gt;Half 2 is the dangerous one. And CI can't catch it by definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why LLMs make this worse
&lt;/h2&gt;

&lt;p&gt;When a REST API changes, your code throws an error. A missing field causes a &lt;code&gt;TypeError&lt;/code&gt;. A renamed endpoint returns a &lt;code&gt;404&lt;/code&gt;. The failure is loud.&lt;/p&gt;

&lt;p&gt;When an MCP tool schema changes, the LLM doesn't crash. It &lt;em&gt;adapts&lt;/em&gt;. If a parameter gets renamed from &lt;code&gt;search_query&lt;/code&gt; to &lt;code&gt;query_text&lt;/code&gt;, the LLM might:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass the old parameter name and get an empty result&lt;/li&gt;
&lt;li&gt;Interpret the empty result as "no data found" instead of "wrong parameter"&lt;/li&gt;
&lt;li&gt;Tell the user "I couldn't find any matching documents" — a plausible lie&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The agent looks healthy. No errors in your logs. No alerts from your monitoring. The user gets a wrong answer and has no way to know.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI testing actually catches
&lt;/h2&gt;

&lt;p&gt;CI-based MCP schema validation is good at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema-implementation mismatches&lt;/strong&gt;: The tool says a parameter is optional, but the server actually requires it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression testing&lt;/strong&gt;: After &lt;em&gt;you&lt;/em&gt; change something, verify it still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type validation&lt;/strong&gt;: Ensure your inputs match declared types.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are real problems worth solving. If you're building MCP servers, you should absolutely have CI tests for your tool schemas.&lt;/p&gt;

&lt;h2&gt;
  
  
  What CI testing misses
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Third-party MCP server changes&lt;/strong&gt;: You don't run CI for someone else's server. When Stripe's MCP server renames a tool parameter, your pipeline doesn't trigger.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Between-deploy drift&lt;/strong&gt;: The MCP server you depend on ships a breaking change on Saturday night. Your agent is broken from Saturday to Monday morning when someone finally notices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gradual schema evolution&lt;/strong&gt;: A tool starts accepting a new optional parameter. Two weeks later, the old parameter gets deprecated. A month later, it's removed. CI only sees one snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Runtime behavior changes&lt;/strong&gt;: The schema says a field is a &lt;code&gt;string&lt;/code&gt;. It was always a URL. Now it's a UUID. The type didn't change, but your agent's downstream logic breaks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The monitoring gap
&lt;/h2&gt;

&lt;p&gt;Most teams have this setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI/CD: Schema tests on deploy ✓
Staging: Smoke tests ✓
Production: Uptime checks ✓ (is the server responding?)
Drift monitoring: ??? (is the server responding *correctly*?)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gap is in the last line. Your uptime check confirms the MCP server returns &lt;code&gt;200 OK&lt;/code&gt;. It doesn't check whether &lt;code&gt;tools/list&lt;/code&gt; returns the same tool definitions it returned last week.&lt;/p&gt;

&lt;h2&gt;
  
  
  What continuous MCP monitoring looks like
&lt;/h2&gt;

&lt;p&gt;Instead of (or in addition to) CI-time validation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Poll &lt;code&gt;tools/list&lt;/code&gt; on a schedule&lt;/strong&gt; — hourly, daily, whatever fits your risk tolerance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diff the tool schemas&lt;/strong&gt; against a known baseline — parameter names, types, required flags, descriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify changes by severity&lt;/strong&gt; — new optional parameter = informational. Renamed required parameter = breaking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on breaking changes&lt;/strong&gt; — Slack, email, webhook, whatever gets to the right person.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintain a timeline&lt;/strong&gt; — know &lt;em&gt;when&lt;/em&gt; the change happened, not just that it happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This catches the Saturday-night breaking change before Monday morning. It catches the gradual deprecation cycle. It catches the third-party server update that your CI pipeline will never see.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI and continuous monitoring are complementary
&lt;/h2&gt;

&lt;p&gt;This isn't an either/or choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CI testing&lt;/strong&gt; validates that &lt;em&gt;your code&lt;/em&gt; works with the current MCP server schemas at deploy time. It's a pre-deployment gate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuous monitoring&lt;/strong&gt; validates that &lt;em&gt;the MCP server schemas&lt;/em&gt; haven't changed since your last deploy. It's a post-deployment safety net.&lt;/p&gt;

&lt;p&gt;If you only have CI tests, you're assuming the world doesn't change between your deploys. In a world where MCP servers are updated independently by different teams (or different companies), that assumption is broken.&lt;/p&gt;

&lt;p&gt;The minimum viable setup:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CI schema validation for MCP servers you build&lt;/li&gt;
&lt;li&gt;Continuous monitoring for MCP servers you depend on&lt;/li&gt;
&lt;li&gt;Severity-based alerting so you're not drowning in noise&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first catches your mistakes. The second catches everyone else's.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors REST APIs and MCP servers for schema drift. Free tier covers 5 endpoints with daily checks — enough to monitor your most critical MCP dependencies.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>monitoring</category>
      <category>devops</category>
    </item>
    <item>
      <title>The Complete Guide to API Schema Drift Monitoring in 2026</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Mon, 20 Apr 2026 04:02:52 +0000</pubDate>
      <link>https://forem.com/flarecanary/the-complete-guide-to-api-schema-drift-monitoring-in-2026-259b</link>
      <guid>https://forem.com/flarecanary/the-complete-guide-to-api-schema-drift-monitoring-in-2026-259b</guid>
      <description>&lt;p&gt;You've written your integration. The tests pass. The API docs say the response looks like this:&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;"user"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;123&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&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;Six weeks later, it looks like this:&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;"user"&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;"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;"usr_123"&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;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email_verified"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;id&lt;/code&gt; changed from number to string. A new field appeared. Your code didn't crash — it just started silently mishandling data. Welcome to &lt;strong&gt;API schema drift&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Is API Schema Drift?
&lt;/h2&gt;

&lt;p&gt;Schema drift is when an API's actual response structure diverges from what consumers expect — whether that expectation comes from an OpenAPI spec, documentation, or just "what it returned last week."&lt;/p&gt;

&lt;p&gt;It's not the same as downtime. Drift is subtler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A field changes type (&lt;code&gt;integer&lt;/code&gt; → &lt;code&gt;string&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A required field becomes nullable&lt;/li&gt;
&lt;li&gt;An enum gains new values your switch statement doesn't handle&lt;/li&gt;
&lt;li&gt;A nested object gains or loses properties&lt;/li&gt;
&lt;li&gt;A field gets renamed or removed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to KushoAI's 2026 "State of Agentic API Testing" report, &lt;strong&gt;41% of APIs experience schema drift within 30 days&lt;/strong&gt; and &lt;strong&gt;63% within 90 days&lt;/strong&gt;. Schema/validation failures account for 22% of all API failures — second only to authentication issues.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Schema Drift Matters More in 2026
&lt;/h2&gt;

&lt;p&gt;Three trends are making drift monitoring essential:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. External API Dependencies Are Everywhere
&lt;/h3&gt;

&lt;p&gt;Modern applications depend on dozens of third-party APIs. You don't control their release cycles, and they rarely notify you of non-breaking changes. But "non-breaking" by their definition may be breaking for your code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI Agents Depend on Tool Schemas
&lt;/h3&gt;

&lt;p&gt;The explosion of MCP (Model Context Protocol) servers means AI agents discover and call tools based on schema definitions. If a tool's input schema changes silently, the LLM doesn't throw an error — it adapts to the wrong contract. This is a new class of drift that didn't exist two years ago.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Microservice Sprawl
&lt;/h3&gt;

&lt;p&gt;Even internal APIs drift. Team A ships a change, Team B's consumers don't update. With 50+ internal services, manual tracking is impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Approaches to Drift Detection
&lt;/h2&gt;

&lt;p&gt;There are fundamentally different ways to catch drift, and they solve different problems:&lt;/p&gt;

&lt;h3&gt;
  
  
  Spec-to-Spec Diffing (CI-Time)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Compare two versions of an OpenAPI/Swagger spec file.&lt;br&gt;
&lt;strong&gt;When it catches drift:&lt;/strong&gt; Before deploy, in CI/CD pipelines.&lt;br&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Only works if specs exist and are kept up-to-date. Doesn't catch cases where the spec is wrong or the server deviates from the spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;oasdiff&lt;/strong&gt; (open source, 1K+ GitHub stars) — CLI and GitHub Action. 300+ breaking change rules. The standard for spec-to-spec diffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bump.sh&lt;/strong&gt; — Documentation platform with built-in changelog generation from spec diffs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optic&lt;/strong&gt; — Was the leader here. Acquired by Atlassian (2024), repository archived January 2026. No standalone product remains.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Spec-to-Reality Monitoring (Runtime)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Make actual HTTP requests to live endpoints and compare responses against an OpenAPI spec or learned baseline.&lt;br&gt;
&lt;strong&gt;When it catches drift:&lt;/strong&gt; After deploy, continuously in staging or production.&lt;br&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Requires network access to the endpoints. May not catch drift in rarely-used response variants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FlareCanary&lt;/strong&gt; ($0-49/mo) — SaaS platform for live drift monitoring. Compares responses against OpenAPI specs or inferred baselines. Severity classification (breaking/warning/info), multi-sample learning to reduce false positives. Also monitors MCP server tool schemas. Free tier: 5 endpoints, daily checks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Drift Alert&lt;/strong&gt; ($149-749/mo) — Dedicated drift monitoring. Severity-aware routing, PagerDuty integration. Enterprise-focused pricing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rumbliq&lt;/strong&gt; ($0-69/mo) — General monitoring platform with JSON response diffing as one feature. 25 free monitors. Broader but shallower on drift specifically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;watchflow.io&lt;/strong&gt; (beta, free until May 2026) — API uptime + schema monitoring. Pricing TBD.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Traffic-Based Detection (Passive)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Analyze actual API traffic patterns to detect deviations.&lt;br&gt;
&lt;strong&gt;When it catches drift:&lt;/strong&gt; Continuously, but only for APIs with sufficient traffic.&lt;br&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Requires deploying agents or SDKs into your infrastructure. Only monitors APIs you own (not third-party dependencies).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Treblle&lt;/strong&gt; ($0-300/mo) — API intelligence platform. SDK-based, monitors your own APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tusk Drift&lt;/strong&gt; (open source) — Records production traffic into test suites. Catches deviations between recorded and new responses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Levo.ai&lt;/strong&gt; ($2,500+/mo) — Enterprise API security platform with eBPF-based traffic analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  MCP/AI Tool Schema Monitoring
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What:&lt;/strong&gt; Track changes in MCP server tool catalogs and input/output schemas.&lt;br&gt;
&lt;strong&gt;When it catches drift:&lt;/strong&gt; Continuously, as MCP servers update their tool definitions.&lt;br&gt;
&lt;strong&gt;Limitation:&lt;/strong&gt; Emerging category. Limited tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;FlareCanary&lt;/strong&gt; — Consumer-side MCP monitoring (monitors third-party MCP servers you depend on).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bellwether&lt;/strong&gt; (open source) — CI-time MCP schema snapshots and diffing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specmatic&lt;/strong&gt; — Commercial MCP schema testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentry/Grafana/AWS CloudWatch&lt;/strong&gt; — Server-side MCP observability (requires owning the MCP server).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Choosing the Right Approach
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Best approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You maintain OpenAPI specs in version control&lt;/td&gt;
&lt;td&gt;Spec-to-spec diffing (oasdiff) in CI + live monitoring as a safety net&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You consume third-party APIs you don't control&lt;/td&gt;
&lt;td&gt;Spec-to-reality monitoring (FlareCanary, API Drift Alert)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want to monitor your own APIs' behavior&lt;/td&gt;
&lt;td&gt;Traffic-based detection (Treblle) or spec-to-reality monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your AI agents depend on MCP tool servers&lt;/td&gt;
&lt;td&gt;MCP schema monitoring (FlareCanary for consumer-side, Bellwether for CI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You have no OpenAPI specs at all&lt;/td&gt;
&lt;td&gt;Baseline-learning tools that infer schema from responses (FlareCanary, Rumbliq)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Monitoring Stack
&lt;/h2&gt;

&lt;p&gt;For thorough coverage, combine approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: oasdiff or similar catches spec changes before merge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-deploy&lt;/strong&gt;: FlareCanary or API Drift Alert catches spec-to-reality divergence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting&lt;/strong&gt;: Route breaking changes to PagerDuty/Slack, warnings to email&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This layered approach catches drift at every stage — spec authoring, deployment, and runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you've never monitored for schema drift:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory your external API dependencies.&lt;/strong&gt; List every third-party API your application calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identify the critical ones.&lt;/strong&gt; Which APIs, if they changed silently, would cause the worst customer impact?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start monitoring those.&lt;/strong&gt; Even daily checks on 3-5 critical endpoints will catch most drift before it causes production incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up alerts.&lt;/strong&gt; Breaking changes → immediate notification. Warnings → daily digest.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most drift monitoring tools offer free tiers. Start there, prove the value, then expand coverage.&lt;/p&gt;

</description>
      <category>api</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>API Schema Drift Detection Tools Compared (2026)</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Fri, 17 Apr 2026 04:01:50 +0000</pubDate>
      <link>https://forem.com/flarecanary/api-schema-drift-detection-tools-compared-2026-1ib4</link>
      <guid>https://forem.com/flarecanary/api-schema-drift-detection-tools-compared-2026-1ib4</guid>
      <description>&lt;p&gt;The "API schema drift" problem has finally gotten enough attention that multiple tools exist to solve it. But they solve it in very different ways, at very different price points, with very different assumptions about your workflow.&lt;/p&gt;

&lt;p&gt;This is a practical comparison of every tool I've found that handles schema drift detection as of April 2026. I'll be upfront: I built &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt;, so I'm biased — but I'll try to be honest about where each tool is strongest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The approaches
&lt;/h2&gt;

&lt;p&gt;There are four fundamentally different approaches to catching API schema drift:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Spec-to-spec diffing&lt;/strong&gt;: Compare two versions of an OpenAPI spec. Catches changes between spec versions. Requires specs to exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spec-to-reality monitoring&lt;/strong&gt;: Make live API requests and compare against your OpenAPI spec. Catches when reality diverges from documentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reality-to-reality monitoring&lt;/strong&gt;: Make live API requests and compare against a learned baseline. No spec required.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic-based detection&lt;/strong&gt;: Observe real API traffic (via proxy or SDK) and detect anomalies. Requires infrastructure changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most confusion in this space comes from people comparing tools across different approaches. An oasdiff comparison with an API Drift Alert comparison is apples to oranges.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spec-to-spec diffing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  oasdiff
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Compares two OpenAPI spec files and reports breaking changes, deprecations, and additions. 300+ change detection rules.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: CI pipelines. Run it as a GitHub Action to flag breaking changes in PRs before they merge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free and open source. 1M+ downloads, 1,100+ GitHub stars.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Requires two spec files. Cannot tell you if your live API matches your spec. If the spec is wrong (or doesn't exist), oasdiff can't help.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: The gold standard for spec diffing. If you maintain OpenAPI specs and want CI guardrails, start here. But it doesn't solve the "third-party API changed on me" problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  PactFlow Drift (NEW — March 2026)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Generates test suites from OpenAPI specs using AI, then runs them in CI to verify implementations conform to the spec.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams already using PactFlow for contract testing who want to add spec conformance checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Not yet published.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: CI-only — runs at deploy time, not continuously. Requires OpenAPI specs. Doesn't monitor external APIs you consume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Strong brand (PactFlow is well-known in contract testing). If you're in their ecosystem, it's a natural addition. They've also launched a PactFlow MCP Server for AI coding agent integration. But Drift itself is a testing tool, not a monitoring tool — it won't alert you when a third-party API changes between your deploys.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bump.sh
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: API documentation platform with built-in changelog tracking. Upload or push spec files and it shows what changed between versions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: $50/mo (Basic, 10 docs) to $250/mo (Pro, 30 docs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Documentation-first. Requires spec files to be pushed/uploaded. Cannot make live API calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Great for API providers who want to communicate changes to consumers. Not helpful for consumers trying to detect changes providers didn't document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Continuous monitoring (no spec required)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  FlareCanary
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Polls live API endpoints on a schedule and compares responses against either a learned baseline or an OpenAPI spec. Classifies drift by severity (breaking/warning/informational). Also monitors MCP server tool schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams consuming third-party APIs they don't control, or anyone who wants continuous monitoring without maintaining perfect specs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (5 endpoints, daily checks) → $19/mo (25 endpoints, hourly) → $49/mo (100 endpoints, 15-min).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: No CI/CD integration (yet). No self-hosted option. Polling-based, so detection latency depends on check interval.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure&lt;/strong&gt;: I built this. The multi-sample baseline learning (which reduces false positives from conditional fields) and severity classification are the main differentiators I'm most confident about.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Drift Alert
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Dedicated API drift monitoring platform. Detects schema changes in external APIs with severity-aware alert routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams with budget who need enterprise alert routing (PagerDuty integration, business-hours filtering for non-critical changes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: $149/mo (15 APIs, 12-hour checks) → $349/mo (40 APIs, hourly) → $749/mo (100 APIs, 15-min). No free tier — 7-day trial only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Expensive entry point. No free tier means you can't evaluate meaningfully in 7 days. Has severity-aware alert routing (critical changes trigger immediate alerts, non-critical wait for business hours), but limited public technical details about baseline learning methodology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: If your budget starts at $149/mo and you need PagerDuty integration, worth evaluating. For most small teams and individual developers, the pricing is a barrier.&lt;/p&gt;

&lt;h3&gt;
  
  
  APIShift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Live API monitoring with schema drift detection. Claims 5K+ APIs monitored, 50K+ daily checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that want frequent checks at a reasonable price point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (5 APIs, hourly) → $29/mo (50 APIs, 5-min) → $99/mo (unlimited, real-time).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Newer entrant. Recently added severity classification (LOW/MED/HIGH/CRITICAL), but less public information about baseline learning methodology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Competitive pricing, especially the $99/mo unlimited tier. The addition of severity classification brings it closer to feature parity with FlareCanary. If high-frequency checking is your priority, worth a look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rumbliq
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Full monitoring platform (uptime, SSL, DNS, cron, API schema) with drift detection as one feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that want a general-purpose monitoring dashboard with basic drift detection included.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (25 monitors) → $12/mo → $29/mo → $69/mo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Drift detection is simple JSON response diffing — no severity classification, no multi-sample baselines, no spec comparison. Jack of all trades.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: If you need uptime + SSL + basic schema diffing in one tool and price is the priority, Rumbliq's free tier is generous. If schema drift is your primary concern, you'll want something deeper.&lt;/p&gt;

&lt;h3&gt;
  
  
  DiffMon (NEW — March 2026)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Monitors HTML pages and JSON API endpoints for structural changes. Classifies changes as schema change, value change, or mixed. Path-level severity analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Teams that want to monitor both website content and API responses for changes in one tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (3 monitors, 24-hour checks, 3-day history) → $14/mo (20 monitors, 30-min) → $49/mo (75 monitors, 5-min, Smart Schema Validation).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Smart Schema Validation (the most useful drift detection feature) is locked behind the $49/mo Pro tier. No multi-sample baseline learning. No MCP monitoring. No OpenAPI spec comparison. The dual HTML+JSON positioning dilutes the API drift focus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Competitive pricing at the hobby tier ($14/mo for 20 monitors). But the meaningful drift detection features are Pro-only. If you specifically need API schema drift monitoring, the free tier's basic change detection is shallow compared to FlareCanary's severity classification at every tier.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Detective (pre-launch)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Compares live API responses against published documentation for 200-500 popular public APIs. Creates auditable drift records with Wayback Machine references.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Developers monitoring popular public APIs (GitHub, Stripe, Twilio) without setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (popular APIs, 24-hour checks) → Paid (TBD, custom APIs, 60-min checks). Still collecting signups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Not launched yet. Limited to pre-indexed APIs on free tier. No custom API monitoring without paid plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Interesting approach with the pre-configured popular API angle. Worth watching when it launches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traffic-based detection
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Treblle
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: API observability platform. SDK-based — instruments your own API to capture request/response data and detect anomalies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (1 API, 250K requests/mo) → $25-30/mo → $233-300/mo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Monitors your own APIs only (requires SDK installation). Cannot monitor third-party APIs you consume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Strong observability platform with major enterprise clients. But it solves a different problem — monitoring what you serve, not what you consume.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tusk Drift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Records production API traffic and replays it against new code to detect regressions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Requires SDK instrumentation in your infrastructure. Testing tool, not monitoring tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP-specific tools
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Bellwether
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Snapshots MCP server tool schemas and diffs against baseline in CI/CD. Catches when AI agent tool definitions change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: CI-only. Doesn't do continuous monitoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  DriftCop (NEW — April 2026)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Open-source MCP security scanner that diffs MCP server manifests against signed golden baselines using SigStore. Detects tool schema drift, injection attacks, rug-pulls, and typosquats.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for&lt;/strong&gt;: Security teams auditing MCP server supply chains for tampering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: Security-focused, not general schema change monitoring. Designed for "was this MCP server tampered with?" not "did this API's schema evolve?" No continuous monitoring — it's a scanning tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Interesting from a security perspective. Validates that MCP tool schema drift is increasingly viewed as a security vector, not just a reliability concern. Complementary to monitoring tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Specmatic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;What it does&lt;/strong&gt;: Contract-driven testing platform (OpenAPI, gRPC, GraphQL, AsyncAPI) with a recently launched MCP Auto-Test feature. Runs as a Docker container against MCP server endpoints, auto-generates test cases from declared schemas. Now actively marketing as "the first MCP schema drift detector" with aggressive content positioning ("MCP Servers Are Lying About Their Schemas"). Also launched an MCP server for AI coding agents and has a commercial enterprise module.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing&lt;/strong&gt;: Free (open source, MIT) → $10-50/user/mo (Enterprise: analytics, gRPC, GraphQL support).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: CI-only — point-in-time test execution, not continuous monitoring. No scheduled polling, no alerting between deployments, no drift history tracking. MCP is one feature of a much larger testing platform. Catches drift at deploy time, not when it happens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verdict&lt;/strong&gt;: Strong CI-time validation for MCP server schemas, backed by a mature testing platform (366 GitHub stars). But the "first MCP schema drift detector" claim only applies to CI — continuous monitoring between deployments catches the drift that matters most (the overnight schema change that breaks your agent before your next deploy).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: FlareCanary monitors MCP server schemas continuously (not just in CI), which is currently unique among SaaS tools. The distinction matters: CI tools catch drift when you deploy, continuous monitoring catches drift when &lt;em&gt;the other side&lt;/em&gt; changes — which is exactly the scenario that breaks production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comparison matrix
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Free tier&lt;/th&gt;
&lt;th&gt;Entry price&lt;/th&gt;
&lt;th&gt;Needs spec?&lt;/th&gt;
&lt;th&gt;Continuous?&lt;/th&gt;
&lt;th&gt;External APIs?&lt;/th&gt;
&lt;th&gt;Severity levels?&lt;/th&gt;
&lt;th&gt;MCP?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;oasdiff&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (OSS)&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (CI)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PactFlow Drift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;TBD&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (CI)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Unknown&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FlareCanary&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (5)&lt;/td&gt;
&lt;td&gt;$19/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Drift Alert&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;$149/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;APIShift&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (5)&lt;/td&gt;
&lt;td&gt;$29/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DiffMon&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (3)&lt;/td&gt;
&lt;td&gt;$14/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (Pro)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rumbliq&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (25)&lt;/td&gt;
&lt;td&gt;$12/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Detective&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes (planned)&lt;/td&gt;
&lt;td&gt;TBD&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Treblle&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;$25/mo&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (own only)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which should you use?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;You maintain OpenAPI specs and want CI guardrails&lt;/strong&gt; → Start with oasdiff (free). Add PactFlow Drift if you're in their ecosystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You consume third-party APIs and want continuous monitoring&lt;/strong&gt; → FlareCanary (free tier, no spec required), API Drift Alert (if budget allows $149+/mo), or APIShift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You want general monitoring with basic drift detection&lt;/strong&gt; → Rumbliq (generous free tier, broad feature set).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You build AI agents using MCP&lt;/strong&gt; → FlareCanary (only SaaS with continuous MCP monitoring) or Bellwether (CI-only, open source).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need enterprise API observability&lt;/strong&gt; → Treblle (own APIs) or Levo.ai ($2,500+/mo, includes AI security).&lt;/p&gt;

&lt;p&gt;The right answer for most teams is a combination: oasdiff in CI for your own API specs, plus a continuous monitoring tool for the third-party APIs you depend on.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full disclosure: I'm the builder of &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt;. I've tried to be fair to every tool listed here. If I got something wrong, leave a comment and I'll update.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>monitoring</category>
      <category>webdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>41% of APIs Drift Within 30 Days — What the Data Says About API Reliability</title>
      <dc:creator>FlareCanary</dc:creator>
      <pubDate>Wed, 15 Apr 2026 04:03:45 +0000</pubDate>
      <link>https://forem.com/flarecanary/41-of-apis-drift-within-30-days-what-the-data-says-about-api-reliability-bhi</link>
      <guid>https://forem.com/flarecanary/41-of-apis-drift-within-30-days-what-the-data-says-about-api-reliability-bhi</guid>
      <description>&lt;p&gt;Most developers assume the APIs they depend on are stable. The data says otherwise.&lt;/p&gt;

&lt;p&gt;KushoAI's &lt;em&gt;State of Agentic API Testing 2026&lt;/em&gt; report analyzed thousands of API integrations and found that &lt;strong&gt;41% of APIs experience schema drift within 30 days&lt;/strong&gt;. Within 90 days, that number climbs to &lt;strong&gt;63%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let that sink in. If you're integrating with five external APIs, statistically two of them will change shape in the next month. And unless you're actively watching for it, you won't know until something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What counts as "drift"?
&lt;/h2&gt;

&lt;p&gt;Schema drift is any change to what an API actually returns compared to what you expect. This isn't about downtime or 500 errors — those are loud and obvious. Drift is quieter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A field changes from &lt;code&gt;string&lt;/code&gt; to &lt;code&gt;string | null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An integer ID becomes a UUID string&lt;/li&gt;
&lt;li&gt;An enum gains a new value your switch statement doesn't handle&lt;/li&gt;
&lt;li&gt;A nested object gets flattened or restructured&lt;/li&gt;
&lt;li&gt;A field that was always present becomes conditional&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The KushoAI report found that &lt;strong&gt;field additions account for 86% of drift events&lt;/strong&gt;. Most providers consider new fields "non-breaking," but if your code uses strict deserialization, pattern matching, or type-checked interfaces, a new field can absolutely break things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure hierarchy
&lt;/h2&gt;

&lt;p&gt;Not all API failures are created equal. The report breaks down failure categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Frequency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Auth/authorization failures&lt;/td&gt;
&lt;td&gt;34%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema/validation errors&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rate limiting&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout/connectivity&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business logic errors&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Schema drift is the &lt;strong&gt;#2 failure category&lt;/strong&gt; after auth issues. And unlike auth failures (which are immediate and visible), schema drift is insidious. Your code might keep running with wrong data rather than failing cleanly.&lt;/p&gt;

&lt;p&gt;Consider a real scenario: a payment provider changes their webhook payload, moving &lt;code&gt;amount&lt;/code&gt; from an integer (cents) to a string ("19.99"). Your code parses it, JavaScript silently coerces the type, and suddenly you're processing transactions with incorrect amounts. No error. No alert. Just wrong numbers in your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why testing doesn't catch it
&lt;/h2&gt;

&lt;p&gt;The instinctive response is "our tests should catch this." But here's the problem: your tests mock the API response based on what it &lt;em&gt;used to&lt;/em&gt; return. When the real API changes, your mocks don't update — so your tests pass while production fails.&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;// Your test mock (written 3 months ago)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mockResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&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="c1"&gt;// What the API actually returns now&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;realResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;usr_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&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="c1"&gt;// Your tests pass. Your production code breaks on id type change.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contract testing tools like Pact catch drift at CI time — but only for APIs you control both sides of. For third-party APIs, you need something that checks the &lt;em&gt;live&lt;/em&gt; response.&lt;/p&gt;

&lt;h2&gt;
  
  
  The AI agent multiplier
&lt;/h2&gt;

&lt;p&gt;This problem is getting worse, not better. AI agents using MCP (Model Context Protocol) to discover and call tools face the same drift problem, but with an additional failure mode: &lt;strong&gt;silent adaptation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When an LLM encounters a changed tool schema, it doesn't throw an error. It adapts — generating parameters based on the new schema without understanding the semantic change. A renamed parameter or a restructured input might produce syntactically valid but semantically wrong requests.&lt;/p&gt;

&lt;p&gt;Nordic APIs' &lt;em&gt;API Reliability Report 2026&lt;/em&gt; found that AI API providers (OpenAI, Anthropic, Google) show the &lt;strong&gt;highest incident frequency&lt;/strong&gt; across 215+ services they track. The APIs powering the AI ecosystem are themselves among the most volatile.&lt;/p&gt;

&lt;h2&gt;
  
  
  What proactive monitoring looks like
&lt;/h2&gt;

&lt;p&gt;Detection is still overwhelmingly reactive. Most teams discover API drift through one of three signals:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Customer bug reports&lt;/strong&gt; — the worst way to learn&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI failures&lt;/strong&gt; — better, but only catches drift when you deploy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error monitoring spikes&lt;/strong&gt; — delayed signal that something already went wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Proactive monitoring means checking the API &lt;em&gt;before&lt;/em&gt; your code runs against it. The approach:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Establish a baseline&lt;/strong&gt;: Record what the API returns today — field names, types, structure, enums&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll regularly&lt;/strong&gt;: Make the same requests on a schedule (hourly, every 15 minutes, daily)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare and classify&lt;/strong&gt;: Diff the response against the baseline. Not every change matters equally:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Breaking&lt;/strong&gt;: Field removed, type changed, required field became null&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning&lt;/strong&gt;: New enum value, field became nullable, structure reorganized&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Informational&lt;/strong&gt;: New optional field added, metadata changed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert on what matters&lt;/strong&gt;: Notify for breaking changes immediately. Batch warnings for daily digest. Log informational changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the approach we built into &lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt;. Point it at an endpoint, and it learns the response schema from multiple samples (reducing false positives from conditional fields). When something drifts, you get severity-classified alerts explaining exactly what changed.&lt;/p&gt;

&lt;p&gt;No OpenAPI spec required — though if you have one, FlareCanary compares reality against the spec too.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimum viable monitoring setup
&lt;/h2&gt;

&lt;p&gt;If you're not ready for a dedicated tool, here's a starting point:&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="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Save a baseline&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.example.com/v1/users/me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'path(..) | map(tostring) | join(".")'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; baseline.txt

&lt;span class="c"&gt;# Later: compare against baseline&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.example.com/v1/users/me &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'path(..) | map(tostring) | join(".")'&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; current.txt

diff baseline.txt current.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches structural changes (new/removed fields) but misses type changes, nullability shifts, and enum expansions. It's a start, not a solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers don't lie
&lt;/h2&gt;

&lt;p&gt;41% drift rate in 30 days. Schema errors as the #2 failure category. 86% of drift events are field additions that providers consider "non-breaking."&lt;/p&gt;

&lt;p&gt;The gap between what API providers consider non-breaking and what actually breaks your code is where drift monitoring lives. If you're depending on external APIs — and in 2026, everyone is — the question isn't whether they'll change. It's whether you'll know before your users do.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://flarecanary.com" rel="noopener noreferrer"&gt;FlareCanary&lt;/a&gt; monitors your API endpoints for schema drift and alerts you when responses change. Free tier: 5 endpoints, daily checks, no credit card. Built by developers who got burned by silent API changes one too many times.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>api</category>
      <category>webdev</category>
      <category>programming</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
