Field notes · 04
Your connector didn't break. The API under it was retired.
2026-07-10 · 6 دقيقة · Revenue Arc
One of our ad-platform connectors had never worked. Not flaky — never. Every call failed from the day the credentials went in, and the failure had exactly the shape of a credentials problem: authenticated request out, error back. So we did what everyone does. Re-checked the developer token. Re-ran the OAuth consent. Rotated the refresh token. Triple-checked the account id. Nothing — of course nothing. The credentials were fine. The API version pinned in our code had been retired, and this vendor doesn't just deprecate old versions. It deletes the URL path.
What comes back from a deleted path is a bare HTML 404. No JSON error envelope, no error code, no field naming what's wrong — the same page a typo'd URL would get. Every retry policy, every backoff, every credential rotation in the world cannot fix a URL that no longer exists. And nothing in the response says “version”.
SignalA 404 whose body is HTML — no JSON envelope at all — is not an auth failure and not a missing resource. A JSON 404 means “resource not found.” An HTML 404 from an API host means the versioned path itself is gone: the platform retired the version under your connector. Check the version before you touch a credential.
Reading the rings
Once we suspected the version, the diagnosis was one loop: the same cheap read against every version the platform ever shipped. The result is worth writing down, because it's the whole lifecycle of a walled-garden API in a single probe:
- v16–v19: bare HTML 404. The paths are deleted. These versions didn't just stop being supported — they stopped existing.
- v20: a real JSON error naming UNSUPPORTED_VERSION: “requests to this version will be blocked.” The courtesy zone — the platform still spends a response telling you what's wrong.
- v21–v24: live.
Live, then blocked with a helpful error, then gone. Ad APIs sunset roughly yearly, so a pinned version marches through those three states on a schedule — and the window where the platform explains itself is one version wide. Pin a version and forget it, and you're betting you'll touch that integration again before it exits the courtesy zone. We lost that bet.
SignalA pinned API version is config that rots on a schedule, like a certificate — it has an expiry date whether or not you wrote one down. The difference is that a certificate expires loudly, and a sunset API version expires as a 404 that impersonates a credentials problem.
The fix that was a deletion
Same afternoon, second trap. With a live version pinned, the connector got further — and hit USER_PERMISSION_DENIED. The folk wisdom for manager-account setups says: always send your manager id in the login-customer-id header. We had, dutifully. The reality is narrower: that header is a claim — “I'm operating this account through that manager” — and the platform checks the claim. Send it when the manager doesn't actually parent the target account and you get a permission error, even when your OAuth user can reach both accounts directly.
The fix was deleting a line of config, not adding one. That's worth sitting with, because the debugging reflex under a permission error is purely additive — more scopes, more grants, more headers. Sometimes the config isn't missing; it's asserting something false.
The detector that almost lied
Having been bitten, we built the obvious thing: connectors that self-diagnose version rot and say so in plain language. The first cut had a bug that never shipped — code review caught it — and it's the most instructive part of the story. The draft flagged any error containing “is deprecated” as version rot.
But walled gardens say “deprecated” all the time about things that aren't your version. “Field X is deprecated” arrives inside perfectly ordinary error envelopes — some platforms deliver those with HTTP 200. A naive detector reads one of those, swallows the real error, and confidently tells the operator to bump an API version that is actually live. A diagnostic that misfires is worse than no diagnostic: it spends your trust and the operator's afternoon at the same time.
Signal“Deprecated” only means version rot when the message names the version you sent. “v20 is deprecated” is a version problem. “Field X is deprecated” is a request problem wearing similar clothes — surface it untouched.
What we ship now
The checklist, free to steal. None of it is clever; all of it is the difference between a five-minute diagnosis and a week of credential archaeology.
- Version failures self-diagnose. When a connector hits a sunset version, the error names the platform, the dead version, and the exact config knob to turn — not “404”. The classifier is pure and unit-tested, because a misfiring diagnostic is worse than none.
- The active API version is in the UI, next to each connection. It's the value most likely to rot silently, so it doesn't get to hide in an env var nobody reads.
- A read-only smoke command exercises every credentialed connector's cheapest read. Run it on a schedule and version rot announces itself in the courtesy zone, while the platform is still returning JSON that explains the problem.
- Version pins live in one place. One module owns every platform's pin, and the UI, the wire, and the smoke command all read from it — so the version you display can never drift from the version you send.
None of this is a complaint about the platforms. Yearly sunsets are how an API serving millions of developers sheds old code, and the courtesy-zone errors are the vendor doing their part. The failure was ours: treating an integration as finished. An ad platform connector isn't a thing you build — it's a thing you keep, and it needs the same feeding schedule as everything else that expires.
شاهده أثناء العمل
يراقب ArcOps كل إشارة من هذه الإشارات عبر محفظتك بالكامل، ويقدّم توصيات تعتمدها — أو تفوّضها إلى سياسة. الإعداد يستغرق دقيقتين.
احصل على العدد التالي
تحليل واحد في كل مرة، إلى بريدك الوارد. يمكنك إلغاء الاشتراك في أي وقت.