Adapters

Raw football data, in. Ready-to-plot TypeScript, out.

Every football data provider is different. For example — Opta flips axes by period. StatsBomb puts the origin top-left. Wyscout codes outcomes as numeric tags. WhoScored bundles lineups and events into one blob; StatsBomb splits them across three. Without help, every chart you build repeats that cleanup.

Campos adapters do it once per provider:

fromOpta.shots(raw, ctx) → ShotEvent[]

Drop straight into Campos charts, your own React view, or a server-side script. It's just TypeScript.

If you know kloppy or socceraction, we borrow from both — jump to how we differ.

Raw feed
Any supported provider
Provider-shaped. Their coordinates, their qualifier codes, their outcome strings.
Messy
Campos adapter
fromX.shots(raw, ctx)
Flips direction, standardises outcomes, filters own-goals and shootouts, resolves player labels.
One call
Typed data
ShotEvent[]
Drop into <ShotMap />, your own React view, a server-side report, or an analysis script.
Ready
Pick your door

Two ways into this page.

New to normalising football data?

What an adapter actually does.

The adapter absorbs each provider's quirks so your chart code never sees them. One call returns a typed array. That's the whole interface.

import { fromOpta } from "@withqwerty/campos-adapters";
import { ShotMap } from "@withqwerty/campos-react";

const shots = fromOpta.shots(rawEvents, matchContext);
// → ShotEvent[] with canonical coords,
//   outcomes, xG, body part, situation.

<ShotMap shots={shots} />
What you get for free
  • Direction handled — the attacker is always shooting rightward
  • Outcomes mapped to one vocabulary (goal, saved, off-target, blocked, hit-woodwork)
  • Own-goals and penalty shootouts filtered out of shot products
  • Player labels resolved where the provider's event stream doesn't carry them
  • Missing fields and edge cases handled consistently, so your chart never blows up on sparse data
What it's not
  • Not a scraper — you still need to get raw data somewhere
  • Not an analysis layer — no xG models, no xT, no PPDA
  • Not a dataset unifier — packets are shaped for Campos charts, not for a unified multi-provider event lake
Coming from kloppy or socceraction?

Same lineage, narrower goal.

Campos adapters use the same reference tables, the same coordinate conventions, the same quirk-awareness. Credit is in the lineage strip below. The difference is what comes out the other end.

What we share
  • Provider type-ID and qualifier-code mappings (we cross-reference kloppy's tables)
  • Action-taxonomy sanity-check from SPADL / socceraction
  • Coordinate conventions from mplsoccer and the kloppy pitch-standardisation model
  • Scrape-backed narrow-adapter pattern from soccerdata
What we do differently
  • Chart-shaped packets, not dataset-unified events. shots() drops own-goals and shootouts because shot-map semantics demand it. passes() emits exactly what PassMap, PassSonar, and PassNetwork consume.
  • TypeScript-native. No Python runtime, no pandas, no ETL step. npm install, import, call. Useful anywhere you want typed football data — client, server, scripts, Next.js API routes.
  • Provider-honest. Where a provider can't do something — Wyscout's tag-driven duel outcomes, WhoScored's missing xG — the adapter is partial, not pretending. The capability matrix below is the source of truth.
  • Per-provider input shapes. No artificial uniformity. Opta wants events + MatchContext; StatsBomb wants events + lineups + matchInfo; Wyscout wants the public match payload. Each adapter takes what the provider naturally supplies.
Why that tradeoff

Most callers use these adapters alongside Campos charts — packet shapes are designed for that path. Nothing stops you from using them standalone. They're plain TypeScript functions returning well-typed data.

Built in the lineage of

Credit where it's due.

The projects below did the hard football-data normalisation work in the Python ecosystem. Campos adapters borrow specific things from each — type-ID tables, coordinate transforms, action taxonomies, scrape patterns. Concrete, not vibes.

kloppy Python

Provider type-ID → canonical kind mapping, coordinate transformers, and direction-by-period normalisation patterns. Our reference for Opta, StatsBomb, Wyscout, Stats Perform, and Sportec.

socceraction Python

SPADL canonical action taxonomy as a reference when we decided which Wyscout tag combinations collapse into which canonical event.

soccerdata Python

Scrape-backed narrow-adapter pattern; the schemas we read for Understat and FBref schedule and shot rows.

ScraperFC Python

Sofascore status-code table and WhoScored match-centre structure references.

statsbombpy Python

Reference for how raw StatsBomb event envelopes and lineup payloads are typically consumed.

impectPy Python

Reference for Impect open-data envelope structure and player-ID resolution.

mplsoccer Python

Pitch-standardisation conventions we cross-checked our canonical Campos coordinate frame against.

Capability matrix

Which provider ships which surface today.

Honest snapshot. Shipped means tested on real data. Partial means landed but narrower than the full surface — follow the provider link for exact scope.

Provider events shots passes matchLineups formations matchSummary matchContext
Opta Shipped Shipped Shipped Shipped Shipped
StatsBomb Shipped Shipped Shipped Shipped Shipped
WhoScored Shipped Shipped Shipped Shipped Shipped
Wyscout Shipped Partial Partial Partial
Understat Shipped Shipped
FBref Shipped
Sofascore Shipped
Stats Perform Partial Partial Partial Partial Partial Shipped
Impect Partial Partial Partial Partial Partial
Sportec Partial Partial Partial Partial Partial Shipped
Shipped Partial Not supported
The product vocabulary

What each adapter call returns.

events() returns Event[]

Every recognised match event in a canonical shape.

The widest surface. Shots, passes, tackles, cards, duels, clearances, recoveries, substitutions — all in the same TypeScript type, with coordinates already in Campos' attacker-perspective frame.

shots() returns ShotEvent[]

Just the shots, ready to plot.

A focused shot list with outcomes, xG (where the provider supplies it), body part, and situation coded to one vocabulary. Own-goals and shootouts are filtered out so your shot map doesn't have to.

passes() returns PassEvent[]

Pass trajectories with start, end, and result.

Start and end coordinates, completion, pass type, and recipient where known. Direct input for pass maps, flow views, sonars, and network aggregation.

matchLineups() returns MatchLineups

Home and away team sheets with starters and bench.

Starting XI, bench, captain, formation label, shirt numbers, and substitution metadata where the provider supports it. Everything a match header or lineup card needs.

formations() returns FormationTeamData

Kickoff tactical shape for one side.

Narrower lineup view — players placed at their kickoff formation positions. Powers the Formation chart directly.

matchSummary() returns MatchSummary

Scoreline, status, and team-level xG.

For schedules, scorelines, and result cards. Takes a row from a schedule scrape and returns a clean match summary with competition, kickoff, status, score, and team xG when provided.

matchContext() returns MatchContext

Attack direction and period metadata.

The direction context that coordinate-dependent providers need to normalise events. Opta and Sportec expose this; most others bake it into their adapter internally.

Providers

Pick the adapter that matches your source.

Narrow scrape-backed

Built from public scraped sources. Narrower surfaces — typically matchSummary() for scorelines and result cards, plus shots() on the one that has xG (Understat).