Endpoints
All routes are GET, need no authentication and return application/json. The base URL is https://api.chizumulu.net. Other methods, and API paths that are not listed here, return 404 with {"error":"not found"}.
Choosing a route
Section titled “Choosing a route”| Route | Returns | Order | text is from |
Use it to |
|---|---|---|---|---|
/v1/structured |
one item per message | newest first | the latest observation | show current structured data |
/v1/structured/{rawUpdateId}/history |
one message, all its observations | oldest first | each observation | inspect edits |
/v1/feed |
one item per observation | ingestion order, oldest first | that observation | sync everything, incrementally |
/v1/updates |
one item per message | newest first | the first observation | browse and filter raw posts |
/v1/latest |
the newest message | - | the first observation | quick check |
/v1/status |
health | - | - | see if the service is healthy |
/v1/live |
derived matches that are credibly live now | most recently updated first | - | show a live score |
/v1/matches/{matchId} |
one derived match and the observations it was built from | oldest observation first | - | follow one match |
/v1/videos |
recent videos of one YouTube channel feed | the feed’s order (newest first) | - | list the channel’s latest uploads |
/v1/videos/latest |
the newest video of that feed | - | - | quick check |
Parameters that behave the same everywhere
Section titled “Parameters that behave the same everywhere”limit: page size, default20. A value below1becomes1and a value above100becomes100. A value that is not a number is ignored and the default applies.limitnever causes an error.- Errors are always a JSON object with one string field, for example
{"error":"cursor is invalid"}. The message is meant for humans, not for matching in code.
GET /v1/structured
Section titled “GET /v1/structured”One item per source message, newest first, carrying the latest observation and its derived structured result.
| Parameter | Meaning |
|---|---|
limit |
Page size (see above). |
cursor |
The nextCursor of the previous page. Opaque. Invalid values return 400 cursor is invalid. |
competition |
Optional. A competition code, for example nrfa_league_one. Matches items whose post-level competition.code or any match’s competitionCode equals it. |
teamId |
Optional, recommended. A canonical team id, for example chizumulu_united. Matches any match whose homeTeam or awayTeam is one of the source spellings listed for that id (see Team identifiers), so it is stable across spelling variants such as Chizumulu United and Chizumulu United FC. |
teamAlias |
Optional. A reviewed, human-friendly shorthand for a club, for example Chizumulu. It resolves to a teamId from an explicit table (see Team aliases) and then behaves exactly like that teamId. Matched exactly except for letter case: Chizumulu, chizumulu and CHIZUMULU are the same alias. |
team |
Optional. A team name, matched exactly (case-sensitive, as written in the source, including punctuation such as the apostrophe in M’mbelwa Warriors FC) against any match’s homeTeam or awayTeam. No fuzzy matching, no aliases: Mafu Stars does not match Mafu Stars FC. Use it to find one specific spelling; use teamId for a stable filter. |
postType |
Optional. One postType value (see Structured model), matched exactly. |
Filters are optional, combine with each other (an item must satisfy all of them) and with limit and cursor. Notes:
- They are tested against what the item shows: the derived result of the message’s latest observation, and only when it is
done. An older observation’s data, or an item whose latest observation is not processed yet, never matches. - A matching item is returned whole: its other matches, facts and translation are not trimmed.
competitionandteamare tested independently, so with both, the competition and the team may come from different matches of the same item. - A value that matches nothing (including an unknown code or post type) gives an empty page, not an error. An empty value (
team=) is400. - Paging is unchanged: filters are applied before the page is cut,
nextCursorfollows the same rule, and a cursor should be used with the same filters that produced it.
Team identifiers
Section titled “Team identifiers”teamId is a small, explicit table: a canonical id and the exact source spellings known to belong to that club. It is used only for filtering. homeTeam and awayTeam (and every other field) are always returned exactly as written in the source, and matching is plain equality against the listed spellings: there is no fuzzy matching. A spelling that is not listed, or an unknown club, has no id and is not matched by teamId (use team with the exact name for it). An unknown teamId returns an empty page.
GET /v1/structured?teamId=chizumulu_unitedGET /v1/structured?teamId=chizumulu_united&competition=nrfa_league_one&limit=10The first request returns every message in which a match has one of the listed Chizumulu spellings (Chizumulu United FC, Chizumulu United, Chizumulu FC, Chizumulu Utd) as home or away team, whole, with the names as written. team=Chizumulu%20United would find only that one spelling.
teamId |
Source spellings |
|---|---|
chibavi_real_stars |
Chibavi Real Stars FC, Chibavi Real Stars |
jenda_united |
Jenda United FC, Jenda United |
chizumulu_united |
Chizumulu United FC, Chizumulu United, Chizumulu FC, Chizumulu Utd |
chintheche_united |
Chintheche United FC, Chintheche United |
chilumba_barracks |
Chilumba Barracks FC, Chilumba Barracks |
mafu_stars |
Mafu Stars FC, Mafu Stars, Mafu FC, Mafu, MAFU Stars FC |
mmbelwa_warriors |
M'mbelwa Warriors FC, M'mbelwa Warriors, M’mbelwa Warriors FC, M’mbelwa Warriors, M'mberwa Warriors FC, M'mberwa Warriors, M’mberwa Warriors FC, M’mberwa Warriors |
chipolopolo_boys |
Chipolopolo Boys FC, Chipolopolo Boys, Chipolopolo FC |
ekwendeni |
Ekwendeni FC, Ekwendeni |
lube_masters |
Lube Masters FC, Lube Masters |
chihame_all_stars |
Chihame All Stars FC, Chihame All Stars |
raiply |
Raiply FC, Raiply |
euthini_veterans |
Euthini Veterans FC, Euthini Veterans |
vision_s_academy |
Vision S Academy |
luviri |
Luviri FC, Luviri Fc, Luviri |
Only spellings that could be confirmed from the maintained club list, from real posts and by the maintainers are listed; ambiguous ones are left out on purpose (for example Wovwe Vision is not treated as Vision S Academy). New spellings are added to the table when they are reviewed, and ids are never renamed.
Team aliases
Section titled “Team aliases”teamAlias is shorthand for people typing a URL: GET /v1/structured?teamAlias=Chizumulu returns exactly what teamId=chizumulu_united returns. It is a second explicit, reviewed table, not fuzzy matching: the value must equal a listed alias exactly, ignoring only letter case (Chizumulu, chizumulu and CHIZUMULU all work; no trimming and no partial matching; the straight M'mbelwa and the curly M’mbelwa are separate entries). teamId and team are not affected: teamId stays an exact lowercase id and team stays case-sensitive. An alias only resolves to a teamId; it never appears in a response and never changes a team name. An unknown or ambiguous shorthand (for example Vision, which could be Vision S Academy or Wovwe Vision) returns an empty page. For integrations prefer teamId, which does not depend on shorthand.
teamAlias |
Resolves to teamId |
|---|---|
Chibavi |
chibavi_real_stars |
Jenda |
jenda_united |
Chizumulu |
chizumulu_united |
Chintheche |
chintheche_united |
Chilumba |
chilumba_barracks |
Mafu |
mafu_stars |
M'mbelwa |
mmbelwa_warriors |
M’mbelwa |
mmbelwa_warriors |
M'mberwa |
mmbelwa_warriors |
Chipolopolo |
chipolopolo_boys |
Lube |
lube_masters |
Euthini |
euthini_veterans |
Chihame |
chihame_all_stars |
Luviri |
luviri |
teamAlias combines with teamId, team, competition, postType, limit and cursor. Like every filter it is applied to the item as a whole, so teamAlias=Mafu&teamId=luviri returns items that contain a match of each of the two clubs.
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="}Items whose latest observation is not done have "structured": null and "korean": null:
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="Response (a call-up post with facts, and a post that is still awaiting processing)
{ "items": [ { "rawUpdateId": 3, "source": "whatsapp", "sourceMessageId": "3EB0A1F2C4D5E6F70003", "sourceServerId": null, "sourceTimestamp": "2026-09-19T14:10:05.000Z", "observationCount": 1, "latestObservation": { "id": 3, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e03", "receivedAt": "2026-09-19T14:10:07.330Z", "text": "Congratulations to Chizumulu United's Mayamiko Chiusiwa on his call-up to the Flames U23 squad!\n\n#CINRFALeagueOne #NRFATransformingTheGame" }, "processing": { "state": "done", "method": "ai", "model": "deepseek/deepseek-v4-flash-0731", "postType": "community", "translationState": "done", "reusedFromObservationId": null, "error": null }, "structured": { "schemaVersion": 2, "postType": "community", "competition": { "code": "nrfa_league_one", "conflict": false, "evidence": [ { "code": "nrfa_league_one", "signal": "#CINRFALeagueOne", "kind": "hashtag" } ] }, "matches": [], "facts": [ { "index": 0, "type": "call_up", "data": { "player": "Mayamiko Chiusiwa", "club": "Chizumulu United", "position": null, "called_to": "Flames U23", "squad_text": null, "competition": null, "opponent": null, "first_leg_date_text": null, "camp_date_text": null }, "evidence": { "text": true, "attachments": [] } }, { "index": 1, "type": "community", "data": { "occasion": "congratulations", "recipients": [ "Mayamiko Chiusiwa" ] }, "evidence": { "text": true, "attachments": [] } } ] }, "korean": { "text": "치주물루 유나이티드의 마야미코 치우시와가 Flames U23 대표팀에 소집된 것을 축하합니다!\n\n#CINRFALeagueOne #NRFATransformingTheGame", "model": "deepseek/deepseek-v4-flash-0731" } }, { "rawUpdateId": 2, "source": "whatsapp", "sourceMessageId": "3EB0A1F2C4D5E6F70002", "sourceServerId": null, "sourceTimestamp": "2026-09-19T04:48:18.000Z", "observationCount": 1, "latestObservation": { "id": 2, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e02", "receivedAt": "2026-09-19T04:48:20.951Z", "text": "Matchday live" }, "processing": { "state": "awaiting_ai", "method": null, "model": null, "postType": null, "translationState": "none", "reusedFromObservationId": null, "error": null }, "structured": null, "korean": null } ], "nextCursor": "MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="}The last page (fewer items than limit) has "nextCursor": null:
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="Response (last page)
{ "items": [ { "rawUpdateId": 1, "source": "whatsapp", "sourceMessageId": "3EB0A1F2C4D5E6F70001", "sourceServerId": null, "sourceTimestamp": "2026-09-19T04:47:30.000Z", "observationCount": 1, "latestObservation": { "id": 1, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e01", "receivedAt": "2026-09-19T04:47:32.412Z", "text": "Matchday live \n\n#NRFA LEAGUE 2\n#Week 4 Fixtures\n\n#SATURDAY 19-09-2026\n\n#CLUSTER A \nWovwe Vision 🆚 AirSport FC \nWovwe Ground \nMphompha Utd 🆚 Mhuju FC \nMphompha Ground \n\n#NRFADivisionLeague2\n#NRFATransformingTheGame" }, "processing": { "state": "done", "method": "deterministic", "model": null, "postType": "fixtures", "translationState": "none", "reusedFromObservationId": null, "error": null }, "structured": { "schemaVersion": 2, "postType": "fixtures", "competition": { "code": "nrfa_league_two", "conflict": false, "evidence": [ { "code": "nrfa_league_two", "signal": "#NRFA LEAGUE 2", "kind": "hashtag" }, { "code": "nrfa_league_two", "signal": "#NRFADivisionLeague2", "kind": "hashtag" } ] }, "matches": [ { "index": 0, "homeTeam": "Wovwe Vision", "awayTeam": "AirSport FC", "homeScore": null, "awayScore": null, "status": "scheduled", "competition": "NRFA LEAGUE 2", "competitionCode": "nrfa_league_two", "competitionBasis": "match-text", "venue": "Wovwe Ground", "dateText": "SATURDAY 19-09-2026", "timeText": null, "roundText": "Week 4", "minuteText": null, "competitionGroupText": "CLUSTER A", "evidence": { "text": true, "attachments": [] }, "events": [] }, { "index": 1, "homeTeam": "Mphompha Utd", "awayTeam": "Mhuju FC", "homeScore": null, "awayScore": null, "status": "scheduled", "competition": "NRFA LEAGUE 2", "competitionCode": "nrfa_league_two", "competitionBasis": "match-text", "venue": "Mphompha Ground", "dateText": "SATURDAY 19-09-2026", "timeText": null, "roundText": "Week 4", "minuteText": null, "competitionGroupText": "CLUSTER A", "evidence": { "text": true, "attachments": [] }, "events": [] } ], "facts": [] }, "korean": null } ], "nextCursor": null}GET /v1/structured/{rawUpdateId}/history
Section titled “GET /v1/structured/{rawUpdateId}/history”Every observation of one message, oldest first (at most 200), each with its own processing state and structured result. rawUpdateId is the number from the items above.
curl "https://api.chizumulu.net/v1/structured/4/history"Response
{ "rawUpdateId": 4, "source": "whatsapp", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "observations": [ { "id": 4, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e04", "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:20:54.451Z", "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": null, "competitionGroupText": null, "evidence": { "text": true, "attachments": [] }, "events": [ { "index": 0, "type": "goal", "minuteText": null, "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" } }, { "id": 6, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06", "sourceTimestamp": "2026-09-19T15:20:52.000Z", "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" } } ]}400rawUpdateId must be a positive integerwhen the path segment is not a number of up to 15 digits.404not foundwhen no message has that id.
GET /v1/feed
Section titled “GET /v1/feed”Every observation in the order the API stored them. This is the route for keeping your own copy up to date.
| Parameter | Meaning |
|---|---|
after |
Return observations whose id is greater than this. Default 0. Must be a non-negative integer of up to 15 digits, otherwise 400 after must be a non-negative integer id. |
limit |
Page size. |
curl "https://api.chizumulu.net/v1/feed?after=3&limit=3"Response
{ "updates": [ { "id": 4, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e04", "rawUpdateId": 4, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:20:54.451Z", "collectorObservedAt": "2026-09-19T15:20:54.140Z", "content": { "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" } }, { "id": 5, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e05", "rawUpdateId": 5, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70005", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:24:20.000Z", "receivedAt": "2026-09-19T15:24:22.805Z", "collectorObservedAt": "2026-09-19T15:24:22.493Z", "content": { "text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" } }, { "id": 6, "observationId": "8f1c6a52-3b7e-4c1a-9d20-0a1b2c3d4e06", "rawUpdateId": 4, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:26:31.207Z", "collectorObservedAt": "2026-09-19T15:26:30.911Z", "content": { "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" } } ], "nextAfter": 6, "hasMore": true}id is the observation sequence number. nextAfter is the last id returned (or your own after when nothing new exists) and is always safe to store. hasMore is true when the page was full. Details in Pagination.
GET /v1/updates
Section titled “GET /v1/updates”The raw messages, newest first, one item per message. Text and timestamps come from the first observation of each message.
| Parameter | Meaning |
|---|---|
limit |
Page size. |
cursor |
The nextCursor of the previous page. Opaque. 400 when invalid. |
since |
Only messages positioned strictly after this ISO-8601 timestamp. It must include an offset (Z or +02:00). 400 since is not a valid, parseable timestamp otherwise. |
chizumulu |
true keeps messages whose text mentions “chizumulu”, false keeps the others. Any other value is ignored. |
curl "https://api.chizumulu.net/v1/updates?limit=2"Response
{ "updates": [ { "id": 5, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70005", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:24:20.000Z", "receivedAt": "2026-09-19T15:24:22.805Z", "content": { "text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" }, "chizumuluRelated": false }, { "id": 4, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70004", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:20:52.000Z", "receivedAt": "2026-09-19T15:20:54.451Z", "content": { "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" }, "chizumuluRelated": false } ], "nextCursor": "MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="}chizumuluRelated is a cheap check made when the message arrives: does the text contain “chizumulu” (case-insensitive)? It is null when the message has no text. It is not a classification and not derived from the structured data.
GET /v1/latest
Section titled “GET /v1/latest”The newest message, in the same order as /v1/updates. Same item shape as one entry of /v1/updates. Returns 404 no updates yet when nothing has been stored.
curl "https://api.chizumulu.net/v1/latest"Response
{ "id": 5, "source": "whatsapp", "sourceChannelId": "120363420132778835@newsletter", "sourceMessageId": "3EB0A1F2C4D5E6F70005", "sourceServerId": null, "sourceTimestamp": "2026-09-19T15:24:20.000Z", "receivedAt": "2026-09-19T15:24:22.805Z", "content": { "text": "Fulltime \n\nMafu Stars FC 3–1 Luviri FC \n\n#CINRFALeagueOne\n#NRFATransformingTheGame" }, "chizumuluRelated": false}GET /v1/status
Section titled “GET /v1/status”Health of the service. See Status.
Derived matches
Section titled “Derived matches”One real match usually shows up as many observations: live updates, half time, full time and result bundles. The API groups the structured observations that clearly describe the same match into one derived match. It is a derived projection over structured data, built by fixed, explainable rules (no model, no fuzzy matching). Like all derived data it can be missing or wrong. Raw posts and structured observations are never changed by it.
matchIdis an opaque identifier assigned by this API. It stays the same as more observations attach to the match. It is not a WhatsApp id, not a message id and not an observation id.- Conservative by design. A false merge is treated as worse than a missed one. Observations are merged only when they have the same teams (a reviewed team id, or exactly the same written name for clubs that have none), no conflicting competition or explicit date, fall inside a time window, and show scores and status only moving forward. Anything else stays a separate match, so one real match can occasionally appear as two.
- If several matches would fit one observation, it is attached to none of them and listed under
unresolvedinstead of being guessed. qualityisok, orconflictwhen a contradicting observation (for example a score that went down) was kept apart.
GET /v1/live
Section titled “GET /v1/live”The matches that are credibly live right now, most recently updated first. No parameters. It always returns 200; with nothing live, matches is an empty list. asOf is the time the answer was computed.
{ "matches": [ { "matchId": "0b8f6c1e-6f52-4a35-9c58-2f0f5d8e9a11", "competition": { "code": "nrfa_league_one" }, "homeTeam": { "id": "raiply", "name": "Raiply FC" }, "awayTeam": { "id": "chipolopolo_boys", "name": "Chipolopolo Boys FC" }, "status": "live", "score": { "home": 3, "away": 0 }, "minute": "48'", "date": null, "round": null, "firstSeenAt": "2026-09-20T13:04:00.000Z", "lastSeenAt": "2026-09-20T14:19:00.000Z", "completedAt": null, "updateCount": 6, "quality": "ok" } ], "asOf": "2026-09-20T14:30:00.000Z"}- Intentionally conservative. A match is listed only while its evidence is fresh: its status is
liveorhalf_time, its last update is at most 45 minutes old, the literal minute advanced by the time since it was read is still plausible, it is less than 4 hours since its first live update, and nothing contradicts it or is waiting unresolved. If that is uncertain, the match is omitted. A match with no news for over 45 minutes therefore drops out and comes back with its next update. - Omission is not deletion. A match missing from
/v1/live(stale, ambiguous or finished) is still a stored derived match. It stays available at/v1/matches/{matchId}, and its observations stay in/v1/structured.
GET /v1/matches/{matchId}
Section titled “GET /v1/matches/{matchId}”One derived match with everything it was built from. It has the fields shown above, plus updates (every attached observation, oldest first: rawUpdateId, observationId, matchIndex, sourceTimestamp, outcome, status, homeScore, awayScore, minute) and unresolved (observations that fitted several matches and were attached to none). 400 for a malformed id and 404 for an unknown one. The score timeline is what the updates say; events are not merged across observations.
GET /v1/videos
Section titled “GET /v1/videos”A small, separate pass-through of one public YouTube channel feed: the Korean fan channel 창박골 Changbakgol (channel_id=UC5JtIP2gExbPDMbNMFr3aHA). It is not NRFA content, not official, and not connected to the messages or the structured model above. The API reads YouTube’s public Atom feed, normalizes the entries and returns them in the feed’s own order (newest first). No parameters: any query string is ignored.
{ "videos": [ { "videoId": "NjG16iYc0Go", "title": "…", "publishedAt": "2026-09-20T09:03:56.000Z", "url": "https://www.youtube.com/watch?v=NjG16iYc0Go", "thumbnailUrl": "https://i3.ytimg.com/vi/NjG16iYc0Go/hqdefault.jpg" } ], "fetchedAt": "2026-09-20T10:05:00.000Z"}| Field | Meaning |
|---|---|
videos[].videoId |
The YouTube video id. |
videos[].title |
The title as in the feed. |
videos[].publishedAt |
When the video was first published (UTC, ISO 8601), or null. |
videos[].url |
The watch URL. |
videos[].thumbnailUrl |
The feed’s thumbnail image URL, or null. |
fetchedAt |
When this list was fetched from YouTube. |
- Caching. The list is cached for about 10 minutes; requests inside that window do not contact YouTube. The
x-videos-cacheheader isHIT,MISS(just fetched) orSTALE. - If YouTube cannot be reached, the last cached list is served (
x-videos-cache: STALE, andfetchedAtshows its age) and the API waits about a minute before trying again. With nothing cached the response is502{"error":"video feed unavailable"}. - A feed with no entries is
200with"videos": [].
GET /v1/videos/latest
Section titled “GET /v1/videos/latest”The entry of the same feed with the newest publishedAt, as a single object with the fields above (no wrapper). Returns 404 no videos yet when the feed has no entries and 502 video feed unavailable under the same rule as /v1/videos.
Response shapes
Section titled “Response shapes”The complete, machine-readable description of every response is the OpenAPI document.