Surviving API schema versioning when a vendor upgrades mid season
API schema versioning is the mechanism a vendor uses to change response shapes without breaking existing clients, usually through a version header or an account level default. Record the version each integration runs on, pin it explicitly, and diff a stored payload against a fresh one to catch shape changes early.
The exhibitor sync ran at 04:00 as usual and loaded 612 rows. No errors, no alerts, exit code zero. Two days later somebody notices that the company name column in the exhibitor universe report is blank for every row created since Tuesday, because the vendor changed company from a string to an object with a name inside it, and the loader dutifully wrote the object's string representation into a column that expected a name.
API schema versioning exists to stop exactly this, and the reason it fails in event stacks is rarely that the vendor lacked a versioning scheme. It is that nobody recorded which version each integration was running on, so the upgrade arrived as a surprise in the middle of a sales cycle.
The upgrade you did not schedule
Vendors ship changes on their calendar. You consume them on yours, and your calendar has a show in it.
The dangerous changes are the ones that do not error. A removed field returns null. A renamed field appears as a new key while the old one quietly stops being populated. A scalar becomes an object. An enum gains a value your case statement does not handle and falls through to the default. Every one of those passes a JSON parse, returns a 200, and lands in the warehouse looking like data.
GitHub's list of what counts as a breaking change is a useful checklist to hold vendors against, because it names the cases people argue about: removing operations or fields, renaming parameters, adding required parameters, changing parameter types, removing enum values, and changing authentication requirements (GitHub, 2026). Notice that adding an optional field is absent from that list, and correctly so, which means a vendor can add fields to your payload at any time without breaking their own policy. Your loader has to tolerate that.
What does pinning an API version actually buy you?
Time, and a named thing to test against.
The two published schemes worth copying work slightly differently. GitHub uses a dated header, X-GitHub-Api-Version, with values like 2022-11-28, and commits that when a new version ships the previous one is supported for at least 24 more months, after which requests for it return 410 Gone (GitHub, 2026). Requests with no header default to a fixed older version rather than the latest, which is a deliberate choice to keep unversioned clients working.
Stripe uses named major releases such as Acacia, with monthly releases in between that carry only backward compatible changes and reuse the major release name, so the current version reads as a date plus a name (Stripe, 2026). Requests use the account's default version unless the Stripe-Version header overrides it, and the default is controlled in the dashboard by whoever has access.
Both give you the same thing, which is a decision you can schedule. Twenty four months of support on an annual show is two full editions. That converts an abstract migration into a concrete one: upgrade after this year's post show reporting closes, test through the quiet quarter, and be on the new version before next year's sale opens. An integration with no pinned version has no such window, because the upgrade happens when the vendor decides.
The version your webhooks run on is a separate setting
This is the trap that costs a day of debugging, and it is worth stating on its own because the two surfaces look like one system.
Stripe's documentation says webhook events use the API version set during endpoint creation, and otherwise the account's default version (Stripe, 2026). So the payload arriving at your listener can be shaped by a version chosen eighteen months ago by whoever set the endpoint up, while your polling code, running through a current SDK, sees a different shape for the same object. Both are correct. They disagree because they are pinned to different points in time.
In an event stack this shows up as a registration that looks one way when the webhook fires and another way when the nightly reconciliation pull fetches it. If your matching logic compares the two, it will find differences that have nothing to do with the data. Check the version on every endpoint separately, write it down next to the endpoint URL, and treat a version mismatch between push and pull as a defect rather than a quirk.
What belongs in the version register
One table, one row per integration, and it takes an hour to build. For each connection: the vendor, the endpoint or product, the version string in force, where that version is set (header, SDK pin, account default), the date it was last confirmed, and the end of support date if the vendor publishes one.
The "where it is set" column is the one that earns its place. A version can be pinned in four different places for the same vendor, and they override each other in an order nobody remembers: the account default, a header on the request, the version baked into a strongly typed SDK release, and the version stored on a webhook endpoint. Stripe's own documentation walks through this per language because the answer differs by library. If your register says "latest" for any row, that row is unpinned and will move without asking.
Confirmed dates matter too. An account default that someone changed in the dashboard six months ago is indistinguishable from one nobody has touched, unless you have a date next to it and a habit of rechecking.
How do you detect a shape change before it reaches the warehouse?
Store a payload and diff against it. The mechanism is unglamorous and it catches the silent cases that schema validation misses.
Pick one stable object per integration, a registration record and an exhibitor record are the obvious choices, and save the full raw JSON response to a file the first time you fetch it. Then run a small job on a schedule that fetches the same object and compares the key structure against the stored copy. Compare keys and types, ignore values, because values change constantly and structure should not.
Work the arithmetic on what that costs. A registration payload in a typical event platform runs to somewhere between 40 and 80 fields once custom questions are included. Say 62. A nightly canary pulls one record, walks 62 keys, and reports any key that appeared, disappeared or changed type. That is one API call a night against a rate budget measured in thousands, and it turns a silent shape change into an alert the morning after the vendor deploys, instead of a blank column noticed two weeks later by a report consumer.
The diff wants to be structural, so a new optional field raises a notice and a removed field raises an alarm. Those are different severities and treating them the same trains people to ignore both. Where the changed field is one of your own custom registration questions rather than a vendor field, the fix belongs to a different discipline, and keeping custom field mappings stable between editions covers it properly.
What to do when the vendor does not version at all
Plenty of event platform APIs have no version header, no dated releases and no deprecation policy. The documentation page just changes.
You still have three moves. Snapshot the documentation, because a saved copy of the field list with a date on it is the only evidence you will have that the shape changed. Keep the raw payload for every load rather than only the parsed rows, so that when a column goes wrong you can replay history rather than argue about it. And make your loader tolerant in one direction only: accept unknown fields silently, and fail loudly on a missing or retyped known field. A loader that shrugs at both is how a blank company name column survives two days.
Ask for notice in writing at renewal. Even vendors with no formal versioning will usually commit to an email before a breaking release, and the ask is cheap enough that it rarely gets pushed back. Pair it with a request for the retrievable history window, which is a related question covered in finding the API history limits.
Where this stops
Version pinning protects the shape of the payload. It does nothing about the meaning behind it.
A vendor can keep every field name and type identical while changing what a field counts. If the platform starts including cancelled registrations in the same collection it previously excluded them from, your pinned schema validates perfectly and your registration total jumps. No diff catches that, because nothing in the structure moved. The only defence is a volume and distribution check on the loaded data, which is a different control from schema versioning and worth having alongside it.
The second limit is scheduling. Pinning buys you a migration window, and a window is only useful if somebody owns it. Every stack I have seen with a clean version register also had one person whose job included reading the vendor changelogs, and the register decayed within two quarters when that person moved on. Put the recheck on a calendar tied to the show cycle rather than to a person, and keep upgrades out of the period when changes are frozen for show week, which is the one time a schema surprise cannot be absorbed.
This week, pick your busiest integration and answer one question in writing: what version is it running on, and where is that set. If the answer takes more than ten minutes to establish, that is the first row of the register, and the integration stack has more like it.
Questions people ask about api schema versioning
- What is API schema versioning?
- It is the practice of labelling the shape of an API's requests and responses so that a change to that shape can be released without breaking clients written against the older one. GitHub uses a dated header value and supports each previous version for at least 24 months. Stripe names major releases and lets an account set a default that individual requests can override.
- What happens if you never send a version header?
- You inherit somebody else's decision. GitHub defaults requests with no version header to a fixed older version. Stripe defaults to whatever version the account is set to, which anyone with dashboard access can change. In both cases your integration's behaviour depends on a setting you are not looking at and did not record.
- Can webhook payloads be on a different version from your API calls?
- Yes, and this catches people out. Stripe states that webhook events use the API version set when the endpoint was created, otherwise the account default, so the shape arriving at your listener can differ from the shape your polling code receives. Check the version on every endpoint separately and record it with the integration.