Docs checking…
The Grid · /api/v1

API reference

The public contract, in one page.

no key required CORS-open OpenAPI spec

Conventionsbase URL https://data.sierragridteam.org, everything under /api/v1/ · read-only, keyless, CORS-open · responses are camelCase throughout, timestamps RFC 3339, errors gRPC-standard {code, codeName, message, details}, most reads carry an ETag · every GET below is a live link into the running service · a feed's sourceStatus is OK, STALE or UNAVAILABLE — an unavailable feed is reported as such, never as an all-clear (§6)

1. Overview & conventions

Base URL and media types

Base URL
https://data.sierragridteam.org — all endpoints under /api/v1/
Default format
JSON only (protojson), camelCase throughout (observedAt, areaLabel, nextPageToken) — the gRPC RPCs via the gateway marshaler and the hand-built .geojson layers via matching camelCase keys (sourceStatus, severityRank)
GeoJSON
the .geojson path extension marks the RFC 7946 projection — a pure path, no headers needed, so map clients can consume the URL directly

Conventions

  • Timestamps are RFC 3339 strings (e.g. 2026-07-05T14:02:00Z).
  • Enum values render as their proto names (WILDFIRE, ACTIVE, SEVERE). Query parameters accept them case-insensitively, and layer parameters additionally accept the lowercase layer slugs used in .geojson paths (wildfire).
  • Repeatable filters: parameters documented as repeatable may appear multiple times — ?layer=wildfire&layer=evacuation.
  • Pagination is cursor-based on the event lists — events, events/{id}/history, and history: pass the opaque nextPageToken from a response as the page_token query parameter on the next request (base64 keyset cursors; treat them as opaque). The directory lists — places, sources, and scanners — are not paginated: each returns its complete set in one response by design (a bounded, slowly-changing directory), with no nextPageToken. Read them whole; don't build paging logic for them.
  • Query parameters stay snake_case regardless of response casing: severity_min, page_token, page_size (camelCase forms are also accepted).
  • ETags: most read endpoints carry a weak ETag; a conditional request with a matching If-None-Match gets 304 Not Modified and skips the work. Coverage: events/{id} + .../history, the events and history lists, places + places/{place}, and the .geojson layers (body-hash). Not yet instrumented: conditions, sources, places:resolve, scanners, and summary. Independently, every proto RPC carries Cache-Control: public, max-age=30, so a proxy/CDN may serve any of them up to 30 s stale (the .geojson layers use max-age=60).
  • Errors from the proto RPCs are gRPC-standard {code, codeName, message, details} alongside the mapped HTTP status; the one hand-built endpoint (.geojson) emits the older google.rpc.Status shape ({code, message, details}, no codeName):
HTTP/1.1 404 Not Found
Content-Type: application/json

{"code": 5, "codeName": "NOT_FOUND", "message": "event not found"}

code is the numeric gRPC status code (5 = NOT_FOUND, echoed as codeName). Clients should key on the HTTP status and surface message.

  • Canonical client sort for events: severity descending, then observedAt descending.
  • Places are addressable by slug or id: ebbetts-pass and area:ebbetts-pass both work anywhere a {place} appears. Slugs are globally unique (§4).

CORS

All /api/v1 endpoints are served with open CORS — any origin can fetch them from a browser. This site is itself an unprivileged client of that same surface: every number on every page here is a same-origin browser fetch you can replay with curl (see the footer of any page).

2. Endpoint reference

Two idioms, one rule: entities (events, places, sources, conditions, scanners) are global collections filtered by query parameter; place-derived documents (summary, map layers) nest under the place path. The two hot paths — summary and map layers — take no query string at all, so CDN cache keys stay clean.

Every entry expands to its parameter table and example requests. The examples are not illustrations: press RUN and the page fetches that exact URL live and prints the real response, status, timing and byte count. An example that has gone stale therefore fails in front of you rather than quietly misleading you.

building the endpoint reference…

3. The Event envelope

