UK Planning Applications & Site Constraints (Planning Data) avatar

UK Planning Applications & Site Constraints (Planning Data)

Pricing

from $1.40 / 1,000 planning record returneds

Go to Apify Store
UK Planning Applications & Site Constraints (Planning Data)

UK Planning Applications & Site Constraints (Planning Data)

Read the UK Planning Data register: planning applications with reference, site address, coordinates, proposal, decision and decision date, plus the designations covering a site — conservation area, article 4, listed building outline, flood risk zone. Filter by council, radius, text or date.

Pricing

from $1.40 / 1,000 planning record returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

11 days ago

Last modified

Share

Read the official UK Planning Data platform (planning.data.gov.uk, run by the Ministry of Housing, Communities & Local Government) as flat rows. Two jobs in one actor: the planning-application register — council reference, site address and postcode, coordinates, the proposal text, the decision and its date, a link to the council's own file where the council publishes one — and a site constraint check: which designations cover a coordinate (conservation area, article 4 direction, listed building outline, tree preservation zone, flood risk zone, green belt and eight more layers).

Filter by council, by a circle around a coordinate, by keyword, by decision wording, by decision date or by reference. Every date is a UTC calendar date, every row links back to the record it came from. No API key, no proxy, no browser, no council portal is touched.

The platform emptied its planning-application dataset: on 2026-10-09 it holds 0 records, so mode: "applications" returns no application for any council. The designation layers (mode: "constraints") are national and unaffected. Read Coverage before you plan a project around the register.

Use cases

  • Planning history of a site (surveyors, developers, architects): every application within 300 m–2 km of a coordinate, with the proposal, the decision and the date, as the evidence base for a new application.
  • Appeal and refusal research (planning consultants): one council filtered to decisionContains: "refus", newest decision first, to build a precedent list of what was turned down and why it was proposed — or "=refused" for outright refusals only, without the part-granted ones.
  • Lead pipelines for trades and suppliers: approved works that mention solar, heat pump, loft conversion or basement, each with the site address and, for Adur and Worthing, a direct link to the council's application file.
  • Local reporting and analysis: one authority and one year (decisionDateFrom / decisionDateTo) as a spreadsheet — the raw material for "how many extensions were refused in 2023".
  • Site constraint checks before buying or extending (owners, conveyancers, solar installers): mode: "constraints" with radiusMeters: 0 answers "is this spot inside a conservation area, an article 4 direction or a flood risk zone", each answer with the designation's name, reference, publishing authority and a GeoJSON boundary link.
  • Heritage screening (heritage consultants, retrofit assessors): listed building outlines and conservation areas within a radius of an address, to see what sits around a site and not only on it.
  • Keeping a copy in sync: onlyNew: true in a scheduled task writes only records the task has not delivered before, so a warehouse table grows by the batch the platform publishes.

Coverage

The designation datasets are national and maintained: at the time of writing the flood-risk-zone layer alone held 780,636 rows, and an article 4 row at the test coordinate carried a register date of 2026-08-10.

The planning-application register is empty since 2026-10-09. The platform marks the dataset "incomplete and not yet ready for use" and it was always a snapshot republished in batches, not a live feed; it now holds nothing at all. Its own dataset page reports Total 0 and Data providers: None, every entity.json query for the dataset answers count: 0, and the records that were there answer HTTP 404. The platform's note on the dataset's origin reads: "Data created by MHCLG. We will replace this with data from authoritative sources when it is available."

So mode: "applications" returns no application today — whatever the council, the keyword or the date window. Each such run checks the dataset's own total and writes one row with found: false instead of an empty result, and its status message says what happened. Nothing in this actor caches the four authorities below, so applications come back by themselves as soon as the platform republishes the dataset.

Counted on 2026-09-19, while the dataset still had contents, it held 100,627 applications from four authorities — this is what returns if the platform restores it:

CouncilShort codeOrganisation entityRowsSite addressCoordinatesReceived dateDecision wordingCouncil file link
London Borough of CamdenCMD9077,499yesyesnodecisionType (Granted, Refused, …), copied into decisionno
Worthing Borough CouncilWOT38213,629yesyesyesdecision (Approve) + decisionType (Delegated)yes
Adur District CouncilADU267,585yesyesyesdecision + decisionTypeyes
Doncaster Metropolitan Borough CouncilDNC1091,914nonononone, only decisionDateno

Nothing in the actor hard-codes those four. Councils are resolved against the platform's live local-authority register (379 authorities), so an authority the platform adds later works without an update here. A council that resolves but has no applications in the register simply returns zero rows, and the run says so.

