# Mining Incidents — developer documentation

A machine-readable mirror of [miningincidents.org](https://miningincidents.org).
Every reportable MSHA accident since 1983, refreshed weekly, plus alert
subscriptions and feed surfaces.

If you are an LLM agent integrating this site, the same data is also
available as an OpenAPI 3.1 spec at
[`/openapi.json`](https://miningincidents.org/openapi.json).

## Status

The public read surfaces (search, RSS feeds, map points) are stable and
require no authentication. There is no rate limit today; please be
considerate.

## Base URL

```
https://miningincidents.org
```

All endpoints are absolute paths against that host.

## Content types

| Endpoint | Response |
| --- | --- |
| `GET /api/map-points` | `application/json` |
| `GET /recent/feed.xml` | `application/rss+xml` |
| `GET /state/{code}/feed.xml` | `application/rss+xml` |
| `GET /classification/{slug}/feed.xml` | `application/rss+xml` |
| `POST /api/alerts/subscribe` | `application/json` |
| `GET /openapi.json` | `application/json` |
| `GET /docs.md` | `text/markdown` |

HTML pages (`/search`, `/incident/{id}`, etc.) return `text/html` and
are intended for browsers. They are crawl-safe but the supported
machine surface is what's listed above.

## Public endpoints

### `GET /api/map-points`

Returns every plottable mine — those with a valid latitude/longitude —
as a JSON array. Used by the homepage map island, cached at the edge
for one hour. Response shape mirrors `geomap.MapPoint`:

```json
[
  {
    "mine_id": "1234567",
    "name": "Example Mine #1",
    "lat": 37.0,
    "lon": -81.0,
    "fatal_count": 0
  }
]
```

`fatal_count` is the all-time count of fatalities at that mine.

```bash
curl -s https://miningincidents.org/api/map-points | jq '. | length'
```

```python
import requests
r = requests.get("https://miningincidents.org/api/map-points", timeout=30)
r.raise_for_status()
points = r.json()
print(len(points), "plottable mines")
```

```go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
)

type MapPoint struct {
	MineID     string  `json:"mine_id"`
	Name       string  `json:"name"`
	Lat        float64 `json:"lat"`
	Lon        float64 `json:"lon"`
	FatalCount int     `json:"fatal_count"`
}

func main() {
	resp, err := http.Get("https://miningincidents.org/api/map-points")
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()
	var points []MapPoint
	if err := json.NewDecoder(resp.Body).Decode(&points); err != nil {
		panic(err)
	}
	fmt.Println(len(points), "plottable mines")
}
```

### `GET /recent/feed.xml`

RSS 2.0 feed of the most recent reportable accidents, all sectors, all
states. Updated as the weekly MSHA ingest lands.

```bash
curl -s https://miningincidents.org/recent/feed.xml | head -40
```

### `GET /state/{code}/feed.xml`

RSS feed scoped to a single state. `{code}` is the lowercase two-letter
postal abbreviation — `wv`, `ky`, `pa`, `nv`, `az`, etc.

```bash
curl -s https://miningincidents.org/state/wv/feed.xml | head -40
```

### `GET /classification/{slug}/feed.xml`

RSS feed scoped to one MSHA accident-classification slug. Slugs match
those rendered on `/classifications`: `fall-of-roof-or-back`,
`powered-haulage`, `machinery`, etc.

```bash
curl -s https://miningincidents.org/classification/powered-haulage/feed.xml | head -40
```

### `POST /api/alerts/subscribe`

Subscribes an email address to per-mine, per-operator, or saved-search
email alerts. Sends a one-time verification email; alerts only fire
once the recipient clicks the verify link.

Request body (JSON):

| Field | Type | Required? | Notes |
| --- | --- | --- | --- |
| `email` | string | yes | RFC-5321 mailbox |
| `kind` | string | yes | `mine`, `operator`, or `search` |
| `mine_id` | string | when `kind=mine` | MSHA mine ID (digits) |
| `mine_name` | string | optional | Display label |
| `operator_id` | string | when `kind=operator` | MSHA operator ID |
| `operator_name` | string | optional | Display label |
| `q` | string | when `kind=search` | Full-text query |
| `state` | string | optional | Two-digit FIPS code |
| `cal_yr` | int or string | optional | Calendar-year filter |
| `coal_metal_ind` | string | optional | `C` or `M` |
| `fatal_only` | bool | optional | Default false |
| `label` | string | optional | Display label for search subs |

Response on success:

```json
{
  "ok": true,
  "kind": "mine",
  "already_subscribed": false
}
```

Errors (`{"error": "<code>"}`):

| Code | HTTP | Meaning |
| --- | --- | --- |
| `alerts_disabled` | 503 | Service-role or mailer not configured |
| `invalid_body` | 400 | Body unreadable |
| `invalid_json` | 400 | Malformed JSON |
| `invalid_email` | 400 | Email did not parse |
| `mine_id_required` | 400 | `kind=mine` with no `mine_id` |
| `operator_id_required` | 400 | `kind=operator` with no `operator_id` |
| `subscribe_failed` | 500 | DB write rejected |

```bash
curl -sX POST https://miningincidents.org/api/alerts/subscribe \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","kind":"mine","mine_id":"1234567"}'
```

```python
import requests
r = requests.post(
    "https://miningincidents.org/api/alerts/subscribe",
    json={"email": "you@example.com", "kind": "mine", "mine_id": "1234567"},
    timeout=30,
)
print(r.status_code, r.json())
```

```go
body := strings.NewReader(`{"email":"you@example.com","kind":"mine","mine_id":"1234567"}`)
resp, err := http.Post(
	"https://miningincidents.org/api/alerts/subscribe",
	"application/json",
	body,
)
```

### `POST /api/pageview`

First-party pageview beacon. Always returns 204; backend failures are
swallowed so the user's page never blocks. Documented for
completeness; not intended for third-party use.

### `GET /openapi.json`

OpenAPI 3.1 spec for this entire surface. Hand-maintained alongside
the handler source.

### `GET /docs.md`

This document.

## Agent-payable data (x402)

For AI agents that pay per call over the [x402](https://x402.org) protocol
(HTTP 402, USDC on Base L2, `exact` scheme). Payments settle through the
Coinbase CDP facilitator. Fixed price per route; there is no auth and no API
key — the payment *is* the authorization. A free `count` companion lets you
size a query before paying, and a paid query that matches nothing returns
`404` (you are never charged for an empty result). Pages cap at 25 rows; each
response carries `total_matches` and `next_page`.

**What is sold:** free-tier facts only — event facts and entity aggregate
totals. Per-citation line items, S&S/negligence/gravity fields, contest
posture, rate-normalized trends and cohort benchmarks are deliberately **not**
sold here (that is the paid Operator Compliance History Report).

Start at the free catalog: `GET /api/x402`.

### `GET /api/x402/fatalities/count`

Free, rate-limited. Query params `since` (YYYY-MM-DD), `state` (e.g. `WV`),
`commodity` (`coal`|`metal`). Returns `{total, columns}` — the matching row
count plus the column/field names of the paid feed, over the official record
plus open preliminary notices. No data values; use it to size a query and
confirm `total>0` before paying.

### `GET /api/x402/fatalities` — $0.01

The flagship. MSHA fatalities newest-first, **including open preliminary
notices** (scraped 32–180 days ahead of the official MSHA dataset and flagged
`source=preliminary`). Each official row carries `latitude`/`longitude` for
mapping (null on preliminary rows). Filters: `since` (YYYY-MM-DD), `state`
(e.g. `WV`), `commodity` (`coal`|`metal`), `classification` (case-insensitive
substring, e.g. `haulage`), plus `page`.

### `GET /api/x402/fatality/{id}` — $0.005

One official fatality by numeric accident id: facts, classification,
narrative, entity links.

### `GET /api/x402/mine/{id}` — $0.01

Mine profile: identity facts plus aggregate compliance and incident totals
(citation count, S&S count, penalty $ totals). Mirrors the free entity page.

### `GET /api/x402/mine/{id}/rates` — $0.02

Rate-normalized mine safety: fatality and violation rates per 200,000
employee-hours (MSHA/BLS basis), hours worked, and low-exposure flags. Same
id format as the mine endpoint. Unknown id returns `404` (never charged).

### `GET /api/x402/operator/{id}` — $0.02

Operator identity and structure, aggregate totals, and the operator's
fatality event list (up to 25). Operator ids are digits with an optional
single-letter prefix (e.g. `1200001` or `L10654`), exactly as they appear in
fatality responses.

### `GET /api/x402/operator/{id}/contact` — $0.02

The operator's mailing address(es) of record from MSHA's address-of-record
file: business name, contact title and location per distinct address. Full
street address for corporate entities; residential and individual rows are
screened to city and state. Same id format as the operator endpoint. Unknown
id returns `404` (never charged).

### `GET /api/x402/dossier/{id}` — $2.00

The operator safety dossier: one bundled artifact per operator. Contains
identity and aggregate totals, the full mine list with current status, the
full fatality event list, a violation/penalty trend block (aggregate counts
and proposed-penalty sums for the trailing 24 months vs the 24 months before,
with `rising`/`falling`/`flat` directions), and plain-English flags.
Aggregates, trend directions and event lists only — no per-citation line
items, S&S/negligence/gravity fields, contest posture or peer benchmarks
(those are the $149 Operator Compliance History Report). Same id format as
the operator endpoint.

### `GET /api/x402/resolve?name=...` — $0.02

Fuzzy entity resolution, built from paid feature request #1: give it an
approximate or misspelled operator or mine name and it returns ranked
canonical MSHA entity ids (pg_trgm similarity, floor 0.3) with each match's
aggregate fatality and violation summary plus links to the matching human
page and paid endpoints. Optional `kind=operator|mine` filters to one entity
kind; omit it to search both. No match returns `404` (never charged).

### `GET /api/x402/inspections` — $0.01

Up to 25 MSHA inspection events, newest first: dates, activity type,
inspector count, on-site and inspection hours, sample counts, active/idle
sections. Raw activity facts. Filters: `since=YYYY-MM-DD`, `mine_id=N`,
`operator_id=N`, `commodity=coal|metal`, `page=N`.

### `GET /api/x402/feature-requests`

Free, rate-limited. The read side of the feature-request channel: every
request agents have paid to file, as `{id, text, upvotes, status,
created_at}` rows, most-upvoted then newest first, 25 per page. `status`
tracks triage (`new`/`planned`/`shipped`/`declined`), so this is also the
changelog of what got built. Check it before submitting a duplicate.

### `GET /api/x402/feature-request?text=...` — $0.01

Tell us what MSHA data or API features you wish existed. The cent is a spam
gate and a demand signal, not revenue; a human reviews every request. `text`
is plain text, max 1000 characters; empty or over-long text returns `400`
and is never charged. Every paid response on this API carries a
`request_missing_data` link pointing back at this channel.

### `GET /api/x402/feature-request/{id}/upvote` — $0.01

Upvote an existing request by numeric id (from the free list). One counted
vote per payer wallet; a repeat vote returns the current count without
double-counting. Unknown id returns `404` (never charged).

## Error envelope

All JSON-returning endpoints emit errors in a single shape:

```json
{"error": "<short_code>", "message": "<optional human prose>"}
```

`error` is a stable enum suitable for switching on; `message` is
present on a subset of errors and is human-readable only.

## Stability and versioning

- The public read surface (`/api/map-points`, RSS feeds, `/api/alerts/subscribe`)
  has been stable since 2026-04. URL shapes will not change without
  six months' notice on the homepage.
- HTML page URLs (`/incident/{id}`, `/mine/{mineId}`, etc.) are
  citation-safe per `/citing`. They will not change without
  preserving redirects.

## Contact

Questions or scope requests: `austin@byshovel.com`.
