Developer documentation

UK Planning Applications API reference

Structured, classified, source-linked UK planning application data over a REST JSON API. Base URL https://api.plota.co.uk/v1

Authentication

Every request needs an API key, sent as a bearer token (preferred) or the X-API-Key header. Keys are secret - never expose them in client-side code.

shell
curl "https://api.plota.co.uk/v1/applications?postcode=SW11&limit=5" \
  -H "Authorization: Bearer plota_live_your_key"

Endpoints

EndpointPurpose
GET /v1/applicationsSearch applications with filters + cursor pagination
GET /v1/applications/{id}Retrieve one application by public id or numeric id
GET /v1/applications/nearbyApplications near a coordinate or postcode
GET /v1/councilsCovered councils with coverage metadata
GET /v1/categoriesPlota planning categories
GET /v1/alertsSaved alerts belonging to your key’s email
GET /v1/alerts/{id}Retrieve one saved alert
GET /v1/alerts/{id}/matchesApplications matching a saved alert
GET /v1/applications/{id}/historyStatus & decision timeline
POST /v1/exportsBulk export as CSV/JSON (Starter+)
POST /v1/webhooksRegister a signed webhook (Starter+)
GET / DELETE /v1/webhooks/{id}List or remove webhooks
POST /v1/webhooks/{id}/testSend a test event

Query parameters

GET /v1/applications accepts:

ParameterDescription
postcodeFull or partial postcode, e.g. SW11 or SW11 6HB
councilCouncil slug (see /v1/councils), e.g. wandsworth
nationengland, scotland, wales, northern-ireland
categoryOne or more category slugs, comma-separated (see /v1/categories)
qKeyword match in description, address, or reference
referenceExact council reference, e.g. 3PL/2024/0561/HOU - case- and space-insensitive. Unique only within a council, so add council to pin one application
statusSubstring match on status, e.g. pending, approved
date_from / date_toFilter by received date, YYYY-MM-DD
limitPage size, capped per plan (Demo 10 → Business 250)
cursorOpaque pagination cursor from meta.next_cursor
formatjson (default) or csv
include_contactContact Data add-on only: false omits contact fields on this call so nothing is metered against your monthly contact allowance

Pagination

List endpoints use opaque cursor pagination. Pass meta.next_cursor back as cursor to fetch the next page. A null next_cursor means the last page.

application/json
{
  "data": [
    "..."
  ],
  "meta": {
    "limit": 25,
    "count": 25,
    "next_cursor": "eyJzIjoiMjAyNi0wNi0xOCIsImkiOjE4fQ"
  }
}

Rate limits & quotas

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, plus monthly counterparts. A 429 includes Retry-After.

PlanPer minutePer monthMax pageExport rows
Demo30 / min500 total (1,000 rows)10-
Starter120 / min20,000 / month505,000 / month
Pro300 / min100,000 / month100100,000 / month
Business600 / min500,000 / month250500,000 / month
Enterprise1,200 / minCustom500Custom

Export rows = the total POST /v1/exports rows a key can pull per calendar month, across all export calls. Per-call caps still apply; the response reports both via meta.monthly_rows_used and meta.monthly_rows_cap.

Errors

Errors use a consistent shape with a request_id you can quote in support.

application/json
{
  "error": {
    "type": "invalid_request",
    "message": "The postcode parameter is invalid.",
    "param": "postcode",
    "request_id": "req_01jz7k8a2p8b"
  }
}
HTTPtypeMeaning
400invalid_requestA parameter is missing or malformed
401authentication_errorMissing, invalid, or revoked API key
403permission_errorYour plan does not allow this resource
404not_foundNo resource with that id
429rate_limit_errorPer-minute or monthly quota exceeded
500server_errorAn unexpected error on our side
501not_implementedA roadmap endpoint not yet live

The application object

One consistent shape across every council. Beyond the core record, each application carries enrichment read from the full council file - ward, parish, UPRN, decision detail, procedural dates, document and public-comment counts. A decided application looks like this:

