ClubhouseFootball, connected.

Developers

Competition data, as an API.

League tables, fixtures, results and player statistics, maintained by Clubhouse and delivered as JSON to any website. You don't need a Clubhouse club site to use it.

Quick start

Ask Clubhouse to enable the competitions you need. You receive an API key that starts with chc_live_.

List the competitions and seasons your key can read:

curl https://clubhouse.digitaldignifiedsolutions.com/api/competitions/v1/competitions \
  -H "Authorization: Bearer chc_live_YOUR_KEY"

Then fetch a table, using a season id from that response:

curl https://clubhouse.digitaldignifiedsolutions.com/api/competitions/v1/seasons/SEASON_ID/standings \
  -H "Authorization: Bearer chc_live_YOUR_KEY"

Authentication

Send your key in the Authorization header as a bearer token. X-API-Key: <key> also works. Every endpoint is a GET and returns JSON. Responses use camelCase names, ISO 8601 timestamps in UTC and a data / meta envelope. Every response carries an X-Request-Id header.

List the competitions and seasons your key can read

GET /competitions

Start here to discover season ids. Only published seasons of competitions granted to your account are returned.

Example response

{
  "data": [
    {
      "id": "4e22fe5f-4762-4730-b1a5-9f3eb68cfce4",
      "slug": "uganda-premier-league",
      "name": "Uganda Premier League",
      "type": "league",
      "seasons": [
        {
          "id": "6057c6be-ebd9-483f-a50b-4be228dd7bbf",
          "name": "2026/27",
          "status": "in_progress",
          "isCurrent": true
        }
      ]
    }
  ],
  "meta": {
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

League table for a season

GET /seasons/{seasonId}/standings

Calculated live from finished matches, including any rulings (see `/rulings`). Ordered by points, then by the competition's own tie-break rules (returned in the `/rulings` meta as `tieBreakers`), then team name. `pointsAdjustment` is the total of the rulings in force for the team. `form` lists the last five results, oldest first. `outcome` says what the team's current position leads to (promotion, a place in another competition, a play-off, relegation), or `null`; `meta.outcomes` lists the bands behind it, counted from the `top` or `bottom` of each group. `outcome.status` is `projected` while the season runs (the table as it stands today) and `confirmed` once the governing body's final outcomes are recorded (`meta.outcomesConfirmedAt` says when). When a team's outcome was decided rather than read off the table, `outcomeNote` gives the reason, for example a place that passed down. A `competition` is only named once that competition is published.

ParameterInDescription
seasonId requiredpathA season id from the competitions endpoint.
groupqueryOnly return one group of a group-stage competition, for example `A`.

Example response

{
  "data": [
    {
      "position": 1,
      "group": null,
      "team": {
        "id": "c2000000-…",
        "name": "KCCA FC",
        "shortName": "KCC",
        "crestUrl": null
      },
      "played": 3,
      "won": 2,
      "drawn": 1,
      "lost": 0,
      "goalsFor": 7,
      "goalsAgainst": 3,
      "goalDifference": 4,
      "pointsAdjustment": 0,
      "points": 7,
      "form": [
        "W",
        "W",
        "D"
      ],
      "withdrawn": false,
      "outcome": {
        "id": "0b6e1c1e-…",
        "type": "qualification",
        "label": "Qualifies for the Regional Cup",
        "competition": {
          "id": "a2000000-…",
          "name": "Regional Cup"
        },
        "stage": "Group stage",
        "status": "projected"
      },
      "outcomeNote": null
    }
  ],
  "meta": {
    "seasonId": "6057c6be-…",
    "outcomesConfirmedAt": null,
    "outcomes": [
      {
        "id": "0b6e1c1e-…",
        "type": "qualification",
        "label": "Qualifies for the Regional Cup",
        "competition": {
          "id": "a2000000-…",
          "name": "Regional Cup"
        },
        "stage": "Group stage",
        "group": null,
        "counted": "top",
        "from": 1,
        "to": 1
      },
      {
        "id": "9c1d7a52-…",
        "type": "relegation",
        "label": "Relegated to the Championship",
        "competition": null,
        "stage": null,
        "group": null,
        "counted": "bottom",
        "from": 1,
        "to": 2
      }
    ],
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

Rulings behind a table

GET /seasons/{seasonId}/rulings

The decisions a governing body made after matches were played: points deducted or awarded, and matches awarded to one side. Only public rulings that are in force are returned, newest first. `meta.tieBreakers` lists the rules, in order, that separate teams level on points; anything still level is ordered by team name.

ParameterInDescription
seasonId requiredpathA season id from the competitions endpoint.

Example response

{
  "data": [
    {
      "id": "8d0c1c0e-…",
      "type": "points_deduction",
      "team": {
        "id": "c2000000-…",
        "name": "Vipers SC"
      },
      "points": -3,
      "reason": "Fielded an ineligible player",
      "decidedBy": "Disciplinary Committee",
      "reference": "DC/2026/07",
      "decidedOn": "2026-09-18",
      "match": null
    },
    {
      "id": "1f5b7a9e-…",
      "type": "match_awarded",
      "team": {
        "id": "c2000001-…",
        "name": "KCCA FC"
      },
      "points": 0,
      "reason": "Opponent failed to field a team",
      "decidedBy": "Disciplinary Committee",
      "reference": "DC/2026/09",
      "decidedOn": "2026-09-09",
      "match": {
        "id": "65f931a3-…",
        "home": "SC Villa",
        "away": "KCCA FC",
        "homeScore": 0,
        "awayScore": 3
      }
    }
  ],
  "meta": {
    "seasonId": "6057c6be-…",
    "tieBreakers": [
      "head_to_head_points",
      "goal_difference",
      "goals_for"
    ],
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

Fixtures and results for a season

GET /seasons/{seasonId}/matches

`kickoff` is null until the date is confirmed. Scores are null until a match has started.

ParameterInDescription
seasonId requiredpathA season id from the competitions endpoint.
statusquery`upcoming` (not yet played), `results` (finished), or an exact status: scheduled, live, half_time, full_time, postponed, abandoned, cancelled, awarded.
teamqueryOnly matches involving this team id.
fromqueryKick-off on or after this ISO date or time.
toqueryKick-off on or before this ISO date (a bare date includes that whole day).
orderquery`asc` (default) or `desc` by kick-off.
limitquery1–500, default 200.
offsetqueryMatches to skip, for paging.

Example response

{
  "data": [
    {
      "id": "65f931a3-…",
      "stage": "League",
      "matchday": 1,
      "kickoff": "2026-08-22T17:32:00.000Z",
      "venue": "Mandela National Stadium",
      "status": "full_time",
      "home": {
        "id": "c2000000-…",
        "name": "KCCA FC",
        "crestUrl": null,
        "score": 3,
        "penalties": null
      },
      "away": {
        "id": "c2000000-…",
        "name": "Police FC",
        "crestUrl": null,
        "score": 0,
        "penalties": null
      },
      "note": null
    }
  ],
  "meta": {
    "seasonId": "6057c6be-…",
    "total": 16,
    "limit": 200,
    "offset": 0,
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

Match sheet: line-ups, bench officials and events

GET /seasons/{seasonId}/matches/{matchId}

Everything recorded about one match, for both teams. `starters` and `substitutes` are ordered by shirt number; `minutesPlayed` is null when it was not recorded. `officials` are the coaches, medics and managers on each team's bench, with a `role` of head_coach, assistant_coach, goalkeeper_coach, fitness_coach, team_manager, doctor, physio, kit_manager or other. `events` are ordered by minute; `type` is goal, own_goal, penalty_scored, penalty_missed, yellow_card, second_yellow or red_card, and an own goal belongs to the player's own team. A player named only on an event, and not on the team's roster, has a null `player.id`. Line-ups and officials are empty until they are entered, which can be after the match. Get match ids from `/matches`.

ParameterInDescription
seasonId requiredpathA season id from the competitions endpoint.
matchId requiredpathA match id from the matches endpoint.

Example response

{
  "data": {
    "match": {
      "id": "65f931a3-…",
      "stage": "League",
      "matchday": 1,
      "kickoff": "2026-08-22T17:32:00.000Z",
      "venue": "Mandela National Stadium",
      "status": "full_time",
      "home": {
        "id": "c2000000-…",
        "name": "KCCA FC",
        "crestUrl": null,
        "score": 1,
        "penalties": null
      },
      "away": {
        "id": "c2000001-…",
        "name": "Police FC",
        "crestUrl": null,
        "score": 0,
        "penalties": null
      },
      "note": null
    },
    "home": {
      "team": {
        "id": "c2000000-…",
        "name": "KCCA FC",
        "crestUrl": null
      },
      "starters": [
        {
          "player": {
            "id": "265c58ca-…",
            "name": "Daniel Sserunkuma"
          },
          "shirtNumber": 9,
          "position": "Forward",
          "minutesPlayed": 78
        }
      ],
      "substitutes": [
        {
          "player": {
            "id": "7a0d34b1-…",
            "name": "Brian Kiwanuka"
          },
          "shirtNumber": 14,
          "position": "Midfielder",
          "minutesPlayed": 12
        }
      ],
      "officials": [
        {
          "id": "5d1c0f6e-…",
          "name": "Grace Nansubuga",
          "role": "head_coach"
        },
        {
          "id": "0e93a2c8-…",
          "name": "Peter Ssemwogerere",
          "role": "physio"
        }
      ]
    },
    "away": {
      "team": {
        "id": "c2000001-…",
        "name": "Police FC",
        "crestUrl": null
      },
      "starters": [],
      "substitutes": [],
      "officials": []
    },
    "events": [
      {
        "id": "b8a4e1d2-…",
        "type": "goal",
        "minute": 55,
        "addedTime": null,
        "teamId": "c2000000-…",
        "player": {
          "id": "265c58ca-…",
          "name": "Daniel Sserunkuma"
        },
        "assist": {
          "id": "7a0d34b1-…",
          "name": "Brian Kiwanuka"
        }
      }
    ]
  },
  "meta": {
    "seasonId": "6057c6be-…",
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

Player statistics for a season

GET /seasons/{seasonId}/player-stats

Built from line-ups and match events, so only competitions where Clubhouse records them have data. Yellow cards include a second yellow; red cards include a sending-off after a second yellow.

ParameterInDescription
seasonId requiredpathA season id from the competitions endpoint.
teamqueryOnly players of this team id.
sortquerygoals (default), assists, appearances, minutes_played, yellow_cards or red_cards.
limitquery1–200, default 50.

Example response

{
  "data": [
    {
      "player": {
        "id": "265c58ca-…",
        "name": "Daniel Sserunkuma"
      },
      "team": {
        "id": "05608bd0-…",
        "name": "KCCA FC"
      },
      "appearances": 3,
      "starts": 3,
      "minutesPlayed": 270,
      "goals": 4,
      "penaltiesScored": 1,
      "ownGoals": 0,
      "assists": 2,
      "yellowCards": 1,
      "redCards": 0
    }
  ],
  "meta": {
    "seasonId": "6057c6be-…",
    "sort": "goals",
    "generatedAt": "2026-09-19T17:00:00.000Z"
  }
}

Errors

Failures return a JSON body with a stable code to program against, a readable message and the requestId.

{
  "error": {
    "code": "invalid_api_key",
    "message": "The API key is not valid, has been revoked, or has expired.",
    "requestId": "0b6f8c3e-…"
  }
}
StatusCodeMeaning
400invalid_parameter / invalid_season_id / invalid_match_idA query parameter, or the season or match id in the URL, is malformed.
401missing_api_key / invalid_api_keyNo key was sent, or it is wrong, revoked or expired. The response carries a WWW-Authenticate header.
403client_inactiveYour account is suspended or cancelled.
403not_grantedThe season does not exist, is not published, or is not part of your plan.
404match_not_foundThe match id is not part of the season in the URL.
429rate_limit_exceededYour monthly request limit is used up. Retry-After gives the seconds until it resets (the first of the month, UTC).
500internal_errorA problem on our side. Quote the requestId when you contact us.
503service_unavailableTemporarily unavailable. Retry shortly.

Limits & caching

Your plan can include a monthly request limit. When it is used up, requests return 429 with a Retry-After header until the limit resets on the first of the month (UTC). Tables and results change only when Clubhouse records a match event, so a cache of 30–60 seconds on your side keeps you well inside your limit. Responses are marked Cache-Control: private, max-age=30.

Keeping your key safe

Call the API from your server, not from browser JavaScript. Anything sent to a browser can be read by visitors, and a copied key can be used until it is revoked. The API deliberately does not allow cross-origin browser requests. Fetch the data on your server, then render it into your pages. If a key leaks, tell Clubhouse and it is revoked immediately.

A machine-readable description is available at /api/competitions/v1/openapi.json (OpenAPI 3).