PSGC API reference
A read-only JSON API over the Philippine Statistics Authority's Philippine Standard Geographic Code: 18 regions, 82 provinces, 149 cities, 1,493 municipalities, the 14 sub-municipalities of Manila, and 42,010 barangays (3Q 2026). No API key needed.
https://psgc.billyjamesramos.com/api/v1Conventions
- Codes are 10-digit strings (
RR PPP MM BBB: region, province, city/municipality, barangay). Always treat them as strings; they have leading zeros. - Hierarchy quirks: NCR has no provinces; Highly Urbanized Cities (HUCs), the City of Isabela and the BARMM Special Geographic Area are not under a province (
provinceCode: null, useprovince=none).geographicProvinceCodetells you which province such a city sits in on the map. Manila's barangays also carry asubMunicipalityCode. - Lists return
{ data: [...], meta: { total, page, limit, totalPages, hasNextPage } }. Single items return{ data: {...} }. - Errors always look like
{ error: { status, code, message } }with codesBAD_REQUEST,NOT_FOUND,SERVICE_UNAVAILABLE,INTERNAL_ERROR. - CORS:
Access-Control-Allow-Origin: *, so you can call it straight from a browser. - Caching:
Cache-Control: public, max-age=3600, s-maxage=86400, stale-while-revalidate=604800(meta: 5 minutes). Every response hasX-PSGC-VersionandX-PSGC-Releaseheaders.
HTTP/1.1 400 Bad Request
{ "error": { "status": 400, "code": "BAD_REQUEST", "message": "limit must be an integer between 1 and 500." } }
HTTP/1.1 404 Not Found
{ "error": { "status": 404, "code": "NOT_FOUND", "message": "Barangay 1380100003 not found." } }Endpoints
/api/v1/metaCurrent release: version, PSA release name/date, counts, when it went live. Poll this to detect quarterly updates.
{
"data": {
"version": "2026-Q3",
"releaseId": "2026-Q3-20261021T071052Z",
"releaseName": "Third Quarter 2026 PSGC Updates: Correction of the Name of One Barangay",
"releaseDate": "2026-10-12",
"referenceNo": "2026-331",
"asOf": "30 September 2026",
"sourceFile": "https://psa.gov.ph/system/files/scd/PSGC-3Q-2026-Publication-Datafile.xlsx",
"counts": { "regions": 18, "provinces": 82, "cities": 149, "highlyUrbanizedCities": 33,
"independentComponentCities": 5, "componentCities": 111, "municipalities": 1493,
"subMunicipalities": 14, "barangays": 42010 },
"totalsMatch": true,
"promotedAt": "2026-10-21T07:10:54.404Z",
"...": "importedAt, notes, attribution, psaPublishedTotals"
}
}/api/v1/regionsAll 18 regions with roll-up counts.
| Param | In | Description |
|---|---|---|
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-100 (default 100). |
{
"data": [{ "code": "0100000000", "name": "Region I (Ilocos Region)", "correspondenceCode": "010000000",
"population": 5342453, "provinceCount": 4, "cityCount": 9, "municipalityCount": 116,
"subMunicipalityCount": 0, "barangayCount": 3267 }],
"meta": { "total": 18, "page": 1, "limit": 1, "totalPages": 18, "hasNextPage": true }
}/api/v1/regions/{code}One region.
| Param | In | Description |
|---|---|---|
| code | path | 10-digit region code, e.g. 1300000000 (NCR). |
{ "data": { "code": "1300000000", "name": "National Capital Region (NCR)", "provinceCount": 0, "cityCount": 16, "municipalityCount": 1, "barangayCount": 1715, "...": "..." } }/api/v1/regions/{code}/provincesProvinces in a region. NCR returns an empty list (it has no provinces).
| Param | In | Description |
|---|---|---|
| code | path | Region code. |
{ "data": [{ "code": "1400100000", "name": "Abra", "regionCode": "1400000000", "incomeClass": "1st", "population": 246948, "cityCount": 0, "municipalityCount": 27, "barangayCount": 303, "...": "..." }], "meta": { "total": 6, "page": 1, "limit": 100, "totalPages": 1, "hasNextPage": false, "regionCode": "1400000000" } }/api/v1/regions/{code}/cities-municipalitiesCities/municipalities in a region. Use province=none for LGUs outside any province (HUCs, NCR, City of Isabela, BARMM Special Geographic Area).
| Param | In | Description |
|---|---|---|
| code | path | Region code. |
| province | query | Province code, or none. |
{ "data": [{ "code": "0730600000", "name": "City of Cebu", "type": "City", "cityClass": "HUC", "provinceCode": null, "geographicProvinceCode": "0702200000", "...": "..." }, "..."], "meta": { "total": 3, "...": "..." } }/api/v1/provincesAll 82 provinces.
| Param | In | Description |
|---|---|---|
| region | query | Filter by region code. |
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-100 (default 100). |
{ "data": [{ "code": "1400100000", "name": "Abra", "...": "..." }], "meta": { "total": 6, "...": "..." } }/api/v1/provinces/{code}One province.
| Param | In | Description |
|---|---|---|
| code | path | 10-digit province code. |
{ "data": { "code": "0102800000", "name": "Ilocos Norte", "regionCode": "0100000000", "...": "..." } }/api/v1/provinces/{code}/cities-municipalitiesComponent cities and municipalities of a province (HUCs are independent and are not listed under a province).
| Param | In | Description |
|---|---|---|
| code | path | Province code. |
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-500 (default 500). |
{ "data": [{ "code": "0102801000", "name": "Adams", "type": "Mun", "...": "..." }, "..."], "meta": { "total": 23, "...": "..." } }/api/v1/cities-municipalitiesAll 1,642 cities and municipalities, filterable.
| Param | In | Description |
|---|---|---|
| region | query | Region code. |
| province | query | Province code, or none. |
| type | query | city, municipality, huc, icc or cc. |
| name | query | Name contains (accent-insensitive). |
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-2000 (default 2000). |
{
"data": [{
"code": "0102812000", "name": "City of Laoag", "type": "City", "cityClass": "CC", "incomeClass": "3rd",
"regionCode": "0100000000", "provinceCode": "0102800000", "geographicProvinceCode": "0102800000",
"specialAreaCode": null, "isCapital": true, "correspondenceCode": "012812000", "oldNames": null,
"population": 112117, "subMunicipalityCount": 0, "barangayCount": 80
}, "..."],
"meta": { "total": 2, "page": 1, "limit": 2000, "totalPages": 1, "hasNextPage": false }
}/api/v1/cities-municipalities/{code}One city or municipality.
| Param | In | Description |
|---|---|---|
| code | path | 10-digit code. |
{ "data": { "code": "1381300000", "name": "Quezon City", "type": "City", "cityClass": "HUC", "...": "..." } }/api/v1/cities-municipalities/{code}/barangaysBarangays of a city/municipality. Limit up to 1000 so even Manila (897) fits in one call.
| Param | In | Description |
|---|---|---|
| code | path | City/municipality code. |
| subMunicipality | query | Manila only: filter by sub-municipality code. |
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-1000 (default 1000). |
{ "data": [{ "code": "0102812001", "name": "Bgy. No. 1, San Lorenzo (Pob.)", "urbanRural": "U", "isPoblacion": true, "population": 1250, "...": "..." }, "..."], "meta": { "total": 80, "...": "..." } }/api/v1/cities-municipalities/{code}/sub-municipalitiesSub-municipalities (districts) of the City of Manila.
| Param | In | Description |
|---|---|---|
| code | path | 1380600000 (Manila). |
{ "data": [{ "code": "1380601000", "name": "Tondo I/II", "cityMunicipalityCode": "1380600000", "population": 637942, "barangayCount": 259 }, "..."], "meta": { "total": 14, "...": "..." } }/api/v1/barangaysAll 42,010 barangays, filterable and paginated.
| Param | In | Description |
|---|---|---|
| region | query | Region code. |
| province | query | Province code, or none. |
| cityMunicipality | query | City/municipality code (alias: city). |
| subMunicipality | query | Sub-municipality code. |
| urbanRural | query | U or R (2024 CPH). |
| name | query | Name contains. |
| page | query | Page number, starting at 1 (default 1). |
| limit | query | 1-500 (default 100). |
{
"data": [{
"code": "1380601003", "name": "Barangay 3", "regionCode": "1300000000", "provinceCode": null,
"cityMunicipalityCode": "1380600000", "subMunicipalityCode": "1380601000", "urbanRural": "U",
"isPoblacion": false, "correspondenceCode": "133901003", "oldNames": null, "population": 1232
}],
"meta": { "total": 897, "page": 3, "limit": 1, "totalPages": 897, "hasNextPage": true }
}/api/v1/barangays/{code}One barangay.
| Param | In | Description |
|---|---|---|
| code | path | 10-digit barangay code. |
{ "data": { "code": "0301413033", "name": "Friendship Village Resources", "oldNames": "FVR", "urbanRural": "U", "population": 14296, "...": "..." } }/api/v1/searchName search across all levels (accent-insensitive; exact and prefix matches rank first, then higher levels).
| Param | In | Description |
|---|---|---|
| q | query | Search text, 2-100 characters (required). |
| level | query | Comma list: region, province, city, municipality, sub-municipality, barangay. |
| region | query | Restrict to a region. |
| province | query | Restrict to a province. |
| limit | query | 1-100 (default 20). |
{
"data": [{
"code": "0305403003", "name": "Batasan", "level": "barangay",
"fullName": "Batasan, Arayat, Pampanga, Region III (Central Luzon)",
"ancestry": {
"region": { "code": "0300000000", "name": "Region III (Central Luzon)" },
"province": { "code": "0305400000", "name": "Pampanga" },
"cityMunicipality": { "code": "0305403000", "name": "Arayat", "type": "Mun", "cityClass": null },
"subMunicipality": null
}
}],
"meta": { "query": "batasan", "total": 8, "limit": 1, "returned": 1 }
}/api/v1/codes/{code}Look up any code (any level) with its full ancestry. Also accepts the older 9-digit correspondence code.
| Param | In | Description |
|---|---|---|
| code | path | 10-digit PSGC code or 9-digit correspondence code. |
{
"data": {
"code": "1380100001", "name": "Barangay 1", "regionCode": "1300000000", "provinceCode": null,
"cityMunicipalityCode": "1380100000", "subMunicipalityCode": null, "urbanRural": "U",
"isPoblacion": false, "correspondenceCode": "137501001", "oldNames": null, "population": 2356,
"level": "barangay"
},
"ancestry": {
"region": { "code": "1300000000", "name": "National Capital Region (NCR)" },
"province": null,
"cityMunicipality": { "code": "1380100000", "name": "City of Caloocan", "type": "City", "cityClass": "HUC" },
"subMunicipality": null
},
"fullName": "Barangay 1, City of Caloocan, National Capital Region (NCR)"
}Copy-paste examples
// JavaScript (browser or Node 18+)
const BASE = "https://psgc.billyjamesramos.com";
const res = await fetch(`${BASE}/api/v1/codes/1380100001`);
if (!res.ok) throw new Error((await res.json()).error.message);
const { data, ancestry, fullName } = await res.json();
console.log(fullName); // "Barangay 1, City of Caloocan, National Capital Region (NCR)"
// Paginate through all barangays of a province
let page = 1, all = [];
while (true) {
const r = await fetch(`${BASE}/api/v1/barangays?province=0102800000&limit=500&page=${page}`).then(r => r.json());
all.push(...r.data);
if (!r.meta.hasNextPage) break;
page++;
}Integration plan: using this API in another app
- Configure the base URL as an environment variable so you can switch between this hosted API, a staging copy, or your own self-hosted instance without code changes..env.local
# .env.local in YOUR app NEXT_PUBLIC_PSGC_API_BASE_URL=https://psgc.billyjamesramos.com - Build the address form as cascading dropdowns: Region → Province → City/Municipality → Barangay. Handle the two special cases: when a region has no provinces (NCR) go straight to its cities, and offer an “Independent city (HUC) / other” choice that loads
?province=none.AddressPicker.tsx (React)"use client"; // Cascading address picker: Region -> Province -> City/Municipality -> Barangay. // Stores PSGC CODES (stable), shows names (which PSA can correct). import { useEffect, useState } from "react"; const BASE = process.env.NEXT_PUBLIC_PSGC_API_BASE_URL; type Opt = { code: string; name: string }; const get = (path: string): Promise<Opt[]> => fetch(BASE + path).then((r) => r.json()).then((j) => j.data); export function AddressPicker({ onChange }: { onChange: (v: Record<string, string | null>) => void }) { const [regions, setRegions] = useState<Opt[]>([]); const [provinces, setProvinces] = useState<Opt[]>([]); const [cities, setCities] = useState<Opt[]>([]); const [barangays, setBarangays] = useState<Opt[]>([]); const [independent, setIndependent] = useState<Opt[]>([]); const [v, setV] = useState({ region: "", province: "", city: "", barangay: "" }); useEffect(() => { get("/api/v1/regions").then(setRegions); }, []); async function pickRegion(region: string) { setV({ region, province: "", city: "", barangay: "" }); setCities([]); setBarangays([]); const [p, independent] = await Promise.all([ get(`/api/v1/regions/${region}/provinces`), // HUCs / NCR cities / special areas are not under a province: get(`/api/v1/regions/${region}/cities-municipalities?province=none`), ]); setProvinces(p); setIndependent(independent); setCities(p.length === 0 ? independent : []); // NCR: skip straight to cities } async function pickProvince(province: string) { setV((s) => ({ ...s, province, city: "", barangay: "" })); setBarangays([]); setCities(province === "none" ? independent : await get(`/api/v1/provinces/${province}/cities-municipalities`)); } async function pickCity(city: string) { setV((s) => ({ ...s, city, barangay: "" })); setBarangays(await get(`/api/v1/cities-municipalities/${city}/barangays?limit=1000`)); } function pickBarangay(barangay: string) { const next = { ...v, barangay }; setV(next); onChange({ regionCode: next.region, provinceCode: next.province || null, cityMunicipalityCode: next.city, barangayCode: barangay }); } const sel = (value: string, opts: Opt[], on: (c: string) => void, label: string, extra?: Opt) => ( <select value={value} onChange={(e) => on(e.target.value)} disabled={!opts.length && !extra} aria-label={label}> <option value="">{label}</option> {opts.map((o) => <option key={o.code} value={o.code}>{o.name}</option>)} {extra && <option value={extra.code}>{extra.name}</option>} </select> ); return ( <div style={{ display: "grid", gap: 8 }}> {sel(v.region, regions, pickRegion, "Region")} {sel(v.province, provinces, pickProvince, "Province", provinces.length ? { code: "none", name: "Independent city (HUC) / other" } : undefined)} {sel(v.city, cities, pickCity, "City / Municipality")} {sel(v.barangay, barangays, pickBarangay, "Barangay")} </div> ); } - Store PSGC codes, not names. Save
regionCode,provinceCode,cityMunicipalityCode,barangayCode(as strings) plus theversionyou validated against. Names get corrected by PSA (the 3Q 2026 release itself is a barangay name correction); codes stay stable. Render names at display time, or cache them with the version. - Handle quarterly updates with a version check. PSA publishes PSGC updates quarterly. Call
/api/v1/metaon app start or daily; whenversionchanges, drop cached lists and re-validate stored codes via/api/v1/codes/{code}(a 404 means the code was dissolved or merged).version check// Check once a day (or on app start) whether PSGC data changed const KEY = "psgc-version"; const meta = await fetch(`${BASE}/api/v1/meta`).then((r) => r.json()); if (localStorage.getItem(KEY) !== meta.data.version) { // new quarterly release: clear cached dropdown lists and re-validate stored codes caches?.delete?.("psgc"); localStorage.setItem(KEY, meta.data.version); } // Server side: re-check stored codes with GET /api/v1/codes/{code}; a 404 means the code was // removed/merged in the new release. Store the version alongside each saved address. - Cache aggressively; be gentle. Lists change at most once a quarter. Cache region/province lists for the app lifetime and barangay lists per city for at least a day (respect
Cache-Control). Debounce search-as-you-type (200-300 ms, min 2 chars). Paginate/barangayswithlimit=500rather than hammering. There is no hard rate limit, but abusive traffic may be throttled at the edge. - Server-side validation: before saving an address, verify the chain with
/api/v1/codes/{barangayCode}and compareancestryto the submitted parent codes. - Self-host when you need guarantees (offline, SLA, data residency): deploy this repo with your own Postgres databases, see Self-hosting.
How data updates work (staging → review → main)
New PSA files never go straight to the public API. Every import lands in a separate staging database, is checked by sanity guards and diffed against main, and only a human approval on the signed review page promotes it. The previous main is kept so a rollback is one command.
flowchart TD
PSA["psa.gov.ph PSGC page"] -->|"weekly check (browser updater; GitHub Action as backup)"| FILE["New Publication Datafile (.xlsx)"]
FILE --> STAGE["npm run stage -- file.xlsx<br/>parse + verify vs PSA National Summary<br/>writes STAGING database only"]
STAGE --> GUARDS{"Sanity guards<br/>count drop > 1%, malformed codes,<br/>truncated file, totals mismatch"}
GUARDS --> DIFF["npm run diff<br/>staging vs main<br/>changes/*.md + *.json + signed review URL"]
DIFF --> EMAIL["Email to owner<br/>change summary + review link"]
EMAIL --> REVIEW["/admin/review<br/>signed HMAC link or ADMIN_SECRET"]
REVIEW -->|Approve| PROMOTE["Promote in ONE transaction on MAIN<br/>main -> previous snapshot, staging -> main, audit row"]
REVIEW -->|Reject| DISCARD["Discard staged release"]
PROMOTE --> API["Public API /api/v1/*<br/>reads MAIN only"]
PROMOTE -.->|"npm run rollback"| PREV[("Previous snapshot")]
PREV -.-> API- Sanity guards flag any level whose count drops by more than 1%, more than 0.5% of codes removed, malformed or orphan codes, truncated files (below plausible national minimums) and mismatches against PSA's own National Summary sheet. Critical warnings must be explicitly acknowledged to approve.
- Review links are HMAC-signed with
ADMIN_SECRET, tied to one staged release id, expire (14 days by default) and stop working once that release is approved, rejected or superseded. Approval is a POST with a confirmation checkbox, so link scanners in mail clients can't trigger it. - Promotion runs in one Postgres transaction on main: current data →
areas_previous, staged rows →areas,release_currentupdated, anaudit_logrow written.npm run rollback -- --yesswaps current and previous back.
Self-hosting
git clone <this repo> && cd psgc-api && npm ci
cp .env.example .env.local # set DATABASE_URL_MAIN, DATABASE_URL_STAGING, ADMIN_SECRET, PUBLIC_BASE_URL
npm run db:seed -- seed/PSGC-3Q-2026-Publication-Datafile.xlsx \
--release-name "Third Quarter 2026 PSGC Updates: Correction of the Name of One Barangay" \
--release-date 2026-10-12 --reference-no 2026-331
npm run build && npm startData attribution: Philippine Statistics Authority (PSA). This project is not affiliated with PSA.