Connect your system to Pasay City GIS
The API gives other systems the same data the map uses: the GIS input entries (public facilities, utilities, overpasses, bridges, businesses and parcels) with their attachments, the registered businesses per barangay, and the zone parcel layers with their tax declaration details. With a read and write key, a system can also add, edit and delete GIS input entries.
http://localhost/api/v1/Everything changed through the API shows on the map straight away, and is recorded in the platform's history under the key's account, marked via API key.
Getting a key
Keys are created by a platform administrator (a staff account):
- Open the map, click the gear ⚙ at the top right, then API keys
(or go to
/admin/pasay/apikey/). - Click Add API key. Give it a name that says who uses it, e.g. City Hall ERP.
- Choose the account it acts as. Entries added or edited with the key are recorded under that account, so a dedicated account per system (e.g. erp-integration) keeps the history clear.
- Choose the access: Read only (fetch data) or Read and write (also add, edit and delete entries). Optionally set an expiry.
- Save. The key is shown once — copy it then. Only a fingerprint of it is kept, so a lost key can't be recovered: revoke it (select it in the list → Revoke selected keys) and add a new one.
A key looks like pasay_1a2b3c4d.Xy…. The part before the dot identifies it in the admin list.
Authentication
Send the key with every request, in either header:
Authorization: Bearer pasay_1a2b3c4d.your-secret-part
X-API-Key: pasay_1a2b3c4d.your-secret-part
Requests without a valid key get 401. Revoked keys, expired keys and keys whose account has been
deactivated stop working immediately. Read only keys get 403 on POST, PUT,
PATCH and DELETE.
Quick start
Check the key works — the service answers with the key's name and access:
export PASAY_GIS_API_KEY="pasay_1a2b3c4d.your-secret-part"
curl -H "Authorization: Bearer $PASAY_GIS_API_KEY" http://localhost/api/v1/
# Parcel/Lot entries in Zone 9, changed since 1 January
curl -H "Authorization: Bearer $PASAY_GIS_API_KEY" \
"http://localhost/api/v1/entries/?category=parcel&zone=9&updated_since=2026-01-01"
import os
import requests
API = "http://localhost/api/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['PASAY_GIS_API_KEY']}"
print(session.get(f"{API}/").json())
# Every Business entry, page by page
url, params = f"{API}/entries/", {"category": "business", "page_size": 500}
while url:
page = session.get(url, params=params)
page.raise_for_status()
body = page.json()
for feature in body["features"]:
props = feature["properties"]
print(props["id"], props["name"], props["details"].get("acctno"))
url, params = body["next"], None # "next" already carries the parameters
// Node 18+ (or any server-side JavaScript with fetch). Keep keys out of web pages.
const API = "http://localhost/api/v1";
const headers = { Authorization: `Bearer ${process.env.PASAY_GIS_API_KEY}` };
const service = await fetch(`${API}/`, { headers }).then(r => r.json());
console.log(service.key);
const res = await fetch(`${API}/zones/9/parcels/?q=claudio&geometry=0`, { headers });
if (!res.ok) throw new Error((await res.json()).error.message);
const { count, features } = await res.json();
console.log(count, features.map(f => f.properties.PIN));
Responses & errors
Successful calls return 200 (or 201 when something was created) with JSON. Errors always have this shape:
{
"error": {
"code": "invalid",
"message": "Please correct the highlighted fields.",
"fields": { "details.acctno": ["This field is required."] }
}
}
| Status | code | When |
|---|---|---|
| 400 | invalid | The data didn't pass validation; fields says which fields and why. Field names match the request, with form columns as details.<key>, and attachment problems under files. |
| 400 | invalid_json · invalid_parameter | The body isn't a JSON object, or a query parameter is malformed. |
| 401 | invalid_key | No key, an unknown key, or a revoked or expired one. |
| 403 | read_only_key | A read only key tried to change data. |
| 404 | not_found | The entry, file, barangay, zone or parcel doesn't exist (or was deleted). |
| 405 | method_not_allowed | That method isn't supported on that URL; the Allow header lists the ones that are. |
| 429 | rate_limited | Too many requests; wait the seconds given in Retry-After. |
Times are ISO 8601 with time zone, e.g. 2026-11-06T09:15:00+00:00. Coordinates are WGS 84 (longitude, latitude), as in GeoJSON.
Paging & limits
Lists are paged. Pass page (from 1) and page_size; every list response carries
count, page, page_size, pages, and full next /
previous URLs (null at the ends) — follow next until it's null.
| List | Default page size | Maximum |
|---|---|---|
| Entries | 100 | 1000 |
| Zone parcels | 200 | 2000 |
| Businesses | 500 | 5000 |
Rate limit: 120 requests per minute per key. Responses include
X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with
Retry-After. For syncing, prefer updated_since over re-reading everything.
Endpoints
Paths are relative to http://localhost/api/v1. GET works with any key;
POST PATCH DELETE need a read and write key.
GET /
Checks the key and lists the main endpoints. Includes key.name, key.access (read or write),
key.acts_as and key.expires_at.
GET /forms/ GET /forms/{category}/
The GIS input forms and their fields — the same definitions the platform validates against
(public_facility, utility, overpass, bridge, business, parcel).
Summarised under Form fields.
GET /entries/
GIS input entries, newest change first, as a GeoJSON FeatureCollection (plus the paging fields).
| Parameter | Meaning |
|---|---|
category | One form: public_facility, utility, overpass, bridge, business, parcel |
zone | Zone number or id (9 or zone_9) — Parcel/Lot entries |
pin | A parcel's PIN — Parcel/Lot entries |
parcel_ref | An unsure parcel's reference (it has no PIN) — Parcel/Lot entries |
q | Text in the entry's name |
updated_since | Only entries changed at or after this date / date-time |
page, page_size | See Paging |
Each feature (an entry):
{
"type": "Feature",
"id": 42,
"geometry": { "type": "Point", "coordinates": [120.998812, 14.541731] },
"properties": {
"id": 42,
"category": "parcel",
"category_label": "Parcel/Lot",
"name": "123-01-086-03-010",
"type": "Residential",
"asset_type": "Residential", "asset_type_other": "", "condition": "",
"details": { "arp_tdn": "C1-086-00173", "parcel_class": "Residential", "area": 64.0 },
"zone": "zone_9", "pin": "123-01-086-03-010",
"latitude": 14.541731, "longitude": 120.998812,
"created_by": "Juan Dela Cruz",
"created_at": "2026-11-06T01:20:11+00:00",
"updated_at": "2026-11-06T01:20:11+00:00",
"attachments": [
{ "id": 7, "name": "front.jpg", "kind": "image", "content_type": "image/jpeg", "size": 248113,
"url": "http://localhost/api/v1/files/7/", "uploaded_at": "…", "uploaded_by": "Juan Dela Cruz" }
],
"url": "http://localhost/api/v1/entries/42/"
}
}
POST /entries/
Adds an entry. Send JSON — or, to include attachments in the same call, multipart/form-data with the
JSON in a field named data and the files in fields named files. Returns 201 with the new entry.
The fields depend on the form: see Adding & editing data and Form fields.
GET PATCH PUT DELETE /entries/{id}/
GET returns one entry. PATCH (or PUT) changes it: send only the fields to change;
details is merged column by column, so {"details": {"noe": 12}} changes just that column.
Add "remove_files": [7, 8] to drop attachments. An entry can't move to another form.
DELETE removes the entry from the map and deletes its attachments; its history is kept.
POST /entries/{id}/files/
Adds attachments to an existing entry: multipart/form-data with one or more files fields.
Only forms that take attachments (Parcel/Lot) accept them. Returns the entry with its attachments.
GET /entries/{id}/history/
Every add, edit and delete of the entry, newest first: action, timestamp, by,
via_api_key (the key's name, when made through the API), changes (for edits: label, from, to) and
data (the entry as it stood after that change). Also works for deleted entries.
GET DELETE /files/{id}/
GET downloads an attachment with its own content type (images and videos inline, PDF and CSV as
attachment); Range requests are supported for video. DELETE removes it from its entry.
Accepted uploads: JPG, PNG, GIF, WebP, MP4, MOV, WebM, PDF and CSV, up to 20 MB each. The platform also has a total
storage limit; a save that would exceed it is refused with a message in fields.files.
GET /barangays/
Each barangay with its number of registered businesses (one per business account, from the BPLO listing).
GET /barangays/{number}/businesses/ GET /businesses/
The businesses of one barangay, or of all of them: acctno, name, owner, description,
barangay, barangay_number. Filter with q (account no., name, owner or description) and, on
/businesses/, barangay (a number). Read only.
GET /zones/
Zones 1–18 with their parcel counts, how many parcels have a PIN, how many are unsure parcels that can be filled in
(unsure_parcels), and the count per class (Residential, Commercial, Unsure).
GET /zones/{zone}/parcels/
A zone's parcels as GeoJSON polygons with their tax declaration properties (PIN, ARP/TDN, Class,
AREA, PROPERTY_OWNER, LOCATION, …). {zone} is 9 or zone_9.
Filters: q (PIN, ARP/TD No., owner or location), class, has_pin=1;
geometry=0 leaves the shapes out for a much smaller response.
Every parcel carries a parcel_ref: a hash of its shape in the zone file, which stays the same unless the
zone file is redrawn. It is how an unsure parcel (no PIN, class Unsure; find them with class=Unsure)
and a parcel whose shape is being updated are named. A parcel whose shape was updated is served with its new shape
and the entry that holds it in shape_entry.
GET /zones/{zone}/parcels/{pin}/
One parcel with its shape, and under entry the Parcel/Lot entry for it (with attachments), or null.
Adding & editing data
A business — placed by coordinates; its BPLO columns go in details:
curl -X POST http://localhost/api/v1/entries/ \
-H "Authorization: Bearer $PASAY_GIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"category": "business",
"name": "ABACUS BOOK & CARD CORPORATION",
"latitude": 14.5376,
"longitude": 120.9912,
"details": {
"acctno": "02-000028",
"owner": "ABACUS BOOK & CARD CORPORATION",
"owndesc": "CORPORATION",
"statcode": "R",
"foryear": 2025,
"commaddr": "UNIT 115 G/F, METROPOINT MALL, TAFT AVE., Barangay 097",
"barangay": "Barangay 97",
"busdesc": "Retailer - General Merchandise"
}
}'
A parcel with photos and a PDF — placed by its zone and PIN (no coordinates; the platform puts it inside the parcel), files in the same call:
curl -X POST http://localhost/api/v1/entries/ \
-H "Authorization: Bearer $PASAY_GIS_API_KEY" \
-F 'data={"category": "parcel", "zone": "zone_9", "pin": "123-01-086-03-010",
"details": {"arp_tdn": "C1-086-00173", "parcel_class": "Residential"}}' \
-F files=@front.jpg \
-F files=@tax-declaration.pdf
An unsure parcel — it has no PIN, so give its parcel_ref from
GET /zones/{zone}/parcels/?class=Unsure. Only the class is required; send whatever else is known:
curl -X POST http://localhost/api/v1/entries/ \
-H "Authorization: Bearer $PASAY_GIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category": "parcel", "zone": "zone_10", "parcel_ref": "1tiu9qd7px7",
"details": {"parcel_class": "Commercial", "actual_use": "C"}}'
The entry's values then show on the zone layer in place of "Unsure".
Each parcel has one live Parcel/Lot entry; adding a second for the same PIN (or parcel_ref) fails with a message
naming the existing entry — find it with GET /entries/?category=parcel&pin=… (or &parcel_ref=…)
and edit that one.
A parcel's updated shape — any parcel, by its parcel_ref. The shape is a GeoJSON
Polygon or MultiPolygon in WGS84 longitude / latitude (reproject a shapefile first); it replaces the parcel's shape on the
zone layer until the entry is deleted. One live shape update per parcel.
curl -X POST http://localhost/api/v1/entries/ \
-H "Authorization: Bearer $PASAY_GIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"category": "parcel_shape", "zone": "zone_10", "parcel_ref": "1tiu9qd7px7",
"shape": {"type": "Polygon", "coordinates": [[[120.9897, 14.5384], [120.9899, 14.5384],
[120.9899, 14.5386], [120.9897, 14.5386], [120.9897, 14.5384]]]},
"details": {"change_type": "Resurvey", "remarks": "New survey plan"}}'
Edit — only what changes:
curl -X PATCH http://localhost/api/v1/entries/42/ \
-H "Authorization: Bearer $PASAY_GIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"details": {"noe": 12}, "remove_files": [7]}'
Python — add attachments to an existing entry, then delete one:
with open("site-visit.mp4", "rb") as video:
r = session.post(f"{API}/entries/42/files/", files=[("files", video)])
r.raise_for_status()
attachments = r.json()["properties"]["attachments"]
session.delete(f"{API}/files/{attachments[0]['id']}/").raise_for_status()
Form fields
What each form needs when you add an entry. details fields go inside the details object.
Choice fields take one of the listed values exactly. Also available as JSON from /forms/.
Public Facilities "category": "public_facility"
Markets, parks, waiting sheds, sports complexes and community centers — their location and current condition.
| Field | Required | Value |
|---|---|---|
name | yes | Name of Public Facility |
latitude | yes | Decimal degrees, about 14.48 to 14.58 |
longitude | yes | Decimal degrees, about 120.95 to 121.05 |
asset_type | yes | Facility Type: one of Market, Park, Waiting Shed, Sports Complex, Community Center, Others |
asset_type_other | — | The type in words, when asset_type is "Others" |
condition | yes | Public Facility Condition: one of Operational, Damaged, Under Repair |
Power and Utility Infrastructure "category": "utility"
Power lines, transformers, streetlights and telecom towers — their location and operational condition.
| Field | Required | Value |
|---|---|---|
name | yes | Name of Infrastructure |
latitude | yes | Decimal degrees, about 14.48 to 14.58 |
longitude | yes | Decimal degrees, about 120.95 to 121.05 |
asset_type | yes | Infrastructure Type: one of Power Line, Transformer, Streetlight, Telecom Tower, Others |
asset_type_other | — | The type in words, when asset_type is "Others" |
condition | yes | Operational Condition: one of Operational, Non-Operational, Damaged, Under Repair |
Overpass "category": "overpass"
Pedestrian, roadway and railway overpasses — their location and structural condition.
| Field | Required | Value |
|---|---|---|
name | yes | Name of Overpass |
latitude | yes | Decimal degrees, about 14.48 to 14.58 |
longitude | yes | Decimal degrees, about 120.95 to 121.05 |
asset_type | yes | Overpass Type: one of Pedestrian, Roadway, Railway |
asset_type_other | — | The type in words, when asset_type is "Others" |
condition | yes | Structural Condition: one of Good, Fair, Poor, Under Repair |
Bridge "category": "bridge"
Concrete, steel and composite bridges — their location and physical condition.
| Field | Required | Value |
|---|---|---|
name | yes | Name of Bridge |
latitude | yes | Decimal degrees, about 14.48 to 14.58 |
longitude | yes | Decimal degrees, about 120.95 to 121.05 |
asset_type | yes | Construction Type: one of Concrete, Steel, Composite, Others |
asset_type_other | — | The type in words, when asset_type is "Others" |
condition | yes | Physical Condition: one of Good, Fair, Poor, Under Repair |
Business "category": "business"
Registered business establishments — the BPLO payment listing columns (account, owner, address, classification, receipts) and exact coordinates.
| Field | Required | Value |
|---|---|---|
name | yes | Business Name |
latitude | yes | Decimal degrees, about 14.48 to 14.58 |
longitude | yes | Decimal degrees, about 120.95 to 121.05 |
details.acctno |
yes | Account No. · text · source column acctno |
details.owner |
yes | Owner · text · source column owner |
details.owndesc |
yes | Ownership Type · choice · source column owndesc
SINGLECORPORATIONPARTNERSHIPINCORPORATIONCOOPERATIVEFOUNDATIONGOVERNMENT |
details.statcode |
yes | Application Status · choice · source column statcode
RN |
details.noe |
— | Number of Employees · int · source column noe |
details.foryear |
yes | Year Covered · int · source column foryear |
details.commaddr |
yes | Business Address · text · source column commaddr |
details.barangay |
yes | Barangay · choice · source column brgydesc
Barangay 1Barangay 2Barangay 3Barangay 4Barangay 5Barangay 6Barangay 7Barangay 8Barangay 9Barangay 10Barangay 11Barangay 12… 201 values ("Barangay 1" to "Barangay 201") |
details.distdesc |
— | District · choice · source column distdesc
District 01District 02District 03District 04District 05District 06District 07 |
details.buscode |
— | Business Code · text · source column buscode |
details.busdesc |
yes | Business Description · text · source column busdesc |
details.catcode |
— | Category Code · text · source column catcode |
details.catdesc |
— | Category Description · text · source column catdesc |
details.line_stat |
— | Line Status · choice · source column Line Stat
RNC |
details.product |
— | Product · text · source column Product |
details.area |
— | Area (sq m) · decimal · source column area |
details.grossrec |
— | Gross Receipts · decimal · source column grossrec |
details.capital |
— | Capital · decimal · source column capital |
details.orno |
— | Official Receipt No. · text · source column orno |
details.datepaid |
— | Date Paid · date · source column datepaid |
details.user_id |
— | Processed By (User ID) · text · source column user_id |
details.stamt |
— | Line Amount · decimal · source column stamt |
details.tamt |
— | Total Amount Paid · decimal · source column tamt |
details.brgyclr |
— | Barangay Clearance · decimal · source column BRGYCLR |
details.bqtrind |
— | Business Quarter Indicator · choice · source column bqtrind
01234 |
details.gqtrind |
— | Gross Quarter Indicator · choice · source column gqtrind
01234 |
Parcel/Lot "category": "parcel"
takes attachments
Pick a parcel by its PIN from a zone layer, or click a parcel with no data (red) to fill it in. Check its tax declaration details (ARP/TDN, class, area, owner, location) and attach photos, videos, PDFs or CSVs.
| Field | Required | Value |
|---|---|---|
zone | yes | Zone of the parcel, e.g. "zone_9" |
pin | yes | The parcel's PIN in that zone (or give parcel_ref) |
parcel_ref | — | For an unsure parcel (no PIN): its parcel_ref from the zone parcels list. Then only parcel_class is required |
details.arp_tdn |
yes | ARP / Tax Declaration No. · text · source column ARP/TDN |
details.parcel_class |
yes | Class · choice · source column Class
ResidentialCommercialUnsure |
details.kind |
— | Kind · text · source column KIND |
details.sub_class |
— | Sub-class · text · source column SUB_CLASS |
details.actual_use |
— | Actual Use · text · source column ACTUAL_USE |
details.area |
— | Area (sq m) · decimal · source column AREA |
details.location |
— | Location of Property · text · source column LOCATION |
details.property_owner |
— | Property Owner · text · source column PROPERTY_OWNER |
details.owner_address |
— | Owner Address · text · source column ADDRESS_OF_PROPERTY_OWNER |
Parcel Lot Shape "category": "parcel_shape"
Select a parcel on a zone layer and replace its shape with the updated one from your shapefile (.zip, or the .shp with its .shx, .dbf and .prj) or GeoJSON.
| Field | Required | Value |
|---|---|---|
zone | yes | Zone of the parcel, e.g. "zone_9" |
parcel_ref | yes | The parcel's parcel_ref from the zone parcels list (any parcel) |
shape | yes | The new shape: a GeoJSON Polygon or MultiPolygon in WGS84 longitude / latitude |
details.change_type |
yes | Type of Change · choice
Boundary correctionResurveyGeoreferencing fixOthers |
details.survey_ref |
— | Survey / Plan No. · text |
details.remarks |
yes | Reason / Remarks · text |
details.source_file |
— | Source File · text |
Field Assessment "category": "field_assessment"
takes attachments
On site: find the parcel from your location, redraw its shape by tapping its corners on the map or walking them with GPS, and add photos of what you found.
| Field | Required | Value |
|---|---|---|
zone | yes | Zone of the parcel, e.g. "zone_9" |
parcel_ref | yes | The parcel's parcel_ref from the zone parcels list (any parcel) |
shape | yes | The new shape: a GeoJSON Polygon or MultiPolygon in WGS84 longitude / latitude |
details.assessed_on |
yes | Date Assessed · date |
details.assessor |
yes | Assessor / Team · text |
details.change_type |
yes | Type of Change · choice
Boundary correctionResurveyGeoreferencing fixOthers |
details.observed_use |
— | Observed Use · choice
ResidentialCommercialMixed useIndustrialInstitutionalVacant lotOthers |
details.remarks |
yes | Findings / Remarks · text |
details.capture_method |
— | How the Shape Was Captured · choice
Tapped on the mapGPS cornersGPS and map |
details.gps_accuracy_m |
— | GPS Accuracy (m) · decimal |
Numbers: int and decimal fields take numbers (or numeric strings), 0 or more. Dates are YYYY-MM-DD.
Security checklist
- Keep keys on servers. Don't put a key in a web page or mobile app — anyone can read it there. Call the API from your back end.
- Use HTTPS so keys and data are encrypted in transit.
- One key per system, with the least access it needs (read only unless it must write), and an expiry where it makes sense.
- Store keys like passwords — environment variables or a secrets manager, never in source code.
- Revoke a key straight away if it may have leaked, and issue a new one.
- Attachments can contain personal or property documents — protect what you download.