# Point hazard lookup: what the pinned data says about a coordinate

`GET /hazard`, with `peril`, `lat` and `lon` in the query, answers one question — *what has happened at
this place, in the record this build pins* — and answers it synchronously.
There is no object to create first and no job to poll: a coordinate goes in, a
hazard metric comes out, and the response names the exact snapshot it was
computed from.

It is the first slice of Analyse (hazard lookups, event footprints,
accumulation). This build serves all of it: the point lookup here, [the event
catalogue and event footprints](events.md), [the analyse job over an
object](analyse.md) — which is this same lookup, aimed at an object's own
locations — and [accumulation across a set of objects](accumulation.md).

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`.

## What it is for, and what it is not

A trigger is a threshold, and a threshold is a decision about a place. Before
you write `attachment.value` into a risk object, this route tells you what the
public record for that place actually holds: how often the ground there has
broken above magnitude 6, or what daily rainfall total the last forty years
put a one-in-ten-year label on. That number is the input to the decision, and
you can check it against the same bytes the price path reads.

What it is not:

* **not a price.** No expected loss, no loading, no premium. That is
  `POST /objects/{id}/price`, and it needs a whole structure.
* **not a view of risk.** The response says what the record holds, never
  whether the hazard should be carried or on what terms. Every answer carries
  that sentence in its `disclosure` field.
* **not a hazard model.** Nothing is fitted, interpolated or extrapolated. The
  answer is arithmetic over a pinned public record, with the limitations of
  that record listed in the response itself.

## The three parameters

| Parameter | What it is |
| --- | --- |
| `peril` | One of the perils this build pins data for. `GET /health` will not tell you which; the refusal for a wrong one does, and so does the table below. |
| `lat` | Latitude in decimal degrees, negative for south. Degrees, minutes and seconds are not read. |
| `lon` | Longitude in decimal degrees, negative for west. |

Nothing else is read. A fourth parameter is refused rather than ignored — a
call answered as though `lng` were `lon` is a lookup of somewhere else.

## What can be asked, and where

A peril is answerable when a snapshot of it is committed, because the engine
reads snapshots and never the network. So the answerable set is exactly the
pinned data, listed in full on [the pinned data feeds](feeds.md):

| `peril` | Answered from | Where it answers |
| --- | --- | --- |
| `earthquake` | `usgs-eq-kanto` — the USGS catalogue extract | 34.5–36.5N, 138.5–141.0E: the extract's box, less the half-degree cell the lookup counts in |
| `weather_station` | `era5-rain-bangkok`, or any of the four pinned wind series | within 0.25° of one of their grid points (listed below) |
| `tropical_cyclone` | the four pinned wind series — a daily wind maximum is a named-storm index | within 0.25° of one of their grid points (listed below) |

The grid points, which are the publisher's cell centres and not the coordinates
the snapshots were requested at:

| Feed | Grid point |
| --- | --- |
| `era5-rain-bangkok` | 13.743409N, 100.495865E |
| `era5-wind-galveston` | 29.279436N, -94.87326E |
| `era5-wind-miami` | 25.764498N, -80.196075E |
| `era5-wind-new-orleans` | 29.982424N, -90.10489E |
| `era5-wind-tampa` | 27.94376N, -82.49155E |

Two shapes of snapshot, two shapes of answer:

* a **catalogue extract** answers with exceedance — how many solutions the
  record holds at each magnitude threshold inside a half-degree cell around
  your point, as an annual rate. It also groups aftershocks into occurrences
  (72 hours from the first event of a sequence) and reports that rate beside
  it, because a sequence is one event to a structure and many to a catalogue.
* a **gridded daily series** answers with return levels — the record reduced
  to one maximum per calendar year, ranked, and read at the 2-, 5-, 10- and
  20-year return periods by the Weibull plotting position. Nothing beyond the
  record is estimated: a return period the record cannot reach is absent.

Coverage is checked rather than approximated. A cell that reached over the
edge of an extract would count fewer events than really occurred and report a
rate too low for a reason nothing records, so a point near the edge is refused
with the edge it crossed. The area is an axis-aligned cell in degrees rather
than a radius in kilometres, deliberately: a great-circle distance needs
arithmetic that does not replay byte for byte, and replay is worth more here
than a round search area.

---

## Walk it

The block below looks up earthquake hazard at a point in Tokyo and wind at a
point in Miami, prints what each answer rests on, and then fetches the manifest
of the snapshot the first answer named — so the feed id, the vintage and the
digest in the response are three strings you can follow, not three strings you
have to take on trust.

<!-- 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 — Earthquake hazard at 35.6284N, 139.7387E (Tokyo).
curl -sS "$CEDE_BASE_URL/hazard?peril=earthquake&lat=35.6284&lon=139.7387" \
  -H "Authorization: Bearer $CEDE_API_KEY" > quake.json

python3 -c 'import json
answer = json.load(open("quake.json"))
feed = answer["feed"]
print("feed     :", feed["id"], feed["vintage"], feed["sha256"][:12] + "…")
record = answer["assumptions"]["period_of_record"]
print("record   :", record["start"], "to", record["end"],
      "(" + str(record["years"]), "years)")
metrics = answer["metrics"]
print("cell     :", metrics["cell"]["min_latitude"], "to",
      metrics["cell"]["max_latitude"], "N /",
      metrics["cell"]["min_longitude"], "to",
      metrics["cell"]["max_longitude"], "E")
print("events   :", metrics["events_in_cell"])
for row in metrics["exceedance"]:
    print("   M%-5s %4d event(s), %s a year; %d occurrence(s), %s a year"
          % (row["magnitude"], row["events"], row["annual_rate"],
             row["occurrences"], row["annual_occurrence_rate"]))
largest = metrics.get("largest_event")
if largest:
    print("largest  : M%s on %s" % (largest["magnitude"], largest["time"]))
print("disclosure:", answer["disclosure"])'

# 2 — Named-storm wind at 25.7617N, -80.1918E (Miami).
curl -sS "$CEDE_BASE_URL/hazard?peril=tropical_cyclone&lat=25.7617&lon=-80.1918" \
  -H "Authorization: Bearer $CEDE_API_KEY" > wind.json

python3 -c 'import json
answer = json.load(open("wind.json"))
metrics = answer["metrics"]
measure = metrics["measurement"]
print("measures :", measure["variable"], "in", measure["unit"],
      "(" + measure["resolution"] + ")")
grid = metrics["grid_point"]
print("grid cell:", grid["latitude"], grid["longitude"],
      "offset", grid["offset_degrees"]["latitude"],
      grid["offset_degrees"]["longitude"], "degrees")
print("days     :", metrics["days_observed"], "observed,",
      metrics["days_missing"], "missing")
print("wettest/windiest day:", metrics["maximum_daily"]["value"],
      "on", metrics["maximum_daily"]["date"])
for level in metrics["return_levels"]:
    print("   1-in-%-3d %s %s" % (level["return_period_years"],
                                  level["level"], measure["unit"]))
print("known limits:")
for line in answer["assumptions"]["limitations"]:
    print("  -", line)'

# 3 — The snapshot the first answer named, fetched by the id and version in it.
FEED_ID="$(python3 -c 'import json; print(json.load(open("quake.json"))["feed"]["id"])')"
VERSION="$(python3 -c 'import json; print(json.load(open("quake.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, sys
answer = json.load(open("quake.json"))
manifest = json.load(open("manifest.json"))
same = manifest["sha256"] == answer["feed"]["sha256"]
print("the lookup read", manifest["id"], manifest["version"],
      "published by", manifest["publisher"])
print("digests agree:", same)
sys.exit(0 if same else 1)'
```

