Documentation

Overview

The Indonesia Region API publishes Indonesian administrative region codes and names as static JSON files. There is no server, database or API key: every endpoint is a file on a CDN, so any HTTP client in any language can use it.

This is an independent project providing machine-readable data derived from publicly available Indonesian government regulatory documents. It is not an official government API, and the Ministry of Home Affairs (Kemendagri) does not endorse it. For legal purposes, rely on the official documents.

What “kodifikasi wilayah” means

Kode dan data wilayah administrasi pemerintahan is the national numbering of government administrative areas maintained by the Ministry of Home Affairs. Every province, regency, city, district, kelurahan and desa has a numeric code that is used across government systems (population registration, elections, statistics, budgeting). The rules for assigning codes are set by Permendagri 58/2021. The full code table is issued, and periodically updated, as a Keputusan Menteri Dalam Negeri (ministerial decree). The current one is Kepmendagri 300.2.2-2138/2025.

Administrative levels

Level (API)IndonesianCodeCountNotes
provinceProvinsi3138Includes special regions (Aceh, DKI Jakarta, DI Yogyakarta, the Papua provinces).
regency, type kabupatenKabupaten32.04416Regency. Code 01–69.
regency, type kotaKota32.7398City. Code 71–99. A kota is not a kabupaten: “Kabupaten Bandung” (32.04) and “Kota Bandung” (32.73) are different areas.
districtKecamatan (Distrik in Papua)32.73.017,285
village, type kelurahanKelurahan32.73.01.10018,496Urban administrative village (part of the district administration). Code starts with 1.
village, type desaDesa (Gampong, Nagari, Kampung, …)11.01.01.200175,252Self-governing village community. Code starts with 2.
village, type desa_adatDesa adat91.03.01.300714Customary village. Code starts with 3. Official recaps count these as desa (75,266 in total).

The hierarchy is strict: every regency/city belongs to one province, every district to one regency/city, every village to one district.

Province            31              Daerah Khusus Ibukota Jakarta
└── Regency/City    31.71           Kota Administrasi Jakarta Pusat
    └── District    31.71.01        Gambir
        └── Village 31.71.01.1001   Gambir (kelurahan)

Code format

From Permendagri 58/2021, Pasal 4–6:

Codes are strings. Store them as text, never as numbers. Leading zeros are significant ("11.01"), and the dots are part of the published code. To get the 10-digit form used in some systems, remove the dots (3171011001).

Endpoints

EndpointReturns
/api/v1/meta.jsonDataset version, effective date, sources, counts, known issues, endpoint list.
/api/v1/provinces.jsonArray of all provinces.
/api/v1/regencies.jsonArray of all regencies and cities.
/api/v1/districts.jsonArray of all districts (≈750 KB).
/api/v1/villages.jsonDeprecated (kept unchanged in v1): all villages in one file (≈24 MB, ≈3.1 MB compressed), close to the hosting's 25 MiB per-file limit. Use the per-province or per-district files.
/api/v1/provinces/{province}.jsonOne province, with counts and source.
/api/v1/provinces/{province}/regencies.jsonRegencies/cities of a province.
/api/v1/provinces/{province}/villages.jsonAll villages of a province (the same records as villages.json; at most ≈2.5 MB, ≈0.3 MB compressed).
/api/v1/regencies/{regency}/villages.jsonAll villages of a regency/city (at most ≈240 KB).
/api/v1/regencies/{regency}.jsonOne regency/city, with its province, counts and source.
/api/v1/regencies/{regency}/districts.jsonDistricts of a regency/city.
/api/v1/districts/{district}.jsonOne district, with its regency/city, province, counts and source.
/api/v1/districts/{district}/villages.jsonVillages of a district. This is also how to look up a single village: take the first 8 characters of its code as the district.
/api/v1/changes.jsonIndex of change lists between source documents.
/api/v1/export/indonesia-regions.{csv|json|xml|sql|xlsx|pdf}The whole dataset as one downloadable file per format.
/api/v1/postal-codes/{first 3 digits}.jsonPostal code lookup: every village with a given postal code (see Postal codes).
/api/v1/csv/{provinces|regencies|districts|villages}.csvThe same data as CSV, for spreadsheets and bulk import.

An unknown code returns HTTP 404. There are no per-village files, because 83,762 extra files would exceed the Cloudflare Pages file limit.

