New features, changes & important service notices — announced first on our Telegram channel. Updates & important notices — first on Telegram.

Join @inplayguru_info
On this page

Working with the data

Live football data is messy in ways that only show up in production. This page is the field manual: how the feed actually behaves, how to parse payloads so the mess never corrupts your systems, and whose numbers are canonical when two copies disagree.

The live data almanac #

Systems built on live sports data fail in predictable ways — the same ways, every season, for everyone. Every behaviour below is normal: expected, not exceptional. Production-grade receivers are built for all of them from day one.

Behaviour Why it happens Defensive pattern
Scores go down A goal is ruled out (offside, VAR review) or the feed corrects a misreported "ghost goal". Never assume score is monotonic. If logic keys on "a goal happened", require the new score to persist rather than acting on first sight.
Stats get revised Providers correct counts mid-match — a shot reclassified, a card reattributed, corners recounted. Expect small backwards adjustments between the pick.created snapshot and pick.settled full-time stats. Don't treat per-stat deltas as an exact ledger of events.
Timers drift and jump Stoppage-time handling varies by competition; feed hiccups can freeze or jump the clock. Treat timer.min as scene-setting, accurate to about a minute — not a clock to synchronise against.
Coverage drops mid-match A stat that updated at minute 30 can silently stop updating — the provider's scout lost it. Detect staleness by change over time, not presence. A frozen number looks exactly like a quiet match.
Data lags at the worst moments Goalmouth scrambles, VAR reviews and simultaneous kickoffs are precisely when providers fall behind. An event describes the feed's view at trigger time — reality may already have moved on. Build margins into anything time-sensitive.
Matches get interrupted Abandonments, postponements, walkovers. Picks on interrupted matches may never settle — expire your trackers (see Idempotency & ordering).
Quotes flicker and suspend Markets suspend around goals and reviews; brief outlier values appear at exactly those moments. Discard entries with suspended: true and sanity-bound quote values before using them.
Names change, IDs don't Team and league names get renamed, rebranded and respelled — sometimes mid-season. Key everything on IDs. Names are display-only.

To be equally candid about our side: match data originates from third-party providers, and when their feed degrades, so do payloads. We surface what we receive — we can't correct what we never got. That is exactly why the patterns above aren't optional polish; they're the integration.

The one rule that contains all the others: treat every value as last-known-good, never as gospel.

Parsing & precision #

Absent means unknown — never zero

A stat key missing from data.match.stats means the stat is not covered for that match. Storing it as 0 is the classic silent bug: months later, every average, model and threshold trained on that data is quietly wrong, and nothing ever errored. Store missing as NULL / None and exclude it from aggregates.

The same rule extends beyond stats: enrichment objects and fields — team form, formation, manager, market_value, squad indicators, stadium, season, weather — are present when coverage allows, never guaranteed. Read them all through the same "absent means unknown" lens.

absent ≠ zero Python
stats = event["data"]["match"]["stats"]

xg = stats.get("xg")          # None when not covered — keep it None
if xg is not None:
    home_xg, away_xg = xg     # only use it when it exists

# WRONG: turns "not covered" into "0.0 xG"
home_xg, away_xg = stats.get("xg", [0, 0])

The zero-is-falsy trap

favorite is 0 for home, 1 for away, null for none — and 0 is falsy in most languages. The truthiness shortcut silently skips every home favorite:

favorite — test against null, not truthiness JS
// BUG: home favorite is 0, and 0 is falsy — every home favorite is skipped.
if (match.favorite) { trackFavorite(match); }

// Correct: null means "no favorite"; 0 and 1 are both real values.
if (match.favorite !== null) { trackFavorite(match); }

Numbers

  • Quotes are decimal numbers. Never compare them with float equality; if you need exactness (accounting, dedupe keys), store them as strings or fixed-point decimals, not doubles.
  • Percentage stats (possession, action_areas) are 0–100 values that may not sum to exactly 100 after provider rounding.
  • xg values are floats with provider-defined precision — treat trailing-digit differences as noise, not signal.
  • Some numeric-looking fields arrive as strings — stadium capacity, league table positions, season round / max_rounds. Cast explicitly before doing math; a schema that types them as numbers will reject real payloads.

Identifiers

  • Two id families exist: pick and strategy IDs are integers that can exceed 32-bit range (store as 64-bit or strings), while match, team, league, image and manager IDs are opaque encoded strings.
  • All IDs are opaque either way: never parse, decode or order by them — compare byte-for-byte. The simplest uniform rule is to store every id as a string.
  • Join across events and store keys on IDs — never on names (see almanac).

Strings & dates

  • Team, league and venue names are UTF-8 with the full character range real football brings — accents, apostrophes, non-Latin scripts. Escape on output; never assume ASCII or a length cap.
  • Timestamps are ISO 8601 UTC, in two spellings: data.match.date uses Z notation with milliseconds, while envelope fields like event_created_at use a +00:00 offset. Parse with a real ISO parser, not substring surgery, and never assume a local timezone.
  • Stat tuples are always two-element [home, away] arrays — index 0 is home, everywhere, always.

System of record #

The webhook is a transport, not a ledger. The canonical record of your picks and their results is your strategy history inside the platform. When your stored copy and the platform disagree, the platform record is authoritative.

Copies drift for reasons that are all expected:

  • Deliveries missed while your endpoint was down are never replayed — your store has a gap the platform doesn't.
  • Upstream corrections after delivery are not re-sent — your stored snapshot preserves a value the platform has since improved.
  • Your own processing bugs — dropped events, failed parses — leave holes only on your side.

Point-in-time values drift by design

strike_rate and strike_rate_league in a payload are the strategy's numbers at the moment the event was generated. Look at the same strategy today and the numbers will differ — more picks have settled since. That's not an inconsistency; a stored payload is a snapshot, the platform shows the live aggregate. Don't file the difference as a bug, and don't "reconcile" old snapshots against today's UI.

Reconciliation, done right

If your use case needs your store to be trustworthy, spot-check it periodically: compare your recent settled picks against the strategy history page. Where you find a gap, the first suspect is a missed delivery on your side — the delivery log will show the attempts and their outcomes. Reconcile toward the platform record, never away from it.