snapdata

Schema reference

The envelope, the observation, the status enum, and the instrument_id grammar, all generated from one Zod source.

packages/schema is the single source of truth. TypeScript types and JSON Schema are both generated from Zod, never hand-written, and CI fails if the committed artifacts drift from the code.

/schema/v1/observation.json
/schema/v1/envelope.json

Envelope

Every data response has the same outer shape.

{
  "schema_version": "1.0",
  "generated_at": "2026-08-11T13:02:11Z",
  "dataset": "gold",
  "scope": "in",
  "resolution": "daily",
  "calendar": "IN-BULLION",
  "unit": { "quantity": "gram", "currency": "INR" },
  "coverage": { "from": "2026-08-11", "to": "2026-08-11" },
  "sources": [ ... ],
  "observations": [ ... ]
}
FieldTypeNotes
schema_version"1.0"Bumped only on a breaking change.
generated_atRFC 3339 UTCWhen these numbers were produced, not when a cron last ran. Unchanged data keeps its original stamp.
datasetenum | "all"gold, silver, equity-indices, fx, crude.
scopein | world | allThe denomination of the values, not your nationality.
resolutiondaily | monthlymonthly only when every value in the file came from a monthly upstream.
calendarcalendar id | "MIXED"MIXED on aggregates.
unitobject | nullnull on aggregates, whose rows genuinely disagree.
coverage{ from, to }Inclusive date bounds of the observations present.
derivationstringPresent only on derived datasets. The formula, verbatim. No dataset is currently derived; the field remains part of the schema for ones that will be.
sourcesarrayEvery source that contributed, with licence and retrieval time.

sources

{
  "id": "ecb",
  "name": "European Central Bank reference rates",
  "url": "https://www.ecb.europa.eu/stats/eurofxref/",
  "licence": "ECB permissive reuse with attribution",
  "retrieved_at": "2026-08-11T13:01:00Z"
}

Derived rows resolve to the sources of their inputs. Gold's envelope names the actual upstreams rather than saying "derived" and leaving you to guess.

retrieved_at moves only when the upstream body actually changed. A re-fetch that returns identical bytes taught us nothing, and the file should not claim otherwise.

Observation

{
  "date": "2026-08-11",
  "instrument": "XAU.24K",
  "instrument_id": "XAU.24K.INR.G",
  "value": 15280.30,
  "close": 15280.30,
  "prev_close": 15064.10,
  "change_pct": 1.4340,
  "status": "final",
  "is_trading_day": true,
  "filled_from": null,
  "source": "derived",
  "revision": 0
}
FieldTypeNotes
dateYYYY-MM-DDMarket's own local calendar. Never a timestamp.
instrumentstringShort display name. Unique only within (dataset, scope).
instrument_idstringGlobally unique. See the grammar below.
valuenumber | nullnull only when status is missing or not_backfilled.
closenumber | nullSame as value for daily series.
prev_closenumber | nullPrevious trading day's close. Skips closed days.
change_pctnumber | nullPercent vs prev_close, 4dp. null when either side is absent.
statusenumSee below.
is_trading_daybooleanFrom packages/calendar, never from whether a source returned data.
filled_fromYYYY-MM-DD | nullThe date a value was carried forward from. Always earlier than date.
sourcestringA source id, or derived.
revisioninteger0 until a correction lands after the settling window.

Invariants the schema enforces

These are not conventions; they are validated before anything is published, and a violation fails the run:

  • A null value requires status of missing or not_backfilled.
  • missing and not_backfilled must carry a null value.
  • A value on a non-trading day must declare filled_from. Silent forward-fill is rejected outright.
  • filled_from is strictly earlier than date.
  • Every observation's date falls inside the envelope's coverage.

status

final | provisional | estimated | missing | not_backfilled

Full semantics are on the Dates & trading days page. The short version: missing means no value exists, not_backfilled means one exists upstream and we have not imported it, and derived values inherit the weakest status among their inputs.

instrument_id grammar

SYMBOL[.QUALIFIER].CURRENCY.UNIT
UnitMeaning
Ggram
KGkilogram
OZTtroy ounce
BBLbarrel
RATEcurrency ratio
IDXindex points

XAU.24K means INR per gram under scope in and USD per troy ounce under scope world. Joining across scopes on the short name produces silent garbage: a number three orders of magnitude off, in the wrong currency, that will not look obviously wrong on a chart. Always join on instrument_id.

Examples:

XAU.24K.INR.G      Gold 24K, INR per gram
XAU.22K.INR.G      Gold 22K, INR per gram
XAU.USD.OZT        Gold, USD per troy ounce
XAG.INR.KG         Silver, INR per kilogram
NIFTY50.INR.IDX    Nifty 50
USD.INR.RATE       US dollar / Indian rupee
BRENT.USD.BBL      Brent crude spot

/api/v1/meta.json

Per-source health. Its one job is to never report healthy while serving stale.

{
  "schema_version": "1.0",
  "generated_at": "2026-08-11T13:02:11Z",
  "status": "degraded",
  "commit_sha": "c426fc9",
  "last_run": { "id": "01J...", "at": "...", "trigger": "India-egress runner", "outcome": "partial" },
  "duty_table_version": "2026.08.1",
  "sources": [
    {
      "id": "nse",
      "name": "National Stock Exchange of India",
      "latest_date": "2026-08-08",
      "last_success_at": "2026-08-08T10:04:00Z",
      "staleness_hours": 75.0,
      "staleness_slo_hours": 96,
      "stale": false,
      "consecutive_failures": 3,
      "last_error_class": "blocked"
    }
  ],
  "stale_datasets": ["equity-indices"]
}

status is ok only when every source is inside its SLO. A source with no staleness reading at all is treated as stale, not as fine.

/api/v1/coverage.json

Per-series earliest date, resolution, known gaps, licence, and, the part that matters, why coverage stops where it does. See Coverage & backfill.

/api/v1/revisions.json

Corrections made after a dated file's 48-hour settling window closed.

{
  "instrument_id": "XAU.24K.INR.G",
  "date": "2026-08-11",
  "revision": 1,
  "previous_value": 15280.30,
  "new_value": 15281.05,
  "reason": "upstream USD/INR restatement",
  "revised_at": "2026-08-16T09:12:00Z"
}

If you cache dated files (and you should, they are immutable) this is how you find out one changed.

On this page