GET /api/v1/provinces.json

[
  {"code":"11","name":"Aceh","source_page":31,"latitude":4.2…,"longitude":96.9…,"spatial_status":"available","bbox":[95.0…,2.0…,98.3…,6.1…]},
  {"code":"12","name":"Sumatera Utara","source_page":320,"latitude":…,"longitude":…,"spatial_status":"available","bbox":[…]},
  ...
]

GET /api/v1/provinces/31/regencies.json

[
  {"code":"31.01","name":"Kabupaten Administrasi Kepulauan Seribu","type":"kabupaten","province_code":"31","source_page":1334},
  {"code":"31.71","name":"Kota Administrasi Jakarta Pusat","type":"kota","province_code":"31","source_page":1335},
  ...
]

GET /api/v1/districts/31.71.01.json

{
  "code": "31.71.01",
  "name": "Gambir",
  "level": "district",
  "regency": { "code": "31.71", "name": "Kota Administrasi Jakarta Pusat", "type": "kota" },
  "province": { "code": "31", "name": "Daerah Khusus Ibukota Jakarta" },
  "counts": { "villages": 6, "kelurahan": 6, "desa": 0, "desa_adat": 0 },
  "source": { "document": "KEPMENDAGRI-300-2-2-2138-2025.pdf", "page": 1335 },
  "latitude": -6.17…,
  "longitude": 106.82…,
  "spatial_status": "available",
  "bbox": [106.80…, -6.19…, 106.84…, -6.16…],
  "links": {
    "self": "/api/v1/districts/31.71.01.json",
    "villages": "/api/v1/districts/31.71.01/villages.json",
    "regency": "/api/v1/regencies/31.71.json",
    "province": "/api/v1/provinces/31.json",
    "boundary": "/api/v1/boundaries/regencies/31.71/districts.geojson"
  },
  "geometry": {"type":"Polygon","coordinates":[[[106.81…,-6.16…], …]]}
}

GET /api/v1/districts/31.71.01/villages.json

[
  {"code":"31.71.01.1001","name":"Gambir","type":"kelurahan","district_code":"31.71.01","regency_code":"31.71","province_code":"31","source_page":1335},
  ...
]

Record fields

List endpoints return arrays of flat records. A record has the same shape whether it comes from a full list or a hierarchy list.

FieldLevelsDescription
codeallOfficial code, string, dotted.
nameallName as printed in the source document (see normalizations). Regency names include their type: “Kabupaten …” or “Kota …”.
typeregency, villagekabupaten or kota; kelurahan, desa or desa_adat. Derived from the code and cross-checked against the table column.
province_coderegency, district, villageParent province.
regency_codedistrict, villageParent regency/city.
district_codevillageParent district.
source_pageall1-based page of the source PDF (meta.json → source.base_document.file) where the record is listed in the detail tables. This is the PDF page index, not the number printed on the page.
latitude, longitudeallRepresentative point of the region's BIG boundary polygon, in decimal degrees: a point guaranteed to lie inside the polygon (see Administrative boundaries). null when BIG has no polygon for the code.
spatial_statusallavailable (BIG polygon joined by code) or not_available.
postal_codevillagePos Indonesia postal code (kode pos), a 5-digit string, or null when no exact match was found (see Postal codes).
bboxallBounding box of the polygon, [min_longitude, min_latitude, max_longitude, max_latitude], or null. Handy for fitting a map view.

Detail endpoints (/provinces/{code}.json, /regencies/{code}.json, /districts/{code}.json) add level, the parent objects, counts of children, source (document and page) and links to related endpoints. They also carry the region's polygon: geometry (a GeoJSON Polygon/MultiPolygon, or null) and links.boundary (the boundary file it comes from). Villages have no detail endpoint; their polygons are in /api/v1/boundaries/regencies/{regency}/villages.geojson and the GeoJSON downloads. Detail endpoints also list postal_codes: the sorted postal codes of the region's villages.

Islands and coordinates

The decree's island table (Lampiran D.c) lists 17,374 named islands. Each has a code (NN.NN.4NNNN), a name, point coordinates, an area (when printed), and a status: inhabited (BP) or uninhabited (TBP). The recap total of 17,380 also counts the 6 main islands (Sumatera, Jawa, Kalimantan, Sulawesi, Timor, Papua), which have no rows. Islands that span several regencies (e.g. Pulau Nias) use a province-level code NN.00.4NNNN and have regency_code: null.

