ESPGeiger Stations - Public WebAPI
ESPGeiger can optionally contribute its readings to the public map at stations.espgeiger.com, a global radiation-monitoring grid built from community-run Geiger counters. Participation is opt-in, signed end-to-end, and privacy-respecting.
What gets sent
The device speaks MessagePack to the server - a compact binary encoding chosen to keep heap pressure low on ESP8266. The schemas below describe the logical contents; on the wire they’re encoded as fixmaps with single- or two-letter keys.
Per-minute post (CPM Readings mode):
| Field | Type | Description |
|---|---|---|
id | uint32 | Station ID (assigned at first handshake) |
n | uint32 | Unix timestamp of the reading (seconds) |
c | float32 | Current CPM |
u | float32 | Microsieverts per hour |
hv | float32 | HV tube reading (ESPGeiger-HW only) |
In Heartbeat mode the radiation fields (c/u/hv) are omitted
- the post becomes a minimal
{id, n}body so the server still sees the station as alive without any readings leaving the device.
Every 15 minutes (health snapshot, piggy-backed on the post above):
| Field | Type | Description |
|---|---|---|
ut | uint32 | Uptime in seconds |
mh | uint32 | Free heap (bytes) |
l | uint32 | Loop iterations per second |
tk | uint32 | Peak ticker duration in the last 60 s (µs) |
rs | int32 | WiFi signal strength (dBm) |
Health fields live only in the raw tier server-side - they age out and are never aggregated into long-term history.
Handshake (once per hour, or on first boot / reset):
| Field | Type | Description |
|---|---|---|
n | uint32 | Unix timestamp of the handshake (seconds) |
pk | string | Device public key, base64 (ECC secp192r1, 64 chars) |
ci | string | Device chip ID (hex). The server looks up or creates a station for this chip and returns the numeric id the device quotes in subsequent posts. |
v | string | Firmware release version - tag/sha when built from a non-tagged commit (e.g. devel/7a2407d), or just tag for released builds (e.g. 0.9.3). |
bd | string | Chip model (e.g. esp8266, ESP32-C3) |
gm | string | PlatformIO build environment name (e.g. espgeigerlog_serial) |
gc | string | Geiger counter model - auto-detected on Serial builds, user-set on Pulse builds |
td | uint32 | Tube detection bitmask: bit 0 = α, bit 1 = β, bit 2 = γ. 0 = unknown / not set on the device. Bits 3-7 reserved. |
fl | uint32 | Feature-flag bitmask (which optional output modules are compiled in) |
rr | uint32 | Normalised last-reset reason |
m | uint8 | Sharing mode: 1 = heartbeat-only, 2 = full readings (mode 0 doesn’t handshake) |
la | float32 | Optional. Latitude in degrees (−90..90), 2-decimal-place rounded on device for privacy (~1.1 km cells). Omitted when both la and lo are 0 (the “use IP” signal). |
lo | float32 | Optional. Longitude in degrees (−180..180). Same rounding and omission rules as la. |
The handshake is what registers your station on the map and keeps the server’s metadata (version, board, mode, etc.) current. The server responds with the assigned station ID, which the device then uses as id in subsequent per-minute posts.
Configuring on the device
The /webapi page on each device shows the ESPGeiger Network controls:

