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.
- Base URL:
https://indonesia-region.caturseptian.site/api/v1/(or/api/v1/on any self-hosted copy). - Read-only,
GETonly, no authentication. - UTF-8 JSON, deterministic ordering (by code), no build timestamps.
- Current dataset:
kepmendagri-300.2.2-2138-2025+2430-2025, effective 2025-06-23.
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) | Indonesian | Code | Count | Notes |
|---|---|---|---|---|
province | Provinsi | 31 | 38 | Includes special regions (Aceh, DKI Jakarta, DI Yogyakarta, the Papua provinces). |
regency, type kabupaten | Kabupaten | 32.04 | 416 | Regency. Code 01–69. |
regency, type kota | Kota | 32.73 | 98 | City. Code 71–99. A kota is not a kabupaten: “Kabupaten Bandung” (32.04) and “Kota Bandung” (32.73) are different areas. |
district | Kecamatan (Distrik in Papua) | 32.73.01 | 7,285 | |
village, type kelurahan | Kelurahan | 32.73.01.1001 | 8,496 | Urban administrative village (part of the district administration). Code starts with 1. |
village, type desa | Desa (Gampong, Nagari, Kampung, …) | 11.01.01.2001 | 75,252 | Self-governing village community. Code starts with 2. |
village, type desa_adat | Desa adat | 91.03.01.3007 | 14 | Customary 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:
- Province, 2 digits. The first digit is the island group, west to east (1–2 Sumatra, 3–4 Java, 5 Bali and Nusa Tenggara, 6 Kalimantan, 7 Sulawesi, 8 Maluku, 9 Papua). The second digit is the order of formation.
- Regency/city, province code + 2 digits.
01–69is a kabupaten and71–99is a kota. - District, regency/city code + 2 digits.
- Village, district code + 4 digits. The first of the 4 digits is
1for kelurahan,2for desa and3for desa adat. - When a region splits, the parent keeps its code and the new region gets the next free code. A renamed region keeps its code.
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
| Endpoint | Returns |
|---|---|
| /api/v1/meta.json | Dataset version, effective date, sources, counts, known issues, endpoint list. |
| /api/v1/provinces.json | Array of all provinces. |
| /api/v1/regencies.json | Array of all regencies and cities. |
| /api/v1/districts.json | Array of all districts (≈750 KB). |
| /api/v1/villages.json | Deprecated (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}.json | One province, with counts and source. |
| /api/v1/provinces/{province}/regencies.json | Regencies/cities of a province. |
| /api/v1/provinces/{province}/villages.json | All villages of a province (the same records as villages.json; at most ≈2.5 MB, ≈0.3 MB compressed). |
| /api/v1/regencies/{regency}/villages.json | All villages of a regency/city (at most ≈240 KB). |
| /api/v1/regencies/{regency}.json | One regency/city, with its province, counts and source. |
| /api/v1/regencies/{regency}/districts.json | Districts of a regency/city. |
| /api/v1/districts/{district}.json | One district, with its regency/city, province, counts and source. |
| /api/v1/districts/{district}/villages.json | Villages 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.json | Index 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}.json | Postal code lookup: every village with a given postal code (see Postal codes). |
| /api/v1/csv/{provinces|regencies|districts|villages}.csv | The 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.
| Field | Levels | Description |
|---|---|---|
code | all | Official code, string, dotted. |
name | all | Name as printed in the source document (see normalizations). Regency names include their type: “Kabupaten …” or “Kota …”. |
type | regency, village | kabupaten or kota; kelurahan, desa or desa_adat. Derived from the code and cross-checked against the table column. |
province_code | regency, district, village | Parent province. |
regency_code | district, village | Parent regency/city. |
district_code | village | Parent district. |
source_page | all | 1-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, longitude | all | Representative 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_status | all | available (BIG polygon joined by code) or not_available. |
postal_code | village | Pos Indonesia postal code (kode pos), a 5-digit string, or null when no exact match was found (see Postal codes). |
bbox | all | Bounding 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).
| Endpoint | Returns |
|---|---|
| /api/v1/islands.json | All islands (≈5.7 MB). |
| /api/v1/islands.geojson | The same islands as a GeoJSON FeatureCollection of points, for maps and GIS tools (≈0.4 MB compressed). |
| /api/v1/provinces/{province}/islands.json | Islands of a province, including province-level islands. |
| /api/v1/regencies/{regency}/islands.json | Islands 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}
latitude/longitudeare decimal degrees converted from the printed degrees-minutes-seconds (U/S= north/south,T/B= east/west), rounded to 6 decimals. The printed values are kept inlatitude_dms/longitude_dms. The decree does not state a datum.inhabitedistrue(BP),false(TBP) ornullwhen the decree prints no status (2,793 islands).area_km2isnullwhen no area is printed.- Every island names its own
source_document. Kepmendagri 300.2.2-2430/2025 moved Pulau Lipan, Mangkir Gadang, Mangkir Ketek and Panjang from Kabupaten Tapanuli Tengah (12.01) to Kabupaten Aceh Singkil (11.10, new codes 11.10.40081–40084), so those four cite the amendment.
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.
| Level | With boundary / records | BIG 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 |
| Endpoint | Returns |
|---|---|
| /api/v1/boundaries/provinces.geojson | All provinces (the map's initial load, ≈0.14 MB compressed). |
| /api/v1/boundaries/provinces/{province}/regencies.geojson | Regencies/cities of a province. |
| /api/v1/boundaries/regencies/{regency}/districts.geojson | Districts of a regency/city. |
| /api/v1/boundaries/regencies/{regency}/villages.geojson | Villages 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":[…]}}
latitude/longitudeis a representative point: the pole of inaccessibility of the region's largest polygon. It is guaranteed to lie inside the published polygon, which is checked for every region. It is not a centroid, and not the centre of any map view. GeoJSON coordinates are[longitude, latitude].- Geometry is simplified for the web with mapshaper (Visvalingam, topology-aware, so neighbouring regions keep shared borders). Holes, multipolygons and small islands are kept, and intersections are repaired. Where BIG polygons overlap, the overlap goes to the smaller polygon, so a village drawn partly inside a neighbour keeps its area. Interval: province 1 km, regency 250 m, district 60 m, village 20 m. It is display geometry, not a legal survey boundary.
- BIG is used only at build time. Browsers never contact BIG; the files are static like the rest of the API.
- The district layer specified originally (
BATAS_WILAYAH/MapServer/14) contains only 66 districts in Kota Kendari and Kabupaten Konawe. The nationwide district layerBATAS_KECAMATAN_ARfrom the same BIG service folder is used instead.
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.
| Endpoint | Returns |
|---|---|
| /api/v1/postal-codes/{first 3 digits}.json | Every 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"}], …}
- Method. Pos Indonesia has no bulk download, so it is queried once per postal code at build time (one request per second; browsers never contact it). The list of codes to query is the union of two public datasets: Kode Pos Desa Kelurahan di Indonesia (Satu Data Indonesia / Diskominfo Jawa Barat, 2023) and cahyadsn/wilayah_kodepos (MIT). Their postal code assignments are not used as data.
- Join. Pos Indonesia publishes names, not region codes. Each result is matched by exact name after normalization (case, spaces and punctuation;
KAB.andKOTA ADM.expanded): regency/city, then kecamatan within it, then village within the kecamatan. Nothing is fuzzy. Two villages with the same name in one kecamatan, or one village with two different codes, getnull. - Why not the two datasets. Over all villages matched to Pos Indonesia, the government dataset gives the same postal code for 77% of the villages it covers and cahyadsn for 61%; the two agree with each other on only 70% of villages.
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(®encies); 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:
| File | Uncompressed | gzip |
|---|---|---|
provinces.json | 2 KB | 0.6 KB |
regencies.json | 54 KB | 7 KB |
districts.json | 753 KB | 73 KB |
villages.json (deprecated) | 24 MB | 3.1 MB |
one provinces/{code}/villages.json | ≤ 2.5 MB | ≤ 0.3 MB |
search/villages.json (code, name, type of every village) | 3.3 MB | 0.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.
| Format | File | Size | Contents |
|---|---|---|---|
| CSV | indonesia-regions.csv | — | All levels in one table (one row per region: level, code, name, type, parent codes, source page, latitude, longitude, spatial status). |
| JSON | indonesia-regions.json | — | One object with provinces, regencies, districts, villages arrays plus dataset metadata. |
| XML | indonesia-regions.xml | — | <indonesia-regions> with one element per region; attributes hold the fields. |
| SQL | indonesia-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. |
| indonesia-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:
| Level | File | Contents |
|---|---|---|
| Province | export/boundaries/provinces.geojson | All 38 provinces. |
| Regency/City | export/boundaries/regencies.geojson | All 514 regencies/cities. |
| District | export/boundaries/districts-{province}.geojson | Districts, one file per province (e.g. districts-31.geojson). |
| Village | export/boundaries/villages-{province}.geojson | Villages, 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.
Search
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
- API version (
/api/v1/): the URL layout and record shape. Within v1, fields may be added but are never removed or renamed. Breaking changes would be published under/api/v2/while v1 stays available. - Dataset version (
meta.json → dataset_version): named after the government documents it is built from, e.g.kepmendagri-300.2.2-2138-2025+2430-2025(base decree + amendment). It changes only when a new official document is incorporated. No invented semantic version numbers are used for government data. effective_dateandlast_updatedcome from the documents (the enactment date of the latest amendment), not from the build time. Rebuilding produces byte-identical files.
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:
- Permendagri 137/2017 (34 provinces, 83,437 villages)
- Kepmendagri 100.1.1-6117/2022 (37 provinces, 83,763 villages), revoked by 300.2.2-2138/2025
/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
| Document | Enacted | Role |
|---|---|---|
| Kepmendagri 300.2.2-2138 Tahun 2025 | 2025-04-25 | Current code table. Source of every record. |
| Kepmendagri 300.2.2-2430 Tahun 2025 | 2025-06-23 | Amends 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 2021 | 2021-12-13 | Code format and assignment rules. No code table. |
| Kepmendagri 100.1.1-6117 Tahun 2022 | 2022-11-09 | Previous code table (revoked). Historical snapshot. |
| Permendagri 137 Tahun 2017 | 2017-12-27 | Older code table (replaced). Historical snapshot. |
| PP 72 Tahun 2019 | 2019-10-14 | Amends PP 18/2016 on regional apparatus (Perangkat Daerah). Legal context only; contains no codes. |
How the current data is checked
- Records are read from the per-regency detail tables of the annex (Lampiran C), which list the full hierarchy with page numbers.
- Totals equal the official recap (Lampiran A, confirmed unchanged by the 2430/2025 amendment): 38 provinces, 416 kabupaten, 98 kota, 7,285 districts, 8,496 kelurahan and 75,266 desa (including 14 desa adat).
- For all 514 regencies/cities, the kecamatan/kelurahan/desa counts printed on the regency row equal the rows listed under it. The same holds for every district whose row prints counts (6,688 of 7,285; the Papua Selatan/Tengah/Pegunungan/Barat Daya tables print none).
- Every village's type from its code (1/2/3) matches the column (kelurahan or desa) it is printed in.
- Names are cross-checked against the summary tables. The only differences are summary cells that truncate long names.
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
- 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.
- The tables are extracted from the PDF again, then normalized and validated. An update is rejected if totals, parents, types or printed counts disagree.
- Findings that need a human decision are checked against the rendered PDF page and published in
meta.json → known_issues. Nothing is silently corrected. - 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
- Area, population and capitals of regions in the decree are not extracted yet; islands are (see above).
- No per-village endpoint (file-count limit). Look villages up through their district file.
- Two pairs of desa in the current decree share a name within one district, as printed: “Tounelet Satu” (71.02.11.2003 and 71.02.11.2018) and “Kogekotu” (94.03.01.2011 and 94.03.01.2018). They are kept as printed and marked for review in
meta.json → known_issues. - One village is printed as “Teuw*” (91.20.07.2005) with no footnote. It is kept as printed.
- The historical 2017 table has internal inconsistencies of its own (Kota Sorong 92.71 prints 52 kelurahan but lists 41). These are recorded internally and do not affect the current data.
- The source set includes PP 72/2019 (Perangkat Daerah), not Permendagri 72/2019 (which amended Permendagri 137/2017). The latter is not used.
- Island coordinates are printed values. Three islands of Kab. Halmahera Selatan (82.04.40451, 82.04.40458, 82.04.40463) are printed with West Kalimantan longitudes, very likely a typo in the source. They are kept as printed and flagged on the map and in
meta.json → known_issues. The replacement island pages of 2430/2025 are image-only: the changed rows were verified against the page images, and the unchanged rows are taken from the digital 2138 table. - Boundary polygons come from BIG, not from the decree. BIG's data may be older than the 2025 decree, so regions created or recoded since then can be without a boundary (see the counts above and
meta.json → boundaries.levels). - Postal codes are joined by name because Pos Indonesia publishes no region codes. Villages renamed or re-districted since Pos Indonesia's data, and postal codes in neither candidate dataset, have no postal code. Pos Indonesia returns at most 50 rows per search; capped codes are completed by kecamatan and village-name searches, and — code(s) could still not be confirmed complete. Names are compared exactly, so a village whose name differs only in spelling (e.g. “Cibodasari” / “Cibodas Sari”) is not matched. Pos Indonesia's site states no terms of use; it is queried politely at build time only.
- Regulations enacted after 23 June 2025 are not included until they are added to the source set.
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
- Source documents and data. The region codes and names are taken from Indonesian government regulatory documents (Kementerian Dalam Negeri). This project makes no claim about the copyright status of those documents. It does not relicense them or claim ownership of the underlying data. Users are responsible for complying with any terms that apply to the source material.
- Please attribute the source when you use the data, e.g. “Data: Kepmendagri 300.2.2-2138/2025 (as amended by 300.2.2-2430/2025), via Indonesia Region API”.