Planning Data API
REST API for UK planning application data, enforcement notices and appeals.
329 councils with complete history from
January 2023, updated daily — more join as their backfill finishes. The API
also serves in-progress councils; check production_ready on
/v1/councils. Base URL: https://ukplanningapi.co.uk
Authentication
Two ways to authenticate: a static API key — one header, works until you revoke it, simplest for server-to-server use — or OAuth 2.1, for a client that signs a user in itself and should never see or store a shared secret. Both resolve to the same account, spend the same monthly quota, and work identically on REST and the MCP server.
API key
Include your API key in every request using the X-API-Key header
(an Authorization: Bearer <key> header works identically, for
clients that only offer a Bearer field):
curl -H "X-API-Key: pk_your_key_here" \ "https://ukplanningapi.co.uk/v1/applications?postcode=NW3+6UP"
Get a free key at /api-signup — no OAuth setup needed for this path.
OAuth 2.1
A standard OAuth 2.1 authorization server — RFC6749, PKCE required (RFC7636), dynamic client registration (RFC7591) — sitting in front of the same accounts API keys use. It's what powers the no-code ChatGPT/Claude connector setup; the same flow works for any REST client. Unless you're building something that signs in a user who should never see your app's own credential, an API key is simpler — reach for OAuth when a shared static secret isn't acceptable.
| Endpoint | Purpose |
|---|---|
GET /.well-known/oauth-authorization-server | RFC8414 metadata — endpoints, supported grants, scopes |
POST /register | Dynamic client registration (RFC7591) |
GET /authorize | User sign-in and consent (RFC6749 §4.1.1) |
POST /token | Exchange a code, or refresh, for an access token |
POST /revoke | Revoke a token (RFC7009) |
- Register a client. One-time, no account needed, and every
field below is something you supply — nothing here is looked up or
assigned by us first:
redirect_uris— your own app's callback URL(s), an endpoint you build and host./authorizeand/tokenwill only accept a value from this exact list.client_name— freeform text you choose. It's shown to your user on the consent screen ("Allow Your App to access their account?") and later in their dashboard's key list — pick whatever they should recognise.token_endpoint_auth_method—"none"if your client can't keep a secret (a browser, mobile or CLI app; PKCE covers you either way), or omit it for a confidential server-side client, which gets aclient_secretback.
curl -X POST https://ukplanningapi.co.uk/register \ -H "Content-Type: application/json" \ -d '{ "redirect_uris": ["https://yourapp.example.com/callback"], "client_name": "Your App", "token_endpoint_auth_method": "none" }'The response is the only placeclient_id(and, for a confidential client,client_secret) comes from — store it, there's no dashboard lookup for it afterwards. - Send the user to
/authorize. Generate a PKCEcode_verifierand its S256code_challenge, then open this in a browser — not a background request, the user signs in here with the same one-time email link as /api-login:https://ukplanningapi.co.uk/authorize ?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https://yourapp.example.com/callback &code_challenge=YOUR_CODE_CHALLENGE &code_challenge_method=S256 &state=YOUR_CSRF_STATE
Clicking the email link redirects back to yourredirect_uriwith?code=...&state=.... - Exchange the code for a token.
curl -X POST https://ukplanningapi.co.uk/token \ -d grant_type=authorization_code \ -d code=THE_CODE_FROM_THE_REDIRECT \ -d code_verifier=YOUR_ORIGINAL_CODE_VERIFIER \ -d redirect_uri=https://yourapp.example.com/callback \ -d client_id=YOUR_CLIENT_ID
{ "access_token": "oat_...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ort_...", "scope": "mcp" }Useaccess_tokenexactly like an API key, as anAuthorization: Bearerheader:curl -H "Authorization: Bearer oat_..." "https://ukplanningapi.co.uk/v1/usage"
It authenticates the same account and spends the same monthly quota, and shows up in the dashboard's key list labelled with your app's client name — revoke it there, or viaPOST /revoke, same as any key. - Refresh before it expires. Access tokens last 1 hour;
refresh tokens last 180 days and rotate on every use — the old one stops
working the moment you get a new one, so store the latest, not the first.
curl -X POST https://ukplanningapi.co.uk/token \ -d grant_type=refresh_token \ -d refresh_token=YOUR_REFRESH_TOKEN \ -d client_id=YOUR_CLIENT_ID
Single scope (mcp) for now — it covers the whole
API, REST and MCP alike; there's no per-endpoint scoping yet.
Error codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | MISSING_FILTER | A search with no filter on it. The body lists every filter the endpoint accepts and gives a worked example. |
| 400 | UNKNOWN_PARAMETER | A query parameter this endpoint doesn't understand. It is refused, never ignored — silently dropping it would return a wider result set than you asked for and call it success. The body names the parameter, suggests the closest valid one, and lists the rest. |
| 400 | CONFLICTING_PARAMETER | The same filter supplied twice with different values — including via two names that mean the same thing (q and text, say). |
| 400 | MALFORMED_ID | An application id that isn't {council_id}-{reference} — a bare reference, an internal database number, or an id from another system. Previously this returned 404, indistinguishable from a correctly-formed id whose record doesn't exist. |
| 401 | UNAUTHORIZED | Missing or invalid credential (API key or OAuth token) |
| 403 | FORBIDDEN | Account suspended |
| 404 | NOT_FOUND | Resource does not exist. For an application id, the message says which half is missing — an unknown council number, or a real council that has no such reference (in which case it quotes one of that council's actual references, so you can see the shape they use). |
| 429 | RATE_LIMIT_EXCEEDED | Monthly request limit reached — the response body names your tier, the next tier's price, and the dashboard URL in an upgrade block; GET /v1/usage keeps working and is not metered |
| 503 | TEXT_SEARCH_UNAVAILABLE | Only from the text filter on /v1/applications, and only while its search index is rebuilding. Refused rather than served slowly, and rather than quietly ignoring the filter and returning a wider result set than you asked for. Every other filter is unaffected — retry without text. |
All errors return JSON. Alongside error and code, every
4xx carries how_to_fix (one plain sentence saying what to do now)
and example (a complete request that works, with real values —
not a placeholder). Both are safe to show to an end user or hand to an
LLM, which is the point: the reader is usually neither the person who
wrote the integration nor an API engineer.
{
"error": "This request uses 'postcod', which is not something this endpoint understands, so it was not applied to your search. Nothing was filtered by it.",
"code": "UNKNOWN_PARAMETER",
"how_to_fix": "Rename 'postcod' to 'postcode' and send the request again.",
"example": "https://ukplanningapi.co.uk/v1/applications?postcode=BS7 9TB",
"unknown": ["postcod"],
"valid": ["agent_id", "application_type", "council_id", "decision", "from", "postcode", "status", "text", "to"]
}
Finding an application
Almost every task starts the same way: search first, then fetch by
the id you got back. References are only unique within
one council, so there is no way to look one up on its own — and building an
id by hand from a reference you found elsewhere is the single most common
way to get a 404 here.
- Search for the site, usually by postcode:
GET /v1/applications?postcode=BS7%209TB - Take the
idstraight from a result — it is the first field of every one. It already looks like123-2026/3373/P— the council's number, a hyphen, then that council's own reference. The council's name is never part of it. - Fetch the detail with it, unchanged:
GET /v1/applications/123-2026/3373/P
References contain slashes 96% of the time, and that is fine: pass the id
as-is, or URL-encode the slashes as %2F — both work.
GET /v1/applications/123-2026%2F3373%2FP is the same
request. What does not work is a bare reference with no council
number, or an internal database id; both now return
MALFORMED_ID explaining the difference.
The MCP tools answer the same way. get_application,
get_site_history and get_application_appeals all
return the same diagnosis as a tool error, quoting the equivalent tool call
— search_applications(postcode="BS7 9TB") — rather than a URL,
since an AI client cannot follow one.
Don't know the council's number? GET /v1/councils?search=Camden.
Rate limits
| Tier | Limit | Price |
|---|---|---|
| Developer | 500 req/month | Free |
| Starter | 2,500 req/month | £19/month |
| Growth | 15,000 req/month | £99/month |
| Enterprise | Unlimited | Custom |
GET /v1/applications
Search planning applications. At least one filter is required.
Query parameters
| Parameter | Type | Description |
|---|---|---|
postcode | string | Prefix match at any precision: area (NW), outcode (NW3), sector (NW3 6), partial incode (NW3 6U) or full unit (NW3 6UP). Case and spacing are ignored. |
council_id | integer | Council ID from /v1/councils |
agent_id | integer | Planning agent ID |
status | string | Normalised status: received · under_consideration · decided · withdrawn · no_information · unknown. (Until 2026-08 this matched each council's raw wording, so the same query returned different sets per council — it now uses the cross-council vocabulary, like decision.) |
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) |
text | string | Free-text search of the application description — what the works actually are (loft conversion, solar panels). Supports "quoted phrases", OR, and -excluded terms. It does not search addresses, postcodes or council names — combine it with postcode/council_id for those. Counts as a filter in its own right. Also accepted as q, search or query, because those are what people reach for first. |
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": [
{
"id": "123-2026/0123/P",
"reference": "2026/0123/P",
"address": "27 Church Row, London",
"postcode": "NW3 6UP",
"description": "Single storey rear extension",
"application_type": "Householder Application",
"status": "Decided",
"status_normalized": "decided",
"decision": "Approved",
"decision_normalized": "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,
"total_is_capped": false,
"total_cap": null,
"limit": 20,
"limit_requested": 20,
"limit_applied": 20,
"truncated": false,
"offset": 0,
"has_more": true,
"next_offset": 20
}
The id is the key to everything else. Pass it
unchanged to GET /v1/applications/:id
(and to /appeals and /site-history beneath it):
GET /v1/applications/123-2026/0123/P
It is {council_id}-{reference}: the council's number
(council.id, here 123), a hyphen, then that council's
own reference. Don't assemble one from the council name
(/v1/applications/Camden/2026/0123/P is a 404) or from the bare
reference (rejected as MALFORMED_ID). The slashes can
stay as they are or be encoded as %2F; both work.
limit is capped server-side at 200 whatever you request; when that
happens truncated is true and limit_applied
carries the cap. Page with next_offset, never with the limit you
asked for — has_more: false means you have everything.
total counts up to 10,000 and then stops. If more than that
match, total is 10000,
total_is_capped is true and
total_cap carries the limit — meaning at least this
many, never fewer. Counting every match exactly made the cost of a search
depend on how common the word was: text=extension matches over
half a million applications and spent around eleven seconds counting them,
whether you asked for one row or fifty. Narrow with
council_id, postcode or a date range to get an
exact figure. has_more is always exact and is the right thing
to page on — it comes from the rows themselves, not from
total.
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.
The response also carries the paths onward: related_cases lists the
appeals, enforcement notices and related applications the council's portal
explicitly links to this one (each with a followable url; an empty list
means no link published, not that nothing exists), and links points at
/appeals (appeals across all sources) and
/site-history (everything at the address).
appeal_decision_normalized on the application is the council's own
note and is often stale or unclassified; when related_cases.appeals is
non-empty, prefer that appeal's own outcome instead.
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 councils in the database with coverage metadata.
| Parameter | Type | Description |
|---|---|---|
search | string | Case-insensitive match on the council name — camden finds Camden, west finds every West-something. Omit it to list them all. Also accepted as q, query or name. |
curl -H "X-API-Key: pk_..." "https://ukplanningapi.co.uk/v1/councils?search=Camden"
{
"data": [
{
"id": 123,
"name": "Camden",
"region": "London",
"country": "England",
"last_scraped_at": "2026-06-15T03:00:00Z",
"production_ready": true
}
],
"count": 395
}
production_ready: false means the council's history from 2023 is
still being backfilled: its data is a lower bound and its daily changes are
excluded from /v1/changes and webhooks by default. The list
returns every active council, complete or not — the flag is how you apply
the completeness gate yourself.
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 |
include_partial | true to include councils still being backfilled (default false — their "changes" are mostly re-scrape artefacts, not real events) |
limit | Default 100, max 500 |
curl -H "X-API-Key: pk_..." \ "https://ukplanningapi.co.uk/v1/changes?date=2026-06-14&type=decision_issued"
The feed covers production-ready councils by default; the response's
gated key says which feed you got. A council mid-backfill emits
thousands of spurious "changes" as its history is captured — that noise is
excluded unless you ask for it.
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, all production-ready councils |
Webhooks deliver changes from production-ready (complete-history) councils.
Set "include_partial": true at registration to also receive
changes from councils still being backfilled; each change carries a
production_ready flag so the two are distinguishable.
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
159 of the 396 councils
we cover. 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
3,206 of 30,745 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 | 59,960 | indicative | Appeals as recorded on council portals (215 councils) — all kinds,
not just enforcement: ~77% planning and other appeal types, ~8% enforcement, ~16% untyped.
Filter appeal_type to separate them. pending is a real
outcome here, not an error. |
application | 26,862 usable | indicative | The appeal field on the application record. Of 370,879 populated values only 26,862 say anything (11,655 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.
Every row carries application_ref — the linked application's
GET /v1/applications/:id id, or
null if the appeal has no application link (most pins
rows: only ~10% link at all). Pass it straight to the application endpoint;
it is already in the right form.
Changed 2026-08-21: these responses used to
carry an application_id alongside it, holding an internal row
number that the application endpoint rejected. A field named
application_id that is not a usable id is a trap — documenting
it was not enough, because a caller reading JSON keys never sees the
warning. It has been removed rather than repurposed, so a request built on
it fails loudly instead of silently looking up the wrong record.
GET /v1/applications/:id/appeals likewise now identifies its
anchor as application, carrying the public id.
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 four different
findings: found, pending (a real appeal not yet decided),
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 council published a postcode and both records
resolve to the same property; rare), property_key_inferred
(same, but at least one postcode was inferred from the address and
ward rather than published — real, and slightly stronger than an address
match since it can unify two spellings of one address, but not something
the source asserted, so weigh it as address-strength evidence)
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"
Area & directory lists
Narrowing tools: turn an area into concrete postcodes, addresses or properties
(1,617,373 postcodes,
25,119,705 addresses and
29,132,718 properties on register), or a name
into an officer/agent id — then fan out the per-item endpoints. Every list
requires at least one filter, caps at 200 rows and pages with
next_offset. Absence from a register means not in our
register, never proof a place doesn't exist.
GET /v1/postcodes
| Parameter | Description |
|---|---|
query | Postcode prefix at any precision (NW3, NW3 6) |
ward · council | Names, case-insensitive |
country | England · Wales · Scotland · Northern Ireland |
curl -H "X-API-Key: pk_..." \ "https://ukplanningapi.co.uk/v1/postcodes?ward=Hampstead+Town&council=Camden"
GET /v1/addresses
Scope by postcode (any precision) and/or ward;
query matches the street name and requires one of those scopes.
Returns number, street, postcode and UPRN.
curl -H "X-API-Key: pk_..." \ "https://ukplanningapi.co.uk/v1/addresses?postcode=NW3+6UP"
GET /v1/properties
The full-UK property register with planning-relevant attributes: address, area
hierarchy (district · council · county ·
region · country · ward), property type and
age band, and constraint flags — conservation_area (true/false
or an area name), article4.
Describes properties, not planning history — follow up with
site history per address.
curl -H "X-API-Key: pk_..." \ "https://ukplanningapi.co.uk/v1/properties?conservation_area=Hampstead&council=Camden"
GET /v1/officers · GET /v1/agents
Find ids by name. /v1/officers?name=ahmed&council_id=123 returns
officer ids for officer analytics;
/v1/agents?name=&company= (at least one) returns agent ids —
contact details are only on the agent detail endpoint.
GET /v1/councils/:id/wards
Ward names observed in one council's planning data — feeds ward stats and the ward filters above. An unlisted ward means no records mention it, not that the ward doesn't exist.
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.
Figures describe your account: all your API keys share one
allowance (a new key is not a new allowance); this_key breaks out
the calling key's contribution.
| 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.
Found a problem?
Wrong, missing or stale planning data, and anything the API or the MCP tools get wrong, belong on the public issue tracker. Quote the council and the application reference, say what you expected, and the fix helps everyone querying that council rather than just you.
Two things worth checking first, because both are documented behaviour
rather than faults: an empty result usually means the council does not
publish that field — per-council field coverage
tells you which are populated — and
status/outcome of no_information means
we hold no data, never a negative finding.
Anything about your account, your keys or billing is not public: email
help@ukplanningapi.co.uk. If you
hit a 500, send the request_id from the response
body and we can trace the exact 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 →