application/json
{
  "id": "a1b2c3d4",
  "reference": "2026/2284/FUL",
  "authority": {
    "slug": "wandsworth",
    "name": "Wandsworth"
  },
  "address": "81 Example Road, London SW11 6HB",
  "postcode": "SW11 6HB",
  "ward": "Northcote",
  "parish": null,
  "uprn": "100022543210",
  "description": "Single storey rear extension.",
  "category": {
    "slug": "extensions",
    "label": "Extensions & alterations"
  },
  "categories": [
    {
      "slug": "extensions",
      "label": "Extensions & alterations"
    }
  ],
  "planning_route": "Full planning permission",
  "dwelling_count": null,
  "status": "Application Permitted",
  "stage": "approved",
  "decision": {
    "outcome": "Application Permitted",
    "issued_date": "2026-07-14",
    "level": "Delegated"
  },
  "appeal": null,
  "date_received": "2026-05-18",
  "date_validated": "2026-05-20",
  "date_decided": "2026-07-10",
  "key_dates": {
    "target_decision": "2026-07-15",
    "committee": null,
    "consultation_end": "2026-06-19",
    "advertised": "2026-05-28",
    "statutory_expiry": "2026-07-15",
    "permission_expiry": "2029-07-10"
  },
  "location": {
    "lat": 51.4601,
    "lng": -0.1702
  },
  "documents_count": 14,
  "comments": {
    "objections": 2,
    "supporting": 5,
    "received": 8,
    "consulted": 21,
    "counted_at": "2026-07-29T06:40:12Z"
  },
  "links": {
    "plota": "https://plota.co.uk/application/a1b2c3d4",
    "council": "https://..."
  },
  "source": "live"
}

Enrichment & data depth

Normalised vs verbatim. Two rules keep integrations dependable. Filter and aggregate on stage - the normalised status (pending, approved, refused, withdrawn, decided, other), identical across every council. status and decision.outcome carry the council's own wording verbatim - use them for display and for verifying a record against the council source, never for filtering, because every authority words them differently.

Enrichment blocks. decision, appeal, key_dates and comments are read from the full council record on Plota's recurring checks and are null where the authority publishes nothing for them - councils vary widely in how much of the file they publish, so depth differs by authority, not by Plota. Within comments, 0 is real data: counted, none found. uprn is the authoritative UK property identifier for joining planning records to UPRN-keyed property systems. dwelling_count is Plota-derived: the number of homes explicitly stated in the description, null where none is stated.

Live vs historical. Enrichment is a live-data feature (2026 onward). Plota archive records (source: "historical", paid plans) carry the core record with all enrichment blocks null - except dwelling_count, which is backfilled onto the archive too. Single-application responses additionally carry created_at and updated_at; note updated_at moves on every re-check, including no-change ones, so it is not a content-change signal.

Contact Data add-on. With the add-on, records also include applicant, agent, agent_email, agent_phone, case_officer, case_officer_email and case_officer_phone where the council publishes them, metered as one monthly allowance (10,000 records; only records that deliver a contact value count). Pass include_contact=false to skip contact fields and metering on any call. Without the add-on these fields are absent entirely.

Webhooks

Available on Starter and above. Register an HTTPS endpoint in your dashboard (Webhooks → Add a webhook - no code needed) or with POST /v1/webhooks, linked to a saved alert. Plota POSTs an application.match.created event for each new match, with retries, and auto-disables endpoints that keep failing. Every delivery carries a Plota-Event-Id header and event id; retries reuse the same id, so treat it as an idempotency key. The full event body is the WebhookEvent schema in the OpenAPI document. Verify each delivery: compute HMAC-SHA256 over ${Plota-Timestamp}.${rawBody} with your webhook secret and compare to the Plota-Signature header.

node
const sig = crypto.createHmac('sha256', secret)
  .update(req.headers['plota-timestamp'] + '.' + rawBody).digest('hex');
if (sig === req.headers['plota-signature']) { /* trusted */ }

Integration guides (Zapier, Make, Slack, Sheets, Airtable, HubSpot, Pipedrive). Manage keys & webhooks in your dashboard. Machine-readable schema: /openapi.json. Licence: API terms. Building with AI: paste /llms.txt into your assistant, or use the MCP server for live access. Questions? hello@plota.co.uk.