Compare two teams head to head
Head to head is a single call and it answers three questions at once: the all-time balance, the recent form against this specific opponent, and the list of past meetings. It is the cheapest useful widget in the API.
1. The call
Section titled “1. The call”The endpoint hangs off one participant and takes the other as a parameter:
curl "https://api.statscore.com/v2/participants/136761/compare?token=YOUR_TOKEN&compare_participant_id=136763"| Parameter | Where | Required | Meaning |
|---|---|---|---|
id | path | yes | the first team |
compare_participant_id | query | yes | the second team |
events_details | query | no | yes includes each meeting's participants and results |
2. What comes back
Section titled “2. What comes back”Three things under api.data, and they are unrelated shapes:
{ "api": { "data": { "participants": [ { "id": 136761, "name": "Lech Poznan", "short_name": "Lech Poznan", "area_name": "Poland", "type": "team" }, { "id": 136763, "name": "Legia Warszawa", "short_name": "Legia Warszawa", "area_name": "Poland", "type": "team" } ], "head2head": { "all_matches_stats": { "participant1_won": 20, "participant1_draw": 15, "participant1_lost": 27, "participant2_won": 27, "participant2_draw": 15, "participant2_lost": 20, "total_events_played": 62 }, "last_10_matches_stats": { "participant1_won": 3, "participant1_draw": 5, "participant1_lost": 2, "participant2_won": 2, "participant2_draw": 5, "participant2_lost": 3 } }, "h2h_events": { "competitions": [ "…" ] } } }}The stats are symmetric by construction: participant1_won equals participant2_lost, and the
draws match. Useful as a sanity check on your own parsing, not as extra information.
3. The event list is nested five levels deep
Section titled “3. The event list is nested five levels deep”h2h_events is not a flat array of matches. Meetings are grouped by competition, and each
competition repeats the full tree the rest of the API uses:
h2h_events.competitions[] -> seasons[] -> stages[] -> groups[] -> events[]For Lech against Legia that was 3 competitions holding 5 events between them. Two consequences:
- You must flatten it. A "last meetings" list needs a walk, not an index.
- Matches are split by competition, so sorting by date only works after flattening. The league meeting and the cup meeting sit in different branches.
Each event carries the usual shape, so scores are read exactly as in
recipe 03 - by result id, from participants[].results[]:
{ "id": 5978349, "name": "Lech Poznan - Legia Warszawa", "start_date": "2026-04-26 15:30", "status_type": "finished", "status_name": "Finished", "participants": [ { "short_name": "Lech Poznan", "counter": 1, "results": [{ "id": 2, "value": "4" }] }, { "short_name": "Legia Warszawa", "counter": 2, "results": [{ "id": 2, "value": "0" }] } ]}counter: 1 is home and 2 is away, so you get the venue for free. Remember that result id: 2
excludes the penalty shootout - for a cup meeting decided on penalties you also need id: 7.
4. Putting it together
Section titled “4. Putting it together”import { apiGet } from "./client.js"; // z recipe 01
const RESULT_FINAL = 2;
/** Splaszcza h2h_events.competitions -> seasons -> stages -> groups -> events */function flattenEvents(h2hEvents) { const out = []; for (const comp of h2hEvents?.competitions ?? []) { for (const season of comp.seasons ?? []) { for (const stage of season.stages ?? []) { for (const group of stage.groups ?? []) { for (const ev of group.events ?? []) { out.push({ ev, competition: { id: comp.id, name: comp.name }, season: { id: season.id, name: season.name } }); } } } } } return out;}
export async function getHeadToHead(teamId, opponentId) { const data = await apiGet(`/v2/participants/${teamId}/compare`, { compare_participant_id: opponentId, events_details: "yes", });
// `participant1` / `participant2` w head2head to POZYCJE, nie id. // Mapujemy je przez participants[], zeby odwrocenie wywolania nie przestawilo bilansu. const [p1, p2] = data.participants ?? []; const balance = (bucket) => bucket && { [p1.id]: { won: bucket.participant1_won, draw: bucket.participant1_draw, lost: bucket.participant1_lost }, [p2.id]: { won: bucket.participant2_won, draw: bucket.participant2_draw, lost: bucket.participant2_lost }, };
const meetings = flattenEvents(data.h2h_events) .map(({ ev, competition, season }) => { const side = (p) => ({ id: p.id, name: p.short_name ?? p.name, home: p.counter === 1, // result id 2 = regulaminowy + dogrywka, BEZ karnych (patrz recipe 03) goals: Number((p.results ?? []).find((r) => r.id === RESULT_FINAL)?.value ?? NaN), }); const sides = (ev.participants ?? []).map(side); return { eventId: ev.id, date: ev.start_date, finished: ev.status_type === "finished", competition, season, home: sides.find((s) => s.home) ?? null, away: sides.find((s) => !s.home) ?? null, }; }) // sortujemy PO splaszczeniu - w drzewie mecze sa rozbite po rozgrywkach .sort((a, b) => String(b.date).localeCompare(String(a.date)));
return { teams: { [p1.id]: p1.short_name ?? p1.name, [p2.id]: p2.short_name ?? p2.name }, allTime: { ...balance(data.head2head?.all_matches_stats), played: data.head2head?.all_matches_stats?.total_events_played ?? null }, last10: balance(data.head2head?.last_10_matches_stats), meetings, };}# Python - ten sam kontraktRESULT_FINAL = 2
def flatten_events(h2h_events): for comp in (h2h_events or {}).get("competitions", []): for season in comp.get("seasons", []): for stage in season.get("stages", []): for group in stage.get("groups", []): for ev in group.get("events", []): yield ev, comp, season
def get_head_to_head(team_id: int, opponent_id: int): data = api_get(f"/v2/participants/{team_id}/compare", compare_participant_id=opponent_id, events_details="yes")
p1, p2 = data.get("participants", [None, None])
def balance(bucket): if not bucket: return None # participant1/2 to pozycje - mapujemy przez participants[] return { p1["id"]: {"won": bucket.get("participant1_won"), "draw": bucket.get("participant1_draw"), "lost": bucket.get("participant1_lost")}, p2["id"]: {"won": bucket.get("participant2_won"), "draw": bucket.get("participant2_draw"), "lost": bucket.get("participant2_lost")}, }
meetings = [] for ev, comp, season in flatten_events(data.get("h2h_events")): sides = [] for p in ev.get("participants", []): goal = next((r.get("value") for r in p.get("results", []) if r.get("id") == RESULT_FINAL), None) sides.append({"id": p.get("id"), "name": p.get("short_name") or p.get("name"), "home": p.get("counter") == 1, "goals": int(goal) if goal not in ("", None) else None}) meetings.append({ "event_id": ev.get("id"), "date": ev.get("start_date"), "finished": ev.get("status_type") == "finished", "competition": {"id": comp.get("id"), "name": comp.get("name")}, "home": next((s for s in sides if s["home"]), None), "away": next((s for s in sides if not s["home"]), None), })
# sortujemy PO splaszczeniu - w drzewie mecze sa rozbite po rozgrywkach meetings.sort(key=lambda m: str(m["date"]), reverse=True)
all_stats = (data.get("head2head") or {}).get("all_matches_stats") or {} return { "teams": {p1["id"]: p1.get("short_name"), p2["id"]: p2.get("short_name")}, "all_time": {**(balance(all_stats) or {}), "played": all_stats.get("total_events_played")}, "last10": balance((data.get("head2head") or {}).get("last_10_matches_stats")), "meetings": meetings, }What this does not give you
Section titled “What this does not give you”Worth knowing before you promise a feature:
- No per-match statistics. The meetings carry scores and status, not possession or shots.
For that you call
GET /v2/events/{id}per match. - No aggregate goals.
head2headcounts wins, draws and losses; goals scored across the rivalry are not there. Sum them from the flattened list. - The event list is not the full history. All-time says 62 meetings;
h2h_eventsreturned 5. It is a recent sample, not an archive, so label it "recent meetings" rather than "all meetings".
Errors
Section titled “Errors”| Status | Cause |
|---|---|
400 | missing compare_participant_id |
401 | missing, expired or malformed token |
403 | one of the teams is outside your contract |
404 | unknown participant id |
- Get one match with score and stats - per-match detail for a meeting
- Show top scorers, cards and assists - the other aggregate view
- Display a league table - where these teams stand right now
Full reference: GET /v2/participants/{id}/compare ·
GET /v2/events/{id}