🏫 School API

A read-only static JSON API for Indonesian schools. No auth, no rate limit beyond the CDN β€” just plain fetch() against JSON files.

πŸ§ͺ Open the example app β†’ πŸ—ΊοΈ Kabupaten map β†’ live demo: filter by region & search by NPSN

311,885Schools
39Provinces
523Kabupaten
7,270Kecamatan

Endpoints

PathReturns
./index.json List of provinces [{ kode_provinsi, provinsi, bps_kode_provinsi }]
./meta.json Generation stats & shard config
./by_region/<prov>/index.json Kabupaten in a province [{ kode_kabupaten, kabupaten, bps_kode_kabupaten }]
./by_region/<prov>/<kab>/index.json Kecamatan in a kabupaten [{ kode_kecamatan, kecamatan, bps_kode_kecamatan }]
./by_region/<prov>/<kab>/<kec>/schools.json Schools in a kecamatan (with npsn), sorted by name
./by_npsn/index.json How to locate a school by NPSN (shard metadata)
./by_npsn/shards/<shard>.json { "<npsn>": <full record>, ... } β€” look up one school
./geojson/kabupaten.json Admin boundaries keyed by Dapodik kode_kabupaten { "<kode_kabupaten>": { type: "FeatureCollection", features: [...] } }

Look up a single school by NPSN

There are 311,885 schools β€” one file each would exceed hosting limits, so by_npsn is sharded into 4,000 files. The shard id is deterministic and client-computable.

// 1) compute the shard (pad to 4 digits)
const npsn = "20532914";
const shard = String(Number(npsn) % 4000).padStart(4, "0"); // "0914"

// 2) fetch the shard, then index by npsn
const res  = await fetch(`/by_npsn/shards/${shard}.json`);
const bucket = await res.json();     // { "<npsn>": <record>, ... }
const school = bucket[npsn];         // the full school record

Live example for NPSN 20532914 β†’ ./by_npsn/shards/0914.json (then read the "20532914" key).

Navigating the region tree

  1. ./index.json β†’ find kode_provinsi: "050000" (Jawa Timur)
  2. ./by_region/050000/index.json β†’ find kode_kabupaten: "056000" (Kota Surabaya)
  3. ./by_region/050000/056000/index.json β†’ find kode_kecamatan: "056020" (Tambaksari)
  4. ./by_region/050000/056000/056020/schools.json β†’ schools in that kecamatan
  5. Pick an NPSN; compute npsn % 4000 β†’ fetch the shard β†’ index by npsn.

Try it

Open any link above directly in the browser β€” it's just JSON. A tiny client could look like:

const provinces = await (await fetch("/index.json")).json();
const schools   = await (await fetch(
  "/by_region/050000/056000/056020/schools.json"
)).json();
console.log(schools.length, "schools in Tambaksari");

Admin boundaries by kabupaten (GeoJSON)

./geojson/kabupaten.json is one static asset whose keys are the Dapodik kode_kabupaten (six-digit, e.g. "056000" for Kota Surabaya) and whose values are GeoJSON FeatureCollection objects. Source village/kelurahan boundaries are grouped by the source KDPKAB and then re-keyed to the matching school kode_kabupaten by name; source codes with no Dapodik match are skipped. A code resolves to a renderable polygon set without a server.

const data = await (await fetch("/geojson/kabupaten.json")).json();

// one city/kabupaten by its Dapodik kode_kabupaten (e.g. Kota Surabaya)
const collection = data["056000"];
console.log(collection.type);                 // "FeatureCollection"
console.log(collection.features.length);      // > 0
console.log(collection.features[0].properties.KDPKAB); // "35.78" (source code)

πŸ—ΊοΈ Try the Leaflet map β€” pick a kabupaten and draw its boundaries.

Source & license: boundaries are grouped (not dissolved) from ardian28/GeoJson-Indonesia-38-Provinsi, pinned source ( copyright Β© 2024 Ardian Saputra Hasibuan, MIT license). The source advertises BIG geospatial-service data. Keys are this API's Dapodik kode_kabupaten (6-digit, e.g. "056000"); each feature keeps its source KDPKAB (e.g. "35.78").

Region-indonesia (BPS) codes

Region codes in this API are Dapodik codes (050000, 056000, 056020). Each entry in the region index files also carries the matching dotted BPS/Kemendagri code used by most other Indonesian datasets, from region-indonesia.pages.dev:

// ./by_region/050000/056000/index.json
[
  { "kode_kecamatan": "056020", "kecamatan": "Kec. Tambaksari", "bps_kode_kecamatan": "35.78.10" },
  ...
]

bps_kode_provinsi (e.g. "35"), bps_kode_kabupaten ("35.78") and bps_kode_kecamatan ("35.78.10") are matched by name. They are null when there is no match β€” e.g. Luar Negeri (schools abroad) and a few kecamatan whose Dapodik name has no BPS counterpart.

Client examples

Base URL: https://school-static-api.pesonaedu.workers.dev (CORS-enabled, so browsers can fetch it cross-origin; PHP can read it with file_get_contents).

JavaScript

const BASE = "https://school-static-api.pesonaedu.workers.dev";

// 1) list all provinces
const provinces = await (await fetch(`${BASE}/index.json`)).json();

// 2) schools in a kecamatan (Jawa Timur > Kota Surabaya > Tambaksari)
const schools = await (await fetch(
  `${BASE}/by_region/050000/056000/056020/schools.json`
)).json();
console.log(schools.length, "schools in Tambaksari");

// 3) look up ONE school by NPSN (sharded: npsn % 4000)
const npsn  = "20532914";
const shard = String(Number(npsn) % 4000).padStart(4, "0"); // "0914"
const bucket = await (await fetch(`${BASE}/by_npsn/shards/${shard}.json`)).json();
console.log(bucket[npsn]); // full school record

PHP

<?php
const BASE = "https://school-static-api.pesonaedu.workers.dev";

function api(string $path): array {
  $ctx = stream_context_create(['http' => ['method' => 'GET']]);
  $raw = @file_get_contents(BASE . $path, false, $ctx);
  return json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
}

// 1) list all provinces
$provinces = api('/index.json');

// 2) schools in a kecamatan
$schools = api('/by_region/050000/056000/056020/schools.json');
echo count($schools) . " schools in Tambaksari\n";

// 3) look up ONE school by NPSN (sharded: npsn % 4000)
$npsn  = '20532914';
$shard = str_pad((string)((int)$npsn % 4000), 4, '0', STR_PAD_LEFT); // "0914"
$bucket = api("/by_npsn/shards/{$shard}.json");
print_r($bucket[$npsn]); // full school record