Every discrete occurrence is one grid.v1.Event message. The common envelope is identical across layers — a client can render any event's card from headline / severity / provenance / observedAt without knowing its kind. Typed per-kind data lives in exactly one detail block.

Fields, beside a live record

fetching a sample record…

Every field

FieldTypeSemantics
idstringGlobally unique, source-namespaced: {namespace}:{native_id}. Namespaces: chp: (road incidents), wx: (weather alerts), usgs: (earthquakes), calfire: (CAL FIRE incidents), firis: (fire perimeters), evac: (evacuation zones), pge: (PG&E outages), psps: (Public Safety Power Shutoffs, psps:{eventId}:{timePeriod}), meshcore: (mesh nodes, keyed by Ed25519 public key). FIRIS standalone ids are firis:{normalized-name} with -2, -3 disambiguators.
layerLayerLayer taxonomy (table below)
categorystringSource sub-type slug ("active", "order", …)
severitySeverityUnified 5-level scale (table below)
statusEventStatusLifecycle state (table below)
headlinestringCard-renderable one-liner, kind-agnostic
summarystringAI-enhanced 2–3 sentences where applicable (see enhancement)
descriptionstringLong form / original upstream text, always preserved verbatim
areaLabelstringHuman area label ("Hathaway Pines & Avery")
canonicalUrlstringUpstream detail URL; scheme-validated — only https:///http:// survive
geometryGeometryMay be absent (county-wide advisories). See the base64 note below.
placeIdsrepeated stringPlace intersections, precomputed at ingest (§4 ids)
provenanceProvenancesourceId, sourceName, attribution, sourceUrl, fetchedAt
effectiveTimestampWhen the event takes effect (future for SCHEDULED)
expiresTimestampUpstream expiry, when the source provides one
observedAtTimestampUpstream last-update time
ingestedAtTimestampWhen this service ingested the current revision
revisionuint32Monotonic per event; a new revision is written only on content change
enhancementEnhancementAI-enhancement provenance: model, enhancedAt, fields (which Event fields were generated), and the model I/O — request (the prompt sent) and response (the raw structured JSON returned). Present only on enhanced events — clients badge AI-summarized text, show what was sent/returned, and can always fall back to the verbatim description. Persisted with the event (in every revision); excluded from the content hash. The heavy request/response are omitted unless ?enhancement_io=true is passed (event endpoints); the lightweight fields are always present.
detailoneofExactly one typed block, keyed by kind (below)

Detail blocks (the oneof)

Each block carries only kind-specific fields. Anything already in the envelope is not repeated: the incident/alert type is category, the human location is areaLabel, the short line is headline, the sending office is provenance.sourceName, and the source page is canonicalUrl.

KeyFields
wildfireacres (double), containment (int32), county, cause (string), hasPerimeter (bool)
evacuationzoneId, level (ORDER | WARNING | ADVISORY | SHELTER_IN_PLACE, as string), eventType, county (string)
weatherAlertnwsSeverity, certainty, urgency, instruction, areaDesc (string), zones (repeated string)
earthquakemagnitude (double), depthKm (double), felt (int32)
roadIncidentlogNumber (string), impact, duration (string), metadata (map<string,string>)
poweroutageId, cause (string), customersAffected (int32), crewStatus (string), estimatedRestoration (timestamp); PSPS adds eventId, eventName, timePeriod, stage (Watch | Warning, as string), medicalBaselineAffected (int32), deEnergizationStart, deEnergizationEnd, allClear (timestamp). estimatedRestoration, deEnergizationEnd and allClear are ESTIMATES PG&E routinely overruns — they are deliberately not mapped onto expires, so never use them to hide an event.
meshpublicKey (hex string), nodeType (companion | repeater | room_server | sensor), name (string), telemetry { snr (double), rssi (int32), hopCount (uint32), path (repeated string — relay hops as repeater pubkey-prefix hashes), pathNodes (repeated string — path resolved to full node public keys, parallel to path), gateways (repeated string), lastAdvertAt (timestamp) }

