Skip to content

Raw vs structured

The API separates what was seen from what was derived from it.

  • A message is one logical post in the source. It has a rawUpdateId (a number) and a sourceMessageId (the source’s own key).
  • An observation is one time the collector saw that post: its text (or caption) at that moment. Every observation has an id (ingestion sequence number, the key of /v1/feed) and an observationId (a stable string).
  • Observations are stored as received and are never modified or deleted.
  • Structured data is derived from one observation’s text. It is a separate layer: the original text is always returned next to it, never replaced by it.

If a post is edited in the source, the collector sees it again. The API stores that as another observation of the same message (same rawUpdateId, new id and observationId, observationCount goes up). The earlier observation is kept.

The same can happen for other reasons, such as the source re-delivering the same text. The API records what the collector observed; it does not claim to know the source’s own revision order.

Route Text of the…
/v1/updates, /v1/latest first observation of each message
/v1/feed each observation, as its own item
/v1/structured latest observation of each message (“latest” = the highest ingestion id, not a source timestamp)
/v1/structured/{rawUpdateId}/history every observation, oldest first, at most 200

Below, message 4 was edited: the first version says 88 GOAL! and the edit says 88' GOAL!. Look at observationId, receivedAt and the different minuteText in each observation’s structured result.

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

Compare the same message in /v1/updates (first observation’s text) with /v1/structured (latest observation’s text) in the endpoint examples.

Every observation moves through processing on its own. processing.state of the latest observation is one of:

processing.state Meaning structured korean
unregistered Stored, not picked up yet. Only state is present in processing. null null
pending Registered, waiting to be processed or being retried. null null
awaiting_ai Needs the language-model step, which has not run yet. null null
done Processed. the result, when there is one the translation, when there is one
failed Processing gave up. processing.error holds a short diagnostic. The original text is unaffected. null null

Rules you can rely on:

  • structured is never shown for an observation that is not done. In particular an older observation’s facts are never presented as the current state of an edited message.
  • A done post can legitimately have zero matches and zero facts (a birthday greeting is a valid result). That is different from structured: null.
  • A done observation with no text (for example an image-only post) has method: "none" and structured: null: there was nothing to extract from.

When state is not unregistered, processing also contains:

Field Meaning
method How the result was produced: deterministic (rule-based parser), ai (a language model, constrained by the parser when it could read the post), reused (copied from an earlier observation with identical text), none (no text to process), or null until done.
model The model behind an ai result, otherwise null.
postType The post type once done with a result, otherwise null.
translationState none, done or failed.
reusedFromObservationId Set when method is reused.
error A short diagnostic for failed. Do not depend on its wording.

See the awaiting-processing item in the second page example.

korean is a machine translation of the whole post text, produced together with the structured data. It is derived, may contain mistakes, and never replaces latestObservation.text. It is null unless the observation is done and a translation exists.