Until the register has contents again, the useful half of this actor is mode: "constraints": the designation layers listed below are national, maintained and answer every coordinate in England.

Input

Nothing is required. With no input at all the actor reads the register from its start; the prefills (Camden, the Primrose Hill coordinate, the six everyday designation layers) make the first run a real search.

FieldTypeDefaultAllowed values / notes
modestringapplicationsapplications = the register, constraints = designations covering a point — see Modes
councilstring(prefill Camden)Name or short code (Camden, London Borough of Camden, CMD). Resolved against the live register — see Councils. Empty = every council. Not used in constraints
latitudenumber(prefill 51.5413)WGS84, 49.5 … 61.0. Required for constraints
longitudenumber(prefill -0.1543)WGS84, −9.0 … 2.5, negative west of Greenwich. Works only together with latitude
radiusMetersinteger10000 … 20000. 0 in constraints = the exact point; 0 in applications is read as 1000
datasetsstring[](prefill: the six everyday layers)Which designation layers constraints asks about — see Designation datasets. Empty = the six defaults
textContainsstring—Case-insensitive text in the proposal, the address or the reference (solar, loft conversion, BN43). Applied by this actor over maxScanRows
decisionContainsstring—Case-insensitive text in either decision field (granted, refus, approve, withdrawn, delegated); a leading = asks for the whole wording (=refused) — see Decision wording
decisionDateFromstring—YYYY-MM-DD, inclusive. Applied by this actor over maxScanRows
decisionDateTostring—YYYY-MM-DD, inclusive
referencestring—Exact council reference (2011/0724/C, AWDM/1227/21). Applied by the API, so it is the cheapest lookup
entryDateSincestring—YYYY-MM-DD. The date the platform loaded the record, not the date it was submitted or decided
onlyNewbooleanfalseOutput only entity ids no earlier run with the same input delivered (state in a named key-value store). Only rows really written are remembered — see Limits
maxScanRowsinteger5000100 … 100000. How many register rows a run may read before the actor's own filters. Never lower than maxItems
sortBystringsourceOrdersourceOrder, decisionDateDesc, decisionDateAsc — sorted among the rows the run read, see Limits
maxItemsinteger251 … 5000 output rows
fieldsstring[]allKeep only these output fields, in this order. entityId and dataset are always kept (first, unless you list them). Names from Output; another letter case is corrected and an unknown name skipped, both noted in the run status; a list with no known name stops the run before anything is written

Filters combine with AND. The API applies council, the circle around the coordinate, reference and entryDateSince; this actor applies textContains, decisionContains, the decision-date window and the exact distance to your coordinate, over the rows the run reads. That split is a property of the source (see Limits & FAQ), and every run reports it in its SUMMARY record.

Reference

Modes

modeWhat one row isNeedsTypical input
applicationsOne planning application from the registernothing (council / coordinate / reference recommended){"council":"Camden","radiusMeters":800,"latitude":51.5413,"longitude":-0.1543}
constraintsOne designation that covers the point or the circlelatitude + longitude{"mode":"constraints","latitude":51.5413,"longitude":-0.1543,"radiusMeters":0}

In constraints mode council, decisionContains and the decision-date window have nothing to act on and are reported as unused in the run log and SUMMARY; textContains is matched against the designation name and reference.

Councils

council accepts the short code or any form of the name; the value is matched against the platform's live local-authority register in this order: short code, exact name, name without its legal suffix ("Camden" → "London Borough of Camden"), a name containing what you typed, and finally a small typo budget ("camdon" → Camden, reported in the run status). An authority that was abolished is used only when no active authority matches, so "Buckinghamshire" resolves to the unitary council and not to the county council that closed in 2020.

Two things never happen: the search is never widened silently, and an ambiguous name never picks a winner. "North" ends the run with the 18 candidates listed; an unknown name ends it with the five closest names. The four authorities whose applications the register holds today are in Coverage; every other council resolves but has no application rows yet.

Designation datasets

datasets (used by mode: "constraints"):

idLayer
conservation-areaConservation area
article-4-direction-areaArticle 4 direction area (permitted development removed)
listed-building-outlineListed building outline
tree-preservation-zoneTree preservation zone
scheduled-monumentScheduled monument
world-heritage-siteWorld heritage site
site-of-special-scientific-interestSite of special scientific interest
flood-risk-zoneFlood risk zone
green-beltGreen belt
ancient-woodlandAncient woodland
brownfield-landBrownfield land register entry
national-parkNational park
area-of-outstanding-natural-beautyNational landscape (AONB)
local-authority-districtLocal authority district