Unit-bearing fields name their unit (depthKm). A fire_weather detail slot is reserved in the schema but fire weather is condition-backed (a map layer, not an event stream). The mesh block's telemetry is the last-heard signal state and is volatile — it never mints a new event revision (only a node's identity, role, name, location, or status does). For "when did this node last advertise", the trustworthy answer is the event's observedAt (when the Grid last heard it); telemetry.lastAdvertAt is the node's self-reported stamp and is diagnostic only (mesh node clocks skew). Slots are also reserved for future layers: power, gauge, air_quality, announcement.

Enum: Layer

Name#SlugBacking
WILDFIRE1wildfireevents
EVACUATION2evacuationevents
WEATHER_ALERT3weather_alertevents
FIRE_WEATHER4fire_weatherconditions (map layer only)
EARTHQUAKE5earthquakeevents
ROAD_INCIDENT6road_incidentevents
POWER10powerevents (PG&E outages + Public Safety Power Shutoffs; category is unplanned | planned | psps). The statewide median outage affects ONE customer, so most rows are INFO — pair with severity_min. INFO power events are excluded from the place-summary rollup (they still appear here and in the power domain).
MESH13mesh_nodeevents (MeshCore presence) — ?layer= accepts mesh, mesh_node, or the legacy network
GAUGE, AIR_QUALITY, ANNOUNCEMENT11, 12, 14reserved for future pollers

road_segment and chain_control are condition-backed map-layer slugs only — they are projections of live road state, not Layer enum values, and never appear in /api/v1/events.

Enum: Severity (the canonical scale + color ramp)

One comparable scale across every source. It expresses response urgency to the public, not physical magnitude — an Evacuation Order (EXTREME) intentionally outranks an M5 earthquake (SEVERE). Rank equals the enum value. Always pair the color with the text label — color is never the only signal.

SeverityRankOn paperOn inkBadge
EXTREME4#b3261e#ff5544EXTREME
SEVERE3#c2410c#e0913fSEVERE
MODERATE2#a16207#d0a24aMODERATE
MINOR1#4d7c0f#4d9c3aMINOR
INFO0#1d4ed8#3d8bffINFO

There are two ramps, not one: the same severity resolves to a print-weight colour on paper and a lighter, foreground-tuned one inside the black data panes, because no single value is legible on both. Ranks and enum names are identical across them — only the rendering differs, and the swatch is only ever a second signal beside the always-present text label.

Enum: EventStatus (lifecycle)

SCHEDULED ──▶ ACTIVE ──▶ RESOLVED   (upstream confirmed over)
                   └──▶ EXPIRED    (aged out; upstream went quiet)
Status#Meaning
SCHEDULED1effective is in the future — e.g. an NWS watch issued ahead of its window, a planned PSPS
ACTIVE2In effect now
RESOLVED3Disappeared from an authoritative active-only feed (Cal OES, CAL FIRE, CHP, Caltrans) — upstream confirmed it over
EXPIRED4Missing from feed and past expires (or a per-source grace period) — sources that don't confirm endings (NWS, FIRIS)

Every status transition writes a revision — the all-clear is part of history. By default /api/v1/events returns only ACTIVE,SCHEDULED; closed events remain queryable by passing status= explicitly and through /api/v1/history.

Geometry and the base64 note

Event.geometry is {geojson: bytes, bbox: {minLat, minLng, maxLat, maxLng}, centroid: {lat, lng}}. The geojson field is a raw RFC 7946 geometry object stored as proto bytes — protojson renders bytes as base64, so decode before parsing:

const g = event.geometry;                          // may be absent entirely
const geom = g?.geojson ? JSON.parse(atob(g.geojson)) : null;
// geom is an RFC 7946 geometry object — coordinates are [lng, lat]

bbox and centroid are always populated at ingest when geometry exists, so list views never need to decode. For map rendering, prefer the .geojson endpoints — the base64 field exists for detail views.

4. Places & the id scheme

