API reference

Every example on this page runs against the real API.

Basics

  • Base URL: https://bmkg-api.dhanypedia.com/v1
  • Every endpoint is a GET and works without an API key.
  • CORS is open, so the API can be called from a browser.
  • The data belongs to BMKG. The attribution value needs to be shown wherever the data is published.

Response format

A successful response always has data, meta and attribution.

200
{
  "data": { "magnitude": 5.4, "region": "18 km TimurLaut Pekalongan" },
  "meta": { "fetched_at": "2026-02-16T08:30:00Z", "cache_ttl": 60 },
  "attribution": "BMKG (Badan Meteorologi, Klimatologi, dan Geofisika)"
}

A failed response has error, message and status.

404
{
  "error": "not_found",
  "message": "Weather data not available for ADM4 code '99.99.99.9999'.",
  "status": 404
}
StatusMeaning
200Success
400An argument has the wrong format
404No data for that code
422A required parameter is missing or out of range
429Rate limit exceeded. A retry works after one minute
503BMKG is unreachable, or this server reached its request budget towards BMKG

Caching and rate limits

BMKG updates its data at fixed intervals. Every response is cached for as long as its data stays current, on this server and at the CDN in front of it. A request made more often than the cache time receives the same answer.

  • Cache-Control states how long a response stays fresh.
  • X-Cache is HIT when this server answered from its cache.
  • The limit is 30 requests per minute per endpoint for each client. X-RateLimit-Remaining shows what is left.

For heavier use there is a guide to running a separate instance.

Earthquakes

GET /v1/earthquake/latest cached 60 s

The single latest earthquake: magnitude, depth, coordinates, region, tsunami potential, felt reports and a shakemap URL. Times are UTC.

Open in the explorer /v1/earthquake/latest
GET /v1/earthquake/recent cached 5 min

The last 15 earthquakes of magnitude 5.0 or more, newest first.

Open in the explorer /v1/earthquake/recent
GET /v1/earthquake/felt cached 5 min

The last 15 earthquakes reported as felt, small ones included, with where and how strongly each was felt.

Open in the explorer /v1/earthquake/felt
GET /v1/earthquake/nearby cached 5 min

Earthquakes from the recent and felt lists within a radius of a point, nearest first, each with distance_km. An empty list means none among BMKG's latest records, not none ever.

latrequiredLatitude, -90 to 90
lonrequiredLongitude, -180 to 180
radius_kmoptionalSearch radius in km, default 100
Open in the explorer /v1/earthquake/nearby?lat=-6.2&lon=106.8&radius_km=300

Weather

GET /v1/weather/{adm4_code} cached 15 min

The forecast for one village over three days in 3-hour steps: temperature, humidity, weather in Indonesian and English, wind and cloud cover. visibility_m can be null.

adm4_codein pathVillage code as 2.2.2.4 digits, for example 31.74.04.1006
Open in the explorer /v1/weather/31.74.04.1006
GET /v1/weather/{adm4_code}/current cached 15 min

The forecast entry closest to now for one village. This is BMKG's forecast for the current 3-hour slot, not a station observation.

adm4_codein pathVillage code as 2.2.2.4 digits, for example 31.74.04.1006
Open in the explorer /v1/weather/31.74.04.1006/current

Warnings

GET /v1/nowcast cached 2 min

Provinces that currently have a severe weather warning, each with a code for the detail endpoint. An empty list means no warning is active.

langoptionalid (default) or en
Open in the explorer /v1/nowcast
GET /v1/nowcast/warnings cached 2 min

Every active warning in full in one request, each with its affected areas as polygons. meta.missing lists the codes of warnings whose detail could not be read from BMKG, so a short list is not mistaken for a complete one.

langoptionalid (default) or en
Open in the explorer /v1/nowcast/warnings
GET /v1/nowcast/{alert_code} cached 2 min

One warning in full: event, severity, validity period, description, an infographic URL and the affected areas as polygons. Only codes from the active list exist; any other code returns 404.

alert_codein pathA code from /v1/nowcast
langoptionalid (default) or en
Open in the explorer /v1/nowcast/{alert_code}
GET /v1/nowcast/check cached 2 min

Active warnings whose text mentions a place name. has_warnings set to false means no active warning mentions it, which is not a forecast of good weather.

locationrequiredName of a district, city or regency as BMKG writes it
langoptionalid (default) or en
Open in the explorer /v1/nowcast/check?location=Jakarta

Regions

GET /v1/wilayah/provinces cached 24 h

All provinces with their two-digit codes.

Open in the explorer /v1/wilayah/provinces
GET /v1/wilayah/districts cached 24 h

The regencies (kabupaten) and cities (kota) of one province.

provincerequiredTwo-digit province code, for example 31
Open in the explorer /v1/wilayah/districts?province=31
GET /v1/wilayah/subdistricts cached 24 h

The districts (kecamatan) of one regency or city.

districtrequiredRegency or city code, for example 31.74
Open in the explorer /v1/wilayah/subdistricts?district=31.74
GET /v1/wilayah/villages cached 24 h

The villages (desa and kelurahan) of one district. Their codes are the adm4 codes the weather endpoints take.

subdistrictrequiredDistrict code, for example 31.74.04
Open in the explorer /v1/wilayah/villages?subdistrict=31.74.04

Working with warning polygons

The warning detail endpoint returns each affected area as a polygon. Two things often go wrong:

  • Points are written as [lat, lon]. Most map libraries expect [lon, lat].
  • One area's list holds several rings joined without a separator.

The maps on this site split them as follows:

JavaScript
// One area's polygon is a flat list of [lat, lon] points holding several
// closed rings back to back. A ring ends when a point returns to the ring's
// start, or when the next point is more than 10 km away.
function splitRings(coords) {
  const rings = [];
  let start = coords[0];
  let ring = [coords[0]];

  for (let i = 1; i < coords.length; i++) {
    const point = coords[i];

    if (ring.length >= 4 && distanceKm(ring.at(-1), point) > 10) {
      rings.push([...ring, ring[0]]);
      start = point;
      ring = [point];
      continue;
    }

    ring.push(point);
    if (samePoint(point, start) && ring.length >= 4) {
      rings.push(ring);
      ring = [];
      if (i + 1 < coords.length) {
        start = coords[i + 1];
        ring = [start];
        i++;
      }
    }
  }

  if (ring.length >= 4) rings.push([...ring, ring[0]]);
  return rings;
}

// Real boundaries have short edges. A ring with an edge over 20 km joins
// two distant clusters and is dropped.
const drawable = splitRings(area.polygon).filter((ring) =>
  ring.every((point, i) => i === 0 || distanceKm(ring[i - 1], point) <= 20)
);