Property Licence

Property Licence API

Check whether each property in a portfolio needs a selective, additional HMO or mandatory HMO licence. We keep the results up to date as councils change their schemes and tell you what changed.

Access and keys

API access is set up per organisation. We agree the number of properties and any council limits with you, then issue API keys. Keys start with plk_. We show each key once, so store it as a secret.

Send the key on every request:

Authorization: Bearer plk_…

Each key has a limit per minute. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. When you reach the limit you get a 429 with Retry-After.

Every endpoint needs a key, including the single postcode check.

Endpoints

GET

/api/v1/postcode/{postcode}

Check one postcode.

POST

/api/v1/properties/import

Add or update properties by your reference, as JSON or CSV.

GET

/api/v1/imports/{id}

See an earlier import and its row report.

GET

/api/v1/properties

List your properties with their licence results.

GET

/api/v1/properties/{ref}

One property. DELETE archives it.

GET

/api/v1/properties/export.csv

Download the results as CSV.

POST

/api/v1/determine

Check up to 500 properties without saving them.

GET

/api/v1/events

Events raised for your organisation.

GET

/api/v1/webhooks

List, add, change and test webhook endpoints.

GET

/api/v1/organisation

Your limits and portfolio counts.

The OpenAPI specification has every parameter and response field.

Add your properties

Send up to 25,000 properties per request as CSV or as JSON ({ "properties": [ … ] }). Each property is matched by your ref, so you can send the full list again at any time. Unchanged rows are skipped and changed rows are checked again.

The response reports every row as created, updated, unchanged or error, with the reason for each error. Add ?report=errors to return only rows with problems. Properties in imports of up to 1,000 changes are checked before the response. Larger imports are checked in the background: follow the import with /api/v1/imports/{id}.

curl https://propertylicence.org.uk/api/v1/properties/import \
  -H "Authorization: Bearer $PROPERTYLICENCE_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @properties.csv
ref,uprn,address_line_1,town,postcode,is_hmo,occupants,households
FLAT-0001,100023336956,10 Example Road,Leeds,LS8 5AA,true,6,3
HOUSE-0002,,22 Sample Street,Liverpool,L7 8TH,false,3,1
ref
Required. Your own reference, up to 100 characters. Sending the same ref again updates that property.
postcode
Required.
uprn
Recommended. We use the UPRN's exact location to decide which side of a boundary the property is on.
address_line_1 to 3, town
Used to find the street for street-based schemes.
latitude, longitude
Optional. Used when there is no UPRN.
is_hmo
true or false.
occupants, households
Number of people living there and the number of separate households.
storeys
Number of storeys.
building_type
house, flat, purpose_built_flat, converted_flat, bedsit, section_257 or other.

Common column names such as Property Reference, Post Code or Occupiers are recognised.

Reading a result

licence_required is yes, no or depends_on. When it depends, missing_facts lists what we need: occupants, households, is_hmo, street, location or council_confirmation. Send those facts in your next import and the result updates.

match_basis says how we placed the property. uprn and coordinates use the property's own location. postcode means we only had the postcode, so a scheme that covers part of a postcode can't be confirmed. Check those properties against the council's map or send a UPRN.

Each scheme lists its status, dates, fee, application page and the council page we read it from. last_changed_at is when the result last changed.

{
  "licence_required": "depends_on",
  "licence_type": null,
  "missing_facts": ["households"],
  "licence_required_from": null,
  "match_basis": "uprn",
  "confidence": "high",
  "schemes": [
    {
      "name": "Additional HMO licensing",
      "type": "additional_hmo",
      "status": "active",
      "applies": "depends_on",
      "start_date": "2025-04-01",
      "end_date": "2030-03-31",
      "fee": { "amount": 950, "notes": null },
      "application_url": "https://…",
      "source_url": "https://…"
    }
  ],
  "checked_at": "2026-10-19T06:12:40.000Z",
  "last_changed_at": "2026-10-19T06:12:40.000Z"
}

Webhooks

Add an HTTPS endpoint with POST /api/v1/webhooks. The address must be public: we check where the host points when you add it and again on every delivery. We return a signing secret once. Scheme events list the refs of your affected properties, so you don't need to poll.

scheme.consultation_opened
A council in your portfolio has opened a licensing consultation.
scheme.designated
A council has confirmed a new scheme.
scheme.starting
A confirmed scheme starts in 30 days, or has started.
scheme.expiring
A scheme ends in 180, 90 or 30 days.
scheme.fee_changed
A scheme's licence fee has changed.
scheme.ended
A scheme has ended or been withdrawn.
property.determination_changed
The result for one of your properties has changed.

Every request has a PropertyLicence-Signature header in the form t=1760850000,v1=…. v1 is the HMAC-SHA256 of the timestamp, a full stop and the raw request body, using your secret. Reject requests older than 5 minutes.

Reply with a 2xx status within 10 seconds. We don't follow redirects, so use the final URL. We try each delivery up to 10 times over about 8 hours, waiting longer after each failure. Use the PropertyLicence-Event-Id header to ignore repeats. POST /api/v1/webhooks/{id}/test sends a test event, and /deliveries shows recent attempts.

import crypto from "node:crypto";

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (age > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? ""));
}

How results stay up to date

We check council licensing pages every day. When a council's schemes change, we check your properties in that council again and send webhooks for what changed. Properties in other councils are left alone.

We can also send one summary email per organisation after each daily check, instead of an email per property.