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
| HTTP | Code | Meaning |
|---|---|---|
| 401 | UNAUTHORIZED | Missing or invalid API key |
| 403 | FORBIDDEN | Account suspended |
| 404 | NOT_FOUND | Resource does not exist |
| 429 | RATE_LIMIT_EXCEEDED | Monthly request limit reached |
All errors return JSON: {"error": "message", "code": "ERROR_CODE"}
Rate limits
| Tier | Limit | Price |
|---|---|---|
| Developer | 500 req/month | Free |
| Starter | 10,000 req/month | £99/month |
| Growth | 100,000 req/month | £499/month |
| Enterprise | Unlimited | Custom |
GET /v1/applications
Search planning applications. At least one filter is required.
Query parameters
| Parameter | Type | Description |
|---|---|---|
postcode | string | Full postcode (NW3 6UP) or district prefix (NW3) |
council_id | integer | Council ID from /v1/councils |
agent_id | integer | Planning agent ID |
status | string | Filter by status (e.g. Decided) |
decision | string | Normalised 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_type | string | Partial match (e.g. Householder) |
from | date (YYYY-MM-DD) | Received on or after |
to | date (YYYY-MM-DD) | Received on or before |
limit | integer | Results per page (default 50, max 200) |
offset | integer | Pagination 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.
| Parameter | Description |
|---|---|
date | Date to retrieve (YYYY-MM-DD, default: today) |
type | new_application | status_change | decision_issued | new_document |
council_id | Filter to one council |
limit | Default 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.
| Plan | Webhooks |
|---|---|
| Developer | — |
| Starter | 1, filtered by council_ids |
| Growth / Enterprise | Unlimited, 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.
| Parameter | Description |
|---|---|
postcode | Prefix match at any precision (SE, SE1, SE1 9SG) |
council_id | Filter to one council |
status | open · notice_issued · complied · appealed · decided · withdrawn · closed · no_information |
notice_type, from_date, to_date | Optional filters |
limit, offset | Default 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.
| source | Rows | Authority | What it is |
|---|---|---|---|
pins | 182,937 | authoritative | 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. |
council | 49,421 | indicative | 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. |
application | 19,260 usable | indicative | 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. |
| Parameter | Description |
|---|---|
outcome | allowed · allowed_with_conditions · split_decision · dismissed · withdrawn · invalid · pending · decided_outcome_unknown · unknown (text we could not classify) · no_information (we hold no data) |
source | Restrict to one dataset |
postcode, council_id, from_date, to_date | Optional filters |
include_no_information | true 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).
| Parameter | Description |
|---|---|
postcode | Full 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.
| Parameter | Description |
|---|---|
period | Past 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.
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 →