The first six are the default. Pick ids from this list: the platform checks it before the run starts and refuses any other value (a near miss such as conservation-areas included) with the list of allowed ids. Not every council has published every layer — a layer with no row at your point simply returns nothing, which is an answer, not an error.

Decision wording

Councils word decisions differently and the platform passes the words through, so the actor does too: decision is the council's decision, decisionType its route or its own classification, and decisionContains searches both.

decisionContains matches a substring, so refus also returns Camden's Part Granted/Refused rows — outright refusals are the rows whose wording is exactly Refused (Camden) or Refuse (Worthing, Adur), and mixed outcomes always carry Part in front. To keep only one exact wording, start the value with =: =refused returns Camden's outright refusals without the Part Granted/Refused rows, =refuse does the same for Worthing and Adur. The comparison ignores case and extra spaces, and either decision field may match.

CouncildecisiondecisionTypestatus
Camden(same as decisionType — the platform has no separate decision field for Camden, so the actor copies it)Granted, Refused, Withdrawn Decision, Granted Subject to a Section 106 Legal Agreement, No Objection to Works to Tree(s) in CA, Approve Works (TPO), Part Granted/Refused, …Final Decision, Withdrawn, Appeal Decided, Appeal Lodged, Registered
Worthing, AdurApprove, Refuse, Withdrawn, Split, Prior not required - prior approval not requiredDelegated, CommitteeWithdrawn or empty
Doncaster(empty)(empty)(empty)

Use a fragment to cast wide, = to be exact: refus catches both Refused (Camden) and Refuse (Worthing/Adur), grant catches Granted and Part Granted/Refused, =granted only Granted.

Examples

Planning history around a site in Camden (800 m)

{ "mode": "applications", "council": "Camden", "latitude": 51.5413, "longitude": -0.1543,
"radiusMeters": 800, "maxScanRows": 3000, "maxItems": 30 }

Refusals in one council, newest decision first

{ "mode": "applications", "council": "Camden", "decisionContains": "refus",
"sortBy": "decisionDateDesc", "maxScanRows": 4000, "maxItems": 25 }

Approved works with a link to the council's file (Worthing)

{ "mode": "applications", "council": "Worthing", "decisionContains": "approve",
"maxScanRows": 2000, "maxItems": 30 }

Solar work as a sales pipeline (keyword over a whole council)

{ "mode": "applications", "council": "Worthing", "textContains": "solar",
"maxScanRows": 15000, "maxItems": 20 }

One council, one year, as a table

{ "mode": "applications", "council": "Doncaster", "decisionDateFrom": "2023-01-01",
"decisionDateTo": "2023-12-31", "sortBy": "decisionDateAsc", "maxScanRows": 2000, "maxItems": 25 }

What binds this plot? (exact point)

{ "mode": "constraints", "latitude": 51.5413, "longitude": -0.1543, "radiusMeters": 0,
"datasets": ["conservation-area", "article-4-direction-area", "listed-building-outline",
"tree-preservation-zone", "flood-risk-zone", "green-belt"], "maxItems": 20 }

Listed buildings and conservation areas within 300 m of an address

{ "mode": "constraints", "latitude": 51.5197, "longitude": -0.127, "radiusMeters": 300,
"datasets": ["listed-building-outline", "conservation-area"], "maxItems": 25 }

One application by its reference, trimmed to four fields

{ "mode": "applications", "reference": "2011/0724/C",
"fields": ["reference", "address", "decision", "decisionDate", "url"], "maxItems": 5 }

Output

One row per application (or per designation). A real row from the cloud run of the first example above (run nzp1RvTtCtCIe8APz):

