Structured model (schema v2)
structured is the derived result for one observation. structured.schemaVersion is 2.
structured├─ schemaVersion 2├─ postType what kind of post this is├─ competition { code, conflict, evidence } post-level classification├─ matches[] 0..N matches, each with events[]└─ facts[] 0..N facts that are not matches (call-ups, statistics, standings, notices, transfers, community)A post is not forced into a match. A birthday greeting produces matches: [] and facts: [...] (or both empty), and that is a successful result.
Unknown is null
Section titled “Unknown is null”The one rule behind every field: if the post does not say it, the value is null. The API does not guess a score, a team, a minute, a date or a competition, and it does not “helpfully” fill gaps.
nullmeans not established by the source. It is not0and not an empty string.- Values are copied as written: spelling, capitalization, punctuation, dates (
SATURDAY 19-09-2026) and minutes (45+2',88') are never normalized or corrected. If the source contradicts itself or contains a typo, that is preserved. (Real example: a post that writesM'mberwa Warriorswhile other posts writeM'mbelwa Warriorsis returned as written.) - Fields that describe a kind (
status, eventtype,postType,competitionCode) use fixed values, listed below, ornull.
postType
Section titled “postType”| Value | Meaning |
|---|---|
result |
Finished matches: a “Results” list, a “Full time” report, or a recap of final scores. |
live_update |
A match in progress: kick-off, goal, half-time, substitution, chance, or minute-by-minute commentary. |
fixtures |
Upcoming matches with no result yet. |
match_update |
Match news that is none of the above (for example a postponement). |
standings |
A league table or positions. |
player_news |
About a player (call-up, transfer, statistics). |
administrative |
An association or club notice. |
promotional |
A teaser, link or event promotion. |
community |
Greetings such as birthdays or congratulations. |
other |
Anything else. |
Matches
Section titled “Matches”matches[] has one item per match mentioned, all of them: a results post can list ten. Several updates about the same match inside one post are one item.
| Field | Type | Meaning |
|---|---|---|
index |
integer | Position inside this observation. Not a global match identity. |
homeTeam, awayTeam |
string or null |
In A vs B, A 🆚 B and A 1-0 B the left team is home. A host B means A is home. When the source does not establish home and away (for example A will face B) both are null. |
homeScore, awayScore |
integer or null |
Only scores stated for that match. A statistic such as “8 appearances” is never a score. |
status |
scheduled, live, half_time, full_time, postponed, cancelled or null |
The state the post states or clearly implies: a “Results” heading means full_time, a “Fixtures” heading means scheduled. |
minuteText |
string or null |
The match clock the post states for this snapshot, literally (7 MINUTES, 43'). null when there is none or when several different clocks are stated (then each event carries its own). |
competition |
string or null |
See below. |
competitionCode, competitionBasis |
see below | Deterministic classification. |
venue, dateText, timeText, roundText |
string or null |
As written (Wovwe Ground, SATURDAY 19-09-2026, Week 4). |
competitionGroupText |
string or null |
The cluster or group heading the match sits under, as written, without a leading # (CLUSTER A). |
evidence |
{ text, attachments } |
Where the item was read from: the post text, and/or the zero-based index of an attached image. |
events |
array | See below. |
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQwNDo0ODoxOC4wMDBafDI="A fixtures post with clusters (message 1)
{ "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}Match events
Section titled “Match events”events[] lists things the post reports as happening in the match. A post that merely describes possession or general play should produce none, and a clock-only update (43' followed by commentary) keeps the clock in minuteText with no event.
| Field | Type | Meaning |
|---|---|---|
index |
integer | Position inside the match. |
type |
kickoff, goal, own_goal, penalty, half_time, second_half_kickoff, full_time, substitution, yellow_card, red_card, chance, save, free_kick, corner, other |
other is used only for a real in-match incident none of the others fit. |
minuteText |
string or null |
As written. |
team |
string or null |
The team as written in the source. |
homeScore, awayScore |
integer or null |
The score stated at that moment, only when the post states one for the event. |
players |
array of { name, role } |
role is one of scorer, assist, build_up, player_in, player_out, carded, taker, keeper, involved, coach, referee, official, other, or null. |
By convention, an explicit “Full time” line for one match gives status full_time and one full_time event, while a generic “Results” list gives each match full_time without a full_time event per match. Treat the full_time event as optional and rely on status.
facts[] holds things that are not matches. Each item is { index, type, data, evidence }. type is one of the values below and decides the keys of data.
type |
data keys |
|---|---|
call_up |
player, club, position, called_to, squad_text, competition, opponent, first_leg_date_text, camp_date_text |
player_stats |
player, club, competition, stats (array of { name, value }; name is the exact word from the post that labels the number) |
standings |
competition, round_text, date_text, season_text, rows (array of { position, team, played, won, drawn, lost, goal_difference, points }) |
notice |
notice_type (club_rename, registration, disciplinary, schedule_change, venue_change, event_notice, other), subject, old_value, new_value, date_text |
transfer |
player, from_club, to_club, position |
community |
occasion (birthday, congratulations, condolence, thanks, event_promo, other, or null), recipients (array of strings) |
Facts read from an image
Section titled “Facts read from an image”A retained image can be read for one kind of data: a league standings table. It appears as an ordinary standings fact, next to whatever the post text produced, and its evidence says it came from the image and not from the text:
{ "index": 0, "type": "standings", "data": { "competition": "…", "round_text": "…", "rows": [ … ] }, "evidence": { "text": false, "attachments": [0] } }- It is only added; matches, events, the post type and the translation come from the text and are not changed by it. It is not added when the text already contains a standings table.
- Values are copied as printed. A cell that is not a clean number (unreadable, or printed with a letter) is
null; nothing is calculated or corrected. A table whose rows are inconsistent (a gap in the positions, played not equal to won + drawn + lost) is not stored. - The goals-for and goals-against columns are not captured, and other content of the image (fixture or result graphics, lineups, photos) is not read. The table is read whether or not the text produced a structured result. When the post has one, the fact is in
structured.facts. When it has none (an image-only post, or a post whose text processing failed), the fact is returned asattachmentFactsnext tostructured: null. It is never merged into a text result, andprocessingstill describes only the text. - It is derived data like everything else and can be missing or wrong. The image itself is not served by this API.
A call-up post and its facts (the first item of this page):
curl "https://api.chizumulu.net/v1/structured?limit=2&cursor=MjAyNi0wOS0xOVQxNToyMDo1Mi4wMDBafDQ="A call-up post: call_up and community facts, plus a Korean translation
{ "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="}Competition text vs competition code
Section titled “Competition text vs competition code”Two separate things, on purpose.
competition(on a match) is text: the competition name only when it is written as words in the post, copied as written (NRFA LEAGUE 2,Chiwemi Investment NRFA League One, also after a#when the words are there:#NRFA LEAGUE 2givesNRFA LEAGUE 2). An encoded hashtag such as#CINRFALeagueOneor#NRFADivisionLeague2is not a competition name: it is never copied and never expanded into words. With only such a hashtag,competitionisnull.competitionCodeis a classification, decided by fixed rules from known wording and hashtags in the post. Current values:nrfa_league_oneandnrfa_league_two. It isnullwhen there is no unambiguous signal.competitionBasissays how a match got its code:match-text(from that match’s owncompetitiontext) orpost-signal(inherited from a single unambiguous signal elsewhere in the post, such as a hashtag), ornull.structured.competitionis the post-level view:code,conflict(truewhen the post carries signals for more than one competition, and thencodeisnull) andevidence(the signals that produced it, each{ code, signal, kind }withkindhashtagortext).
Use competitionCode to filter by competition. Use competition only when you want the words the post used.