events()
returns Event[] Every recognised match event in a canonical shape.
WhoScored's match-centre payload carries everything — events, players, formations, captain, shirt numbers, formation intervals — in a single self-contained JSON. The adapter's lineup surface is the richest Campos ships today, including honest substitution metadata derived from tactical intervals.
fromWhoScored.shots(matchData, matchInfo) ShotEvent[] <ShotMap />, your own React view, a server-side
report, or an analysis script.
Unlike Opta's split feeds, WhoScored's matchCentreData carries teams, players,
formations, captain, and events in one payload. That's why it ships the richest lineup
surface in Campos today — the adapter doesn't need a second join step.
import { fromWhoScored } from "@withqwerty/campos-adapters";
const events = fromWhoScored.events(matchData, matchInfo);
const shots = fromWhoScored.shots(matchData, matchInfo);
const passes = fromWhoScored.passes(matchData, matchInfo);
const lineups = fromWhoScored.matchLineups(matchData, matchInfo);
const formation = fromWhoScored.formations(matchData.home); events()
returns Event[] Every recognised match event in a canonical shape.
shots()
returns ShotEvent[] Just the shots, ready to plot.
WhoScored notes: xG is not present in the source payload.
passes()
returns PassEvent[] Pass trajectories with start, end, and result.
matchLineups()
returns MatchLineups Home and away team sheets with starters and bench.
WhoScored notes: Captain, bench, formation coordinates, and substitution metadata.
formations()
returns FormationTeamData Kickoff tactical shape for one side.
Supported cards plot from this adapter with no extra work. Partial cards plot, but read the scope note. Dimmed cards need a surface this provider doesn't ship.
Plot events, shots, passes, and formations directly on the pitch.
Time-series and xG-driven views from shots().
These charts are adapter-independent — they take pre-aggregated inputs.
Starters, bench, captain, formation label, shirt numbers, formation coordinates, and substitution metadata derived from formation intervals. Honest about what the source supplies — nothing invented, nothing hidden.
{
"input": "matchCentreData.home / .away",
"includes": [
"players[]",
"formations[]",
"captainPlayerId",
"formationPositions",
"formation intervals"
]
} If you're building a match header with starters, bench, captain markers, formation coordinates, and substitution timing — this is the current richest adapter for that work.
WhoScored's match-centre payload doesn't carry per-shot xG. shots()
still works, but xG-driven charts like XGTimeline need a different provider
— Opta, StatsBomb, or Understat.
The adapter derives sub timing from formation intervals, which is enough for a clean team-sheet UI. It's not the same as perfect minute-by-minute tactical-state reconstruction — that needs a richer event stream than the public source.
Concrete credits for the projects that did the underlying research or laid the reference tables we read from.
Sofascore status-code table and WhoScored match-centre structure references.
{
"id": 2924611301,
"eventId": 482,
"minute": 90,
"second": 12,
"teamId": 13,
"playerId": 332325,
"relatedEventId": 481,
"relatedPlayerId": 367185,
"x": 90.8,
"y": 51.2,
"expandedMinute": 93,
"period": {
"value": 2,
"displayName": "SecondHalf"
},
"type": {
"value": 16,
"displayName": "Goal"
},
"outcomeType": {
"value": 1,
"displayName": "Successful"
},
"qualifiers": [
{
"type": {
"value": 103,
"displayName": "GoalMouthZ"
},
"value": "4.4"
},
{
"type": {
"value": 55,
"displayName": "RelatedEventId"
},
"value": "481"
},
{
"type": {
"value": 72,
"displayName": "LeftFoot"
}
},
{
"type": {
"value": 154,
"displayName": "IntentionalAssist"
}
},
{
"type": {
"value": 17,
"displayName": "BoxCentre"
}
},
{
"type": {
"value": 76,
"displayName": "LowLeft"
}
},
{
"type": {
"value": 22,
"displayName": "RegularPlay"
}
},
{
"type": {
"value": 29,
"displayName": "Assisted"
}
},
{
"type": {
"value": 56,
"displayName": "Zone"
},
"value": "Center"
},
{
"type": {
"value": 214,
"displayName": "BigChance"
}
},
{
"type": {
"value": 89,
"displayName": "OneOnOne"
}
},
{
"type": {
"value": 102,
"displayName": "GoalMouthY"
},
"value": "53.4"
}
],
"satisfiedEventsTypes": [
91,
24,
9,
10,
202,
0,
3,
14,
16,
19,
27
],
"isTouch": true,
"isGoal": true,
"isShot": true,
"goalMouthY": 53.4,
"goalMouthZ": 4.4
} {
"matchId": "1974917"
} [
{
"kind": "shot",
"id": "1974917:2924611301",
"matchId": "1974917",
"teamId": "13",
"playerId": "332325",
"playerName": "Kai Havertz",
"minute": 90,
"addedMinute": 3,
"second": 12,
"period": 2,
"x": 90.8,
"y": 48.8,
"xg": null,
"xgot": null,
"outcome": "goal",
"bodyPart": "left-foot",
"isOwnGoal": false,
"isPenalty": false,
"context": "regular-play",
"goalMouthY": 14.6,
"goalMouthZ": 11.6,
"provider": "whoscored",
"providerEventId": "2924611301",
"sourceMeta": {
"typeId": 16,
"eventId": 482,
"outcome": 1,
"satisfiedEventsTypes": [
91,
24,
9,
10,
202,
0,
3,
14,
16,
19,
27
]
}
}
]