- Station ID - assigned by the server on first handshake. Click to view your station on
stations.espgeiger.com. - Sharing - tri-state: Off / Heartbeat / CPM Readings (see below).
- Latitude / Longitude - optional. Leave both at
0to fall back to coarse IP-based geolocation. Coordinates are rounded to 2 decimal places (~1 km resolution) before publication. - Find location - uses browser geolocation to fill in the lat/lon fields.
- Advanced -
Reset key(rotate identity, register as a new station) andForget station(server-side delete; see below).
Sharing modes
A single tri-state pref (webapi.mode, default 2) controls what leaves the device:
| Mode | Label | Behaviour |
|---|---|---|
2 | CPM Readings (default) | Hourly handshake, per-minute posts with c/u, 15-min health snapshots. Your station appears on the map. |
1 | Heartbeat | Hourly handshake + 15-min health snapshots only. No radiation data leaves the device. Your station appears in fleet-size / uptime stats but not on the map. |
0 | Off | Nothing sent - no handshake, no posts. |
Switching modes is a single click on the device’s /webapi page. The posting cadence changes immediately; the server learns the new mode at the next handshake (within an hour, declared via the handshake’s m field).
Identity and signing
Each device generates an ECC keypair (secp192r1) on first run and stores the private key in LittleFS. Every post and handshake is SHA-256’d and signed; the signature is base64-encoded P1363 (r||s) and sent in the X-Auth header. The server keeps your public key from the first handshake and refuses any post whose signature doesn’t verify. This means:
- Your station can’t be impersonated.
- Losing the device’s flash means a new keypair on next boot, which registers as a new station (same chip ID, new server-side row).
- No shared secrets to configure - no passwords to leak.
Reset key
The /webapi page’s Advanced → Reset key button wipes the local private key and reboots. The next handshake registers a fresh station; the previous one is orphaned and eventually retired by server-side staleness rules. Useful if you want to migrate to a new identity but keep contributing data.
Forget station
The /webapi page’s Advanced → Forget station button sends a signed delete request to /api/1/forget. The server purges the station row, all readings, and all history. Sharing is then automatically switched to Off. This is irreversible - re-enabling later registers a brand-new station with no link to the deleted one. This is the GDPR right-to-erasure path.
Map presence
Once you’ve submitted a handshake, visit https://stations.espgeiger.com/station/<id> to see your station. Each station also gets a memorable three-word alias - e.g. curie-counts-photons - which you can share directly: https://stations.espgeiger.com/station/curie-counts-photons.
Pin colours
Each station is a teardrop pin on the map. Colour reflects the station’s current reading vs its own baseline, not a global threshold. A tube with high local background (e.g. sitting next to granite) is only flagged when it departs from its own typical rate.
| Colour | State | Meaning |
|---|---|---|
| Blue | New | Fewer than ~4 h of data. Baseline not yet established. |
| Green | Normal | Reading consistent with baseline. |
| Yellow | Elevated | Mild departure above baseline. Often routine variation. |
| Orange | Warning | Larger or longer-lasting departure. Common for weather events (radon washout after rain). |
| Red | Danger | Sustained or large excursion. Hardware fault, point source, or strong environmental event. |
| Dark red | Critical | Severe or prolonged excursion. Operationally significant. |
| Grey | Silent | No post received in the last 15 minutes. Likely lost connectivity, lost power, or device failure. |
The score is a Poisson CUSUM (cumulative sum) detector running against the station’s 7-day baseline, with a per-hour-of-day adjustment so stable daily cycles (heating, occupancy, indoor radon) get absorbed rather than triggering the same alert every day. Routine Poisson noise is absorbed by a slack term; sustained or large excursions accumulate across consecutive 15-minute buckets and open an event when they cross the configured threshold. The accumulator is normalised by each station’s own σ so a 2σ excursion sustained for an hour reads the same on a 10 cpm tube and a 1000 cpm tube - same operational severity, same alert level. The same algorithm handles spikes, persistent drops, and slow drift. Refresh cadence is 10 minutes server-side.
Cluster pins use a weighted vote with geometric weights: Critical weighs 16, Danger 8, Warning 4, Elevated 2, others 1. A single Critical child decisively tints clusters up to ~16 members dark red; a single Warning tints up to ~4 members orange. Larger clusters need the alert state to dominate by weighted total. Silent and New never apply a tint, so dead tubes don’t colour the neighbourhood.
Events timeline
The map and the station detail page both use the same five severity bands. The Events panel on the station page additionally shows direction (↑/↓), duration, and peak departure score:
| Badge | Meaning |
|---|---|
| Elevated | Above noise floor. Often weather-driven (radon washout after rain). |
| Warning | Unambiguous excursion. Worth investigating. |
| Danger | Sustained or large excursion. Hardware fault or genuine source likely. |
| Critical | Severe or long-running excursion. Tube failing, point source nearby, or pulse line shorted. |
Pin colour follows the same band exactly: a station whose event peaks at Danger shows a red pin on the map and a Danger badge on its event row.
Event rows also show direction (↑ above baseline, ↓ below), duration, peak departure σ-score, and an (ongoing) tag if still active. Offline events use a separate grey badge with no severity, because silence isn’t a severity-banded anomaly.
Endpoint
The canonical URL is http://api.espgeiger.com/api/1/. HTTP-only is intentional - signatures provide tamper-resistance, TLS would just add heap pressure on ESP8266 without additional security in this threat model.
Each request sends:
Content-Type: application/msgpackX-Auth: <base64(P1363 signature over the raw request body)>User-Agent: ESPGeiger/<version> <chip> <build>
The Accept header is omitted; the server infers the response encoding from Content-Type.
Routes:
| Path | Method | Purpose |
|---|---|---|
/api/1/handshake | POST | Register or refresh station metadata |
/api/1/post | POST | Submit a reading (and optionally a health snapshot) |
/api/1/forget | POST | Right-to-erasure - purge station + all history |
Opt-out at any time
Set sharing to Off in the device’s /webapi page. Posts stop immediately. Your existing station remains on the map but its “last seen” timestamp stops advancing; it falls off the active list after the server-side staleness window. Use Forget station instead if you want the server to actively delete it.