Planning Data API

REST API for UK planning application data, enforcement notices and appeals. 395 councils, updated daily. Base URL: https://ukplanningapi.co.uk

Authentication

Include your API key in every request using the X-API-Key header:

curl -H "X-API-Key: pk_your_key_here" \
  "https://ukplanningapi.co.uk/v1/applications?postcode=NW3+6UP"

Error codes

HTTPCodeMeaning
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENAccount suspended
404NOT_FOUNDResource does not exist
429RATE_LIMIT_EXCEEDEDMonthly request limit reached

All errors return JSON: {"error": "message", "code": "ERROR_CODE"}

Rate limits

TierLimitPrice
Developer500 req/monthFree
Starter10,000 req/month£99/month
Growth100,000 req/month£499/month
EnterpriseUnlimitedCustom

GET /v1/applications

Search planning applications. At least one filter is required.

Query parameters

ParameterTypeDescription
postcodestringFull postcode (NW3 6UP) or district prefix (NW3)
council_idintegerCouncil ID from /v1/councils
agent_idintegerPlanning agent ID
statusstringFilter by status (e.g. Decided)
decisionstringNormalised decision: approved · refused · withdrawn · no_objection · not_required · split_decision · conditions_discharged · unknown. Approval rates elsewhere count only approved vs refused — a withdrawn application is neither.
application_typestringPartial match (e.g. Householder)
fromdate (YYYY-MM-DD)Received on or after
todate (YYYY-MM-DD)Received on or before
limitintegerResults per page (default 50, max 200)
offsetintegerPagination offset (default 0)

Example

curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/applications?postcode=NW3&decision=approved&limit=20"
{
  "data": [
    {
      "reference": "2026/0123/P",
      "address": "27 Church Row, London",
      "postcode": "NW3 6UP",
      "description": "Single storey rear extension",
      "application_type": "Householder Application",
      "status": "Decided",
      "decision": "Approved",
      "received_date": "2026-01-15",
      "decision_date": "2026-02-28",
      "officer": { "id": 217, "name": "Jane Smith" },
      "council": { "id": 123, "name": "Camden" },
      "detail_url": "https://..."
    }
  ],
  "count": 20,
  "total": 847,
  "limit": 20,
  "offset": 0
}

GET /v1/applications/:id

Returns full detail — including its timeline and document list — for a single application by its id, not its bare reference (references are only unique within one council). id is {council_id}-{reference}, exactly as returned in the id field from /v1/applications.

curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/applications/123-2026%2F0123%2FP"

GET /v1/officers/:id

Officer profile including approval/refusal rates and application type patterns.

curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/officers/217"
{
  "id": 217,
  "name": "Jane Smith",
  "council": { "id": 123, "name": "Camden" },
  "stats": {
    "total_decided": 412,
    "total_approved": 348,
    "total_refused": 64,
    "refusal_rate": 15.5
  }
}

GET /v1/agents/:id

Planning agent profile — application count, approval rate, councils covered.

curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/agents/456"

GET /v1/councils

List all councils in the database with coverage metadata.

curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/councils"
{
  "data": [
    {
      "id": 123,
      "name": "Camden",
      "region": "London",
      "country": "England",
      "last_scraped_at": "2026-06-15T03:00:00Z",
      "active": true
    }
  ],
  "count": 395
}

GET /v1/councils/:id/stats

Aggregated performance metrics for a council — volumes, decision rates, average decision time.

curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/councils/123/stats"

GET /v1/changes

Daily change feed — new applications, status changes, decisions issued, new documents.

ParameterDescription
dateDate to retrieve (YYYY-MM-DD, default: today)
typenew_application | status_change | decision_issued | new_document
council_idFilter to one council
limitDefault 100, max 500
curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/changes?date=2026-06-14&type=decision_issued"

Webhooks

Get daily changes pushed to your own endpoint instead of polling /v1/changes. Payloads are signed with HMAC-SHA256 (X-Planning-Signature: sha256=<hex>). One retry after 60s on failure; auto-disabled after 3 consecutive failures. Deliveries don't count against your request quota.

PlanWebhooks
Developer
Starter1, filtered by council_ids
Growth / EnterpriseUnlimited, full national feed

POST /v1/webhooks

curl -X POST -H "X-API-Key: pk_..." -H "Content-Type: application/json" \
  -d '{"endpoint_url": "https://you.example.com/receive", "council_ids": [42]}' \
  "https://ukplanningapi.co.uk/v1/webhooks"

Returns signing_secret once, at creation — store it, it isn't shown again. On the Starter plan council_ids is required (COUNCIL_FILTER_REQUIRED otherwise); Growth may omit it to receive every council.

GET /v1/webhooks

List your webhooks with delivery status (last_success_at, consecutive_failures).

DELETE /v1/webhooks/:id

Remove a webhook.

POST /v1/webhooks/:id/test

Send a single test ping ({"event": "test", "date": "..."}) — no retry, doesn't touch delivery history.

GET /v1/enforcement

Planning enforcement notices — alleged breaches of planning control.

ParameterDescription
postcodePrefix match at any precision (SE, SE1, SE1 9SG)
council_idFilter to one council
statusopen · notice_issued · complied · appealed · decided · withdrawn · closed · no_information
notice_type, from_date, to_dateOptional filters
limit, offsetDefault 20, max 200
curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/enforcement?council_id=74&status=closed"

Coverage matters here. Enforcement data is published by 153 of 489 councils. Every response carries a coverage block; when has_enforcement_data is false, an empty result means we hold no data for that council — not that no breaches exist. Do not treat the absence of a notice as evidence of compliance without checking that flag.

