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.

Council planning applications
curl "https://api.planwire.io/v1/applications?council=adur&page=1&limit=5" \
  -H "X-API-Key: YOUR_KEY"
National date-window search
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"
Postcode planning data
curl "https://api.planwire.io/v1/applications?postcode=SW1A&status=Approved&limit=25" \
  -H "X-API-Key: YOUR_KEY"
Radius search
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.

Request headers
X-API-Key: pw_live_a1b2c3d4e5f6...

Errors

All errors return JSON with an error field and an appropriate HTTP status code.

Status Meaning
400Bad request — missing or invalid parameters
401Unauthorised — missing or invalid API key
402Payment required — fresh data or broad coverage requires a paid plan
404Not found — resource does not exist
429Rate limit exceeded — upgrade your plan
500Server 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
Free25£0
Developer1,000£49/mo + VAT
Production10,000£199/mo + VAT
Business50,000£499/mo + VAT
EnterpriseCustomContact us

GET /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

Parameter
Type
Description
council
string
Council ID (e.g. adur, camden)
postcode
string
Exact full postcode or exact outward-code filter. Use postcode_prefix for broad prefix scans.
uprn
string
Filter to records attached to one Unique Property Reference Number.
uprn_in
string
Comma-separated list of up to 100 UPRNs for portfolio or internal asset matching.
status
string
Approved · Refused · Pending · Withdrawn
type
string
Application type substring match
decision
string
Decision outcome substring match (e.g. Approved, Refused)
q
string
Full-text search across address + description
date_from
date
Filter by application date from YYYY-MM-DD, using the best available received or validated date.
date_to
date
Filter by application date to YYYY-MM-DD, using the best available received or validated date.
page
integer
Page number, default 1
limit
integer
Results per page, default 25. Paid max 100; free max 10.
Example
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.

GET /v1/applications/nearby

Find applications within a radius using PostGIS spatial indexing. Extremely fast even over millions of records.

Parameter
Type
Description
lat *
float
Latitude (WGS84)
lng *
float
Longitude (WGS84)
radius_km
float
Search radius in km, default 1. Paid max 50; free max 0.5.
limit
integer
Max results. Paid max 100; free max 10.
Example — 1km around Westminster
curl "https://api.planwire.io/v1/applications/nearby?lat=51.4975&lng=-0.1278&radius_km=1" \
  -H "X-API-Key: YOUR_KEY"
GET /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
idstringInternal PlanWire UUID for fetching this stored record. For durable customer records, store council_id plus reference and use the reference lookup.
council_idstringPlanWire council slug, e.g. leeds.
council_namestringFull authority name.
referencestringThe authority's own application reference.
urlstring | nullLink to the record on the council portal.
addressstring | nullSite address as published.
postcodestring | nullSite postcode where available.
uprnstring | nullUnique Property Reference Number where source-native, officially matched, or matched from OS Open UPRN coordinates.
uprnMatchMethodstring | nullsource_native, official_address_match, os_open_coordinate, or manual.
uprnMatchConfidencenumber | null0 to 1 confidence score for the UPRN match.
uprnMatchedAttimestamp | nullWhen the UPRN was attached to the record.
propertyContextobjectOptional enrichment on single-application endpoints: EPC, HMLR price-paid summary and constraint matches where available.
latnumber | nullLatitude, WGS84.
lngnumber | nullLongitude, WGS84.
descriptionstring | nullProposal text as published by the authority.
application_typestring | nullAuthority's application type label.
statusstring | nullAuthority's status label.
decisionstring | nullDecision text as published.
decision_outcomestring | nullNormalised outcome: approved, refused, withdrawn, other, or null when the source text cannot be classified.
received_datedate | nullDate the authority received the application.
validated_datedate | nullDate the application was validated.
decision_datedate | nullDate the decision was issued.
application_datedate | nullBest available application date: received_date, falling back to validated_date.
application_date_sourcestring | nullWhich field application_date came from: receivedDate or validatedDate.
applicant_namestring | nullApplicant as published. Sparsely populated — present on a minority of records.
agent_namestring | nullAgent as published. Sparsely populated.
documentsarrayObjects of { title, url }. Empty for most records.
source_typestringPortal family the record was sourced from, e.g. idox.
source_last_scraped_attimestamp | nullWhen PlanWire last checked this council's source.
source_age_hoursnumber | nullHours since that check, rounded to one decimal.
created_attimestampWhen PlanWire first recorded this application.
updated_attimestampWhen PlanWire last changed this record.
POST /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

Field
Type
Description
postcodes *
string[]
Full UK postcodes. Partial codes such as SW1 are rejected rather than widened to a sector.

Batch size by plan

Plan Postcodes per request
Free10
Developer100
Production500
Business500
Flags for a batch of postcodes
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
  }
}
GET /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"

GET /v1/councils

List council source records and freshness metadata. Paid plans receive full coverage; free keys receive a limited preview.

Response
{
  "data": [{
    "id":               "adu",
    "name":             "Adur District Council",
    "portalType":       "official",
    "applicationCount": 874
  }]
}
GET /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"

GET /v1/webhooks

List all webhooks registered to your API key.

POST /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

Field
Type
Description
url *
string
HTTPS endpoint to POST events to
filters.councilId
string
Only fire for this council
filters.postcodePrefix
string
Only fire for postcodes starting with (e.g. SW1)
filters.status
string
Only fire for this status value
filters.applicationType
string
Only fire for this application type
Create webhook
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).

Node.js / Bun
import { createHmac } from 'crypto';

function verify(secret, rawBody, sigHeader) {
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(rawBody).digest('hex');
  return sigHeader === expected;
}
Python
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)
DELETE /v1/webhooks/:id

Remove a webhook by its UUID.

POST /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"
GET /v1/webhooks/:id/deliveries

View the last 50 delivery attempts for a webhook — useful for debugging failed deliveries.

Response
{
  "data": [{
    "id":          "a3f2...",
    "event":       "application.new",
    "statusCode":  200,
    "success":     true,
    "createdAt":   "2025-03-19T14:23:01Z"
  }]
}
GET /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.

Example
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 →