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.

EndpointPurpose
GET /.well-known/oauth-authorization-serverRFC8414 metadata — endpoints, supported grants, scopes
POST /registerDynamic client registration (RFC7591)
GET /authorizeUser sign-in and consent (RFC6749 §4.1.1)
POST /tokenExchange a code, or refresh, for an access token
POST /revokeRevoke a token (RFC7009)
  1. 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. /authorize and /token will 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 a client_secret back.
    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 place client_id (and, for a confidential client, client_secret) comes from — store it, there's no dashboard lookup for it afterwards.
  2. Send the user to /authorize. Generate a PKCE code_verifier and its S256 code_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 your redirect_uri with ?code=...&state=....
  3. 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"
    }
    Use access_token exactly like an API key, as an Authorization: Bearer header:
    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 via POST /revoke, same as any key.
  4. 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

HTTPCodeMeaning
400MISSING_FILTERA search with no filter on it. The body lists every filter the endpoint accepts and gives a worked example.
400UNKNOWN_PARAMETERA 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.
400CONFLICTING_PARAMETERThe same filter supplied twice with different values — including via two names that mean the same thing (q and text, say).
400MALFORMED_IDAn 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.
401UNAUTHORIZEDMissing or invalid credential (API key or OAuth token)
403FORBIDDENAccount suspended
404NOT_FOUNDResource 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).
429RATE_LIMIT_EXCEEDEDMonthly 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
503TEXT_SEARCH_UNAVAILABLEOnly 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.

  1. Search for the site, usually by postcode:
    GET /v1/applications?postcode=BS7%209TB
  2. Take the id straight from a result — it is the first field of every one. It already looks like 123-2026/3373/P — the council's number, a hyphen, then that council's own reference. The council's name is never part of it.
  3. 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

TierLimitPrice
Developer500 req/monthFree
Starter2,500 req/month£19/month
Growth15,000 req/month£99/month
EnterpriseUnlimitedCustom

GET /v1/applications

Search planning applications. At least one filter is required.

Query parameters

ParameterTypeDescription
postcodestringPrefix 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_idintegerCouncil ID from /v1/councils
agent_idintegerPlanning agent ID
statusstringNormalised 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.)
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)
textstringFree-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.
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": [
    {
      "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.

ParameterTypeDescription
searchstringCase-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.

ParameterDescription
dateDate to retrieve (YYYY-MM-DD, default: today)
typenew_application | status_change | decision_issued | new_document
council_idFilter to one council
include_partialtrue to include councils still being backfilled (default false — their "changes" are mostly re-scrape artefacts, not real events)
limitDefault 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.

PlanWebhooks
Developer—
Starter1, filtered by council_ids
Growth / EnterpriseUnlimited, 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.

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 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.

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.
council59,960indicative 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.
application26,862 usableindicative 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.
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.

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

ParameterDescription
queryPostcode prefix at any precision (NW3, NW3 6)
ward · councilNames, case-insensitive
countryEngland · 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).

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. 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.

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.

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.

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 →