GET /v1/enforcement/:id

One notice with its event timeline and any linked appeals. Links exist for 1,685 of 19,019 notices, because most councils publish no structured relationship — link_status: "no_link_established" means we could not connect one, never that the notice went unappealed. Cross-check /v1/appeals on the same postcode before concluding anything.

GET /v1/appeals

"Appeal" is not one object — we hold three different appeal signals, from two different worlds, and they are never merged: the same real-world appeal can appear once per source and cannot be deduplicated. Every row carries source, authority and link_confidence (exact_reference · address_match · unlinked). Count and rate per source, never across the union.

sourceRowsAuthorityWhat it is
pins182,937authoritative The Planning Inspectorate's national register — the only independent, nationally consistent outcome record, and the only valid basis for a rate. Outcome populated on 99.7%; 10.4% link to a specific application.
council49,421indicative Appeals as recorded on council portals (206 councils) — all kinds, not just enforcement: ~76% planning and other appeal types, ~6% enforcement, ~18% untyped. Filter appeal_type to separate them. pending is a real outcome here, not an error.
application19,260 usableindicative The appeal field on the application record. Of 282,566 populated values only 19,260 say anything (8,287 with a final outcome); the rest are placeholders and are excluded by default.
ParameterDescription
outcomeallowed · allowed_with_conditions · split_decision · dismissed · withdrawn · invalid · pending · decided_outcome_unknown · unknown (text we could not classify) · no_information (we hold no data)
sourceRestrict to one dataset
postcode, council_id, from_date, to_dateOptional filters
include_no_informationtrue to include placeholder rows (for coverage auditing)
curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/appeals?postcode=LS29&outcome=allowed"

outcome: "no_information" means we hold no appeal data for that record. It never means no appeal happened. Those are opposite facts and the source data can't always tell them apart.

Date filters exclude the application source entirely — it carries appeal outcomes but no appeal dates. The response says so in sources_excluded rather than quietly returning less.

Appeal search reads an index rebuilt nightly at 04:30; coverage.index_built_at tells you how fresh it is.

GET /v1/appeals/:case_ref

One appeal. Planning Inspectorate cases include inspector, appellant, agent and published documents; council-recorded appeals include the event timeline. Add ?source= (pins · council · application) if a reference exists in more than one dataset; otherwise the Planning Inspectorate record wins.

GET /v1/applications/:id/appeals

Every appeal held for one application. status separates three different findings: found, no_appeal_data (an appeal field exists but is a placeholder — we can't say), and no_appeal_found (no record in any source, which is weak evidence given only 10.4% of Inspectorate cases link to an application).

GET /v1/applications/:id/site-history

The property's full planning history: every other application at the same site, plus appeals and enforcement notices matched to it. Matching is deterministic and labelled, never fuzzy — each row's match_basis is property_key (the register links both records to one property; rare) or address_exact (identical normalised address in the same council). A renamed or renumbered site will not match, so an empty list is weak evidence that nothing else happened there.

curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/applications/123-2026%2F0123%2FP/site-history"

GET /v1/constraints

Conservation areas and Article 4 directions at a full postcode — the constraints that decide whether permitted development rights apply at all. We hold 12,119 conservation areas and 6,890 Article 4 directions from 365 publishing organisations (England only).

ParameterDescription
postcodeFull unit required (NW3 6UP). Districts are rejected — their centroid would be meaningless against parcel-scale polygons.
curl -H "X-API-Key: pk_..." \
  "https://ukplanningapi.co.uk/v1/constraints?postcode=NW3+6UP"

The test point is the postcode's centroid, not a property boundary: within lists areas containing the centroid, near those within 100 m of it. A specific property can sit outside an area listed under within — verify against the council's own map before relying on it. found: false means the postcode is unknown to us; it is never evidence of "no constraints".

GET /v1/usage

How much of your monthly allowance you've used, and how much is left. This call is free — it doesn't count against your quota — and it keeps working after you've hit your limit, so you can always see why requests are being rejected.

ParameterDescription
periodPast month as YYYY-MM. Omit for the current month.
curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/usage"
{
  "period_start": "2026-08-01", "period_end": "2026-08-31",
  "tier": "developer", "limit": 500,
  "used": 250, "remaining": 250, "percent_used": 50.0,
  "days_elapsed": 10, "days_remaining": 21,
  "burn_rate_per_day": 25.0,
  "projected_month_end": 775,
  "projected_to_exceed": true,
  "exhaustion_date": "2026-08-20",
  "status": "ok"
}

status is ok below 75%, warning at 75%, critical at 95%, exhausted at 100%, and unmetered on enterprise. On enterprise, limit, remaining and the projections are null rather than zero.

projected_month_end and exhaustion_date extrapolate from your current burn rate — they say where you'll land if today's pace continues, not where you are. REST and MCP share one allowance, so this figure covers both.

Idempotency

Send an Idempotency-Key header on any request you might retry. A retry carrying the same key is recorded once and billed once. Without it, a retry after a timeout counts as a second request — only you can tell us that two calls were the same intent.

curl -H "X-API-Key: pk_..." -H "Idempotency-Key: 7f3c1a92-..." \
  "https://ukplanningapi.co.uk/v1/applications?postcode=LS29"

Keys are scoped to your key and the calendar month, and may be up to 200 characters. Use a fresh value (a UUID) per distinct request.

Ready to start?
Get your free API key — 500 requests/month, no credit card. Also works as an MCP server for AI agents, same key, same quota.
Get free access →