PlanNexus provides a RESTful API for accessing planning application data from councils across the UK.
The API is versioned at /v1 and treated as a stable contract — changes are additive, and any breaking change is announced ahead of time. See the changelog & change policy.
All API requests require an API key passed via the X-API-Key header.
curl https://api.plannexus.io/v1/applications \
-H"X-API-Key: pn_live_your_key_here" \
-G -d"q=extension" -d"postcode=SW1"Rate limits are communicated via response headers:
| Tier | Rate | Monthly | Price |
|---|---|---|---|
| Free | 10/min | 1,000 | £0 |
| Starter | 60/min | 50,000 | £99/mo |
| Pro | 300/min | 150,000 | £299/mo |
| Enterprise | Custom | Unlimited | Custom |
/v1/applicationsSearch and filter planning applications with full-text search, postcode filtering, and faceted results.
Parameters
qstringFull-text search querypostcodestringPostcode prefix filter (e.g. SW1, E8)statusstringFilter by status (comma-separated)application_typestringFilter by typeauthority_iduuidFilter by authoritydate_received_fromdateReceived on or after (YYYY-MM-DD)date_received_todateReceived on or beforesortstringSort field (default: date_received)orderstringasc or desc (default: desc)pageintPage number (default: 1)per_pageintResults per page (default: 25, max: 100)/v1/applications/nearbyFind planning applications near a geographic point.
Parameters
latfloatLatitude (-90 to 90)lngfloatLongitude (-180 to 180)radiusintRadius in metres (default: 1000, max: 50000)/v1/applications/{id}Get full details for a single planning application.
/v1/applications/{id}/documentsList documents associated with an application.
/v1/applications/{id}/historyGet the status change history for an application.
/v1/applications/export.csvPaid. Stream a filtered result set as a CSV download — up to 25,000 rows per call, metered per-row against your monthly export allowance. Takes the same filters as /v1/applications, plus:
Parameters
date_validated_fromdateValidated on or after (YYYY-MM-DD)date_validated_todateValidated on or beforevalidated_or_receivedboolFall back to date_received where a council publishes no validated date — keeps a validated-date feed completelatfloatLatitude for radius filter (use with lng + radius)lngfloatLongitude for radius filterradiusintMetres around (lat, lng) — default 1000, max 50000window_daysintDays back from today when date_received_from is unset (default 90)limitintRows to return (max 25,000)/v1/applications/top-agents.csvPaid. The most-active planning agents within a filtered result set, as CSV — same filters as /v1/applications.
/v1/authoritiesList all local authorities in the database.
/v1/analytics/councilsPublic — no auth. League table — approval rate, decision speed and volume for every authority (rolling 12 months, official MHCLG PS2). Rendered at plannexus.io/leagues/planning.
Parameters
sortstringapproval_rate | speed | decisionsregionstringFilter to one region/v1/analytics/councils/{gss}Paid. One council's scorecard vs national + regional averages, with rank and the major/minor/other split.
/v1/analytics/councils/{gss}/trendsPaid. Full quarterly approval/timeliness series, 1979 to present.
/v1/analytics/councils/{gss}/teaserPublic — no auth. Headline approval rate, national average and rank for one council.
/v1/analytics/activityPaid. Busiest authorities by applications received (live; 12-month and last-90-day volumes).
Parameters
sortstringvolume | recentregionstringFilter to one region/v1/analytics/activity/{gss}Paid. One authority's volume, application-type mix and 24-month trend.
/v1/analytics/agentsPaid. Most-active planning agents by application volume (national, or scoped to one authority).
Parameters
gss_codestringScope to one authority (else national)limitintRows to return (default 50, max 100)/v1/analytics/market/{gss}Paid. Property-market summary for one council from HM Land Registry Price Paid — median price, sales volume and year-on-year change (England & Wales; YoY suppressed on thin samples).
/v1/analytics/market/{gss}/teaserPublic — no auth. Headline median price and sales volume for one council.
/v1/analytics/energyPublic — no auth. Energy-efficiency league built from 22M+ domestic EPC certificates — share rated C or better and mean SAP score per council (suppressed under 100 certificates). Rendered at plannexus.io/leagues/energy.
/v1/analytics/energy/{gss}Paid. One council's EPC profile: rating distribution, mean SAP score and retrofit signals.
/v1/analytics/energy/{gss}/teaserPublic — no auth. Headline share rated C+ and rank for one council.
/v1/analytics/appealsPublic — no auth. Appeals league from Planning Inspectorate casework — overturn rate per council (appeals allowed / decided; suppressed under 20 decided). Rendered at plannexus.io/leagues/appeals.
/v1/analytics/appeals/agentsPaid. Agent win rates at appeal — who actually gets refusals overturned.
/v1/analytics/appeals/{gss}Paid. One council's appeal record: overturn rate vs the national average, volume and 10-year trend.
/v1/analytics/appeals/{gss}/teaserPublic — no auth. Headline overturn rate and rank for one council.
/v1/subscriptionsCreate a webhook subscription to receive notifications for new applications or status changes.
Parameters
webhook_urlstringHTTPS URL to receive payloadsfilter_postcodesstring[]Postcode prefix filterseventsstring[]Event types: new_application, status_change, decision_made/v1/subscriptionsList your webhook subscriptions.
Thin, typed clients around the REST API. Both honour Retry-After on 429 automatically and ship TypeScript/Python types for every response shape.
from plannexus import PlanNexus
with PlanNexus(api_key="pn_live_...") as client:
result = client.applications.search(
q="extension", postcode="SW1", per_page=10
)
for app in result["data"]:
print(app["reference"], app["address"])import { PlanNexus } from "plannexus";
const client = new PlanNexus({ apiKey: "pn_live_..." });
const result = await client.applications.search({
q: "extension",
postcode: "SW1",
per_page: 10,
});
for (const app of result.data) {
console.log(app.reference, app.address);
}import httpx
client = httpx.Client(
base_url="https://api.plannexus.io/v1",
headers={"X-API-Key": "pn_live_your_key"}
)
# Search for extensions in SW1
resp = client.get("/applications", params={
"q": "extension",
"postcode": "SW1",
"per_page": 10
})
data = resp.json()
for app in data["data"]:
print(f"{app['reference']} - {app['address']}")
print(f" Status: {app['status']}")
print(f" Received: {app['date_received']}")
const response = await fetch(
"https://api.plannexus.io/v1/applications? " +
new URLSearchParams({ q: "extension", postcode: "SW1" }),
{
headers: {"X-API-Key": "pn_live_your_key" },
}
);
const { data, meta } = await response.json();
console.log(`Found ${meta.total} applications`);
data.forEach((app) => {
console.log(`${app.reference} - ${app.address}`);
});List endpoints (applications, authorities, subscriptions) return a wrapped envelope:
{
"data": [ /* array of records */ ],
"meta": {
"total": 4218,
"page": 1,
"per_page": 25,
"pages": 169
}
}Single-record endpoints return the bare object. Pagination is page-based with page and per_page query params — `per_page` caps at 100.
| Status | Meaning | What to do |
|---|---|---|
| 400 | Malformed request / bad query | Read the detail field; fix the input |
| 401 | Missing / invalid API key | Add X-API-Key header; rotate if lost |
| 403 | Feature requires a higher tier (e.g. webhooks) | Upgrade at /dashboard/billing |
| 404 | Resource not found | Check the id / reference |
| 429 | Rate limit / quota exhausted | Honour the Retry-After header; retry after |
| 5xx | Server error | Retry with exponential backoff; persistent 5xx → email support |
Create a subscription via POST /v1/subscriptions (or the Webhooks page in your dashboard). PlanNexus POSTs a JSON payload to your endpoint whenever a matching application triggers a subscribed event.
new_application — a council published a new applicationstatus_change — an existing application's status moved (e.g. validated → under-consultation)decision_made — an application was decided (approved/refused/withdrawn){
"event": "new_application",
"timestamp": "2026-04-23T09:15:42.108Z",
"data": {
"application_id": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
"reference": "23/04521/FUL",
"authority_id": "018e1c50-...",
"address": "14 Bishopsgate, London EC2N 4AW",
"postcode": "EC2N 4AW",
"description": "Erection of a single-storey rear extension.",
"status": "validated",
"decision": null,
"application_type": "householder",
"date_received": "2026-04-23" }
}Content-Type : application/jsonX-PlanNexus-Signature : sha256=<hex-digest> — HMAC-SHA256 over the raw body, keyed by your webhook secretX-PlanNexus-Event — same as payload.event; handy for routing before parsing the bodyimport hmac, hashlib
def verify(body: bytes, header: str, secret: str) -> bool:
expected ="sha256=" + hmac.new(
secret.encode(), body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, header)import { createHmac, timingSafeEqual } from "crypto";
export function verify(body, header, secret) {
const expected =
"sha256=" +
createHmac("sha256", secret).update(body).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && timingSafeEqual(a, b);
}A 2xx response is treated as delivered. Anything else counts toward a consecutive-failure tally; a subscription is auto-paused after 10 consecutive failures. Paused subscriptions stop receiving events — resume them from the dashboard once your endpoint is healthy. Click Send test on a subscription to fire a synthetic event: "test" payload — test deliveries don't count against the failure tally.
Try the API directly in your browser with our Swagger UI.
Open Swagger UI