Developers · API v1

Screen a location from your own systems

One endpoint. Post a point in England, get back the same constraint survey the app shows — every layer checked, every source that failed, the vintages, the attribution and the site score. Built for acquisition pipelines, CRMs and dashboards that would rather not have a person retyping results.

Read this before you write any code

A layer missing from results while its source is listed in failures was not checked. It is not clear. Upstream services go down; when one does, we still answer 200, we still return everything we did check, and we tell you exactly what we could not. failures is always present — an empty array on a clean survey — and status is "partial" whenever it is not empty.

If your integration renders a verdict, render the partial state too. Treating silence as absence is the one failure mode of this API that produces a confidently wrong answer about a real site.

Authentication

Keys belong to an organisation on the Practice plan. The practice owner creates them under Account → Organisation → API. A key is shown once, at creation — we store a hash of it and cannot show it to you again. Lost it? Rotate it.

Send it as Authorization: Bearer pc_live_…, or as X-API-Key: pc_live_… if your client is happier with that. There is no session, no cookie and no OAuth flow. Tier and subscription state are re-checked on every single request, so revoking a key — or a subscription lapsing — takes effect on the next call.

Treat a key like a password: server-side only, never in a browser bundle or a mobile app. Name each key after the system that uses it, so revoking one when somebody leaves does not take the rest down with it.

POST /api/v1/screen

Send either a coordinate pair or a postcode. A coordinate wins if you send both — a postcode resolves to the centroid of the postcode unit, which can sit in a different field, flood cell or parcel than the asset you mean.

  • lat, lng — WGS84 decimal degrees. Both or neither.
  • postcode — a UK postcode, e.g. "CB2 1TN".
  • radius — metres. Default 100, maximum 500.

England only. planning.data.gov.uk is MHCLG data and covers England; Scotland, Wales and Northern Ireland are out of scope, not a coverage gap. A point or postcode outside England is refused with a 422 rather than screened against datasets that do not describe it.

curl -X POST https://planningconstraints.com/api/v1/screen \
  -H "Authorization: Bearer pc_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "lat": 52.2053, "lng": 0.1218, "radius": 100 }'

Response (abridged)

{
  "request_id": "0f1a…",
  "api_version": "2026-07",
  "surveyed_at": "2026-07-30T09:14:22.183Z",
  "status": "partial",
  "notice": "One or more sources did not answer. Every layer listed in `failures` was NOT checked — treat it as unknown, never as clear.",
  "query": { "lat": 52.2053, "lng": 0.1218, "radius_m": 100, "postcode": null },
  "summary": {
    "constraints": 3,
    "blockers": 1,
    "material": 2,
    "findings": ["Planning framework", "Heritage"],
    "clear_domains": ["Flood & water", "Ground & subsurface"],
    "not_covered": 1,
    "layers_checked": 41
  },
  "results": [
    {
      "definition_id": "green-belt",
      "name": "Green Belt",
      "domain": "planning-framework",
      "subgroup": "designation",
      "band": "constraint",
      "weight": "blocker",
      "count": 1,
      "entities": [{ "name": "Cambridge Green Belt", "reference": "…" }],
      "vintage": "2025-11"
    },
    {
      "definition_id": "flood-risk-zone",
      "name": "Flood risk zone",
      "domain": "flood-water",
      "subgroup": "fluvial",
      "band": "constraint",
      "weight": "material",
      "count": 0
    }
  ],
  "failures": [
    {
      "source_id": "historic-england",
      "label": "Listed buildings (Historic England)",
      "definition_ids": ["listed-building"]
    }
  ],
  "score": { "version": "v1", "total": 62, "partial": true, "contributions": [] },
  "meta": {
    "api_version": "2026-07",
    "data_vintages": { "green-belt": "2025-11" },
    "attribution": ["Contains public sector information licensed under the Open Government Licence v3.0"],
    "quota": { "included": 1000, "used": 143, "remaining": 857, "resets_at": "2026-08-01T00:00:00.000Z", "month": "2026-07" }
  }
}

Reading the response

  • results[] — every layer that was checked, including the ones that found nothing. count: 0 means checked and clear. A layer that is absent entirely was not checked.
  • failures[] — sources that did not answer, each with the definition_ids its silence covers. Always present.
  • summary— the panel’s own headline counts, computed by the same code the app and the bulk CSV use, so the three never disagree.
  • score — the Site Score at this point, with its contributions. Point mode: no redline, so every layer counts at full presence weight.
  • meta.data_vintages — publication vintage per layer. Stale data must not read as current.
  • meta.attribution — see below. Not optional.

Errors

StatusreasonMeaning
401invalid-keyNo credential, malformed, unknown, or revoked. All four are deliberately indistinguishable.
403practice-requiredThe key’s organisation is not on the Practice plan.
403subscription-lapsedThe organisation’s subscription has lapsed. Keys are not revoked — reactivate and the same keys work again.
422invalid-requestMissing or malformed location, radius over the maximum, or a point outside England. Nothing is fetched and nothing is charged.
429rate-limitedBurst limit (60 requests a minute, per key). `Retry-After` is in seconds.
402quota-exhaustedThe monthly screen allowance is spent. `Retry-After` and `X-Quota-Reset` both point at the reset instant.
503capacityA product-wide daily ceiling was hit. Nothing was charged against your quota.
500server-errorThe survey could not be run at all. Nothing is charged. Quote the `request_id`.

Every error carries error (prose), reason (machine-readable, switch on this) and request_id. Quote the request id and we can find the exact call.

Limits, quota and latency

  • Burst: 60 requests a minute, per key. Over it: 429 with Retry-After.
  • Monthly: 1,000 screens included per organisation per calendar month (UTC), shared across all of its keys. Over it: 402.
  • Headers on every response: X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset. Pace yourself from these rather than from the refusal.

Expect seconds, not milliseconds. A screen fans out to around eighteen live government sources and waits for them; that is what makes the answer current rather than a cached guess. Budget 30 seconds, call it from a queue or a background job rather than inside a user-facing request, and never inside a page render.

Attribution is a licence obligation

The data in every response is public-sector information licensed to us, and the display obligations travel with it. meta.attribution carries the exact strings owed by the layers in that response — if you display the data to anyone, you must reproduce them. See Licences & attribution.

No ownership data crosses this API. HM Land Registry CCOD/OCOD content — proprietor names, title numbers, addresses — is deliberately out of scope for v1 and appears in no response. It is licensed to us under bespoke terms rather than the Open Government Licence, and a machine-consumable surface for it needs designing rather than defaulting.

Versioning

Every response is stamped api_version: "2026-07". Changes to v1 are additive — new fields may appear, existing ones keep their meaning. Anything breaking mints /api/v2; v1 does not change under you.

Not in v1, and each for a stated reason: redline polygons (point and radius only), ownership data, batch endpoints (use bulk CSV screening in the app), webhooks, OAuth and IP allowlists. Tell us which one you need — the order they arrive in is decided by who asks.