Postcode matching or nearby planning applications?
For a single planning enquiry, start with the official council planning source linked from our directory. For a property product, GIS workflow or repeatable integration, use the planning applications API.
- Exact postcode: postcode=SW1A1AA matches the postcode recorded on an application. It does not include neighbouring postcodes.
- Postcode district: postcode=LE2 matches the outward code LE2, not LE20. postcode_prefix=LE2 is a broader prefix search that can include LE20.
- Radius: /v1/applications/nearby searches around coordinates. It can cross postcode boundaries and excludes records without usable coordinates.
Search an exact postcode without geocoding
Set PLANWIRE_API_KEY privately in your local or server environment. Never publish it in a URL, browser script or repository.
curl --fail-with-body --max-time 15 \
'https://api.planwire.io/v1/applications?postcode=SW1A1AA&limit=5' \
-H "X-API-Key: $PLANWIRE_API_KEY"Read records from the response's data array. An empty array means no matches in your accessible data, not proof that a property has no planning history. Missing postcodes, sample restrictions and council coverage can affect results.
Step 1: Convert a postcode to coordinates
Radius search needs latitude and longitude. Postcodes.io's postcode lookup supplies a postcode point without an API key. This is an area reference point, not necessarily the property's exact location. Use verified property coordinates when distance from the building matters.
curl --fail-with-body --max-time 15 'https://api.postcodes.io/postcodes/SW1A1AA'Use result.latitude and result.longitude from the current lookup. The following is an illustrative, shortened response, not a live location or planning-data result:
{
"status": 200,
"result": {
"postcode": "SW1A 1AA",
"latitude": 51.501009,
"longitude": -0.141588
}
}Step 2: Query planning applications by radius
Use /v1/applications/nearby with lat, lng and radius_km. Radius is in kilometres. Do not add coordinates to the ordinary list endpoint and assume they apply a distance filter.
curl --fail-with-body --max-time 15 \
'https://api.planwire.io/v1/applications/nearby?lat=51.501009&lng=-0.141588&radius_km=0.5&limit=20' \
-H "X-API-Key: $PLANWIRE_API_KEY"This asks for up to 20 accessible records within 500 metres of the example point, nearest first. It is a bounded result, not an exhaustive export. Missing coordinates and field values remain possible; consult the response field reference and council coverage evidence.
Step 3: Use postcode search for date and status filters
The nearby endpoint supports coordinates, radius and limit; it does not apply the list endpoint's date, status or keyword filters. For those filters, use a postcode or council-scoped list query. A postcode district is not a substitute for a circular boundary.
Only pending applications
curl --fail-with-body --max-time 15 \
'https://api.planwire.io/v1/applications?postcode=LE2&status=Pending&limit=5' \
-H "X-API-Key: $PLANWIRE_API_KEY"Applications within a chosen date window
curl --fail-with-body --max-time 15 \
'https://api.planwire.io/v1/applications?postcode=LE2&date_from=2026-09-01&date_to=2026-09-07&limit=5' \
-H "X-API-Key: $PLANWIRE_API_KEY"The fixed dates demonstrate the syntax, not "the last seven days". Check date semantics and pagination. Free keys return older samples, so a recent date window may be empty.
Descriptions mentioning householder work
curl --fail-with-body --max-time 15 \
'https://api.planwire.io/v1/applications?postcode=LE2&q=householder&limit=5' \
-H "X-API-Key: $PLANWIRE_API_KEY"Full example in JavaScript
Run this server-side with Node.js 20 or later. It makes two bounded requests, checks HTTP errors and reads the documented data array. It does not retry rate-limit responses automatically.
async function getPlanningByPostcode(postcode, radiusMetres = 500) {
const key = process.env.PLANWIRE_API_KEY;
if (!key) throw new Error('Set PLANWIRE_API_KEY privately');
if (!Number.isFinite(radiusMetres) || radiusMetres <= 0) {
throw new Error('Radius must be a positive number of metres');
}
const geoResponse = await fetch(
`https://api.postcodes.io/postcodes/${encodeURIComponent(postcode.trim())}`,
{ signal: AbortSignal.timeout(15000) }
);
if (!geoResponse.ok) throw new Error(`Postcode lookup HTTP ${geoResponse.status}`);
const geo = await geoResponse.json();
if (geo.status !== 200 || !geo.result) throw new Error('Postcode not found');
const { latitude, longitude } = geo.result;
if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) {
throw new Error('Postcode lookup returned no usable coordinates');
}
const url = new URL('https://api.planwire.io/v1/applications/nearby');
url.searchParams.set('lat', latitude);
url.searchParams.set('lng', longitude);
url.searchParams.set('radius_km', radiusMetres / 1000);
url.searchParams.set('limit', 50);
const res = await fetch(url, {
headers: { 'X-API-Key': key },
signal: AbortSignal.timeout(15000)
});
if (!res.ok) throw new Error(`PlanWire HTTP ${res.status}`);
const payload = await res.json();
if (!Array.isArray(payload.data)) throw new Error('Unexpected PlanWire response');
return payload.data;
}
const apps = await getPlanningByPostcode('SW1A 1AA', 500);
console.log(`Returned ${apps.length} matching records`);Python example
This version uses Python 3 and the requests package. Install it in your virtual environment with python3 -m pip install requests. For a dependency-free council search, use our tested Python planning API tutorial.
import requests
import os
import math
from urllib.parse import quote
def get_planning_by_postcode(postcode, radius_m=500):
key = os.environ["PLANWIRE_API_KEY"]
if not key:
raise ValueError("Set PLANWIRE_API_KEY privately")
if not math.isfinite(radius_m) or radius_m <= 0:
raise ValueError("Radius must be a positive number of metres")
geo_response = requests.get(
"https://api.postcodes.io/postcodes/" + quote(postcode.strip(), safe=""),
timeout=15,
)
geo_response.raise_for_status()
geo = geo_response.json()
if geo.get("status") != 200 or not geo.get("result"):
raise ValueError("Postcode not found")
lat = geo["result"]["latitude"]
lng = geo["result"]["longitude"]
if not isinstance(lat, (int, float)) or not isinstance(lng, (int, float)):
raise ValueError("Postcode lookup returned no usable coordinates")
if not math.isfinite(lat) or not math.isfinite(lng):
raise ValueError("Postcode lookup returned no usable coordinates")
# Query PlanWire
response = requests.get(
"https://api.planwire.io/v1/applications/nearby",
params={"lat": lat, "lng": lng, "radius_km": radius_m / 1000, "limit": 50},
headers={"X-API-Key": key},
timeout=15,
)
response.raise_for_status()
payload = response.json()
if not isinstance(payload.get("data"), list):
raise ValueError("Unexpected PlanWire response")
return payload["data"]
apps = get_planning_by_postcode("SW1A 1AA", radius_m=500)
print(f"Returned {len(apps)} matching records")What radius should I use?
The right radius depends on what you're building:
- 100–200m — immediate neighbours of a specific address (objection/notification tools)
- 500m — typical neighbourhood awareness (property portals, home-owner tools)
- 1–2km — local area monitoring (estate agents, planning consultants)
- 5–10km — regional development pipeline (construction lead gen, site appraisal)
Monitoring: Signed planning webhooks deliver supported changes after PlanWire captures them, not the moment somebody submits an application to a council. Registered filters include council and postcode prefix, not an arbitrary radius. Review plan entitlements and recovery behavior before replacing polling.
Bulk postcode lookups
If you have a list of postcodes to query at once, postcodes.io supports bulk lookups (up to 100 postcodes per request):
curl -X POST "https://api.postcodes.io/postcodes" \
-H "Content-Type: application/json" \
-d '{"postcodes": ["SW1A 1AA", "EC1A 1BB", "W1A 1AA"]}'Coverage
Check current council coverage, history and source freshness for your area. Not every record has a usable postcode or coordinates. Search results are not a complete legal planning history, and permission does not establish that construction started. Keep the original source link and reference when evaluating a result.
Free sandbox
Free keys provide 25 requests per day against limited, potentially older samples. They do not establish current local coverage. Check plan limits and live access before importing records or designing monitoring. Handle 401 authentication errors, 403 entitlement errors and 429 rate limits explicitly; an error is not an empty result.