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.
| Parameter | In | Description |
|---|---|---|
seasonId required | path | A season id from the competitions endpoint. |
group | query | Only 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.
| Parameter | In | Description |
|---|---|---|
seasonId required | path | A 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.
| Parameter | In | Description |
|---|---|---|
seasonId required | path | A season id from the competitions endpoint. |
status | query | `upcoming` (not yet played), `results` (finished), or an exact status: scheduled, live, half_time, full_time, postponed, abandoned, cancelled, awarded. |
team | query | Only matches involving this team id. |
from | query | Kick-off on or after this ISO date or time. |
to | query | Kick-off on or before this ISO date (a bare date includes that whole day). |
order | query | `asc` (default) or `desc` by kick-off. |
limit | query | 1–500, default 200. |
offset | query | Matches 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`.
| Parameter | In | Description |
|---|---|---|
seasonId required | path | A season id from the competitions endpoint. |
matchId required | path | A 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.
| Parameter | In | Description |
|---|---|---|
seasonId required | path | A season id from the competitions endpoint. |
team | query | Only players of this team id. |
sort | query | goals (default), assists, appearances, minutes_played, yellow_cards or red_cards. |
limit | query | 1–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-…"
}
}| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_parameter / invalid_season_id / invalid_match_id | A query parameter, or the season or match id in the URL, is malformed. |
| 401 | missing_api_key / invalid_api_key | No key was sent, or it is wrong, revoked or expired. The response carries a WWW-Authenticate header. |
| 403 | client_inactive | Your account is suspended or cancelled. |
| 403 | not_granted | The season does not exist, is not published, or is not part of your plan. |
| 404 | match_not_found | The match id is not part of the season in the URL. |
| 429 | rate_limit_exceeded | Your monthly request limit is used up. Retry-After gives the seconds until it resets (the first of the month, UTC). |
| 500 | internal_error | A problem on our side. Quote the requestId when you contact us. |
| 503 | service_unavailable | Temporarily 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).