These island points are the only coordinates in the source documents. The decree gives no boundaries or centre points for provinces, regencies, districts or villages. Those come from BIG, as a separately attributed dataset (see Administrative boundaries).

EndpointReturns
/api/v1/islands.jsonAll islands (≈5.7 MB).
/api/v1/islands.geojsonThe same islands as a GeoJSON FeatureCollection of points, for maps and GIS tools (≈0.4 MB compressed).
/api/v1/provinces/{province}/islands.jsonIslands of a province, including province-level islands.
/api/v1/regencies/{regency}/islands.jsonIslands of a regency/city (an empty array when it has none).

There are no island lists per district: the decree assigns islands to a regency (or, for islands spanning several, a province), never to a kecamatan. Deriving one from the BIG boundaries would not work either, because only about a third of the island points fall inside a kecamatan boundary; most are small islets on water or on land too small for the simplified polygons.

{"code":"11.10.40082","name":"Pulau Mangkir Gadang","regency_code":"11.10","province_code":"11",
 "latitude":2.146942,"longitude":98.124719,"latitude_dms":"02°08'48.99\" U","longitude_dms":"098°07'28.99\" T",
 "area_km2":0.0748,"inhabited":false,"remark":"Berita Acara Kesepakatan Bersama ...",
 "source_document":"KEPMENDAGRI-300-2-2-2430-2025.pdf","source_page":14}

The island map shows every island on an OpenFreeMap basemap, with filters by province, regency and status.

Administrative boundaries

Boundary polygons for provinces, regencies/cities, districts and villages come from Badan Informasi Geospasial (BIG). They are joined to this API's Kemendagri records by exact code (BIG's KDPPUM, KDPKAB, KDCPUM and KDEPUM fields). Names are never matched, and names, codes and hierarchy always come from this API. Every record is present in the boundary files. A record without a BIG polygon has "geometry": null and "spatial_status": "not_available", and BIG features whose code is not in the current dataset are left out and reported.

