# Events: the hazard record behind a result

A backtest tells you six years paid and names the events it paid on. This page
is where those names come apart into the record itself: the catalogue Craton
read, the exact solutions that qualified, and the rule that collapsed several
of them into one occurrence.

Two routes, both reads, neither metered:

| Route | What it answers |
| --- | --- |
| `GET /events/{peril}` | The pinned event catalogue for a peril, filtered the way a structure filters it |
| `GET /events/{peril}/{event_id}/footprint` | One event's footprint as the record has it: the occurrence it belongs to, and the box that occurrence spans |

Read [the quickstart](quickstart.md) first for the two values you export
(`CEDE_BASE_URL`, `CEDE_API_KEY`) and how to get a key from `POST /signup`.
[Feeds](feeds.md) is the neighbouring page: it serves the publisher's bytes
verbatim so you can check the digest, and this page serves the same bytes
*parsed the way the engine parses them*.

## What is served, and what is not

Event catalogues are served for perils this build pins a catalogue of. Today
that is **`earthquake`**, from the USGS Kanto extract. A peril whose pinned
record is a gridded daily series — rainfall, wind — has no event catalogue
here, and asking for one is answered with a `404 no_event_catalogue` rather
than with events manufactured out of exceedance days:

```json
{ "error": {
    "status": 404,
    "code": "no_event_catalogue",
    "message": "this build pins no event catalogue for peril 'tropical_cyclone'. Event catalogues are served for earthquake. …" } }
```

The refusal names the perils that do answer, so finding the right one costs a
round trip rather than a search.

**A footprint here is a cluster of catalogue solutions, not a shaking map.**
The pinned snapshot holds hypocentres, depths and magnitudes — it holds no
modelled intensity surface, so none is published. Synthesising one would be a
*model*, with assumptions and a version and a card on it, and a model belongs
in the registry rather than behind a data route. Every footprint response says
this in its own `limitations`.

---

## Reading the catalogue

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first — see the quickstart}"

# 1 — The first page of the pinned earthquake catalogue.
curl -sS "$CEDE_BASE_URL/events/earthquake?limit=5" \
  -H "Authorization: Bearer $CEDE_API_KEY" > events.json

python3 -c 'import json
page = json.load(open("events.json"))
feed = page["feed"]
print("catalogue:", feed["id"], "vintage", feed["vintage"])
print("digest:   ", feed["sha256"])
print("in snapshot:", page["counts"]["events_in_snapshot"],
      "selected:", page["counts"]["selected"],
      "on this page:", page["counts"]["returned"])
for event in page["events"]:
    print("  ", event["time"], "M" + str(event["magnitude"]),
          event["latitude"], event["longitude"],
          "depth", event["depth_km"], "km", "—", event["event_id"])'

# 2 — The same catalogue, filtered the way a structure filters it: a magnitude
#     floor, a box and a period. The response states the filter back.
curl -sS "$CEDE_BASE_URL/events/earthquake?min_magnitude=6.5&min_lat=34&max_lat=37&min_lon=138&max_lon=141.5&from=1986-01-01&to=2026-01-01&limit=50" \
  -H "Authorization: Bearer $CEDE_API_KEY" > qualifying.json

python3 -c 'import json
page = json.load(open("qualifying.json"))
selection = page["selection"]
print("M>=", selection["minimum_magnitude"],
      "in", selection["box"]["min_latitude"], "-", selection["box"]["max_latitude"], "N",
      selection["box"]["min_longitude"], "-", selection["box"]["max_longitude"], "E")
print("from", selection["from"], "to", selection["to"], "(", selection["period_bounds"], ")")
print("defaulted:", selection["defaulted"] or "nothing — every filter was yours")
print(page["counts"]["selected"], "events qualify")
print("next page:", page["page"]["next"])'

# 3 — The digest on the response is the digest of the pinned file, so the
#     events above and the bytes GET /feeds serves are one snapshot.
FEED_ID="$(python3 -c 'import json; print(json.load(open("events.json"))["feed"]["id"])')"
VERSION="$(python3 -c 'import json; print(json.load(open("events.json"))["feed"]["version"])')"

curl -sS "$CEDE_BASE_URL/feeds/$FEED_ID/versions/$VERSION" \
  -H "Authorization: Bearer $CEDE_API_KEY" > manifest.json

python3 -c 'import json
served = json.load(open("events.json"))["feed"]["sha256"]
pinned = json.load(open("manifest.json"))["sha256"]
print("same snapshot:", served == pinned)'
```

### The parameters

| Parameter | Default | What it does |
| --- | --- | --- |
| `feed`, `version` | the latest pinned vintage | Pin the exact snapshot. A result names both in its assumption set; pass them to read the record that result read. |
| `min_magnitude` | the snapshot's own minimum | Magnitude floor. Never below the snapshot's minimum: the events under it are missing from the extract, not from history. |
| `max_depth_km` | none | Focal depth limit. An event with no published depth is excluded when a limit is set, never assumed shallow. |
| `from`, `to` | the snapshot's period of record | `from` inclusive, `to` exclusive, so consecutive periods tile without counting a midnight event twice. A bare date is midnight UTC. |
| `min_lat`, `max_lat`, `min_lon`, `max_lon` | the snapshot's own box | An axis-aligned box, inclusive on every edge. All four or none — three edges describe no region. |
| `limit`, `offset` | 100, 0 | Paging. The ceiling is 1000; follow `page.next`, which pins the feed and version so a walk cannot mix two vintages. |

Anything else is refused with `422 invalid_request` rather than ignored: a
parameter answered as though it were another would hand you a different set of
events with no way to tell.

---

## Reading one event's footprint

<!-- cede:runnable -->

```bash
set -euo pipefail