The last line exits non-zero if the digest on the answer and the digest on the
snapshot ever disagreed. They cannot, and that is the point: the lookup names
the bytes, and the bytes are fetchable at
`GET /feeds/{feed_id}/versions/{version}/data`, where you can recompute the
digest yourself — [the pinned data feeds](feeds.md) walks that check.

---

## Reading the answer

Every response carries five things, and none of them is optional:

* `feed` — the id, the version, the vintage, the kind and the SHA-256 of the
  exact bytes read, with the paths the manifest and the bytes are at. One
  snapshot per answer, always: a number you cannot reproduce from a single
  pinned file is a number nobody can check.
* `point` — the coordinate as it was read, so a mistyped parameter is visible
  in the answer rather than only in your request log.
* `metrics` — the measurement. `basis` says which of the two shapes above it
  is, so a client can switch on it.
* `assumptions` — the data source, the period of record, the method in
  English, and `limitations`: what this answer cannot tell you, in the
  response itself rather than on this page. A hazard number whose limitations
  live in the documentation is a number that will be read without them.
* `disclosure` — the fixed sentence stating that this is a measurement over a
  public record and not a view of risk.

The full response shape is published as
[`hazard.v0.schema.json`](/hazard.v0.schema.json); the map from every route to
the schema governing its responses is
[`api-contract.v0.json`](/api-contract.v0.json).

## When it refuses

Both refusals are `422` in the standard error envelope
([error reference](errors.md)), and both name what would have worked.

A peril nothing is pinned for:

```json
{
  "schema_version": "0.1.0",
  "error": {
    "status": 422,
    "code": "unsupported_peril",
    "message": "this build answers point hazard lookups for earthquake, tropical_cyclone, weather_station; it has no pinned feed that speaks to wildfire. …"
  }
}
```

A point no pinned snapshot of that peril covers:

```json
{
  "schema_version": "0.1.0",
  "error": {
    "status": 422,
    "code": "outside_feed_coverage",
    "message": "no pinned earthquake snapshot covers 0,0. usgs-eq-kanto@2026-08-10: its extract covers 34.0..37.0N and 138.0..141.5E, which does not hold the whole 0.5° cell around this point. …"
  }
}
```

There is no all-of-the-world feed to fall back on, by design. A snapshot that
covered less than the area asked about would answer with less hazard than the
record holds, and nothing in the response would say so.

## What comes next

`GET /hazard` is a lookup, not an artifact: it writes nothing onto an object
and leaves no provenance behind. The verb that does — this same reading at
every location of an object, recorded on that object's provenance with its
assumptions and data vintages — is [`POST /objects/{id}/analyse`](analyse.md).
Two more slices answer elsewhere and write nothing either: [the event
catalogue and footprints](events.md), and [accumulation across a set of
objects](accumulation.md), which totals the exposure you have already stored.
`GET /health` is the list of what the environment in front of you reaches at
all.
