Skip to main content
Bulk downloads hand you the same coded events the API serves, as files instead of pages of JSON. One file per calendar month, plus a rolling full-history file, in two formats.

Files

Reading the two formats

Parquet carries real types: a null is a null, an array is a LIST, and a timestamp is a TIMESTAMP. Prefer it if your tooling can read it. CSV has neither nulls nor arrays, so two conventions apply and they are the only differences between the formats:
  • Array columns are JSON arrays, written as a single quoted field — parse them with your JSON reader, not by splitting on a delimiter. A title containing a comma would break the row otherwise, and titles contain commas.
  • An empty field is a null. CSV cannot distinguish “no value” from “the empty string”, and in this data the distinction matters: see the note on metrics below.

Nulls are information, not gaps

A null metric does not mean zero. It means the column does not apply to that row: the conflict family carries no magnitude, systemic_importance, propagation_potential or market_sensitivity, and the CAMEO+ family carries no fatalities. Coalescing these to 0 on load will invent measurements that were never taken, and any average you compute afterwards will be wrong in a direction you cannot recover. The same applies to civilian_targeting, which is deliberately three-valued: true where a coder judged civilians were targeted, false where a coder judged they were not, and null where no coder evaluated the question at all.

Backtesting: what you can and cannot claim as-of

These files support a real point-in-time filter, and they do not support a full vintage. The difference decides whether a backtest is sound, so it is worth being exact.

The event SET is point-in-time. Use coded_at.

coded_at is the moment our coder produced the event, recorded per row and populated on 100% of rows. Every event is derived from articles published before it, and those articles are cited in source_urls, so the claim is checkable rather than asserted. That means WHERE coded_at <= T gives you the set of events that existed in our database at time T. It is a filter on a recorded fact, not a reconstruction. It is the same lever the API exposes as observed_start / observed_end. Measured over July 2026 (42,603 events): 79.4% were coded within a day of the event date, 98.6% within two days, 99.2% within seven. Median lag 16 hours, 90th percentile 33. Only 0.61% of July events were coded after July ended. So do not filter a backtest on event_date alone. Doing so hands your strategy events it could not have seen — about 1.4% of any given month arrived more than two days late, and the tail runs to weeks.

The VALUES in a row are not vintaged.

The row carries what we know now, not what we knew at coded_at. A later article can sharpen the location, the entity resolver can attach a name it could not resolve at the time, an adjudicator can merge the event with another. The settle overwrites in place; there is no per-field history. So coded_at <= T gives you the right ROWS with today’s VALUES. For most event-driven work that is fine — what happened, where, to whom and when we knew it are all stable. It matters if your signal keys on a revisable field: the metric scores, entity_names, or incident_uid, which is assigned by an adjudicator that runs after coding. One consequence worth designing for: event_uid is a stable hash of the cluster, but event_date is the coder’s judgement and is re-derived on a re-code. An event whose date moves therefore moves BETWEEN monthly files rather than changing position within one. Match on event_uid across files, not on event_uid within a month.

Coverage

Each file records the settled dates it covers against the calendar days in its period. A month is only complete when those two agree; the manifest publishes both, so a partial month is visible rather than silently short.

Events

One row per record, ordered by event_date, event_uid. Every column below is a field /api/v2/events returns — the file and the endpoint read the same table, so a row in the file and the same row from the API agree field for field. 41 columns, 34 of them nullable.

How the export differs from the warehouse

Most columns ship exactly as stored. Two conventions apply to all of them, and a handful of columns are computed at export time. An absent value is null, never an empty string. Wherever a value is missing the file carries a true null in Parquet and an empty field in CSV, so an IS NULL check finds every absent value. Column names are the API’s names, so a row in the file and the same row from /api/v2/events agree field for field.