Getting the files
Sign in and open Bulk Data Download. Every plan can see the page: every file for every period, with itssha256, byte size and row count, a
per-period completeness label so you can tell which dates are settled before you load one,
and a summary of what each file contains. Downloading the files is included on the
Intelligence, Enterprise and Academic plans.
If you need this automated rather than downloaded by hand, tell us — a programmatic manifest
exists and we will publish it as a supported contract once every period exports cleanly.
Files
Reading the two formats
Parquet carries real types: a null is a null, an array is aLIST, 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.
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.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 nomagnitude, systemic_importance, propagation_potential or
market_sensitivity, the CAMEO+ family carries no fatalities, and a story with no coded
events has no fatality count at all. 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 three-valued booleans such as civilian_targeting: 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 atcoded_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.
Events
One row per event, ordered byevent_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.
Summary statistics
Every published file carries a summary of its own contents, computed by the run that cut it and shown on Bulk Data Download. It is there so you can see what is in a period before downloading it. Value counts — the most frequent values, with how many distinct ones exist and how many rows carry none. An absent value is reported separately and never counted as a category.
Distributions — count of rows carrying the metric, minimum, the 10th, 25th, 50th, 75th
and 90th percentiles, maximum and mean. Quantiles are exact and inclusive-interpolated, the
same convention pandas and Excel use, so you can reproduce them from the file you downloaded.
Percentiles are computed over the rows where the metric is PRESENT, so a metric that applies
to part of the file describes that part — it is never padded with zeroes to fill the rest.
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 anIS 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.

