Guides / Syncing data

Syncing data

If your product stores sports data, sync it into your own database: import a season once, then refresh only what can change.

Samples use the small get() helper from Build a live score app. db.upsert stands in for your database’s insert-or-update.

What to store

Store the entities your product shows, keyed by sport and the API’s id — so a football team and a basketball team with the same number never collide. How often each one changes decides how often you refresh it:

Entities to sync
EntityEndpointChanges
Leagues/football/leaguesRarely — mainly when a new season starts.
Teams/football/teamsRarely — names, codes and venues.
Players/football/playersTransfers and season statistics.
Fixtures/football/fixturesDates, status and scores — often on match days.
Standings/football/standingsAfter every result.

Initial import

Import one league-season at a time: reference data first, then fixtures. Every list is paginated, so walk every page — see Pagination.

async function* pages(path, params = {}) {
for (let page = 1; ; page++) {
const { data, meta } = await get(path, { ...params, page });
yield* data;
if (page * meta.per_page >= meta.total) return;
}
}
async function importSeason(leagueId, season) {
const filters = { league_id: leagueId, season };
// Reference data first, then fixtures that point at it
for await (const team of pages("/football/teams", filters)) {
await db.upsert("football_teams", team.id, team);
}
for await (const fixture of pages("/football/fixtures", filters)) {
await db.upsert("football_fixtures", fixture.id, fixture);
}
}
await importSeason(39, 2026);

Keeping data fresh

After the import, refresh each entity on its own schedule and only fetch what can have changed:

  • Fixtures — refresh today and the next few days on a schedule. While matches are in play, follow them with the live endpoint instead (see Handling live matches).
  • Standings — re-fetch a league’s table when one of its fixtures reaches FT.
  • Teams and players — refresh on a slow schedule, such as nightly, and whenever you meet an ID you don’t have yet.
  • Leagues — check for a new current_season around the start of each season.
// Run on a schedule: refresh fixtures for the next few days
async function refreshUpcoming(leagueId, days) {
for (let i = 0; i < days; i++) {
const date = new Date(Date.now() + i * 86400000).toISOString().slice(0, 10);
for await (const fixture of pages("/football/fixtures", { league_id: leagueId, date })) {
await db.upsert("football_fixtures", fixture.id, { ...fixture, synced_at: new Date() });
}
}
}

Handling changes

  • Upsert, never insert. Fixtures change after you first see them: kick-off times move, statuses and scores update.
  • Key by id, never by date and teams. A rescheduled fixture has a new date.
  • Update, don’t delete. Postponed (PST) and cancelled (CANC) fixtures are statuses — store them so your users see what happened.
  • Record when you synced. A synced_at column per row makes stale data easy to find.

Budgeting requests

Every page is one request, so you can estimate a sync before you schedule it. A 20-team league with 380 fixtures needs 1 page of teams and 8 pages of fixtures (380 ÷ 50): the initial import costs about 9 requests. Following live matches at the recommended 60-second interval costs one request a minute, however many fixtures are in play.

Compare that with your plan — the Free plan includes [X,XXX] requests / month — and see Rate Limits for the per-minute limit.