# Schiphol Herrie API reference

Base URL: `https://schipholherrie.nl`. All endpoints below are `GET`, unauthenticated and return `application/json` (UTF-8). **The API is for schipholherrie.nl's own pages and app.** A request without the browser's same-origin marks (`Sec-Fetch-Site: same-origin`, or a `Referer`/`Origin` on schipholherrie.nl) gets `403 {"error":"alleen voor schipholherrie.nl"}`; opening a URL directly in a browser (a top-level navigation) still works, so you can look at a response, but pages and scripts on other origins cannot build on it and there is no CORS. The datasets below (`/plaatsen.json`, `/pc4.json`, `/runways.json`) and every page are open to all, AI crawlers and assistants included: read the pages, not the API. This reference documents what the pages use. Machine-readable spec: [/openapi.json](/openapi.json). Overview for agents: [/llms.txt](/llms.txt).

Everything labelled dB is a **modelled estimate** (see [/over.html](/over.html)), not a measurement. Data collection started 2026-08-30; `since`/`coverage` fields tell you how much history a response is based on.

## Rate limits

Fixed window per client IP, per minute:

| Endpoint | Limit |
|---|---|
| `/api/noise/contour` | 40 |
| `/api/noise/lines` | 90 |
| `/api/noise/timeseries` | 60 |
| every other `/api/noise/*` | 120 (shared) |
| `/api/aircraft` | 120 |

Exceeding a limit returns `429` with a `Retry-After` header (seconds). Responses carry `Cache-Control: public, max-age=N`; polling faster than `N` seconds only returns cached data.

## Light aircraft

Club singles, motor gliders and ultralights (ICAO wake category A1, a list of light types, or a private registration without a type) are tracked and drawn on the live map, but they are no Schiphol traffic and **left out of every statistic**: `/events`, `/lines`, `/stats`, `/timeseries`, `/passages.csv`, `/weekpattern`, `/weekranking`, `/hof` and the share cards. Helicopters and business jets count as usual. `/events`, `/lines`, `/stats` and `/passages.csv` take `ga=1` to include the light aircraft; `/stats` also reports how many were left out as `light_aircraft`. Archived flights carry the flag as `ga` (0/1) in the daily CSV export.

## Coordinates

`lat`/`lon` are WGS-84 decimal degrees. Point queries must lie within the Netherlands bounding box: lat 50.5–54.0, lon 3.0–7.5 (otherwise `400`). Town coordinates are in [/plaatsen.json](/plaatsen.json).

---

## `GET /api/aircraft`: live positions

All aircraft within ~150 NM of the centre of the Netherlands (52.2 N, 5.3 E), refreshed every few seconds. Cache: 4 s.

```json
{
  "now": 1788356010123,
  "stale": false,
  "src": "adsb.lol",
  "ac": [
    {"hex":"484b2f","flight":"KLM1234","t":"B738","category":"A3",
     "alt_baro":2450,"gs":198,"track":87,"baro_rate":1536,"lat":52.3121,"lon":4.6892}
  ]
}
```

| Field | Meaning |
|---|---|
| `now` | server time, Unix ms |
| `stale` | `true` when the upstream feed has not updated recently |
| `src` | upstream in use: `adsb.lol` or `adsb.fi` |
| `ac[].hex` | ICAO 24-bit address (lower-case hex), stable per airframe |
| `ac[].flight` | callsign, e.g. `KLM1234`, may be absent/blank |
| `ac[].t` | ICAO type code, e.g. `B738`, `A320` |
| `ac[].category` | ADS-B emitter category (`A1` light … `A5` heavy) |
| `ac[].alt_baro` | barometric altitude in **feet**, or the string `"ground"` |
| `ac[].gs` | ground speed, knots |
| `ac[].track` | true track, degrees |
| `ac[].baro_rate` | vertical rate, ft/min (negative = descending) |
| `ac[].lat`, `ac[].lon` | position, 4 decimals |

Fields other than `hex`, `lat`, `lon` may be missing for a given aircraft.

## `GET /api/noise/status`: service status

