Skip to content
Console

Earthquake report stream

eq.report.v1 pushes a report when the server receives a new earthquake report from the Central Weather Administration (CWA). The shape is close to a single report from the earthquake report API. The same report usually arrives more than once: an early version first, then another after it is written to the database. Use it to notify on a new earthquake report.

How to connect, decode, and reconnect is in SSE connection.

Upstream terms
  • Central Weather Administration (CWA): CWA's own data terms (such as the Open Government Data License) also apply; CWA's official releases take precedence.

Where to show it, examples and commercial-use requests: Data licensing and attribution.

No token is required (public topic). If you send a valid token, usage is recorded under the permission eq.report, and going over the quota does not disconnect the stream. See SSE connection: Authentication.

  • Multiplexed entry: /api/v1/trem/sse?topics=eq.report.v1, event name eq.report.v1 (the prefix is eq, not trem).
  • Single-topic path:
GEThttps://api.lb.exptech.dev/api/v1/trem/sse/report
Query parameters
  • tokenstringOptional

    API Token (optional). Prefer the Authorization: Bearer header.

Request
curl -N --compressed "https://api.lb.exptech.dev/api/v1/trem/sse/report"
Response

text/event-stream; data is base64(gzip(JSON))

retry: 3000
event: info
data: {"location":"lb-tpe1"}
data: H4sIAAAAAAAC/...

The token is invalid, expired, or revoked

invalid token

mode has no effect on this topic. Every message is sent.

Each decoded data is one JSON object:

{
"reason": 1,
"data": {
"id": "115079-2026-1109-122157",
"lat": 23.62,
"lon": 120.66,
"depth": 10.5,
"loc": "雲林縣政府東南東方 23.2 公里 (位於雲林縣古坑鄉)",
"mag": 4.2,
"time": 1794198117000,
"list": {
"雲林縣": {
"int": 3,
"town": {
"草嶺": { "lat": 23.6, "lon": 120.68, "int": 3 }
}
}
}
}
}
FieldTypeDescription
reasonintWhere this message came from and when it was sent, 0–3. See the table below.
data.idstringReport ID, format XXXXXX-YYYY-MMDD-HHMMSS, the same as GET /api/v2/eq/report/{id}.
data.latnumberEpicenter latitude (degrees).
data.lonnumberEpicenter longitude (degrees).
data.depthnumberFocal depth (kilometers).
data.locstringEpicenter description.
data.magnumberRichter magnitude.
data.timeintOrigin time, Unix milliseconds.
data.listobjectSeismic intensity by county or city. The key is the county or city name.
<縣市>.intintMaximum seismic intensity in that county or city, 0–9.
<縣市>.town.<測站>objectThe station's lat and lon (degrees) and int (seismic intensity 0–9).

The 0–9 intensity values are mapped in Earthquake reports: Intensity scale. The message has no int (maximum intensity across Taiwan), md5, or trem. For the maximum across Taiwan, take the maximum int among the counties and cities in list.

Value Source When it is sent Already available from the HTTP API
0 CWA real-time publication When a new report is detected (the earliest message) Not necessarily
1 CWA report page When a report from today is written to the database Yes
2 CWA report page When a report that was missed earlier is filled in Yes
3 CWA report page When a report is written or backfilled during scheduled maintenance (usually an older report) Yes

A browser receives the single-topic path: onmessage reads and decodes the data. After connecting, it catches up on recent reports with the HTTP API, then deduplicates on data.id.

async function decodeData(b64) {
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"));
return await new Response(stream).text();
}
const seen = new Set();
async function catchUp() {
// This topic does not replay. After connecting, catch up on recent reports with the HTTP API
const res = await fetch("https://api.core.exptech.dev/api/v2/eq/report?limit=10&minIntensity=0");
for (const r of await res.json()) seen.add(r.id);
}
const es = new EventSource("https://api.lb.exptech.dev/api/v1/trem/sse/report");
es.addEventListener("info", () => catchUp());
es.onmessage = async (e) => {
const { reason, data } = JSON.parse(await decodeData(e.data));
if (reason === 3 && Date.now() - data.time > 24 * 3600 * 1000) return; // backfilled older report
const isNew = !seen.has(data.id);
seen.add(data.id);
const maxInt = Math.max(0, ...Object.values(data.list).map((a) => a.int));
console.log(isNew ? "new earthquake report" : "report updated", data.id, `M${data.mag}`, `max intensity ${maxInt}`);
};
  • Nothing is replayed. Connecting (or reconnecting) does not send earlier reports. After you reconnect, call GET /api/v2/eq/report to fill in reports from the gap. Pass minIntensity=0 to include intensity 0. Fetch the single report when you need intensity by area.
  • The same report usually arrives more than once. A message with reason 0 comes first. After it is written to the database, another arrives with 1, 2, or 3. Deduplicate on data.id, and let the later contents replace the earlier ones.
  • When reason is 0, the HTTP API may not have the report yet. GET /api/v2/eq/report/{id} can return 404 at that point. It is available after you receive another reason.
  • The two list values can differ slightly. The reason 0 message and the later one come from different sources, so stations and coordinates can differ. The later message’s coordinates are rounded to 2 decimal places.
  • reason 3 is usually an older report. Do not drop it outright. For example, a report that could not be filled in earlier is sent as 3 during scheduled maintenance. If you only want recent reports, filter on data.time.
  • Time unit. data.time is Unix milliseconds.