Skip to content

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.

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.

  • null means not established by the source. It is not 0 and 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 writes M'mberwa Warriors while other posts write M'mbelwa Warriors is returned as written.)
  • Fields that describe a kind (status, event type, postType, competitionCode) use fixed values, listed below, or null.
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[] 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.
Terminal window
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
}

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)

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 as attachmentFacts next to structured: null. It is never merged into a text result, and processing still 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):

Terminal window
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="
}

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 2 gives NRFA LEAGUE 2). An encoded hashtag such as #CINRFALeagueOne or #NRFADivisionLeague2 is not a competition name: it is never copied and never expanded into words. With only such a hashtag, competition is null.
  • competitionCode is a classification, decided by fixed rules from known wording and hashtags in the post. Current values: nrfa_league_one and nrfa_league_two. It is null when there is no unambiguous signal.
  • competitionBasis says how a match got its code: match-text (from that match’s own competition text) or post-signal (inherited from a single unambiguous signal elsewhere in the post, such as a hashtag), or null.
  • structured.competition is the post-level view: code, conflict (true when the post carries signals for more than one competition, and then code is null) and evidence (the signals that produced it, each { code, signal, kind } with kind hashtag or text).

Use competitionCode to filter by competition. Use competition only when you want the words the post used.