WhoScored adapter

WhoScored: One match-centre blob, full lineup story.

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.

Input shape
WhoScored matchCentreData + a lightweight match-ID wrapper.
Status
Primary provider
Raw feed
WhoScored
Provider-shaped. Their coordinates, their qualifier codes, their outcome strings.
Messy
Campos adapter
fromWhoScored.shots(matchData, matchInfo)
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
Self-contained input

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.

Get it running

One import, one call per surface.

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);
What this adapter ships

The calls you can make today.

Shipped
events() returns Event[]

Every recognised match event in a canonical shape.

Shipped
shots() returns ShotEvent[]

Just the shots, ready to plot.

WhoScored notes: xG is not present in the source payload.

Shipped
passes() returns PassEvent[]

Pass trajectories with start, end, and result.

Shipped
matchLineups() returns MatchLineups

Home and away team sheets with starters and bench.

WhoScored notes: Captain, bench, formation coordinates, and substitution metadata.

Shipped
formations() returns FormationTeamData

Kickoff tactical shape for one side.

What you can build with this

Charts that drop straight onto this adapter's output.

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.

The lineup surface

What comes out of matchLineups().

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"
  ]
}
Current scope

What's honest about this adapter today.

What it's great for

Lineup-heavy match pages.

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.

The one real gap

No xG in the source.

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.

Formation intervals

Substitution metadata, not full tactical reconstruction.

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.

Lineage

What this adapter borrows from whom.

Concrete credits for the projects that did the underlying research or laid the reference tables we read from.

ScraperFC Python

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

Raw example — what the payload looks like before and after
Raw WhoScored event
{
  "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
}
Match info
{
  "matchId": "1974917"
}
Canonical output
[
  {
    "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
      ]
    }
  }
]
Compare providers