Skip to content

Chizumulu API

Chizumulu API turns the public football updates that the Northern Region Football Association (NRFA, Malawi) publishes into JSON you can build on.

Every post is kept exactly as it was observed. Structured data (goals, scores, fixtures, call-ups and so on) is derived from that post and sits next to the original text. When a post does not say something, the API does not say it either: the value is null.

https://api.chizumulu.net

Every public route is a GET and returns JSON. There is no authentication: no API key, no signup, no token. Just send the request.

Ask for the two newest messages with their structured data:

Terminal window
curl "https://api.chizumulu.net/v1/structured?limit=2"
Response
{
"items": [
{
"rawUpdateId": 5,
"source": "whatsapp",
"sourceMessageId": "3EB0A1F2C4D5E6F70005",
"sourceServerId": null,
"sourceTimestamp": "2026-09-19T15:24:20.000Z",
"observationCount": 1,
"latestObservation": {
"id": 5,
"observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e05",
"receivedAt": "2026-09-19T15:24:22.805Z",
"text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame"
},
"processing": {
"state": "done",
"method": "ai",
"model": "deepseek/deepseek-v4-flash-0731",
"postType": "result",
"translationState": "done",
"reusedFromObservationId": null,
"error": null
},
"structured": {
"schemaVersion": 2,
"postType": "result",
"competition": {
"code": "nrfa_league_one",
"conflict": false,
"evidence": [
{
"code": "nrfa_league_one",
"signal": "#CINRFALeagueOne",
"kind": "hashtag"
}
]
},
"matches": [
{
"index": 0,
"homeTeam": "Mafu Stars FC",
"awayTeam": "Luviri FC",
"homeScore": 3,
"awayScore": 1,
"status": "full_time",
"competition": null,
"competitionCode": "nrfa_league_one",
"competitionBasis": "post-signal",
"venue": null,
"dateText": null,
"timeText": null,
"roundText": null,
"minuteText": null,
"competitionGroupText": null,
"evidence": {
"text": true,
"attachments": []
},
"events": [
{
"index": 0,
"type": "full_time",
"minuteText": null,
"team": null,
"homeScore": null,
"awayScore": null,
"players": []
}
]
}
],
"facts": []
},
"korean": {
"text": "풀타임\n\n마푸 스타즈 FC 3–1 루비리 FC\n\n#CINRFALeagueOne\n#NRFATransformingTheGame",
"model": "deepseek/deepseek-v4-flash-0731"
}
},
{
"rawUpdateId": 4,
"source": "whatsapp",
"sourceMessageId": "3EB0A1F2C4D5E6F70004",
"sourceServerId": null,
"sourceTimestamp": "2026-09-19T15:20:52.000Z",
"observationCount": 2,
"latestObservation": {
"id": 6,
"observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06",
"receivedAt": "2026-09-19T15:26:31.207Z",
"text": "88' GOAL! \n\nNorman Ndlovu completes his brace as Mafu Stars extend their lead! \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame"
},
"processing": {
"state": "done",
"method": "ai",
"model": "deepseek/deepseek-v4-flash-0731",
"postType": "live_update",
"translationState": "done",
"reusedFromObservationId": null,
"error": null
},
"structured": {
"schemaVersion": 2,
"postType": "live_update",
"competition": {
"code": "nrfa_league_one",
"conflict": false,
"evidence": [
{
"code": "nrfa_league_one",
"signal": "#CINRFALeagueOne",
"kind": "hashtag"
}
]
},
"matches": [
{
"index": 0,
"homeTeam": "Mafu Stars FC",
"awayTeam": "Luviri FC",
"homeScore": 3,
"awayScore": 1,
"status": "live",
"competition": null,
"competitionCode": "nrfa_league_one",
"competitionBasis": "post-signal",
"venue": null,
"dateText": null,
"timeText": null,
"roundText": null,
"minuteText": "88'",
"competitionGroupText": null,
"evidence": {
"text": true,
"attachments": []
},
"events": [
{
"index": 0,
"type": "goal",
"minuteText": "88'",
"team": "Mafu Stars FC",
"homeScore": null,
"awayScore": null,
"players": [
{
"name": "Norman Ndlovu",
"role": "scorer"
}
]
}
]
}
],
"facts": []
},
"korean": {
"text": "88' 골!\n\n노먼 은들로부가 멀티골을 완성하며 마푸 스타즈가 리드를 넓힙니다!\n\n마푸 스타즈 FC 3–1 루비리 FC\n\n#CINRFALeagueOne\n#NRFATransformingTheGame",
"model": "deepseek/deepseek-v4-flash-0731"
}
}
],
"nextCursor": "MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="
}

Read it top to bottom:

  • rawUpdateId identifies the source message. latestObservation.text is the original text, untouched.
  • processing.state tells you whether the post has been processed. Only when it is done is structured filled in. See Processing states.
  • structured.matches[] holds the matches found in the post, each with events[]. Anything the post did not state is null. See the structured model.
  • korean is a derived Korean translation of the post, or null.
  • nextCursor gets you the next page. See Pagination.
I want to… Use
show current, structured football data /v1/structured
see every edit of one post /v1/structured/{rawUpdateId}/history
keep my own copy in sync, without missing anything /v1/feed
browse or filter the raw posts /v1/updates and /v1/latest
know whether the service is healthy /v1/status