SKILL — Wilayah Indonesia
Use this skill when a developer wants to integrate Indonesian administrative data (provinces, regencies/cities, districts, villages, postal codes, boundaries) into their app. Data is available in two interchangeable ways: Static API (https://www.emsifa.com/api-wilayah-indonesia/v2) and Downloaded files (npx wilayah download --format csv|sql|json). This file only describes what you will receive — columns, keys, and response shapes — so you can build for any stack.
1. Which Source Should You Use?
- Static API: Best for MVPs, frontend-only apps, or edge functions. No database needed — just
fetch JSON. Includes polygons for maps. Data is cached on CDN.
- Download: Best when you need offline validation, foreign keys, or complex queries (
WHERE postal_code = ?). You get the full dataset to self-host in your own database or static hosting.
Ask the user: “Do you want to fetch from the API or self-host the data?”
2. API — What You Get
Base URL: https://www.emsifa.com/api-wilayah-indonesia/v2
Every response has the same wrapper:
{
"data": <object or array>,
"meta": {
"generated_at": "2026-09-02T03:35:49Z",
"level": 1
}
}
data: the actual payload (see below)
meta.generated_at: when the file was generated (RFC3339)
meta.level: depth 0=stats, 1=province, 2=regency, 3=district, 4=village/postal
2.1 What you get for provinces & regencies (detailed)
For provinces.json, provinces/{code}.json, regencies/{province}.json, regencies/{regency}.json you receive rich objects:
{
"id": "32",
"name": "Jawa Barat",
"capital": "Bandung",
"lat": -6.90,
"lng": 107.61,
"elv": 739,
"tz": 7,
"population": 51316378,
"total_area": 37053.33,
"has_path": true,
"province": { "id": "32", "name": "Jawa Barat" },
"regency": { "id": "32.73", "name": "Kota Bandung" }
}
- You'll always get
id, name, and has_path (boolean, true if /paths/{id}.json exists). For provinces and regencies you also get capital, lat, lng (for map marker), elv, tz, population, total_area.
has_path is always present (true/false) — check it before fetching /paths/{id}.json to avoid 404.
- Detail endpoints also include their parent: a regency includes
province, a district includes province + regency, a village includes all three.
2.2 What you get for districts & villages (lists)
For lists, you get a compact form to keep files small:
// districts/{regency}.json
{ "id": "32.73.01", "name": "Sukasari", "lat": -6.87, "lng": 107.59, "has_path": true }
// villages/{district}.json
{ "id": "32.73.01.1001", "name": "Sukarasa", "postal_code": "40152", "lat": -6.87, "lng": 107.59, "has_path": true }
- District lists have
id, name, lat, lng, and has_path (compact but with coordinates).
- Village lists add
postal_code + has_path so you can filter by postal code and know if polygon is available without a separate lookup.
- Detail endpoints (
districts/{code}.json, villages/{code}.json) also include has_path alongside lat/lng and parent objects.
2.3 All endpoints
| Path |
What it returns |
/stats.json |
Totals for all levels |
/provinces.json |
All provinces (detailed) |
/provinces/{code}.json |
One province, e.g. 32 |
/regencies/{province_code}.json |
Regencies in that province |
/regencies/{regency_code}.json |
One regency + its province |
/districts/{regency_code}.json |
Districts in that regency (compact) + has_path |
/districts/{district_code}.json |
One district + its province/regency + has_path |
/villages/{district_code}.json |
Villages in that district + postal_code + has_path |
/villages/{village_code}.json |
One village + postal_code + all parents + has_path |
/postal-codes/{postal_code}.json |
All villages that use that postal code |
/paths/{code}.json |
Boundary polygon for any level (fetch only if has_path=true) |
/missings.json |
Wilayah dimana has_path==false OR has_latlng==false (flat {id,name,has_path,has_latlng} + summary by_level) |
2.4 Totals (stats)
{
"total_provinces": 38,
"total_regencies": 514,
"total_districts": 7285,
"total_villages": 83762,
"total_postal_codes": 10632,
"total_paths": 551,
"total_population": 284973643,
"total_area": 1889518.25,
"total_endpoints": 201309,
"total_filesize": 300423246,
"total_filesize_human": "286.51 MB",
"total_disk_usage": 915771392,
"total_disk_usage_human": "873.35 MB"
}
2.5 Boundaries (polygons)
{
"data": {
"id": "32",
"name": "Jawa Barat",
"path": [[[ -6.98, 106.39 ], [-6.93, 106.39 ]], [[ ... ]]]
}
}
- Polygons are available for all levels when
has_path=true — check has_path on any provinces/regencies/districts/villages response before fetching /paths/{id}.json. Some have one ring (single island), others have many (e.g., Jakarta 2 rings, Aceh 28, Papua Barat 1455). Your map code should handle both: if the first element is a point [lat,lng] it's a single polygon, if it's an array of points it's already a list of rings — collect all rings that have more than 2 points.
2.6 Missings (incomplete data)
// GET /missings.json
{
"data": [
{ "id": "53.09.14.2011", "name": "Watu Pangan", "has_path": false, "has_latlng": false },
{ "id": "32.73.01.1005", "name": "Sukajadi", "has_path": false, "has_latlng": true }
],
"meta": { "updated_at": "2026-09-03", "level": 0 },
"summary": {
"total_missing": 360,
"total_missing_path": 320,
"total_missing_latlng": 40,
"total_missing_both": 12,
"by_level": { "province": 0, "regency": 0, "district": 0, "village": 360 }
}
}
has_path: true if /paths/{id}.json exists, else false.
has_latlng: true if lat and lng present and not 0/null/undefined, else false.
- List is flat across all levels; entry appears if
has_path==false OR has_latlng==false.
summary.by_level counts per level (province=0 dots, regency=1, district=2, village=3).
3. Downloaded Files — What You Get
Run npx wilayah download --format <csv|sql|json> --output ./wilayah.<ext> to get the full dataset to host yourself. You can also get the same shapes via the API, but as static files you control.
3.1 wilayah.csv / wilayah.json (full hierarchy)
Every row is one administrative area:
kode,nama
11,Aceh
11.01,Kabupaten Aceh Selatan
11.01.01,Bakongan
11.01.01.2001,Keude Bakongan
kode: the ID. Count the dots to know the level: no dot = province (2 chars, e.g. 11), one dot = regency (5 chars, 11.01), two dots = district (8 chars, 11.01.01), three dots = village (13 chars, 11.01.01.2001)
nama: the display name
The json version has the same rows as an array of {kode, nama}.
3.2 Rich data for provinces & regencies
When you need coordinates, capital, population, area, or polygon, use the level 1/2 data. In the download this is available as extra columns (for sql/json) or as the API rich objects:
kode,nama,ibukota,lat,lng,elv,tz,luas,penduduk,path
11,Aceh,Banda Aceh,5.57,95.34,11,7,56835.019,5623479,"[[[2.07,97.07],...]]"
ibukota = capital, luas = total_area, penduduk = population, path = same polygon as /paths endpoint
3.3 Postal code mapping
kode,kodepos
32.73.01.1001,40152
kode: village id
kodepos: postal code used to build postal-codes/{code}.json. One postal code can map to many villages.
3.4 What each download format gives you
wilayah.json / api/*.json: same structures as Section 2 — ready to serve as static files.
wilayah.sql: INSERT statements with columns kode, nama, capital, lat, lng, elv, tz, total_area, population, postal_code — import into MySQL/Postgres.
wilayah.csv: merged table with kode,nama,postal_code plus rich columns for provinces/regencies.
4. How IDs and Relationships Work
- The
kode / id is the primary key. Split by . to get the parent: 32.73.01.1001 → province 32, regency 32.73, district 32.73.01.
- Hierarchy is always
province → regency → district → village.
postal_code lives on the village. Use postal-codes/{code}.json to find all villages sharing a code.
5. How to Use (for the AI)
- If the user wants to fetch: call the endpoints in Section 2 in order
provinces → regencies/{prov} → districts/{reg} → villages/{dist}. Always use the real kode from the previous response, never guess. Check has_path before fetching /paths/{id}.json.
- If the user wants to self-host: suggest
npx wilayah download --format <...> and point to the columns above. Let the user choose the format that fits their database.
- For maps, all levels have
lat/lng and may have polygons when has_path=true — check the flag first.
- To monitor coverage: fetch
/missings.json — each entry {id,name,has_path,has_latlng} appears if !has_path || !has_latlng; use summary to see totals by level.
References
- Live API:
https://www.emsifa.com/api-wilayah-indonesia/v2
- Spec:
openapi.yml and wilayah-postman.json at the same base URL
- Stats:
api/stats.json, api/provinces.json
1---2name: wilayah-indonesia3description: Schemas for Indonesian administrative data — API responses and downloadable files for app integration4---56# SKILL — Wilayah Indonesia78Use this skill when a developer wants to integrate Indonesian administrative data (provinces, regencies/cities, districts, villages, postal codes, boundaries) into their app. Data is available in two interchangeable ways: **Static API** (`https://www.emsifa.com/api-wilayah-indonesia/v2`) and **Downloaded files** (`npx wilayah download --format csv|sql|json`). This file only describes what you will receive — columns, keys, and response shapes — so you can build for any stack.910## 1. Which Source Should You Use?1112- **Static API:** Best for MVPs, frontend-only apps, or edge functions. No database needed — just `fetch` JSON. Includes polygons for maps. Data is cached on CDN.13- **Download:** Best when you need offline validation, foreign keys, or complex queries (`WHERE postal_code = ?`). You get the full dataset to self-host in your own database or static hosting.1415Ask the user: “Do you want to fetch from the API or self-host the data?”1617## 2. API — What You Get1819Base URL: `https://www.emsifa.com/api-wilayah-indonesia/v2`2021Every response has the same wrapper:2223```json24{25 "data": <object or array>,26 "meta": {27 "generated_at": "2026-09-02T03:35:49Z",28 "level": 129 }30}31```3233- `data`: the actual payload (see below)34- `meta.generated_at`: when the file was generated (RFC3339)35- `meta.level`: depth `0=stats, 1=province, 2=regency, 3=district, 4=village/postal`3637### 2.1 What you get for provinces & regencies (detailed)3839For `provinces.json`, `provinces/{code}.json`, `regencies/{province}.json`, `regencies/{regency}.json` you receive rich objects:4041```json42{43 "id": "32",44 "name": "Jawa Barat",45 "capital": "Bandung",46 "lat": -6.90,47 "lng": 107.61,48 "elv": 739,49 "tz": 7,50 "population": 51316378,51 "total_area": 37053.33,52 "has_path": true,53 "province": { "id": "32", "name": "Jawa Barat" },54 "regency": { "id": "32.73", "name": "Kota Bandung" }55}56```5758- You'll always get `id`, `name`, and `has_path` (boolean, true if `/paths/{id}.json` exists). For provinces and regencies you also get `capital, lat, lng (for map marker), elv, tz, population, total_area`.59- `has_path` is always present (true/false) — check it before fetching `/paths/{id}.json` to avoid 404.60- Detail endpoints also include their parent: a regency includes `province`, a district includes `province` + `regency`, a village includes all three.6162### 2.2 What you get for districts & villages (lists)6364For lists, you get a compact form to keep files small:6566```json67// districts/{regency}.json68{ "id": "32.73.01", "name": "Sukasari", "lat": -6.87, "lng": 107.59, "has_path": true }6970// villages/{district}.json71{ "id": "32.73.01.1001", "name": "Sukarasa", "postal_code": "40152", "lat": -6.87, "lng": 107.59, "has_path": true }72```7374- District lists have `id`, `name`, `lat`, `lng`, and `has_path` (compact but with coordinates).75- Village lists add `postal_code` + `has_path` so you can filter by postal code and know if polygon is available without a separate lookup.76- Detail endpoints (`districts/{code}.json`, `villages/{code}.json`) also include `has_path` alongside `lat/lng` and parent objects.7778### 2.3 All endpoints7980| Path | What it returns |81|---|---|82| `/stats.json` | Totals for all levels |83| `/provinces.json` | All provinces (detailed) |84| `/provinces/{code}.json` | One province, e.g. `32` |85| `/regencies/{province_code}.json` | Regencies in that province |86| `/regencies/{regency_code}.json` | One regency + its province |87| `/districts/{regency_code}.json` | Districts in that regency (compact) + `has_path` |88| `/districts/{district_code}.json` | One district + its province/regency + `has_path` |89| `/villages/{district_code}.json` | Villages in that district + `postal_code` + `has_path` |90| `/villages/{village_code}.json` | One village + `postal_code` + all parents + `has_path` |91| `/postal-codes/{postal_code}.json` | All villages that use that postal code |92| `/paths/{code}.json` | Boundary polygon for any level (fetch only if `has_path=true`) |93| `/missings.json` | Wilayah dimana `has_path==false` OR `has_latlng==false` (flat `{id,name,has_path,has_latlng}` + `summary` by_level) |9495### 2.4 Totals (stats)9697```json98{99 "total_provinces": 38,100 "total_regencies": 514,101 "total_districts": 7285,102 "total_villages": 83762,103 "total_postal_codes": 10632,104 "total_paths": 551,105 "total_population": 284973643,106 "total_area": 1889518.25,107 "total_endpoints": 201309,108 "total_filesize": 300423246,109 "total_filesize_human": "286.51 MB",110 "total_disk_usage": 915771392,111 "total_disk_usage_human": "873.35 MB"112}113```114115### 2.5 Boundaries (polygons)116117```json118{119 "data": {120 "id": "32",121 "name": "Jawa Barat",122 "path": [[[ -6.98, 106.39 ], [-6.93, 106.39 ]], [[ ... ]]]123 }124}125```126127- Polygons are available for all levels when `has_path=true` — check `has_path` on any `provinces/regencies/districts/villages` response before fetching `/paths/{id}.json`. Some have one ring (single island), others have many (e.g., Jakarta 2 rings, Aceh 28, Papua Barat 1455). Your map code should handle both: if the first element is a point `[lat,lng]` it's a single polygon, if it's an array of points it's already a list of rings — collect all rings that have more than 2 points.128129### 2.6 Missings (incomplete data)130131```json132// GET /missings.json133{134 "data": [135 { "id": "53.09.14.2011", "name": "Watu Pangan", "has_path": false, "has_latlng": false },136 { "id": "32.73.01.1005", "name": "Sukajadi", "has_path": false, "has_latlng": true }137 ],138 "meta": { "updated_at": "2026-09-03", "level": 0 },139 "summary": {140 "total_missing": 360,141 "total_missing_path": 320,142 "total_missing_latlng": 40,143 "total_missing_both": 12,144 "by_level": { "province": 0, "regency": 0, "district": 0, "village": 360 }145 }146}147```148149- `has_path`: true if `/paths/{id}.json` exists, else false.150- `has_latlng`: true if `lat` and `lng` present and not `0`/`null`/`undefined`, else false.151- List is flat across all levels; entry appears if `has_path==false OR has_latlng==false`.152- `summary.by_level` counts per level (province=0 dots, regency=1, district=2, village=3).153154## 3. Downloaded Files — What You Get155156Run `npx wilayah download --format <csv|sql|json> --output ./wilayah.<ext>` to get the full dataset to host yourself. You can also get the same shapes via the API, but as static files you control.157158### 3.1 `wilayah.csv` / `wilayah.json` (full hierarchy)159160Every row is one administrative area:161162```163kode,nama16411,Aceh16511.01,Kabupaten Aceh Selatan16611.01.01,Bakongan16711.01.01.2001,Keude Bakongan168```169170- `kode`: the ID. Count the dots to know the level: no dot = province (2 chars, e.g. `11`), one dot = regency (5 chars, `11.01`), two dots = district (8 chars, `11.01.01`), three dots = village (13 chars, `11.01.01.2001`)171- `nama`: the display name172173The `json` version has the same rows as an array of `{kode, nama}`.174175### 3.2 Rich data for provinces & regencies176177When you need coordinates, capital, population, area, or polygon, use the level 1/2 data. In the download this is available as extra columns (for `sql`/`json`) or as the API rich objects:178179```180kode,nama,ibukota,lat,lng,elv,tz,luas,penduduk,path18111,Aceh,Banda Aceh,5.57,95.34,11,7,56835.019,5623479,"[[[2.07,97.07],...]]"182```183184- `ibukota` = capital, `luas` = total_area, `penduduk` = population, `path` = same polygon as `/paths` endpoint185186### 3.3 Postal code mapping187188```189kode,kodepos19032.73.01.1001,40152191```192193- `kode`: village id194- `kodepos`: postal code used to build `postal-codes/{code}.json`. One postal code can map to many villages.195196### 3.4 What each download format gives you197198- `wilayah.json` / `api/*.json`: same structures as Section 2 — ready to serve as static files.199- `wilayah.sql`: `INSERT` statements with columns `kode, nama, capital, lat, lng, elv, tz, total_area, population, postal_code` — import into MySQL/Postgres.200- `wilayah.csv`: merged table with `kode,nama,postal_code` plus rich columns for provinces/regencies.201202## 4. How IDs and Relationships Work203204- The `kode` / `id` is the primary key. Split by `.` to get the parent: `32.73.01.1001` → province `32`, regency `32.73`, district `32.73.01`.205- Hierarchy is always `province → regency → district → village`.206- `postal_code` lives on the village. Use `postal-codes/{code}.json` to find all villages sharing a code.207208## 5. How to Use (for the AI)209210- If the user wants to fetch: call the endpoints in Section 2 in order `provinces → regencies/{prov} → districts/{reg} → villages/{dist}`. Always use the real `kode` from the previous response, never guess. Check `has_path` before fetching `/paths/{id}.json`.211- If the user wants to self-host: suggest `npx wilayah download --format <...>` and point to the columns above. Let the user choose the format that fits their database.212- For maps, all levels have `lat/lng` and may have polygons when `has_path=true` — check the flag first.213- To monitor coverage: fetch `/missings.json` — each entry `{id,name,has_path,has_latlng}` appears if `!has_path || !has_latlng`; use `summary` to see totals by level.214215## References216217- Live API: `https://www.emsifa.com/api-wilayah-indonesia/v2`218- Spec: `openapi.yml` and `wilayah-postman.json` at the same base URL219- Stats: `api/stats.json`, `api/provinces.json`