Place ids are {kind}:{slug}. Slugs are globally unique, so every place is addressable by bare slug (ebbetts-pass) or full id (area:ebbetts-pass) anywhere a {place} path segment or place= parameter appears. Area slugs are short, stable ids (e.g. ebbetts-pass).

PrefixKind#WhatGeometryExample
area:AREA1Configured service areas (the legacy hazard areas; org footprints live here too)bbox polygonarea:ebbetts-pass
county:COUNTY2Service-area counties (Calaveras, Tuolumne, Amador, Alpine, Stanislaus, San Joaquin, El Dorado, Mariposa) — simplified Census TIGERweb polygonspolygoncounty:calaveras-county
town:TOWN3The monitored towns (the weather locations)pointtown:arnold
evac_zone:EVAC_ZONE4Evacuation zones (Zonehaven/Genasys scheme). Full zone-inventory import is pending; active zones appear as events in the meantime.polygon
corridor:CORRIDOR5The monitored road corridorslinestringcorridor:hwy4-arnold-bearvalley
site:SITE6Specific sites (reserved)point

Each Place carries id, kind, name, slug, geometry, parentId. Parent links: towns point at their county; areas have no parent. /api/v1/places:resolve returns containing places ordered most-specific-first: SITE, EVAC_ZONE, TOWN, CORRIDOR, COUNTY, AREA.

5. The GeoJSON layer contract

Each /api/v1/places/{place}/map/{layer}.geojson response is one RFC 7946 FeatureCollection with a foreign top-level metadata member (allowed by RFC 7946, ignored by map libraries, available to clients that read it):

{
  "type": "FeatureCollection",
  "features": [ … ],
  "metadata": {
    "layer": "evacuation",
    "area": "ebbetts-pass",
    "generatedAt": "2026-07-05T15:42:11Z",
    "sourceStatus": "OK",             // OK | STALE | UNAVAILABLE
    "lastSourceUpdate": "2026-07-05T15:38:00Z",
    "attribution": "Cal OES / California County Governments — reference only",
    "sourceUrl": "https://protect.genasys.com/…",
    "schemaVersion": 1
  }
}

metadata.sourceStatus

Layers are fail-loud: a source error never becomes a fabricated clear state. The three states render differently and must not be collapsed:

sourceStatusFeaturesRequired client rendering
OKcurrentNormal.
STALElast-good servedRender with a "data ~N min old" indicator (N from generatedAt − lastSourceUpdate).
UNAVAILABLEemptySuppress the map layer; show an empty-state banner linking metadata.sourceUrl. Never render as "all clear".

The properties envelope

Every feature, in every layer, shares one common envelope — that is the unification. A client renders any feature's card from headline / severity / source / updatedAt without knowing its kind:

{
  "type": "Feature",
  "geometry": { "type": "Point", "coordinates": [-120.5402, 38.0674] },
  "properties": {
    // ---- common envelope (present on every feature) ----
    "id": "calfire:2026-salt-springs",   // globally unique, source-namespaced
    "layer": "WILDFIRE",                 // Layer enum name (same value as event.layer)
    "kind": "Wildfire",                  // human label
    "category": "active",                // source sub-type slug (free-form, lowercase)
    "severity": "SEVERE",                // unified scale, §3
    "severityRank": 3,                   // 0..4, for sort/color
    "headline": "Salt Springs Fire — 1,377 ac, 20% contained",
    "description": "…",                  // optional, long
    "status": "ACTIVE",                  // EventStatus enum name (lifecycle)
    "effective": "2026-07-05T14:02:00Z", // RFC 3339, nullable
    "expires": null,
    "updatedAt": "2026-07-05T15:40:00Z",
    "areaLabel": "Hathaway Pines & Avery",   // optional
    "source": {
      "id": "calfire", "name": "CAL FIRE",
      "url": "https://www.fire.ca.gov/incidents/…",
      "attribution": "CAL FIRE / FIRIS",
      "fetchedAt": "2026-07-05T15:42:10Z"
    },
    // ---- per-kind typed block (only the relevant one) ----
    "wildfire": { "acres": 1377, "containment": 20, … }
  }
}
  • Per-kind data lives under a single namespaced key (wildfire, earthquake, evacuation, weather, incident, power, mesh) — consumers ignore kinds they don't handle; no field collisions.
  • Coordinate order is [longitude, latitude] (GeoJSON axis order), WGS84, trimmed to 5 decimals (~1.1 m). Polygons are simplified to keep features small.
  • Null geometry is valid (e.g. a county-wide advisory): the feature has "geometry": null with the same properties envelope. Exclude it from the map layer and render it as a full-width banner/list card sorted by severityRank alongside located features, showing headline, source.name, updatedAt, and source.url.
  • URL fields are scheme-validated upstream: any upstream-sourced URL that is not https:///http:// is dropped before it reaches the response. Treat all upstream text (headlines, descriptions) as untrusted anyway.
  • Unit-bearing fields name their unit (windGustMph, depthKm).
  • layer has two forms, by role. As a valueproperties.layer here, and event.layer / topEvents[].layer elsewhere — it is the UPPERCASE Layer enum name (WILDFIRE). As an address — the {layer} path segment, the ?layer= query value, and the metadata.layer / metadata.area echoes above — it is the lowercase slug (wildfire). One rule: values are enum names, URLs and the metadata block are slugs. Mesh-node presence is the one layer whose slug (mesh_node) is not just the lowercased enum name (MESH): as a ?layer= value the query accepts mesh (the enum name), mesh_node (the slug), and the legacy network — all resolve to the same layer. (Before 2026-07-25 this layer's enum was NETWORK; it was renamed to MESH, with network kept as a legacy query alias.)
  • This endpoint is not in the OpenAPI spec. The .geojson layers are hand-built (RFC 7946 geometry doesn't model cleanly in proto3), so a client generated from the spec alone will be missing all eight map layers — integrate them from this section.

6. The evacuation fail-loud contract

An error never becomes a 0.

Absence ≠ all-clear. An empty evacuation response is not a safe response. A source that errored returns null, not 0 — and your client must render unknown, never a blank and never a green "no evacuations." This is a contractual obligation on every consumer, not a styling suggestion.

Evacuation data is life-safety data, and it is reference only. This service surfaces the presence of orders from Cal OES and links out to the authoritative source; it never asserts "you are safe." Every consumer of this API inherits the following obligations — they are part of the contract, not styling advice.

The invariant, mechanically

In /api/v1/places/{place}/summary, summary.activeEvacuations is int | null and summary.evacuationStatus is OK | STALE | UNAVAILABLE:

activeEvacuationsevacuationStatusMeaningRequired client rendering
nullUNAVAILABLEThe Cal OES feed errored. Unknown — not zero."Evacuation status unknown — check Genasys Protect" (or the sourceUrl provided). Never an all-clear, never a blank.
0OKCal OES fetched cleanly and reports no active zones — a caveated confirmed-empty."No active evacuations reported (per Cal OES)" — still linked to the source, never phrased as a guarantee.
n > 0OK / STALEActive zones exist (STALE = counted from the last good fetch).Show the count and the zones; if STALE, show data age.

The distinction between the first two rows is the entire point: null and 0 are different values with different meanings, and the API will never collapse one into the other. Clients must not either. An UNAVAILABLE evacuation source also forces the area mode to at least WATCH — unknown is not quiet.

The same rule applies to the evacuation map layer (/api/v1/places/ebbetts-pass/map/evacuation.geojson): sourceStatus: UNAVAILABLE comes with empty features and must render as an unknown-state banner, never an empty (implicitly safe) map. metadata.sourceUrl links the authoritative Genasys viewer in every state.

Finally: directive life-safety text is never rewritten. Evacuation orders and instructions appear verbatim in description — AI enhancement is prohibited for this layer. Render orders verbatim, with the link out.

The summary shape (quoted from the contract)

{
  "place": "ebbetts-pass", "placeId": "area:ebbetts-pass",
  "placeName": "Calaveras County", "generatedAt": "RFC3339",
  "mode": "QUIET|WATCH|ACTIVE",
  "summary": {
    "highestSeverity": "SEVERE", "highestSeverityRank": 3,
    "severityCounts": {"SEVERE": 1}, "totalActive": 4,
    "activeEvacuations": null, "evacuationStatus": "UNAVAILABLE",
    "topEvents": [{"id","layer","severity","severityRank","headline","source"}]
  },
  "domains": [{"domain","status","highestSeverity","activeCount",
               "headlines":[{"id","severity","headline"}]}],
  "sources": [{"id","status","lastSuccessAt"}]
}

7. Source registry

Every event and every layer traces to one of these sources — the registry behind /api/v1/sources (live board: Sources). Each Source carries id, name, attribution, homepageUrl, pollIntervalSeconds, staleAfterSeconds, expireAfterSeconds, lastSuccessAt, lastAttemptAt, lastError, status. A source is STALE once its last success is older than staleAfterSeconds (default 3× its poll interval) and UNAVAILABLE on persistent failure.

idProvidesEvent idsPollDisappearance policy
nwsNational Weather Service zone alerts (api.weather.gov) — weather_alert events; also the authority behind fire_weather conditionswx:5mexpire — missing and past expires ⇒ EXPIRED
usgsUSGS earthquake feed for the service-area boundsusgs:5m— (see notes)
calfireCAL FIRE active incidents (acres, containment)calfire:5mresolve — active-only feed; missing ⇒ RESOLVED
firisCAL FIRE / FIRIS fire perimeters (joined to CAL FIRE incidents by name; standalone perimeters get firis: ids)firis:5mexpire
caloesCal OES California Evacuation Aggregation Layer (Zonehaven/Genasys zone schema) — reference only, see §6evac:2mresolve
chpCHP incidents (AI-enhanced road_incident events)chp:5mresolve
caltransCaltrans lane closures (road_incident events); also attributed for chain-control conditionschp:5mresolve
pgePG&E electric outages (planned and unplanned), affected-area polygons joined onto the reported pointpge:5mresolve — active-only feed; missing ⇒ RESOLVED
pspsPG&E Public Safety Power Shutoff coverage; one event per de-energization window, empty outside an eventpsps:5mresolve
meshcoreMesh-node presence, from community MeshCore MQTT bridges and operator-run monitors (§8). Every mesh event stays on this source whichever input observed it.meshcore:60sexpire — a mesh has no goodbye packet, so silence is not proof of absence
reporter idsOne row per configured push reporter (§8), e.g. alan-pi. Health only — no event is attributed to a reporter; the row answers "is that monitor still reporting?"

A poller may update multiple source rows (the wildfire tick updates calfire + firis; the incidents tick updates chp + caltrans) — poller ≠ source. Poll intervals and staleness thresholds are server configuration; read the live values from /api/v1/sources rather than hard-coding this table.

Attribution requirements

  • Display the attribution string (from Source.attribution, Event.provenance.attribution, or metadata.attribution on GeoJSON layers) wherever you render the data.
  • Link the upstream: provenance.sourceUrl (events) / metadata.sourceUrl (GeoJSON) on records, homepageUrl on the source.
  • Evacuation data must carry its reference-only framing and the Genasys link in every state (§6).

8. Push ingest (operator-reported data)

This is the only write endpoint on the API, and the only one that requires a credential. Everything else on /api/v1 is public, read-only and keyless; nothing below changes that. It exists for data no upstream feed publishes — an operator's monitor that logs into MeshCore repeaters and reads their admin interface, which is the only way to obtain battery, airtime and packet counters, and the only input that can report a node it tried to reach and could not.

POST /api/v1/ingest/{stream}

Requires Authorization: Bearer <token>. Tokens are issued per reporter by the operator; only a SHA-256 hash of the token is stored in configuration, so the credential itself never appears in this project's public repository. The endpoint is not mounted at all unless a reporter is configured, and the CORS policy (corsAllowMethods: [GET]) leaves it unreachable from a cross-origin browser page.

Success is 202 Accepted, not 200: the report has been authenticated and validated, but it is not yet in the store — the next ingest tick merges it. The response is {reporterId, stream, accepted, warnings, receivedAt}. A malformed entry is a warning, not a rejection — one repeater with a typo'd id must not cost the operator the other eight — while a malformed envelope rejects the whole request.

StatusMeaning
202Accepted and buffered. warnings[] lists entries that were skipped.
401Missing or unknown token. Deliberately carries no detail.
403Valid token, but this reporter is not granted that stream.
404Unknown stream.
413Body over the configured limit.
429Reporting faster than the reporter's configured minimum interval; see Retry-After.
400Malformed JSON, unsupported schema_version, or too many entries.

Stream mesh.repeater (schema_version 1)

POST /api/v1/ingest/mesh.repeater
Authorization: Bearer <token>

{
  "schema_version": 1,
  "generated_at": "2026-09-15T00:23:32+00:00",
  "repeaters": [
    {
      "id": "de0715314cfa9b5e",          // public key, or a prefix of one (≥8 hex)
      "name": "SIERRA Arnold Summit",
      "last_attempt": 1789431672,         // unix seconds
      "last_success": 1789431672,         // null if never reached
      "battery_voltage": 4.14, "battery_percent": 97.0,
      "battery_percent_source": "estimated",
      "temperature_c": 37.0, "humidity": null, "pressure": null,
      "uptime_s": 2615083, "airtime_ms": 49646, "rx_airtime_ms": 211238,
      "noise_floor_dbm": -116, "last_rssi_dbm": -88, "last_snr_db": 12.5,
      "tx_queue_len": 0,
      "nb_sent": 150305, "nb_recv": 649331,
      "sent_flood": 149984, "sent_direct": 321,
      "recv_flood": 642043, "recv_direct": 6702,
      "direct_dups": 53, "flood_dups": 30331,
      "full_evts": 0, "recv_errors": 240197,
      "online": true
    }
  ]
}
  • A report is the reporter's COMPLETE current set. A node omitted from a report is treated as no longer monitored, the same contract a poller's result has with the disappearance sweep. Send every node you watch, every time.
  • Unread values must be null, never 0. A null gauge is published as absent; a zero is published as a measurement. A node that has never been reached (last_success: null) is published with no telemetry block at all rather than a row of zeroed counters.
  • Timestamps are clamped to server time. A stamp in the future is treated as "now" — a monitor's clock is not ours.
  • Reposting an identical report is harmless: entries are keyed by node, so a retry after a network failure overwrites rather than duplicates.

How it surfaces

  • One event per node, not one per input. A node reported by a monitor and heard over MQTT resolves to the same event id — a reported prefix is matched against the known node catalog. While a node is known only by prefix its id is meshcore:{prefix}; once an advert supplies the full key, the event id becomes meshcore:{full key} and the prefix-keyed event is retired as superseded. Don't treat a mesh event id as permanent for a node not yet heard over MQTT.
  • mesh.reachabilityREACHABLE | UNREACHABLE. A node no monitor watches has neither, and must not render as one that is down: on /api/v1/events it reads MESH_REACHABILITY_UNSPECIFIED (the gateway emits unpopulated fields), and on the .geojson layer the property is omitted. It is derived from the age of the last success, not from the reporter's online flag, so a single missed poll does not flip it. It is part of the event's content hash, so a transition appears in /api/v1/events/{id}/history.
  • mesh.telemetry.admin — the last sample, with reporterId and lastSuccessAt. It is excluded from the content hash, so frequent reporting refreshes these values without creating a revision per report: poll events/{id} for current values, and expect /history to show only meaningful changes.
  • A node known only to a monitor has no geometry — a report carries no coordinates and the Grid does not invent them — so it appears in /api/v1/events?layer=mesh and, where the operator has configured a place attachment, in place-scoped views, but its mesh_node map feature has "geometry": null until an advert supplies a position.

↑ top