```json
{"src":"adsb.fi","aircraft":317,"day":"2026-09-02",
 "seconds":{"D":43198,"E":2196,"N":25202},"stale":false,"grid":{"nx":561,"ny":678}}
```

`seconds` = seconds of data accumulated today in the Lden periods Day (07–19), Evening (19–23), Night (23–07), local time. `day` is the current local date.

## `GET /api/noise/events?lat=&lon=`: overflights at a point

Number of distinct aircraft whose modelled level at the point reached ≥ `threshold` dB(A) in the last 5 minutes, hour and 24 hours, plus `loud70` (of which ≥ 70 dB(A)), `peak_hist` (24-hour histogram of peaks in 8 bands of 5 dB from 50), the 30 most recent passages (`recent`, with peak `db`, altitude, distance, callsign, type, registration, runway and operation), `hours[24]`, the loudest passage and the last night. Cache: 15 s.

```json
{"m5":2,"h1":19,"h24":312,"loud70":65,"peak_hist":[176,50,61,55,49,16,0,0],
 "recent":[{"t":1788784647249,"db":52,"alt":99,"dist":1968,"hex":"4bcde3","cs":"SXS9KF","airline":"SXS","type":"B738","reg":"TC-SOC","runway":"18R","op":"landing"},…],
 "hours":[27,16,…],"loudest":{"t":1788758388243,"db":79,"cs":"SIA324","type":"B77W","airline":"SIA"},
 "night":{"from":1788728400000,"to":1788753600000,"running":false,"count":21,"loudest":{…}},"h24coverage":100,"threshold":50}
```

`h24coverage` is the percentage of the 24-hour window for which data exists (100 once the service has run a full day).

## `GET /api/noise/route?cs=KLC1801`: origin and destination of a flight

