Treat a result as revisable state
A finished status is useful for selecting results, but it does not make every stored field immutable. A score, kickoff or participant correction can arrive after your first export. If your application adds every received score to a season total, a repeated response or corrected result can count the same fixture twice.
Use the stable match ID as the identity of the observation and retain the latest validated state for that match. Compute aggregates from those current states or maintain a tested reverse-and-replace ledger. The key decision is replacement by identity, not unconditional addition whenever an HTTP request succeeds.
Choose a small replaceable contribution
The example stores one projected result per match ID and calculates total goals from the current snapshot. Replaying an identical record leaves the total unchanged. Updating the same match from 1-0 to 2-0 changes the total by one, and changing its status to postponed removes its contribution from finished-result totals.
This model is intentionally simple. A production database should wrap the state write and any derived-data invalidation in one transaction. If two workers may process the same match, serialize the update or use a version check so a slower older observation cannot overwrite a newer accepted one.
Run the reproducible example
This Python 3 example uses deliberately synthetic inputs, not a downloaded match or a forecast for a real fixture. Save it as example.py and run python3 example.py. It uses only the standard library and its assertions check the behavior discussed here. Replace the fixture inputs with validated API records when connecting your application; demo identifiers are not production lookup values.
state = {}
def accept(record):
if record["status"] == "finished" and (record["home_team"]["score"] is None or record["away_team"]["score"] is None):
raise ValueError("incomplete finished result")
state[record["id"]] = record
def total_goals():
return sum(r["home_team"]["score"]+r["away_team"]["score"]
for r in state.values() if r["status"] == "finished")
def result(home, status="finished"):
return {"id":"match_demo","status":status,"home_team":{"score":home},"away_team":{"score":0}}
accept(result(1))
assert total_goals() == 1
accept(result(1))
assert total_goals() == 1
accept(result(2))
assert total_goals() == 2
accept(result(None,"postponed"))
assert total_goals() == 0 and len(state) == 1
print("replay=1 correction=2 postponed=0; one stored identity")
Read the result
The four stages produce totals of one, one, two and zero while retaining a single match identity. The in-memory fixture proves replacement arithmetic; your database transaction and concurrency behavior still need integration tests.
Do not overinterpret updated_at
The Match schema's updated_at describes when Stats API observed and stored a normalized change. It is not an upstream event timestamp or a guarantee of sporting freshness. Save your retrieved_at value separately. If updated_at is null, do not fabricate a source time from the HTTP response date.
A correction audit should retain the old and new values, match ID, retrieval time and the job that accepted the update. That history helps explain a changed table without suggesting that a newly retrieved response necessarily describes a newly changed sporting event. Keep secrets and complete authorization headers out of the audit.
Test replay, correction and withdrawal paths
Begin with one known result, replay it, correct it and then make it ineligible. Assert the aggregate after every step. Add a duplicate-worker case and a failed transaction test in your actual persistence layer. The aim is to prove the recovery path, not just the happy-path arithmetic.
For large exports, repeat a bounded overlap window so recently corrected fixtures are revisited. A purely forward checkpoint can miss a result whose kickoff belongs to yesterday but whose normalized score changes today. Document your correction window and recovery procedure rather than promising that one historical export will stay permanently accurate.
Connect this to your project
Apply the same identity and replay rules to your local fixture database before using its totals in customer-facing charts. The MySQL export guide provides the persistence context, and the match documentation identifies the fields whose meaning must survive every update.