Free REST API featuring interactive weather warning maps from BMKG nowcast. Also includes earthquake data, forecasts & region lookup.
curl https://bmkg-restapi.vercel.app/v1/nowcast
Real-time seismic data from BMKG's nationwide sensor network.
/earthquake/latest
/earthquake/recent
/earthquake/nearby
3-day weather forecasts for any location in Indonesia.
/weather/{adm4}
/weather/{adm4}/current
Real-time weather warnings with map visualization & infographic.
/nowcast
/nowcast/{alert_code}
Indonesian region lookup: provinces, districts, villages.
/wilayah/provinces
/wilayah/search
Try the API with live visualization for geo data
Connect BMKG data directly to Claude, Cursor, VS Code, and other MCP-compatible AI assistants
Connect instantly via Smithery or HTTP-compatible clients. No local installation required!
https://mcp-bmkg.dhanypedia.it.com/mcp
Install locally for offline use and full control. Best for Claude Desktop, Cursor.
pipx install bmkg-api-mcp
Claude Code:
claude mcp add --transport http bmkg-api https://mcp-bmkg.dhanypedia.it.com/mcp
Claude Desktop config:
{
"mcpServers": {
"bmkg-api": {
"url": "https://mcp-bmkg.dhanypedia.it.com/mcp"
}
}
}
Claude Desktop
Add to claude_desktop_config.json
Cursor
Settings → MCP Servers → Add
VS Code
Use with Cline/Roo Code extension
4 Earthquake
Latest, recent, felt, nearby
2 Weather
3-day & current
2 Nowcast
Weather warnings
5 Region
Provinces to villages
2 Utility
Cache & debug
Try these natural language queries:
Choose from earthquake, weather, nowcast, or wilayah
Simple HTTP GET request, no authentication required
Clean, structured JSON response with attribution
curl https://bmkg-restapi.vercel.app/v1/earthquake/latest
fetch('https://bmkg-restapi.vercel.app/v1/earthquake/latest')
.then(r => r.json())
.then(data => console.log(data));
import requests
response = requests.get(
'https://bmkg-restapi.vercel.app/v1/earthquake/latest'
)
data = response.json()
print(data)
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
resp, err := http.Get(
"https://bmkg-restapi.vercel.app/v1/earthquake/latest",
)
if err != nil {
panic(err)
}
defer resp.Body.Close()
var data map[string]interface{}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data)
}
$response = file_get_contents(
'https://bmkg-restapi.vercel.app/v1/earthquake/latest'
);
$data = json_decode($response, true);
print_r($data);
require 'net/http'
require 'json'
uri = URI('https://bmkg-restapi.vercel.app/v1/earthquake/latest')
response = Net::HTTP.get(uri)
data = JSON.parse(response)
puts data
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() async {
final response = await http.get(
Uri.parse('https://bmkg-restapi.vercel.app/v1/earthquake/latest'),
);
final data = jsonDecode(response.body);
print(data);
}
The nowcast warning endpoint returns polygon coordinates for affected areas. Here's how to extract and render them on a map using Turf.js.
Each area has a flat
[lat, lon] array with
multiple closed rings concatenated together. Swap to
[lon, lat] for map
libraries.
{ "areas": [{ "polygon": [
[-7.434, 110.216], // Ring 1 start
[-7.444, 110.215], ...
[-7.434, 110.216], // Ring 1 close (matches start)
[-7.565, 110.72], // Ring 2 start (50km+ jump)
...
[-7.565, 110.72] // Ring 2 close
]}]}
Detect ring boundaries with coordinate matching (point equals start) and distance jump (>10km gap = new polygon).
function splitRings(coords) {
const rings = [], eps = 0.0001;
let start = coords[0];
let ring = [[start[1], start[0]]];
for (let i = 1; i < coords.length; i++) {
const c = coords[i], pt = [c[1], c[0]];
// Split on >10km gap (new polygon group)
if (ring.length >= 4) {
const prev = ring[ring.length - 1];
if (turf.distance(prev, pt,
{ units: 'kilometers' }) > 10) {
ring.push(ring[0]);
rings.push(ring);
start = c; ring = [pt]; continue;
}
}
ring.push(pt);
// Split on coordinate match (ring closed)
if (Math.abs(c[0]-start[0]) < eps
&& Math.abs(c[1]-start[1]) < eps
&& ring.length >= 4) {
rings.push(ring); ring = [];
if (i+1 < coords.length) {
start = coords[i+1];
ring = [[start[1], start[0]]]; i++;
}
}
}
if (ring.length >= 4) {
ring.push(ring[0]); rings.push(ring);
}
return rings;
}
Reject strip artifacts by max edge length — real boundaries have short edges (<5km), artifacts have long diagonal edges (50km+).
const rings = splitRings(area.polygon);
rings.forEach((ring, i) => {
try {
const poly = turf.polygon([ring]);
if (turf.area(poly) <= 0) return;
// Reject artifacts: max edge > 20km
let maxEdge = 0;
for (let j = 0; j < ring.length - 1; j++) {
const d = turf.distance(ring[j],
ring[j+1], { units: 'kilometers' });
if (d > maxEdge) maxEdge = d;
}
if (maxEdge > 20) return;
// Add to map
map.addSource(`area-${i}`, {
type: 'geojson', data: poly
});
map.addLayer({
id: `area-${i}-fill`,
type: 'fill',
source: `area-${i}`,
paint: {
'fill-color': '#f59e0b',
'fill-opacity': 0.35
}
});
} catch (e) { /* skip invalid */ }
});
Coordinate order: API returns
[lat, lon], map libraries expect
[lon, lat]. Always swap before rendering.
Why filter? The flat array format can produce thin strip artifacts connecting distant polygons. The 20km edge + 10km gap thresholds eliminate these while preserving real boundaries.
This public demo has rate limits (30 requests/minute) for fair usage. For unlimited access and production use, please self-host your own instance.
Data Source
All data provided by BMKG (Badan Meteorologi, Klimatologi, dan Geofisika)
This API is not affiliated with BMKG. All data belongs to BMKG.