UK Planning Applications & Site Constraints (Planning Data)
Pricing
from $1.40 / 1,000 planning record returneds
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
1
Monthly active users
11 days ago
Last modified
Categories
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 conversionorbasement, 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"withradiusMeters: 0answers "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: truein 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:
| Council | Short code | Organisation entity | Rows | Site address | Coordinates | Received date | Decision wording | Council file link |
|---|---|---|---|---|---|---|---|---|
| London Borough of Camden | CMD | 90 | 77,499 | yes | yes | no | decisionType (Granted, Refused, …), copied into decision | no |
| Worthing Borough Council | WOT | 382 | 13,629 | yes | yes | yes | decision (Approve) + decisionType (Delegated) | yes |
| Adur District Council | ADU | 26 | 7,585 | yes | yes | yes | decision + decisionType | yes |
| Doncaster Metropolitan Borough Council | DNC | 109 | 1,914 | no | no | no | none, only decisionDate | no |
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.
| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
mode | string | applications | applications = the register, constraints = designations covering a point — see Modes |
council | string | (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 |
latitude | number | (prefill 51.5413) | WGS84, 49.5 … 61.0. Required for constraints |
longitude | number | (prefill -0.1543) | WGS84, −9.0 … 2.5, negative west of Greenwich. Works only together with latitude |
radiusMeters | integer | 1000 | 0 … 20000. 0 in constraints = the exact point; 0 in applications is read as 1000 |
datasets | string[] | (prefill: the six everyday layers) | Which designation layers constraints asks about — see Designation datasets. Empty = the six defaults |
textContains | string | — | Case-insensitive text in the proposal, the address or the reference (solar, loft conversion, BN43). Applied by this actor over maxScanRows |
decisionContains | string | — | Case-insensitive text in either decision field (granted, refus, approve, withdrawn, delegated); a leading = asks for the whole wording (=refused) — see Decision wording |
decisionDateFrom | string | — | YYYY-MM-DD, inclusive. Applied by this actor over maxScanRows |
decisionDateTo | string | — | YYYY-MM-DD, inclusive |
reference | string | — | Exact council reference (2011/0724/C, AWDM/1227/21). Applied by the API, so it is the cheapest lookup |
entryDateSince | string | — | YYYY-MM-DD. The date the platform loaded the record, not the date it was submitted or decided |
onlyNew | boolean | false | Output 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 |
maxScanRows | integer | 5000 | 100 … 100000. How many register rows a run may read before the actor's own filters. Never lower than maxItems |
sortBy | string | sourceOrder | sourceOrder, decisionDateDesc, decisionDateAsc — sorted among the rows the run read, see Limits |
maxItems | integer | 25 | 1 … 5000 output rows |
fields | string[] | all | Keep 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
mode | What one row is | Needs | Typical input |
|---|---|---|---|
applications | One planning application from the register | nothing (council / coordinate / reference recommended) | {"council":"Camden","radiusMeters":800,"latitude":51.5413,"longitude":-0.1543} |
constraints | One designation that covers the point or the circle | latitude + 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"):
| id | Layer |
|---|---|
conservation-area | Conservation area |
article-4-direction-area | Article 4 direction area (permitted development removed) |
listed-building-outline | Listed building outline |
tree-preservation-zone | Tree preservation zone |
scheduled-monument | Scheduled monument |
world-heritage-site | World heritage site |
site-of-special-scientific-interest | Site of special scientific interest |
flood-risk-zone | Flood risk zone |
green-belt | Green belt |
ancient-woodland | Ancient woodland |
brownfield-land | Brownfield land register entry |
national-park | National park |
area-of-outstanding-natural-beauty | National landscape (AONB) |
local-authority-district | Local 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.
| Council | decision | decisionType | status |
|---|---|---|---|
| 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, Adur | Approve, Refuse, Withdrawn, Split, Prior not required - prior approval not required | Delegated, Committee | Withdrawn 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"}
| Field | Type | Filled when |
|---|---|---|
entityId | number | Always (the platform's own record id); null only in the "not found" row of a reference lookup |
dataset | string | Always — planning-application or the designation layer the row came from |
reference | string | Applications: the council's reference. Designations: the authority's reference (CA29, LB1694) |
name | string | Designations (the designation's name). Applications carry no name |
council | string | The authority that published the row, resolved from councilEntity; national layers name their body (e.g. Environment Agency) |
councilEntity | number | The publishing organisation's entity id |
description | string | Applications: the proposal text. Camden, Worthing, Adur and Doncaster all fill it |
address | string | Applications, where the council publishes a site address (not Doncaster) |
postcode | string | Parsed out of address when it ends in a UK postcode |
latitude, longitude | number | Applications: the published point (not Doncaster). Constraints: the coordinate you asked about, echoed into every row |
receivedDate | date | Applications: the date the council received it (Worthing and Adur). Designations: the date the designation started |
decisionDate | date | Applications, where the council published a decision date |
decision | string | The council's decision wording; falls back to decisionType for councils that record it there |
decisionType | string | The council's own route or classification (Delegated, Committee, Granted) |
status | string | Camden's application status (Final Decision, Appeal Lodged, …); other councils leave it empty |
developmentClassification | string | Adur and Worthing: the classification URI the council publishes |
documentationUrl | string | Link to the application file on the council's portal — Adur and Worthing only |
entryDate | date | When the platform loaded the record (the batch date, not the application date) |
url | string | The record page on the platform |
geojsonUrl | string | The record's GeoJSON — a point for applications, the boundary polygon for designations |
fetchedAt | date-time | UTC timestamp of the run |
found | boolean | Only 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 ApifyClientclient = 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.
| Run | Cost |
|---|---|
| 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 onefound: falserow (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. Usemode: "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.entryDateSinceandonlyNewwork on that load date, so "new" means "new in the platform's batch", not "submitted this week". - What
onlyNewremembers. 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 bymaxItems, by your spending limit or by the run timeout come in the next run. The "not found" row of areferencelookup 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 againstmaxScanRows, so a complete Camden copy needsmaxScanRowsabove 77,499 (about 155 requests a run). Once the copy is complete,entryDateSinceset 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,decisionContainsand the decision-date window are applied to the rows the run reads.maxScanRowsis 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 andSUMMARYsay 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
decisionDateDescorders 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,longitudeempty. start_datefiltering 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 ondecisionDateinstead, or read the council and filterreceivedDateyourself.- 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-DDin 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-applicationdataset is the better tool. The actor callswww.planning.data.gov.ukand nothing else — the council portal links indocumentationUrlare 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.scanErrorholds 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
maxScanRowsfor 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.