← All notes
API Development7 min

Pin the API version, then read the deprecation headers

Most of our API work is not designing a public surface for other people. It is consuming someone else's: Shopify orders into a print pipeline, Stripe into a payments ledger, Auth0 into a staff app. Those platforms version on a schedule we do not control. The failure mode we keep seeing is the same — a client that never pins a version, never looks at the response headers, and discovers the contract changed when a kitchen printer stops printing.

Unpinned is not flexible. It is undefined.

Shopify releases a stable Admin API every quarter, named by date — 2026-04, 2026-07, and so on. Each version is supported for about a year, with months of overlap so you can migrate without a cliff. If your request does not name a version, Shopify falls forward to whatever it still considers accessible. The response header X-Shopify-API-Version tells you which one actually ran.

That fall-forward behavior looks convenient in week one. It means a forgotten client keeps working after a version retires — until the shape you depended on is gone or silently different. We treat a missing version pin the same way we treat a missing idempotency key: the code runs, the contract is not the one you tested, and the bug shows up in production with no stack trace that points at the real cause.

On Al's Flowers the order path is short and physical. A Shopify webhook hits a Node service, we render a delivery or pickup ticket, PrintNode pushes it to a printer in the back of the shop. If Admin or webhook payload assumptions drift, the shop does not get a stylish error page. They get silence where a ticket should be. Pinning the API version in the client is the cheapest insurance on that path.

Deprecation headers are telemetry, not documentation

When a request touches something Shopify has marked deprecated, the response can include X-Shopify-API-Deprecated-Reason. As of the 2025-04 line that header started returning the actual fields involved — Shop.products, Shop.productVariants — instead of a generic docs URL. That turns a vague nudge into something you can log, count, and alert on.

We wire that the same way we wire 4xx rates. Capture the header on every Admin call. Attribute it to the route or job that made the request. Page when a new reason appears, not when the version finally disappears. By the time a field is removed you want the migration already merged, not discovered because tickets stopped rendering names correctly.

The same instinct applies outside Shopify. Stripe pins accounts to an API version. GitHub uses date headers. The specific mechanism differs; the operational habit does not. If the platform is willing to tell you that your request is living on borrowed time, that signal belongs in your logs next to the status code.

Version upgrades are a scheduled job, not a heroics weekend

A quarterly platform release is predictable enough to put on a calendar. We keep a short checklist for commerce integrations: bump the pinned version in a branch, run the webhook and Admin paths against a development store, compare payloads for the fields we actually read, and only then promote. Release notes matter, but so does a fixture of a real order from last month — the one with the odd shipping line the theme never shows.

Additive changes do not need drama. Optional fields, new endpoints, and expanded enums should not force a version bump in your own code. Breaking changes — renames, removals, meaning shifts — do. The discipline is knowing which of your reads are load-bearing. For Al's Flowers that list is short: order identity, line items, delivery versus pickup, customer contact, and whatever the ticket PDF must print without guessing.

We do not chase every new stable version on release day. We do not stay on a version with three months of life left either. Somewhere in the overlap window is a boring afternoon where the upgrade is a pull request, not an incident.

Your own APIs owe consumers the same clarity

When we do ship an API — a webhook out to a partner, a staff app against a Node service, a mobile client against Supabase edge functions — we steal the parts of this model that fit a small team. Name versions explicitly when a breaking change is possible. Prefer additive evolution for internal consumers you control. Put a deprecation signal somewhere a client can see it without reading a Slack thread from last spring.

URI versions (/v1/, /v2/) are usually enough for the surfaces we build. They show up in logs and curl sessions without special tooling. Date-based pinning like Stripe's is powerful and expensive; most of our clients do not need that machinery. What they need is a promise that we will not silently reshape a payload they already integrated against.

If you cannot say which clients are on which version, you do not have a versioning strategy. You have hope. Hope is how a flower shop finds out about an API change when the Saturday rush starts.

What we check before we call an integration done

During scoping we ask three questions about every third-party API in the build. Where is the version pinned, and is that pin in code rather than in a dashboard someone might click? How will we notice deprecation — headers, a health report, a changelog watch — before removal day? What is the smallest set of fields we actually depend on, so a version bump has a test list instead of a prayer?

Those answers belong in the same place as idempotency keys and retry policy. They are not polish. On a commerce ops pipeline they are the difference between a quiet Monday and a printer that has nothing to say because a field renamed itself three months ago and nobody was listening.

Pin the version. Read the headers. Schedule the bump. The interesting part of the integration is still your domain logic. The versioning hygiene is what keeps that logic talking to the platform you thought you shipped against.

Have something to build?

Tell us what you're working on and we'll tell you honestly whether we're the right fit.

Work with us