Planning Applications API Docs
Use PlanWire as a UK planning applications API for council planning records, postcode planning data, radius search, current applications, decisions, source links, freshness metadata, signed webhooks and MCP workflows.
Base URL: https://api.planwire.io · Version: v1
Common evaluator requests
These are the request shapes most teams test first when comparing UK planning data APIs.
curl "https://api.planwire.io/v1/applications?council=adur&page=1&limit=5" \ -H "X-API-Key: YOUR_KEY"
curl "https://api.planwire.io/v1/applications?date_from=2026-08-28&date_to=2026-08-28&limit=100" \ -H "X-API-Key: YOUR_KEY"
curl "https://api.planwire.io/v1/applications?postcode=SW1A&status=Approved&limit=25" \ -H "X-API-Key: YOUR_KEY"
curl "https://api.planwire.io/v1/applications/nearby?lat=51.4975&lng=-0.1278&radius_km=1" \ -H "X-API-Key: YOUR_KEY"
Authentication
Pass your API key in the X-API-Key header. You can also use Authorization: Bearer YOUR_KEY.
X-API-Key: pw_live_a1b2c3d4e5f6...
Errors
All errors return JSON with an error field and an appropriate HTTP status code.
| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid parameters |
| 401 | Unauthorised — missing or invalid API key |
| 402 | Payment required — fresh data or broad coverage requires a paid plan |
| 404 | Not found — resource does not exist |
| 429 | Rate limit exceeded — upgrade your plan |
| 500 | Server error — contact support |
Webhook deliveries do not count against the request allowance. API reads, lookups, exports, and backfill requests do.
Pagination
All list endpoints return a meta object. Use ?page=2&limit=50 to paginate. Paid plans can request up to 100 records per page. Free keys are capped at 10 records per page and older sample data.
"meta": { "total": 4821, // total matching records "page": 1, // current page (1-indexed) "limit": 25, // records per page "pages": 242 // total pages }
Freshness & dates
PlanWire exposes source-check freshness and application-date freshness separately. lastScrapedAt means the source was checked or refreshed. It should not be treated on its own as proof that new application records were ingested.
For data recency, use latestApplicationDate on councils and dates.applicationDate on applications. Application lists are sorted by applicationDate desc, firstSeenAt desc by default.
"freshness": { "sourceCheckStatus": "fresh", "dataStatus": "stale", "lastScrapedAt": "2026-05-01T09:20:00.000Z", "latestApplicationDate": "2026-03-12" }
Rate limits
Limits reset at midnight UTC. Exceeding the limit returns 429. Free access is a sandbox: 25 calls/day, older sample records, limited council preview, no webhooks, and no fresh planning data.
| Plan | Requests/day | Price |
|---|---|---|
| Free | 25 | £0 |
| Developer | 1,000 | £49/mo + VAT |
| Production | 10,000 | £199/mo + VAT |
| Business | 50,000 | £499/mo + VAT |
| Enterprise | Custom | Contact us |
/v1/applications
List planning applications. Parameters can be combined for council, postcode, date-window, status, decision and keyword workflows. Each application includes council, source, freshness, UPRN property identity where available, decision first-seen, and document metadata so you can verify where the record came from and join it to property datasets.
Query Parameters
adur, camden)postcode_prefix for broad prefix scans.Approved, Refused)curl "https://api.planwire.io/v1/applications?council=adur&q=extension&status=Approved&limit=5" \ -H "X-API-Key: YOUR_KEY"
Operational record fields
propertyIdentity
object
UPRN and match metadata where the authority supplies a source-native UPRN, PlanWire can attach one from an approved official match index, or a record has a conservative OS Open UPRN coordinate match. Use it to join planning records to EPC, property, price-paid and internal asset tables.
propertyContext
object
Optional single-record enrichment joined by UPRN or application geometry where available: EPC facts, safe HMLR price-paid summaries, and constraints such as conservation areas, listed buildings, flood zones, green belt, Article 4, TPOs and brownfield land.
decisionFirstSeenAt
datetime | null
Immutable timestamp for when PlanWire first observed a decision date or usable decision outcome on this record. Use it to separate publication availability from later row updates.
documents
array
Document links and titles where the authority source supplies document metadata. Empty when the source does not expose documents through the checked route.
freshness
object
Includes source-check status, source age, application-date status, first-seen, last-seen, decision date, and decision-first-seen fields for audit workflows.
/v1/applications/nearby
Find applications within a radius using PostGIS spatial indexing. Extremely fast even over millions of records.
curl "https://api.planwire.io/v1/applications/nearby?lat=51.4975&lng=-0.1278&radius_km=1" \ -H "X-API-Key: YOUR_KEY"
/v1/applications/:id
Fetch a single planning application by its UUID. Includes source metadata, document links where available, and current light planning context: EA flood zone plus Land Registry sold prices for the postcode.
# Take the id from a search response and fetch the full record curl "https://api.planwire.io/v1/applications/$APPLICATION_ID" \ -H "X-API-Key: YOUR_KEY"
Application ids are internal and can change if a record is re-ingested from its council. If you need a stable handle, use the council reference lookup below — a council's own reference does not change, so it is the right thing to store against your own records.
Light Planning Context Fields
floodRisk
object | null
EA Flood Map for Planning zone. zone is one of low, 2, 3a, 3b. Cached 30 days. Null on first call, populated within ~10s.
nearbyPrices
object | null
Land Registry price paid data for the same postcode over the last 2 years. Returns avgPrice (pence), count, postcode. Null if no sales in period or on first call.
documents
array
Source document titles and URLs where available. Constraint datasets such as listed-building, conservation-area, green-belt and Article 4 flags are not exposed until backed by loaded source datasets.
Application object
Every field returned on an application record. Field names are camelCase in JSON responses. Fields marked
| null are absent or null where the source authority does not publish them —
coverage varies by council, so treat any single field as optional.
| Field | Type | Notes |
|---|---|---|
| id | string | Internal PlanWire UUID for fetching this stored record. For durable customer records, store council_id plus reference and use the reference lookup. |
| council_id | string | PlanWire council slug, e.g. leeds. |
| council_name | string | Full authority name. |
| reference | string | The authority's own application reference. |
| url | string | null | Link to the record on the council portal. |
| address | string | null | Site address as published. |
| postcode | string | null | Site postcode where available. |
| uprn | string | null | Unique Property Reference Number where source-native, officially matched, or matched from OS Open UPRN coordinates. |
| uprnMatchMethod | string | null | source_native, official_address_match, os_open_coordinate, or manual. |
| uprnMatchConfidence | number | null | 0 to 1 confidence score for the UPRN match. |
| uprnMatchedAt | timestamp | null | When the UPRN was attached to the record. |
| propertyContext | object | Optional enrichment on single-application endpoints: EPC, HMLR price-paid summary and constraint matches where available. |
| lat | number | null | Latitude, WGS84. |
| lng | number | null | Longitude, WGS84. |
| description | string | null | Proposal text as published by the authority. |
| application_type | string | null | Authority's application type label. |
| status | string | null | Authority's status label. |
| decision | string | null | Decision text as published. |
| decision_outcome | string | null | Normalised outcome: approved, refused, withdrawn, other, or null when the source text cannot be classified. |
| received_date | date | null | Date the authority received the application. |
| validated_date | date | null | Date the application was validated. |
| decision_date | date | null | Date the decision was issued. |
| application_date | date | null | Best available application date: received_date, falling back to validated_date. |
| application_date_source | string | null | Which field application_date came from: receivedDate or validatedDate. |
| applicant_name | string | null | Applicant as published. Sparsely populated — present on a minority of records. |
| agent_name | string | null | Agent as published. Sparsely populated. |
| documents | array | Objects of { title, url }. Empty for most records. |
| source_type | string | Portal family the record was sourced from, e.g. idox. |
| source_last_scraped_at | timestamp | null | When PlanWire last checked this council's source. |
| source_age_hours | number | null | Hours since that check, rounded to one decimal. |
| created_at | timestamp | When PlanWire first recorded this application. |
| updated_at | timestamp | When PlanWire last changed this record. |
/v1/property/flags
Planning flags for a batch of postcodes, for filtering a property index before deciding what is worth opening. Returns whether each postcode has any planning history, whether any of it was refused, and when it was last decided — without returning the underlying records.
Metered per property, not per request. A batch of 400 postcodes costs 400 against your daily allowance. Duplicate postcodes are collapsed before charging, so you are not billed twice for the same answer.
Request Body
SW1 are rejected rather than widened to a sector.Batch size by plan
| Plan | Postcodes per request |
|---|---|
| Free | 10 |
| Developer | 100 |
| Production | 500 |
| Business | 500 |
curl -X POST "https://api.planwire.io/v1/property/flags" \ -H "X-API-Key: YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "postcodes": ["SW1A 1AA", "LS10 1BS", "M1 1AE"] }'
Response
Every requested postcode gets a row, including ones with no planning history — that is the answer that lets you skip a
property. hasRefusal reads the normalised
decision_outcome, not council decision text, so it means the same thing in every authority.
{ "data": [ { "postcode": "SW1A 1AA", "hasApplication": true, "hasRefusal": false, "lastDecisionDate": "2025-11-04", "lastApplicationDate": "2025-08-19", "applicationCount": 12 } ], "meta": { "requested": 3, "charged": 3, // charged per postcode, after duplicates collapse "duplicatesCollapsed": 0, "invalidCount": 0, "callsToday": 431 } }
/v1/applications/ref/:council/:reference
Fetch an application using the council's own reference number (e.g. 24/01234/FUL). Single-application reads also include optional property context, flood risk and nearby sold prices where available.
curl "https://api.planwire.io/v1/applications/ref/adur/AWDM%2F0158%2F25" \ -H "X-API-Key: YOUR_KEY"
/v1/councils
List council source records and freshness metadata. Paid plans receive full coverage; free keys receive a limited preview.
{ "data": [{ "id": "adu", "name": "Adur District Council", "portalType": "official", "applicationCount": 874 }] }
/v1/councils/:id
Get a single council's details including the date of the most recent application.
curl "https://api.planwire.io/v1/councils/adu" \ -H "X-API-Key: YOUR_KEY"
/v1/webhooks
List all webhooks registered to your API key.
/v1/webhooks
Subscribe to planning application events. You'll receive an HTTP POST to your URL whenever a matching application is new or updated. Developer keys can create one active webhook scoped to a council or postcode prefix. Production supports up to 100 webhooks, including national coverage; Business supports national webhook workflows subject to fair use.
Request Body
SW1)curl -X POST "https://api.planwire.io/v1/webhooks" \ -H "X-API-Key: YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-app.com/hooks/planning", "filters": { "postcodePrefix": "SW1", "status": "Approved" } }'
Webhook Payload
Requests are signed with HMAC-SHA256 — verify using the X-PlanWire-Signature header.
{ "event": "application.new", // or "application.updated" "timestamp": "2025-03-19T14:23:01Z", "application": { /* full application object */ } }
The application.updated event is emitted when PlanWire stores a changed record, including document links or titles when the authority source supplies a changed non-empty document list.
Verifying signatures
Compute HMAC-SHA256(secret, rawBody) and compare to the X-PlanWire-Signature header value (after stripping the sha256= prefix).
import { createHmac } from 'crypto'; function verify(secret, rawBody, sigHeader) { const expected = 'sha256=' + createHmac('sha256', secret) .update(rawBody).digest('hex'); return sigHeader === expected; }
import hmac, hashlib def verify(secret, raw_body, sig_header): expected = 'sha256=' + hmac.new( secret.encode(), raw_body, hashlib.sha256 ).hexdigest() return hmac.compare_digest(sig_header, expected)
/v1/webhooks/:id
Remove a webhook by its UUID.
/v1/webhooks/:id/test
Send a test event to verify your endpoint is reachable and your signature verification works.
curl -X POST "https://api.planwire.io/v1/webhooks/YOUR_WEBHOOK_ID/test" \ -H "X-API-Key: YOUR_KEY"
/v1/webhooks/:id/deliveries
View the last 50 delivery attempts for a webhook — useful for debugging failed deliveries.
{ "data": [{ "id": "a3f2...", "event": "application.new", "statusCode": 200, "success": true, "createdAt": "2025-03-19T14:23:01Z" }] }
/v1/webhooks/:id/missed
List failed delivery attempts since an ISO date so your system can re-fetch those records and repair local state. Deliveries are attempted three times within a few seconds, then logged rather than retried indefinitely.
curl "https://api.planwire.io/v1/webhooks/YOUR_WEBHOOK_ID/missed?since=2026-09-01T00:00:00Z" \ -H "X-API-Key: YOUR_KEY"
Ready to build?
Get an API key →