Switch sports data providers without breaking clients

SportSRC moved from one upstream provider to another between v2 and v3 while keeping the response shape identical. Here is the approach, the traps, and why the JSON contract is the thing you protect.

Updated 2026-10-23 9 min read

If you run an API that resells or enriches someone else's data, you will one day need to change the source underneath it. Pricing changes, coverage gaps appear, terms change, a provider becomes hostile.

SportSRC did exactly this between v2 and v3. The lessons below are from that migration, not from theory.

The rule: the response shape is the contract

Your users did not integrate with your data source. They integrated with your JSON. They have parsers bound to your field names, type definitions generated from your responses, UI components switching on your status strings, and caches keyed on your ids.

So when the source changes, the response must not. v3 keeps the v2 shape deliberately — same fields, same nesting, same vocabulary — and for most integrations the entire migration is one line:

- https://api.sportsrc.org/v2/
+ https://api.sportsrc.org/v3/

It is tempting to use the swap as a chance to design a cleaner shape. Resist it. A better shape that forces every client to rewrite is, from their side, indistinguishable from an outage you scheduled. Migrations that require work do not happen; they get postponed until the old version is switched off and then they become emergencies.

Where the work actually is: entity matching

The HTTP part of a provider swap is an afternoon. The hard part is that two providers do not agree on names.

The same fixture, from two sources:

Source ASource B
CFR ClujCFR 1907 Cluj
Al-Gharafa Sports ClubAl-Gharafa
Botafogo - RJBotafogo
Vasco da Gama Saf - RJVasco DA Gama

A naive comparison fails in both directions. It misses real pairs because the strings differ, and it invents pairs because a shortened name matches the wrong club. The second failure is far worse: a missed pair shows up as absent data, while a false pair shows your users a match that does not exist, with someone else's score attached.

Measured during this migration: a hand-written normaliser compared one source's fixtures to the other and reported 77% as unmatched. Spot-checking the list found several that were plainly present under a different spelling, and at least one that had matched the wrong opponent. The real figure was nothing like 77%.

Three things make this tractable:

  • Match on more than the name. Kick-off time narrows the candidate set enormously before any string comparison runs.
  • Score candidates, do not accept the first. A threshold you can tune beats a boolean you cannot.
  • Keep the pairing, not just the result. Storing which upstream id maps to which fixture lets you audit, correct and re-run. Recomputing from names every time means the same mistake every time.

The trap inside the trap

Candidate windows based on kick-off time assume the kick-off is correct. When a fixture is rescheduled, the stored time drifts away from the new one, the fixture falls outside its own candidate window, and it can never pair again — no matter how many times matching runs.

This failure is silent. Nothing errors; a fixture simply never gains its data. Re-read kick-off times rather than trusting the first value you stored.

Verify against live data, not fixtures

Unit tests with recorded payloads confirm your parser still parses. They say nothing about whether the new provider actually covers what the old one did.

Things found this way, and only this way:

  • A field documented as a string arriving as an object, so every record silently lost that value.
  • A provider returning a reasonable-looking fixture list that was missing an entire competition.
  • Two entries for the same fixture under different ids, with the team order reversed — a duplicate that only shows up when you compare kick-off times across a whole day's feed.

Run both providers for the same day and diff. Look hardest at what is present in one and absent in the other, since that is where coverage differences and matching failures both surface.

Cost changes shape, not just size

Providers bill differently, and the lever that saved money with one can be irrelevant with the next.

A concrete example: with the current provider, a request for an entire day's fixtures and a request for only the in-play ones cost exactly the same — one call each. Billing is per request, not per fixture. That inverts the intuition that asking for less is cheaper, and it means freshness decisions are about payload size and parsing cost, not quota.

Measure before you optimise. An optimisation carried over from the old provider's billing model can be pure complexity under the new one.

Do not trust your own config for limits

One bug from this migration is worth repeating because of how it failed. Daily usage was computed as:

used = configured_limit - remaining_reported_by_provider

When the plan was upgraded and the config was not edited, that produced a negative number. A negative ratio passes every budget threshold, so the guard meant to stop runaway usage silently stopped guarding. Had the plan been downgraded instead, the same line would have throttled everything to a halt.

Providers report their own limits on every response. Read the limit from upstream and treat local config as a first-boot guess only.

Keep the old version running

v2 still answers. Clients that have not moved are not broken, and nobody is forced to migrate on your schedule. The cost of keeping an old version alive is almost always lower than the cost of the support load and lost trust from switching it off.

Summary

  • The JSON shape is the contract. Change the source, not the shape.
  • Entity matching is the real work; names alone are not enough.
  • Rescheduled fixtures break time-window matching silently.
  • Verify against live data; recorded payloads hide coverage gaps.
  • Re-measure cost — the new provider may bill on a different axis.
  • Read plan limits from the provider, not from your own config.
  • Leave the old version running.

Frequently asked

Does SportSRC v3 break v2 integrations?

No. v3 keeps the v2 JSON contract, so in most cases the only change is the base URL. Field names, nesting and the status vocabulary are unchanged.

Why keep an old response shape instead of designing a better one?

Because the shape is the contract. Every client has parsers, type definitions and UI bound to it. A cleaner shape that forces a rewrite costs your users more than it gains them, and migrations that require work do not get done.

What is the hardest part of swapping a data provider?

Entity matching. Two providers name the same team differently, so a direct string comparison fails in both directions - missing real matches and creating false ones. That matching layer, not the HTTP work, is where the time goes.

How do you verify a provider swap went well?

Compare against live data rather than fixtures. Run both providers side by side, diff the output for the same day, and look specifically for matches present in one and absent in the other.