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.
Data license
Section titled “Data license”AttributionData: Central Weather Administration (CWA), ExpTech Studio (exptech.dev)
- 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.
Authentication
Section titled “Authentication”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.
Subscribe
Section titled “Subscribe”- Multiplexed entry:
/api/v1/trem/sse?topics=eq.report.v1, event nameeq.report.v1(the prefix iseq, nottrem). - Single-topic path:
tokenAPI Token (optional). Prefer the Authorization: Bearer header.
curl -N --compressed "https://api.lb.exptech.dev/api/v1/trem/sse/report"curl -N --compressed "https://api.lb-tpe1.exptech.dev/api/v1/trem/sse/report"curl -N --compressed "https://api.lb-khh1.exptech.dev/api/v1/trem/sse/report"const res = await fetch("https://api.lb.exptech.dev/api/v1/trem/sse/report");if (!res.ok) throw new Error(`HTTP ${res.status}`);
// text/event-stream:逐段讀取const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();for (;;) { const { value, done } = await reader.read(); if (done) break; console.log(value);}const res = await fetch("https://api.lb-tpe1.exptech.dev/api/v1/trem/sse/report");if (!res.ok) throw new Error(`HTTP ${res.status}`);
// text/event-stream:逐段讀取const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();for (;;) { const { value, done } = await reader.read(); if (done) break; console.log(value);}const res = await fetch("https://api.lb-khh1.exptech.dev/api/v1/trem/sse/report");if (!res.ok) throw new Error(`HTTP ${res.status}`);
// text/event-stream:逐段讀取const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();for (;;) { const { value, done } = await reader.read(); if (done) break; console.log(value);}import requests
with requests.get( "https://api.lb.exptech.dev/api/v1/trem/sse/report", stream=True, timeout=30,) as res: res.raise_for_status() for line in res.iter_lines(decode_unicode=True): if line: print(line)import requests
with requests.get( "https://api.lb-tpe1.exptech.dev/api/v1/trem/sse/report", stream=True, timeout=30,) as res: res.raise_for_status() for line in res.iter_lines(decode_unicode=True): if line: print(line)import requests
with requests.get( "https://api.lb-khh1.exptech.dev/api/v1/trem/sse/report", stream=True, timeout=30,) as res: res.raise_for_status() for line in res.iter_lines(decode_unicode=True): if line: print(line)text/event-stream; data is base64(gzip(JSON))
retry: 3000
event: infodata: {"location":"lb-tpe1"}
data: H4sIAAAAAAAC/...The token is invalid, expired, or revoked
invalid tokenmode has no effect on this topic. Every message is sent.
Message format
Section titled “Message format”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 } } } } }}| Field | Type | Description |
|---|---|---|
reason | int | Where this message came from and when it was sent, 0–3. See the table below. |
data.id | string | Report ID, format XXXXXX-YYYY-MMDD-HHMMSS, the same as GET /api/v2/eq/report/{id}. |
data.lat | number | Epicenter latitude (degrees). |
data.lon | number | Epicenter longitude (degrees). |
data.depth | number | Focal depth (kilometers). |
data.loc | string | Epicenter description. |
data.mag | number | Richter magnitude. |
data.time | int | Origin time, Unix milliseconds. |
data.list | object | Seismic intensity by county or city. The key is the county or city name. |
<縣市>.int | int | Maximum seismic intensity in that county or city, 0–9. |
<縣市>.town.<測站> | object | The 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.
reason
Section titled “reason”| 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 |
Advanced example
Section titled “Advanced example”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/reportto fill in reports from the gap. PassminIntensity=0to include intensity 0. Fetch the single report when you need intensity by area. - The same report usually arrives more than once. A message with
reason0comes first. After it is written to the database, another arrives with1,2, or3. Deduplicate ondata.id, and let the later contents replace the earlier ones. - When
reasonis0, the HTTP API may not have the report yet.GET /api/v2/eq/report/{id}can return404at that point. It is available after you receive anotherreason. - The two
listvalues can differ slightly. Thereason0message 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. reason3is usually an older report. Do not drop it outright. For example, a report that could not be filled in earlier is sent as3during scheduled maintenance. If you only want recent reports, filter ondata.time.- Time unit.
data.timeis Unix milliseconds.