{
"entityId": 10000021615,
"dataset": "planning-application",
"reference": "2015/4843/L",
"name": null,
"council": "London Borough of Camden",
"councilEntity": 90,
"description": "Erection of a lower ground floor extension and internal alterations namely the formation of a small opening to the side wall to connect to the lower ground floor extension, the removal of non-original partitions, repairs to structural walls and the renewal of internal fixtures and fittings.",
"address": "11 Prince Albert Road London NW1 7SR",
"postcode": "NW1 7SR",
"latitude": 51.537596,
"longitude": -0.150267,
"receivedDate": null,
"decisionDate": "2015-10-09",
"decision": "Granted",
"decisionType": "Granted",
"status": "Final Decision",
"developmentClassification": null,
"documentationUrl": null,
"entryDate": "2025-09-09",
"url": "https://www.planning.data.gov.uk/entity/10000021615",
"geojsonUrl": "https://www.planning.data.gov.uk/entity/10000021615.geojson",
"fetchedAt": "2026-09-19T22:14:18.683Z"
}
FieldTypeFilled when
entityIdnumberAlways (the platform's own record id); null only in the "not found" row of a reference lookup
datasetstringAlways — planning-application or the designation layer the row came from
referencestringApplications: the council's reference. Designations: the authority's reference (CA29, LB1694)
namestringDesignations (the designation's name). Applications carry no name
councilstringThe authority that published the row, resolved from councilEntity; national layers name their body (e.g. Environment Agency)
councilEntitynumberThe publishing organisation's entity id
descriptionstringApplications: the proposal text. Camden, Worthing, Adur and Doncaster all fill it
addressstringApplications, where the council publishes a site address (not Doncaster)
postcodestringParsed out of address when it ends in a UK postcode
latitude, longitudenumberApplications: the published point (not Doncaster). Constraints: the coordinate you asked about, echoed into every row
receivedDatedateApplications: the date the council received it (Worthing and Adur). Designations: the date the designation started
decisionDatedateApplications, where the council published a decision date
decisionstringThe council's decision wording; falls back to decisionType for councils that record it there
decisionTypestringThe council's own route or classification (Delegated, Committee, Granted)
statusstringCamden's application status (Final Decision, Appeal Lodged, …); other councils leave it empty
developmentClassificationstringAdur and Worthing: the classification URI the council publishes
documentationUrlstringLink to the application file on the council's portal — Adur and Worthing only
entryDatedateWhen the platform loaded the record (the batch date, not the application date)
urlstringThe record page on the platform
geojsonUrlstringThe record's GeoJSON — a point for applications, the boundary polygon for designations
fetchedAtdate-timeUTC timestamp of the run
foundbooleanOnly on a single found: false row: a reference the register does not hold, or the register holding no record at all

entityId and dataset are in every row, also when fields narrows the output: they identify the record and the layer it came from. The "not found" row of a reference lookup also keeps found: false. The dataset carries an item schema with a title, a description and an example for every field above.

Boundary polygons are never written into rows (one conservation area is 50 KB of WKT) — geojsonUrl serves them.

Dataset views: Planning applications (overview), Applications with coordinates (map), Designations covering the site (constraints).

The SUMMARY record in the run's key-value store (linked from the run's Output tab) holds the resolved council, the first request URL, how many rows matched server-side, how many rows were read, whether maxScanRows stopped the scan, how many rows a filter or the onlyNew state dropped, the request count, why the run stopped early if it did (stoppedBy: your spending limit, the run timeout or an abort; scanError: the platform failing mid-scan), every input warning and the licence note.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~uk-planning-applications/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"council":"Camden","latitude":51.5413,"longitude":-0.1543,"radiusMeters":800,"maxItems":30}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/uk-planning-applications').call({
mode: 'constraints', latitude: 51.5413, longitude: -0.1543, radiusMeters: 0,
datasets: ['conservation-area', 'article-4-direction-area', 'flood-risk-zone'], maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/uk-planning-applications").call(run_input={
"council": "Worthing", "textContains": "heat pump", "maxScanRows": 15000, "maxItems": 20,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items

The actor is a thin wrapper over one government API, which makes it easy to hand to an agent as a tool: one JSON input, one flat row shape for both questions, and fields to keep answers short enough for a prompt — {"council":"Camden","radiusMeters":500,"latitude":51.5413,"longitude":-0.1543,"fields":["reference","address","description","decision","decisionDate"]}.

MCP: add https://mcp.apify.com to Claude, Cursor or any MCP client and call the yadroo/uk-planning-applications tool with the same JSON input.

Pricing

Pay per event: $0.001 per run start + $0.002 per row written to the dataset. Nothing else is charged — no proxy, no browser, 256 MB of memory.

RunCost
Site check: 3 designations at one coordinate$0.007
The default 25-row search around a Camden coordinate$0.051
Keyword pipeline: 20 solar applications out of a whole council$0.041
A 500-row extract of one council$1.001

Apify tiers reduce the per-row price automatically: Bronze −10 %, Silver −20 %, Gold and above −30 %. The start event is the same on every plan, and platform usage is included in these prices.

The run respects your Maximum cost per run: it writes exactly as many rows as that amount pays for, then ends with "Stopped at your spending limit: N rows delivered" — no row is written or charged past it.