Looked up by callsign in the route database of [adsbdb.com](https://www.adsbdb.com/) (scheduled flights; charters and private flights usually give `{"none":true}`). Hits are cached for a day, misses for two hours.

```json
{"cs":"KLC1801",
 "from":{"iata":"AMS","icao":"EHAM","city":"Amsterdam","name":"Amsterdam Airport Schiphol","cc":"NL"},
 "to":{"iata":"DUS","icao":"EDDL","city":"Düsseldorf","name":"Düsseldorf Airport","cc":"DE"},
 "airline":{"name":"KLM Cityhopper","icao":"KLC"}}
```

## `GET /api/noise/weekpattern?lat=&lon=`: hour-of-week pattern, daily history and altitude profile at a point

Computed nightly for every town with a page (the nearest town within a few km is used). `counts[168]` = flights ≥ 50 dB(A) per hour of the week (Monday 00:00 first) summed over the last 7 whole days, `per_day[168]` the same averaged per day. `daily[30]` = per day, oldest first, `[flights ≥50, of which night 23–07, ≥70 dB, loudest peak dB]`. `alt.bins` = per altitude band (`alt.edges` in metres, the last band is open-ended) `[flights, mean peak dB or null, flights ≥70]`, so you can see how high the traffic over a place flies and how loud each band is.

```json
{"slug":"zwanenburg","name":"Zwanenburg","computed":1788784824490,"from":1786140000000,"to":1788732000000,
 "days":["2026-08-31","…"],"threshold":50,"counts":[…168],"per_day":[…168],
 "daily":[[300,3,78,83.5],…],
 "alt":{"edges":[0,300,600,1000,1500,2000,3000],"bins":[[1620,61.2,314],[480,60.1,48],…]}}
```

## `GET /api/noise/hof?period=7d|30d`: hall of fame

Loudest airlines, aircraft types, towns and single flights over the last 7 or 30 whole days, as shown on [/hof.html](/hof.html). A flight's score is its highest modelled peak in the centre of any town with a page; only airline callsigns (`KLM836`, not registrations) count, and airlines/types need at least `max(10, 3 × days)` flights. Recomputed nightly.

```json
{"period":"7d","days":7,"from":1788127200000,"to":1788732000000,"flights":10332,
 "airlines":[{"code":"MPH","flights":22,"mean_db":71.6,"loud70":15,"share70":68.2},…],
 "quietest_airlines":[…], "types":[{"code":"B744","flights":46,"mean_db":72.2,"loud70":31,"share70":67.4},…], "quietest_types":[…],
 "places":[{"slug":"zwanenburg","name":"Zwanenburg","flights":2146,"night":24,"loud70":332,"max_db":83.5},…],
 "flights_top":[{"fid":812345,"db":83.5,"alt":183,"t":1788600000000,"place":"zwanenburg","place_name":"Zwanenburg","cs":"KLM836","airline":"KLM","type":"B77W"},…]}
```

## `POST /api/noise/push`: the app's warnings (internal)

Used by the Android/iOS app only (`app/`, a Capacitor shell around this site). Registers a device for a spot and a threshold: `{"token","platform":"android"|"ios","v":<app token>,"lat","lon","min":50–80,"night":bool}`, or `{"token","platform","v","off":1}` to stop. Every poll the server extrapolates each aircraft two minutes ahead and sends a notification (FCM / APNs) when its estimated level at the spot will cross `min`; once per flight per device per 15 min, 40 a day, none between 23:00 and 07:00 unless `night`. `GET` says whether the senders are configured. Limit 20/min per IP; not part of the public API and may change.

## `GET /api/noise/meldingen`: flights visitors reported as noisy

"Meld herrie" on the map's flight card. `live` counts the reports of the last two hours per ICAO hex (the red badge on the plane); `today`, `week` and `month` list the most-reported flights (callsign, or hex when none), airlines and towns, the number of distinct reporters and how many reports came in at night (23–07). Served with an ETag, cache 30 s.

```json
{"live_min":120,"live":{"4baacf":2},
 "today":{"total":14,"reporters":9,"night":1,
   "flights":[{"cs":"THY6ZM","n":2,"hex":"4baacf","al":"THY","airline":"Turkish Airlines"},…],
   "airlines":[{"code":"THY","name":"Turkish Airlines","n":2},…],"towns":[{"name":"Zwanenburg","n":5},…]},
 "week":{…},"month":{…}}
```

`POST` the same path with `{"v":"<hex token>","hex":"4baacf","cs":"THY6ZM","lat":52.3,"lon":4.75,"db":61,"alt":900}` to report a flight yourself (`v`: 16–64 hex characters your client keeps; `lat/lon/db/alt` optional). One report per visitor per flight per two hours (`409 {"error":"al gemeld"}`), at most 40 a day per token and 60 per address. The answer is `{"ok":true,"t":…,"n":2,"today":14,"mine":3}`: `n` reports on this flight now, `mine` your total this month. Stored: the flight, the time, a hash of the token and of the address, the nearest town and the position rounded to ~1 km; kept 30 days.

## `GET /api/noise/passages.csv?lat=&lon=&period=day|7d&min=50&name=`: overflights as CSV

The same passages `/api/noise/events` lists, as a semicolon-separated CSV (UTF-8 with BOM, opens directly in Excel) for the current day or the last 7 days: `datum;tijd;vluchtnummer;maatschappij;type;registratie;hoogte_m;afstand_m;piek_dBA;baan;operatie`. Meant for attaching to a complaint at [BAS](https://www.bezoekbas.nl/). `min` is the peak threshold (default 50), `name` only sets the filename. Cache: 5 min.

## `GET /api/noise/fit`: how far the model sits from the microphones

Around Schiphol there are real microphones: the 42 NOMOS posts (Schiphol) and the Sensornet units (municipalities). Their measurements are theirs and are not republished; what is public is the model's offset per post. `live`: over the last two days of minute rows, every minute in which the model puts 65 dB(A) or more at the post, the modelled minute max minus the measured one (max of that minute and its neighbours), median of those, with the number of minutes. `days`: for NOMOS posts the daily calibration shift (the level at which the model counts as many passages as the microphone at or above 70 dB(A), minus 70), median over the last seven days. Positive = the model is louder than the microphone. `summary` gives the median offset over all posts and how many sit within 2 and 3 dB, also per network. `posts` lists NOMOS posts only (Schiphol publishes those itself); Sensornet units appear only in the aggregates. `history` has one entry per finished day (the same rule over that day's minutes, from the daily minute files; `shift` = that day's NOMOS calibration, median over the posts), growing by a day each night. Shown on [/over.html](/over.html). `Cache-Control: max-age=600`.

```json
{"model":"b845","computed":1788971400000,"minutes":2880,"min_model_db":65,"days":["2026-09-02","…","2026-09-08"],
 "summary":{"posts":54,"median":-1,"within2":42,"within3":49,"nomos":{"posts":27,"median":-1.5,"within2":16,"within3":22},"sensornet":{"posts":27,"median":0,"within2":26,"within3":27}},
 "posts":[{"id":1,"net":"nomos","name":"Zwanenburg","live":{"n":265,"diff":-2},"days":{"n":7,"shift":0.1}}],
 "history":[{"day":"2026-09-08","model":"b845","posts":42,"median":-1,"within2":31,"within3":36,"nomos":{"posts":20,"median":-1,"within2":12,"within3":14},"sensornet":{"posts":22,"median":0,"within2":19,"within3":22},"shift":{"posts":8,"median":-1.7}}]}
```

## `GET /api/noise/lines?lat=&lon=&period=&mode=`: flight paths near a point

Tracks of flights that were audible at the point: estimated at 50 dB(A) or more there, the same yardstick as `/events`.

- `period`: `5m`, `15m`, `1h`, `day` (default, since local midnight), `7d`, `30d`
- `mode`: `leq` (default, all hours), `night` (23–07), `daytime` (07–23)

```json
{"period":"1h","mode":"leq","count":29,"drawn":29,
 "flights":[{"cs":"KLM1234","a":610,"pts":[[52.3119,4.7935],[52.3256,4.7977]]}]}
```

`cs` callsign, `a` lowest altitude (metres) of the flight near the point, `pts` = `[lat, lon]` pairs (thinned when large; `drawn` ≤ `count`).

## `GET /api/noise/track?hex=`: full track of one aircraft

Recent track points for an ICAO hex from the archive. Empty `pts` when unknown.

```json
{"pts":[{"t":1788355000000,"la":52.31,"lo":4.79,"al":610,"b":94}]}
```

`t` Unix ms, `la`/`lo` position, `al` altitude (metres), `b` modelled source level dB(A) at 300 m.

## `GET /api/noise/contour?period=&mode=`: noise grid

A regular lat/lon grid of estimated levels for the whole country. Large (~1–2 MB; gzip is used when `Accept-Encoding: gzip` is sent). Cache: 15 s for short periods, 60 s otherwise.

- `period`: `5m`, `15m`, `1h`, `day` (default), `yesterday`, `7d`, `30d`, `12m`, `decade`, `century`
- `mode`: `leq` (default) equivalent level over the period, `daytime` (07–23), `night` (23–07, i.e. Lnight), `lden` (day-evening-night weighted; `ldenReady:false` until at least one full day, evening and night are covered)

```json
{"period":"60m","mode":"leq","window":"laatste uur","seconds":3573,"since":"2026-08-30",
 "nightSeconds":0,"ldenReady":false,
 "nx":281,"ny":339,"lat0":50.7,"lon0":3.2,"dlat":0.009,"dlon":0.0148,
 "leq":[null,null,42.1, "..."]}
```

Cell `(ix, iy)` covers `lat0 + iy*dlat`, `lon0 + ix*dlon`; value index is `iy*nx + ix`. `null` = no data / below floor. Values are dB(A). For a single town prefer `/plaatsen.json`; only fetch the grid when you need the map.

## `GET /api/noise/stats?day=YYYY-MM-DD`: daily statistics

Defaults to today (local date). Cache: 60 s.

```json
{"day":"2026-09-02",
 "totals":{"flights":5036,"night_flights":545,"airlines":292,"types":322},
 "top_airlines":[{"airline":"KLM","flights":509}],
 "top_types":[{"type":"B738","flights":777}],
 "loudest_types":[{"type":"E3TF","max_src_db":99,"flights":2}],
 "per_hour":[{"hour":"00","flights":33}],
 "db_days":4}
```

Counts cover every flight tracked in the coverage area except light aircraft (see above; `ga=1` includes them, `light_aircraft` is their number). Not only Schiphol's own traffic, so: military, business jets and transit count. `night_flights` = flights seen between 23:00 and 07:00 local. `db_days` = number of days present in the archive.

## `GET /api/noise/runways`: Schiphol runways in use

Derived from the traffic itself: aircraft low, aligned with a runway axis and on the right side of it are counted as landing or departing there. Recomputed every 2 minutes over a 20-minute window; a runway is listed with at least two movements in the window or one in the last ten minutes. Designators follow the direction of travel (`18R` = landing or departing towards the south on the Polderbaan).

```json
{"generated":1788454537903,"window_min":20,"coverage_min":20,
 "runways":[{"id":"18C","name":"Zwanenburgbaan","ends":[[52.3314,4.74003],[52.30178,4.73732]],"op":"landing","count":9,"last":1788454523666},
            {"id":"24","name":"Kaagbaan","ends":[[52.28912,4.73725],[52.30438,4.77697]],"op":"departure","count":6,"last":1788454490112}]}
```

`coverage_min` is lower than `window_min` right after a restart, when fewer minutes of traffic have been seen. `op` is `landing` or `departure`. `ends` are the two runway thresholds as `[lat, lon]`, handy for drawing or zooming to the runway.

Both `/api/noise/contour?rwy=18C` and `/api/noise/lines?rwy=18C&period=day` limit their answer to traffic that was recognised on that runway: the contour is today's Leq contribution of that runway alone (same denominator as the full map, so runway maps add up to the total), the lines are the flights tagged to it in the period. Tagging started on 2026-09-03, so older periods hold nothing.

## `GET /api/noise/timeseries?range=`: activity over time

- `range`: `6h` (10-min buckets), `24h` (default, 30-min), `7d` (1 h), `30d` (6 h)

```json
{"range":"6h","step_ms":600000,"start":1788349200000,"coverage_from":1788349200000,
 "buckets":[{"t":1788349200000,"flights":256,"pts":3027,"maxdb":97,"low":113,"new":59,"night":0}]}
```

Per bucket: `flights` distinct aircraft, `pts` position samples, `maxdb` highest modelled source level, `low` flights below 300 m, `new` flights first seen in the bucket, `night` 1 when the bucket falls in the night period.

---

## Static data files

| File | Content |
|---|---|
| [/plaatsen.json](/plaatsen.json) | towns with a page: `slug`, `name`, `kind`, `lat`, `lon`, `population`, `dist_km`, `dir` (compass from Schiphol), `lden` (estimated dB(A), window in `window`/`since`), `low_per_day` (average flights per day over the last 7 days that passed within 3 km below 1500 m; may be null), `url` |
| [/pc4.json](/pc4.json) | CBS 4-digit postcodes in the Schiphol region: `pc4` maps `"2131"` → `[lat, lon, inwoners, woningen, lden, plaats]` (`fields` lists the order; `lden` is the sampled estimate for the centroid, null outside the grid; `plaats` is the slug of the nearest town page within 6 km or null). Rendered by [/postcode.html?pc=2131](/postcode.html?pc=2131). |
| [/runways.json](/runways.json) | GeoJSON of Schiphol's runways |
| [/sitemap.xml](/sitemap.xml) | all HTML pages |

## Errors

`400` invalid/missing parameters (`{"error":"..."}`), `404` unknown route, `429` rate limited (`Retry-After`). An upstream outage does not produce an error: `/api/aircraft` keeps serving the last snapshot with `stale:true`.

## Terms

The pages and the open datasets (`/plaatsen.json`, `/pc4.json`, `/runways.json`) are free for non-commercial use with attribution: "Schiphol Herrie (schipholherrie.nl), modelled estimate from ADS-B data (adsb.lol / adsb.fi)". The API endpoints serve the site and its app only (see the top of this page). No uptime guarantee; please cache and respect the rate limits. Town data © OpenStreetMap contributors (ODbL).
