NetworkEventsHow it worksContributorsAPIPrivacy
Developers

Build on NE-PULSE

The network's earthquake events and live state are open: no key, no sign-up, free. Use them for an alert bot, a school display, a map, a research notebook, or connect your own sensor and make the network denser.

Experimental research network, not an official warning service. Events can be wrong or retracted; show that to your users. For safety decisions follow the national seismological service and the Ministry of Emergency Situations.

Quick start

Everything is plain HTTPS + JSON. Try it:

shellcurl https://waiver-fans-edward-scsi.trycloudflare.com/api/v1/events?limit=5
curl https://waiver-fans-edward-scsi.trycloudflare.com/api/v1/network

base URL: https://waiver-fans-edward-scsi.trycloudflare.com

Endpoints

MethodPathWhat you get
GET/api/v1/events?limit=50Recent events, newest first (limit 1–200; add &all=1 to include candidates).
GET/api/v1/events/{id}One event with its full decision log: every check the network made and why.
GET/api/v1/events/{id}/tracesShort anonymous waveform snippets recorded around that event.
GET/api/v1/networkLive state: connected and listening phones, active areas (10 km, only if ≥3 phones), noise trace, active events.
GET/api/v1/history?hours=24Per-minute network statistics (phones, samples, triggers, events).
GET/api/v1/configThe detection settings in force (levels, thresholds, algorithm version).
WS/ws/publicLive push: the network state about every second, and every event update the moment it happens.

Live feed

Open one WebSocket and you get two message types: `network` (the same body as /api/v1/network) and `event` (an event was created or changed level). No polling needed.

javascript// Live: every event update, the moment it happens
const ws = new WebSocket("wss://waiver-fans-edward-scsi.trycloudflare.com/ws/public");
const seen = new Set();
ws.onmessage = (m) => {
  const msg = JSON.parse(m.data);
  if (msg.type !== "event") return;           // "network" = live state
  const e = msg.event;
  if (e.level === "confirmed" && !e.retracted && !seen.has(e.id)) {
    seen.add(e.id);
    const t = new Date(e.originT * 1000).toLocaleTimeString();
    console.log(`Earthquake confirmed ${t} near ${e.lat}, ${e.lng} (±${e.locUncKm} km)`);
    // post to your Discord webhook / Telegram channel / school display here
  }
};

Event object

The fields you will use most:

id
Stable event id, e.g. NP-20261011-091500-1
level
candidate → corroborated → confirmed (only confirmed should alert people)
originT
Estimated start time, Unix seconds (±originUncSec)
lat, lng
Estimated epicentre, rounded (±locUncKm); depthKmAssumed is a model value, not measured
levelTimes
When each level was reached, Unix seconds
cells
Areas (10 km) that felt it: first arrival time, peak acceleration, intensity
retracted
true if the event was withdrawn; show it, don't hide it

Examples

A tiny alert bot: print (or post to Discord/Telegram) every newly confirmed earthquake.

↑ Live feed

Research: load the last day of network statistics into pandas.

pythonimport requests, pandas as pd

API = "https://waiver-fans-edward-scsi.trycloudflare.com"
h = requests.get(f"{API}/api/v1/history", params={"hours": 24}).json()
df = pd.DataFrame(h["minutes"])
df["time"] = pd.to_datetime(df["t"], unit="s")
print(df[["time", "connected", "eligible", "samples", "picksAccepted", "events"]].tail())

events = requests.get(f"{API}/api/v1/events", params={"limit": 200}).json()["events"]
print(pd.DataFrame(events)[["id", "level", "originT", "lat", "lng", "locUncKm"]])

Just want alerts? The Telegram bot does it already, no code needed: @ne_pulse_bot

Connect your own sensor

Anything with an accelerometer can join: an ESP32 + MPU6050 (~$5), a Raspberry Pi, an old phone. It must lie still and fixed; that is what makes a good sensor. Three steps:

  1. 1Register once: GET /api/v1/challenge, solve the small proof-of-work (find a nonce so that SHA-256(challenge + ":" + nonce) starts with `difficulty` zero bits), then POST /api/v1/devices. Keep the deviceId and secret.
  2. 2Open a WebSocket to /ws/device and send a hello with your 1 km area (row/col of the 1 km grid; the exact location is never sent).
  3. 3Send a batch every second: sample times as offsets from t0 (0.1 ms units) and x, y, z in mm/s² with gravity included, at 50–100 Hz. The server filters, detects and votes exactly as for phones.
python# pip install requests websockets
import asyncio, hashlib, json, math, time, requests, websockets

API = "https://waiver-fans-edward-scsi.trycloudflare.com"

def solve(challenge, bits):                       # proof-of-work, ~1 s
    n = 0
    while True:
        h = hashlib.sha256(f"{challenge}:{n}".encode()).digest()
        if int.from_bytes(h, "big") >> (256 - bits) == 0:
            return str(n)
        n += 1

def cell_1km(lat, lng):                           # same grid as the site
    kmdeg = 2 * math.pi * 6371.0088 / 360
    dlat = 1 / kmdeg
    row = math.floor((lat + 90) / dlat)
    latc = -90 + (row + 0.5) * dlat
    n = max(1, round(360 * kmdeg * max(math.cos(math.radians(latc)), 1e-3)))
    return row, min(n - 1, math.floor((lng + 180) / (360 / n)))

# 1. register once (store these two values)
ch = requests.get(f"{API}/api/v1/challenge").json()
dev = requests.post(f"{API}/api/v1/devices", json={
    "challenge": ch["challenge"], "nonce": solve(ch["challenge"], ch["difficulty"]),
    "platform": "other", "consent": "v1"}).json()

def read_accel():                                 # <- your sensor here, m/s^2 incl. gravity
    return 0.0, 0.0, 9.81

async def main(lat, lng):
    row, col = cell_1km(lat, lng)
    async with websockets.connect(API.replace("http", "ws", 1) + "/ws/device") as ws:
        # 2. hello with the 1 km area
        await ws.send(json.dumps({"type": "hello", "deviceId": dev["deviceId"], "secret": dev["secret"],
            "platform": "other", "client": "my-sensor/0.1", "location": {"grid": 1, "row": row, "col": col}}))
        seq = 0
        while True:                               # 3. one batch per second, ~100 Hz
            t0 = time.time() * 1000
            dt, x, y, z = [], [], [], []
            while time.time() * 1000 - t0 < 1000:
                ax, ay, az = read_accel()
                dt.append(round((time.time() * 1000 - t0) * 10))
                x.append(round(ax * 1000)); y.append(round(ay * 1000)); z.append(round(az * 1000))
                await asyncio.sleep(0.01)
            seq += 1
            await ws.send(json.dumps({"type": "batch", "seq": seq, "t0": t0, "dt": dt,
                                      "x": x, "y": y, "z": z, "clock": {"offset": 0, "rtt": 0}}))

asyncio.run(main(41.31, 69.28))                   # your approximate location

Ready firmware for ESP32 + MPU6050 is in the source code under firmware/esp32-station.

Rules

Coming next

Not built yet. Tell us what you would make with them.