Limits & FAQ

  • The register holds no application right now (checked 2026-10-09). The platform withdrew the contents of its planning-application dataset, so mode: "applications" answers with one found: false row (charged as one row) and a status message that says the dataset's own total is 0, whatever you filter on. Before that it held four authorities, 100,627 rows, 77 % of them Camden; see Coverage. Use mode: "constraints" for the national designation layers meanwhile. If you need a council's own applications, this actor cannot give them to you — it reads the national platform, not council portals.
  • A snapshot, not a live feed. The register is republished in batches; the rows loaded on 2025-09-09 (Camden) and 2025-09-07 (Worthing, Adur) carry those dates in entryDate. entryDateSince and onlyNew work on that load date, so "new" means "new in the platform's batch", not "submitted this week".
  • What onlyNew remembers. Up to 200,000 delivered entity ids per input — about twice the whole register today (100,627 rows, Camden alone 77,499); past that the ids not seen for the longest time are forgotten and would be delivered again. Only rows really written are remembered: rows cut by maxItems, by your spending limit or by the run timeout come in the next run. The "not found" row of a reference lookup is written once; later runs with the same input say so in the status instead of billing it again. Every run still reads the register from its start: rows delivered earlier are skipped but count against maxScanRows, so a complete Camden copy needs maxScanRows above 77,499 (about 155 requests a run). Once the copy is complete, entryDateSince set to the last load date keeps later runs short.
  • No text search in the API. The platform has no keyword search over proposals, so textContains, decisionContains and the decision-date window are applied to the rows the run reads. maxScanRows is that budget: 15,000 covers Worthing or Adur completely, Camden's 77,499 rows need a higher cap or a coordinate filter. When the cap stops a scan the run status and SUMMARY say so — a partial answer is never presented as a complete one.
  • No sort in the API either. Rows arrive in the order the platform ingested them, so decisionDateDesc orders what the run read, not the whole register. Over a full council scan the two are the same thing; over a capped scan they are not.
  • Doncaster rows have no address and no point. They carry reference, proposal and decision date only, so a coordinate filter can never return them, and their rows leave address, postcode, latitude, longitude empty.
  • start_date filtering is broken upstream. The platform's "received since" filter returns nothing on this dataset even for rows that carry a received date, so the actor does not offer it; filter on decisionDate instead, or read the council and filter receivedDate yourself.
  • Dates are passed through. A handful of council records carry obviously wrong decision dates (one Worthing row says 2046). The actor does not invent or correct them, it only converts to YYYY-MM-DD in UTC.
  • Radius is exact. The circle is sent to the API as a polygon drawn around your coordinate and every returned row is then re-measured, so a 300 m search returns nothing 301 m away. Distances use WGS84 great-circle metres.
  • Designation layers are not complete either. They are national where the data exists, but a council that never published, say, its tree preservation zones has none in the platform. No row at your point means "nothing published here", not "certainly unconstrained" — a formal search still belongs to the council.
  • No personal data. The register publishes proposals and site addresses, and those are what the rows carry. Applicant and agent names are not in the platform's records and are never collected.
  • Polite by design. The platform's API documentation asks callers for polite rate limiting between requests and points large jobs to its bulk dataset downloads. The actor makes one request at a time with a 0.3 s pause between them, retries with backoff when the platform is busy, and a normal run makes 2–30 requests (a full Camden scan is about 155). For a one-off copy of the whole register, the platform's own bulk download of the planning-application dataset is the better tool. The actor calls www.planning.data.gov.uk and nothing else — the council portal links in documentationUrl are passed through as text and never fetched.
  • When the platform fails mid-scan. After its retries, a failing request ends the scan: rows already found are written, the run ends successfully with a status that starts "Stopped early: the platform failed after N register rows read", and SUMMARY.scanError holds the reason. With nothing written yet, the run fails with that message.
  • Long scans and the run timeout. A scan near the run timeout stops starting new requests, writes what it has found (sorted, if you asked for a sort) and ends successfully with "Stopped before the run timeout: N rows saved". Raise the timeout (default 900 s) or lower maxScanRows for a deep keyword search over Camden.
  • Licence and attribution. Planning Data is published under the Open Government Licence v3.0, © Crown copyright and database right 2026. Keep the attribution when you republish the rows. This actor is not affiliated with the Ministry of Housing, Communities & Local Government.

Made by Yadroo. Sibling actors: rightmove-uk, osm-geocode, public-holidays, world-bank-indicators, rss-to-json.