: "${CEDE_BASE_URL:?export CEDE_BASE_URL first — see the quickstart}"
: "${CEDE_API_KEY:?export CEDE_API_KEY first — see the quickstart}"

# 1 — Find a large event. Every event carries the address of its own footprint.
curl -sS "$CEDE_BASE_URL/events/earthquake?min_magnitude=7.0&limit=1" \
  -H "Authorization: Bearer $CEDE_API_KEY" > big.json

FOOTPRINT="$(python3 -c 'import json
page = json.load(open("big.json"))
assert page["events"], "no M7.0+ event in this catalogue"
print(page["events"][0]["footprint"])')"
echo "reading $FOOTPRINT"

# 2 — The footprint: the occurrence this event belongs to.
curl -sS "$CEDE_BASE_URL$FOOTPRINT" \
  -H "Authorization: Bearer $CEDE_API_KEY" > footprint.json

python3 -c 'import json
document = json.load(open("footprint.json"))
occurrence = document["footprint"]["occurrence"]
extent = document["footprint"]["extent"]
print("event:     ", document["event_id"], "M" + str(document["event"]["magnitude"]))
print("catalogue: ", document["feed"]["id"], "vintage", document["feed"]["vintage"])
print("window:    ", occurrence["window"], "from", occurrence["from"], "to", occurrence["to"])
print("solutions: ", occurrence["event_count"], "— peak", occurrence["peak_event_id"])
print("extent:    ", extent["min_latitude"], "-", extent["max_latitude"], "N",
      extent["min_longitude"], "-", extent["max_longitude"], "E")
for event in occurrence["events"][:10]:
    print("   ", event["time"], "M" + str(event["magnitude"]), event["event_id"])
print()
for limitation in document["limitations"]:
    print("*", limitation)'

# 3 — Your structure's own window, not the default: a 24-hour event window
#     collapses fewer solutions into one occurrence than a 72-hour one.
curl -sS "$CEDE_BASE_URL$FOOTPRINT?window=PT24H&min_magnitude=5.0" \
  -H "Authorization: Bearer $CEDE_API_KEY" > narrow.json

python3 -c 'import json
wide = json.load(open("footprint.json"))["footprint"]["occurrence"]
narrow = json.load(open("narrow.json"))["footprint"]["occurrence"]
print(wide["window"], "->", wide["event_count"], "solutions")
print(narrow["window"], "->", narrow["event_count"], "solutions at M>=",
      narrow["minimum_magnitude"])'
```

### What the occurrence means

A cat-in-a-box trigger pays **per occurrence**, and a mainshock's aftershocks
are not separate occurrences. The rule is the aggregation window, measured
from the **first** event of the cluster and never from the previous one — a
chained rule would merge an unbounded aftershock sequence into a single
occurrence, which is not what a 72-hour event window means.

That rule is the step between "the catalogue holds 2409 events" and "the
backtest paid six times", and this route is where you can watch it happen:

* `window` takes your structure's `aggregation_window.duration`;
* `min_magnitude` takes its attachment level, so only the solutions that
  qualify take part in the clustering;
* `occurrence.events` is then exactly the set of catalogue rows the backtest
  collapsed into that one payout, and `peak_event_id` is the magnitude the
  payout function read.

The rule is **time only**, applied across the whole extract. Two solutions far
apart inside the box can land in one occurrence, which is why every member is
listed with its own position rather than summarised into a point — and why
`extent` is a bounding box of solutions rather than a contour of anything.

---

## What these documents are, and are not

* **Hazard data.** A public catalogue of natural events — time, hypocentre,
  magnitude — published by a government agency. Feeds on Craton are aggregate
  only: no schedule, no insured value, no individual-level record appears on
  this surface or any other feed surface.
* **A record, not a view of risk.** No price, no loading, no statement about
  anyone's willingness to take the hazard on. Prices live behind
  [Price](price.md), with their assumptions exposed.
* **Not a modelled footprint.** See the limitations on every footprint
  response. A ground-motion or damage surface would be a model in the registry,
  carrying a card that states its assumptions and its author.
* **Free.** Craton bills per object and per model run; reading the record behind
  a result is neither, so these routes are not metered. `GET /usage` will not
  show them.

The published schemas are `event-catalogue.v0.schema.json` and
`event-footprint.v0.schema.json`, both listed in the API contract document with
every other response shape — validate against them while holding no Craton code.

## Where to go next

* [Feeds](feeds.md) — the same snapshots as bytes, with the digest to check.
* [Backtest](backtest.md) — the results these events explain.
* [Checking a backtest yourself](backtest.md#checking-it-yourself) — the whole
  loop, from a payout back to the catalogue rows behind it.