LevelWith boundary / recordsBIG source layer
Province— / —gis/rest/services/STIG/Batas_Provinsi/MapServer/0
Regency/City— / —gis/rest/services/DISIGT/BatasWilayah/FeatureServer/0
District— / —rbi/rest/services/BATASWILAYAH/BATAS_KECAMATAN_AR/MapServer/0
Village— / —rbi/rest/services/BATASWILAYAH/BATAS_DESAKEL_AR/MapServer/0
EndpointReturns
/api/v1/boundaries/provinces.geojsonAll provinces (the map's initial load, ≈0.14 MB compressed).
/api/v1/boundaries/provinces/{province}/regencies.geojsonRegencies/cities of a province.
/api/v1/boundaries/regencies/{regency}/districts.geojsonDistricts of a regency/city.
/api/v1/boundaries/regencies/{regency}/villages.geojsonVillages of a regency/city. Villages are grouped per regency, not per district, to keep the deployment under Cloudflare's file limit. Filter by district_code.
{"type":"Feature","id":"31.71.01.1001","properties":{"code":"31.71.01.1001","name":"Gambir","level":"village",
  "type":"kelurahan","district_code":"31.71.01","regency_code":"31.71","province_code":"31",
  "latitude":-6.17…,"longitude":106.82…,"spatial_status":"available"},"geometry":{"type":"Polygon","coordinates":[…]}}

Explore the boundaries on the map, which drills down from province to village.

Postal codes

Postal codes (kode pos) come from Pos Indonesia (kodepos.posindonesia.co.id), retrieved —. — of — villages have one (— distinct codes). The Kemendagri decrees contain no postal codes.

EndpointReturns
/api/v1/postal-codes/{first 3 digits}.jsonEvery village by postal code, grouped by the first three digits (one file per code would exceed the hosting's file limit).
GET /api/v1/postal-codes/101.json
{"10110":[{"code":"31.71.01.1001","name":"Gambir","district_code":"31.71.01","regency_code":"31.71","province_code":"31"}], …}

Client examples

curl

curl -s https://indonesia-region.caturseptian.site/api/v1/regencies/32.73/districts.json

JavaScript / TypeScript

type Village = {
  code: string; name: string; type: "kelurahan" | "desa" | "desa_adat";
  district_code: string; regency_code: string; province_code: string; source_page: number;
};

const BASE = "https://indonesia-region.caturseptian.site/api/v1";

export async function villagesOf(districtCode: string): Promise<Village[]> {
  const res = await fetch(`${BASE}/districts/${districtCode}/villages.json`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json();
}

// Look up one village by code: villages are grouped per district.
export async function village(code: string) {
  return (await villagesOf(code.slice(0, 8))).find((v) => v.code === code) ?? null;
}

Python

import requests

BASE = "https://indonesia-region.caturseptian.site/api/v1"
meta = requests.get(f"{BASE}/meta.json", timeout=30).json()
print(meta["dataset_version"], meta["counts"]["villages"])

regencies = requests.get(f"{BASE}/provinces/34/regencies.json", timeout=30).json()
print([r["name"] for r in regencies])

Go

type Regency struct {
	Code         string `json:"code"`
	Name         string `json:"name"`
	Type         string `json:"type"`
	ProvinceCode string `json:"province_code"`
	SourcePage   int    `json:"source_page"`
}

resp, err := http.Get("https://indonesia-region.caturseptian.site/api/v1/provinces/32/regencies.json")
if err != nil { log.Fatal(err) }
defer resp.Body.Close()
var regencies []Regency
if err := json.NewDecoder(resp.Body).Decode(&regencies); err != nil { log.Fatal(err) }

PHP

$json = file_get_contents('https://indonesia-region.caturseptian.site/api/v1/regencies/35.78/districts.json');
$districts = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
foreach ($districts as $d) {
    echo $d['code'], ' ', $d['name'], PHP_EOL;
}

Java

HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(
    URI.create("https://indonesia-region.caturseptian.site/api/v1/provinces.json")).build();
String body = client.send(request, HttpResponse.BodyHandlers.ofString()).body();
// parse with Jackson, Gson, ... ; keep "code" as String

The examples use the official deployment, https://indonesia-region.caturseptian.site. When these pages are served from a preview, they show that address instead.

Sizes and performance

Navigate the hierarchy instead of downloading the full lists. A cascading province → regency → district → village picker needs at most four small requests:

FileUncompressedgzip
provinces.json2 KB0.6 KB
regencies.json54 KB7 KB
districts.json753 KB73 KB
villages.json (deprecated)24 MB3.1 MB
one provinces/{code}/villages.json≤ 2.5 MB≤ 0.3 MB
search/villages.json (code, name, type of every village)3.3 MB0.6 MB
one districts/{code}/villages.json≤ 16 KB≤ 2 KB

Cloudflare compresses responses (gzip/brotli) and serves them from its edge cache.

Downloads

The whole dataset as a single file, generated from the same validated data on every deploy. Sizes and SHA-256 checksums are listed in meta.json → downloads.

FormatFileSizeContents
CSVindonesia-regions.csv—All levels in one table (one row per region: level, code, name, type, parent codes, source page, latitude, longitude, spatial status).
JSONindonesia-regions.json—One object with provinces, regencies, districts, villages arrays plus dataset metadata.
XMLindonesia-regions.xml—<indonesia-regions> with one element per region; attributes hold the fields.
SQLindonesia-regions.sql—CREATE TABLE + INSERT for four tables with primary and foreign keys. Portable: SQLite, PostgreSQL, MySQL/MariaDB.
Excel (XLSX)indonesia-regions.xlsx—Workbook with an About sheet and one sheet per level; codes are text cells, header row frozen with filters.
PDFindonesia-regions.pdf—Printable A4 listing of every region (about 1,270 pages).

JSON and XML hold the region levels only; the islands are in indonesia-islands.json and indonesia-islands.xml, because a single file would exceed the 25 MiB per-file limit of the hosting. CSV has regions only; SQL, XLSX and PDF contain both. Villages carry postal_code in every format.

Every table format carries the representative point and status of each region (latitude, longitude, spatial_status; empty when BIG has no polygon). Polygons do not fit in table cells, so they are downloadable as GeoJSON:

LevelFileContents
Provinceexport/boundaries/provinces.geojsonAll 38 provinces.
Regency/Cityexport/boundaries/regencies.geojsonAll 514 regencies/cities.
Districtexport/boundaries/districts-{province}.geojsonDistricts, one file per province (e.g. districts-31.geojson).
Villageexport/boundaries/villages-{province}.geojsonVillages, one file per province (e.g. villages-31.geojson).

Districts and villages are split per province because a single national file would exceed the 25 MiB per-file limit of the hosting. Feature counts, sizes and SHA-256 checksums are in meta.json → boundaries.downloads. The features are the same as in /api/v1/boundaries/.

To export only part of the data (one province, one district, …), open the API explorer, pick a region, and use Download this response. It writes the same six formats in the browser from the response on screen, plus GeoJSON with the polygons of the regions shown. Codes stay text in every format.

There is no server-side search endpoint, because a static site cannot run queries. For search, download the list you need and filter it on the client. regencies.json and districts.json are small enough for type-ahead (the explorer does this). For villages, first narrow to a district, or load search/villages.json once ({"fields":["code","name","type"],"rows":[…]}, 0.6 MB compressed; the map does this) and index it locally.

Versioning

If your application must pin a dataset, check dataset_version in meta.json.

Historical data

Two earlier code tables are extracted with the same pipeline and kept as internal snapshots:

/api/v1/changes.json lists the differences between consecutive documents. A code in both documents with a different name is renamed, and a code in only one of them is added or removed. Differences in letter case, spacing or the “KAB.” abbreviation are ignored. When a region moves to a new parent, its code changes (for example the regencies that moved to Papua Barat Daya, 92.xx → 96.xx in 2022–2025). Such moves appear as removed + added and are never inferred. The current API serves only the current dataset. Historical snapshots are data files and do not change /api/v1 endpoints.

Data provenance

Every value can be traced to a page of an official document.

Official PDFs (sources/pdf, not deployed)
  → extract    word coordinates via poppler pdftotext -bbox, table rows per page
  → normalize  hierarchy, types, cross-checks, reconciliation with printed totals
  → validate   format, uniqueness, parents, counts, generated files
  → build      static JSON in public/api/v1 → Cloudflare Pages

Source documents

DocumentEnactedRole
Kepmendagri 300.2.2-2138 Tahun 20252025-04-25Current code table. Source of every record.
Kepmendagri 300.2.2-2430 Tahun 20252025-06-23Amends 2138/2025 for island (pulau) data only: 4 islands moved from Sumatera Utara to Kabupaten Aceh Singkil. No region code or name changes. Image-only PDF, inspected visually.
Permendagri 58 Tahun 20212021-12-13Code format and assignment rules. No code table.
Kepmendagri 100.1.1-6117 Tahun 20222022-11-09Previous code table (revoked). Historical snapshot.
Permendagri 137 Tahun 20172017-12-27Older code table (replaced). Historical snapshot.
PP 72 Tahun 20192019-10-14Amends PP 18/2016 on regional apparatus (Perangkat Daerah). Legal context only; contains no codes.

How the current data is checked

Normalizations

Names are kept exactly as printed, including the document's use of " as an apostrophe (e.g. Ra"as). The one exception is regency names printed with the abbreviation “Kab” (53.02 Timor Tengah Selatan and 95.02 Pegunungan Bintang), which are expanded to “Kabupaten” like the other 414 kabupaten. Both are noted in meta.json → provenance.normalizations.

Update process

  1. When a new decree or amendment is published, it is recorded with its enactment date and checksum. If it replaces the code table, it becomes the base document, and the expected totals come from its recap.
  2. The tables are extracted from the PDF again, then normalized and validated. An update is rejected if totals, parents, types or printed counts disagree.
  3. Findings that need a human decision are checked against the rendered PDF page and published in meta.json → known_issues. Nothing is silently corrected.
  4. The change list against the previous document is generated automatically (/api/v1/changes.json), and every update is reviewed as a full diff of the data.

Limitations and known issues

CORS and caching

The API sends Access-Control-Allow-Origin: *, so browser applications on any domain can call it. Requests are simple GETs and need no preflight; do not add custom request headers. This is configured in public/_headers:

/api/*
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET, HEAD, OPTIONS
  Access-Control-Max-Age: 86400
  Cache-Control: public, max-age=3600, stale-while-revalidate=86400

Browsers may cache responses for an hour, and Cloudflare revalidates them with ETags. JSON is served as application/json. HTML pages get security headers (CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy).

Report a correction

Corrections must be backed by an official document. If you find a name or code that differs from the decree, email hello@caturseptian.site with the code, the document and the PDF page.

Data attribution