Site History by Boundary
Search planning history inside a site boundary you already hold or draw on a map.
Search planning history inside a site boundary rather than around a point.
Use this when you already hold the site's extent — a folio parcel, a red-line application area, a title map, or a polygon a user drew on a map — and would otherwise have to approximate it with a radius. The boundary states exactly which site you mean, so a 90 m frontage and a 9 m one are no longer the same query.
Everything about Site History stays as it is. This is an alternative entry point that returns the identical response envelope, so both can render through the same code path.
#Request
POST /ext/v1/precedents/site-history/polygon
#Body
| Field | Type | Required | Description |
|---|---|---|---|
geometry | object | Yes | A GeoJSON Polygon or MultiPolygon geometry object. Not a Feature, not a FeatureCollection — see below. |
limit | integer | No | Maximum records returned. Default 200, capped at 500. |
{
"geometry": {
"type": "Polygon",
"coordinates": [[[-6.26400, 53.34500], [-6.26340, 53.34500], [-6.26340, 53.34527], [-6.26400, 53.34527], [-6.26400, 53.34500]]]
},
"limit": 200
}#Geometry Rules
These are the four rules that matter:
- Coordinate order is
[longitude, latitude], not[lat, lon]. This is the GeoJSON convention and it is the reverse of how thelat/lonquery parameters are named elsewhere in this API. Getting it backwards is the one mistake that does not raise an error: a ring written as[53.345, -6.264]puts your site in the Indian Ocean and quietly returns zero results. An empty result from a boundary you know is populated is almost always this. - Rings must be closed. The last coordinate pair must repeat the first, so a
ring needs at least four positions. Fewer than four is rejected with
400; an unclosed ring of four or more is currently rejected further down the stack and surfaces as a500, so treat closing the ring as your responsibility rather than relying on the error to be descriptive. - WGS84 (EPSG:4326) only. Irish Transverse Mercator (EPSG:2157) and Irish
Grid (EPSG:29903) coordinates are rejected as out of range with
400. Reproject before sending — most Irish folio and red-line data arrives in ITM. - Type must be
PolygonorMultiPolygon.Feature,GeometryCollection,LineString, and bare coordinate arrays are all rejected with400.
Area is capped at 5 km², returning 413. A red-line application boundary is
typically a few hundred to a few thousand square metres, so this only bites if you
send an estate, a townland, or an accidentally malformed ring whose extent spans
the country.
One caveat on that cap: area is measured by summing every ring, so interior rings (holes) add to the measured area instead of subtracting from it. A doughnut-shaped boundary measures larger than it really is. Well under the cap this never matters; near it, send the outer ring alone.
#Sending a Boundary You Already Have
GIS exports almost never hand you a bare geometry. They hand you a Feature or a
FeatureCollection, and posting one of those verbatim is the most common 400 on
this endpoint. Unwrap it first:
// From a Feature
const geometry = feature.geometry;
// From a FeatureCollection — first feature
const geometry = featureCollection.features[0].geometry;
// From a Leaflet layer (already WGS84)
const geometry = layer.toGeoJSON().geometry;A full Feature looks like this. The part you send is the value of geometry and
nothing else — properties is discarded and is not echoed back:
{
"type": "Feature",
"properties": { "site_ref": "INFILL-01", "area_ha": 0.12 },
"geometry": {
"type": "Polygon",
"coordinates": [[[-6.26400, 53.34500], [-6.26340, 53.34500], [-6.26340, 53.34527], [-6.26400, 53.34527], [-6.26400, 53.34500]]]
}
}Use a MultiPolygon when one site is split into parts — a plot severed by a
laneway, or a landholding in two blocks. Send it as a single request rather than
one request per part, so the returned counts describe the whole site:
{
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[[[-6.26400, 53.34500], [-6.26340, 53.34500], [-6.26340, 53.34527], [-6.26400, 53.34527], [-6.26400, 53.34500]]],
[[[-6.26310, 53.34500], [-6.26260, 53.34500], [-6.26260, 53.34522], [-6.26310, 53.34522], [-6.26310, 53.34500]]]
]
}
}#Example Request
curl -X POST "https://api.eireplan.ie/ext/v1/precedents/site-history/polygon" \
-H "Authorization: Bearer ek_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"geometry": {
"type": "Polygon",
"coordinates": [[[-6.26400, 53.34500], [-6.26340, 53.34500], [-6.26340, 53.34527], [-6.26400, 53.34527], [-6.26400, 53.34500]]]
},
"limit": 200
}'#Drawing a Boundary on a Map
Any draw control that emits GeoJSON works. With Leaflet and Leaflet.draw, take the geometry straight off the drawn layer — Leaflet already works in WGS84 and already closes the ring for you:
const map = L.map("map").setView([53.34513, -6.26370], 19);
const drawnItems = new L.FeatureGroup().addTo(map);
map.addControl(
new L.Control.Draw({
draw: { polygon: true, rectangle: true, marker: false, polyline: false, circle: false },
edit: { featureGroup: drawnItems },
})
);
map.on(L.Draw.Event.CREATED, async (event) => {
drawnItems.clearLayers();
drawnItems.addLayer(event.layer);
const geometry = event.layer.toGeoJSON().geometry;
const result = await searchSiteHistory(geometry); // the fetch call above
renderPlanningHistory(result);
});If you use MapLibre with Mapbox Draw, draw.getAll() returns a
FeatureCollection — unwrap it as shown above before sending.
#Response Shape
Identical to Site History:
{ "result": { summary, precedents, used_fallbacks, source, … } }. Two fields
differ in a way worth handling:
| Field | Value here | Why |
|---|---|---|
result.source | user_polygon | Records that the area came from you, not from a geocode or a radius. |
result.query_radius_m | 0 | No radius was involved. Do not render it as "searched within 0 m". |
Every returned precedent carries the strongest match signals, because a hit means the application's point falls inside the area you defined:
| Field | Value |
|---|---|
match_reason | user_polygon |
confidence_tier | S |
same_site_confidence | 0.95 |
same_site_signals | ["user_polygon"] |
#Example Response
{
"result": {
"query_radius_m": 0,
"used_fallbacks": ["user_polygon"],
"summary": {
"total_applications": 2,
"decision_counts": { "GRANTED": 1, "REFUSED": 1 },
"type_counts": { "PERMISSION": 2 }
},
"precedents": [
{
"id": "5f8fcb68-2be1-4ce7-8ef4-6a29c2fb4aa8",
"planning_ref": "DCC-1234-25",
"address": "15 Example Street, Dublin 2",
"decision_norm": "granted",
"match_reason": "user_polygon",
"confidence_tier": "S",
"same_site_confidence": 0.95,
"same_site_signals": ["user_polygon"]
},
{
"id": "f40db59f-91f5-478c-a5ab-5c1b6086f7cd",
"planning_ref": "DCC-9981-24",
"address": "15 Example Street, Dublin 2",
"decision_norm": "refused",
"match_reason": "user_polygon",
"confidence_tier": "S",
"same_site_confidence": 0.95,
"same_site_signals": ["user_polygon"]
}
],
"source": "user_polygon"
}
}#Putting the Results Back on the Map
The site-history response carries no coordinates — it is a list of records, not a
feature collection. To plot the results, fetch geometry per precedent from
Get Precedent Details, which returns a
GeoJSON point and, where the authority publishes one, a polygon:
async function toFeatureCollection(precedents, apiKey) {
const features = await Promise.all(
precedents.map(async (precedent) => {
const response = await fetch(
`https://api.eireplan.ie/ext/v1/precedents/${precedent.id}?geometry=simplified`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const { precedent: detail } = await response.json();
// Prefer the published site boundary; fall back to the address point.
const geometry = detail.polygon ?? detail.point;
if (!geometry) return null;
return {
type: "Feature",
geometry,
properties: {
planning_ref: detail.planning_ref,
decision: detail.decision_norm,
address: detail.address,
},
};
})
);
return { type: "FeatureCollection", features: features.filter(Boolean) };
}
const collection = await toFeatureCollection(result.precedents, apiKey);
L.geoJSON(collection, {
style: (feature) => ({
color: feature.properties.decision === "refused" ? "#b4342a" : "#2f6b45",
weight: 2,
}),
}).addTo(map);That is one metered request per precedent, so plot the precedents a user actually
asked to see rather than the whole result set. Pass geometry=none on detail
requests where you need the record but not the shape.
#What the Boundary Does and Does Not Prove
The query tests each precedent's address point against your area. That point is the geocoded application address, and it is the least reliable field in the record: an application registered to one house number routinely sits tens of metres from its neighbour's pin, and authorities frequently geocode a large site to its road frontage.
So a boundary tells you precisely which site you mean. It is not proof of where the application was.
- A hit is a strong positive signal. The point is inside your site.
- A miss is inconclusive, not a rejection. A permission on your own site that the authority geocoded to the road would silently disappear.
Treat the boundary as a confirming signal alongside address and eircode identity,
never as the sole filter. For a complete picture, run the boundary search and the
address or eircode search at
Site History, then merge the two result
sets on precedent id.
#Errors
| Status | Meaning |
|---|---|
400 | geometry missing, malformed JSON body, wrong geometry type, a ring with fewer than four positions, or coordinates outside valid lon/lat range — the usual cause of the last being unprojected ITM or Irish Grid coordinates. |
401 | Missing or invalid API key. |
413 | Boundary area exceeds 5 km². |
500 | Includes geometry that passes the checks above but that the spatial engine rejects — in practice, an unclosed ring. Close the ring and retry before treating it as an outage. |
#Where It Fits
Use this instead of a radius whenever you hold a real site extent. Use
Site History when all you have is an
address or eircode, and Get Precedent Details
to expand any id either one returns.