# UK Multi-Trade Planning Lead Radar (`plym-actor-factory/uk-planning-trade-lead-radar`) Actor

Match new England planning applications to multi-trade taxonomy (solar/PV/battery, roof, scaffold, extension/loft, HVAC, glazing) + optional postcode radius via planning.data.gov.uk (OGL). Pay only for unique trade-matched leads ($0.02). Scheduled catch-up — not real-time streaming.

- **URL**: https://apify.com/plym-actor-factory/uk-planning-trade-lead-radar.md
- **Developed by:** [Daniel Witney](https://apify.com/plym-actor-factory) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 trade lead delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## UK Multi-Trade Planning Lead Radar

Match **new England planning applications** to a **multi-trade taxonomy** (solar/PV/battery, roof, scaffold, extension/loft, HVAC, glazing — extensible) with an optional **postcode radius**, using the **official [Planning Data](https://www.planning.data.gov.uk/) HTTP API**. Get typed lead rows you can pipe to a CRM, Slack, or outbound queue — not a nationwide dump and not a single-trade SKU.

### Value proposition

- **Official HTTP only** (`planning.data.gov.uk` entity API) — no council portal login scrape, no Idox HTML.
- **Trade-qualified**: proposal-text classifier tags applications to the trades you select.
- **Optional catchment**: `originPostcode` + `radiusMiles` (haversine on published `point`, with address-postcode fallback geocode via public [postcodes.io](https://postcodes.io/) in live mode).
- **Pay only for unique trade-matched newly-validated leads.** Quiet schedules and non-matches stay cheap.
- Durable **checkpoint** (entry-date high-water) + `event_uid` dedupe in the default Key-Value Store.
- Every successful run writes one non-billable **`RUN_STATUS`** health row so Store auto-tests and quiet days still leave a non-empty dataset.

This Actor is a **scheduled catch-up poller**. It is **not** real-time streaming and does **not** claim instant alerts or 100% council coverage.

### PRODUCTION USE (required for real leads)

> **Store default ≠ product.** Schema defaults use `sourceMode=fixture` so Apify Store daily auto-tests leave a non-empty health dataset **without** calling Planning Data. That fixture path only writes a non-billable `RUN_STATUS` / `FIXTURE_HEALTH` row. It is **not** lead generation.

For **production** you **must**:

1. Set **`sourceMode` = `live`**
2. Choose **`trades`** (or leave the built-in multi-trade default)
3. Optionally set **`originPostcode`** + **`radiusMiles`** for a local catchment
4. Optionally restrict **`organisationEntityIds`** (Planning Data organisation-entity ids)
5. Schedule short runs (e.g. every 15–60 minutes) so checkpoints advance

Official docs: [Planning Data API](https://www.planning.data.gov.uk/docs) · [planning-application dataset](https://www.planning.data.gov.uk/dataset/planning-application) · [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/)

### Coverage limits (honest)

- The Planning Data **planning-application** dataset is **alpha / incomplete**. Local planning authorities are **not** all required to publish to this specification today.
- Expect **partial England coverage** — not 100% of councils, not Scotland/Wales/NI via this API.
- Many records omit geometry, agent, or applicant. When those fields are not published, output columns are `null`.
- Detection latency ≈ your schedule interval + Planning Data ingest lag. Title does **not** say real-time or streaming.

### Apify Store daily auto-test / default input

| Field | Default |
|-------|---------|
| `trades` | built-in six trades |
| `lookbackDays` | `14` |
| `maxRunSeconds` | `55` |
| `maxEvents` | `500` |
| `resumeFromCheckpoint` | `true` |
| `sourceMode` | `fixture` |
| `emitFixtureDemoEvents` | `false` |
| `requestPaceMs` | `250` |

On that path the Actor:

- **SUCCEEDS** quickly (no Planning Data network call)
- Pushes exactly one dataset row: `event_type: "RUN_STATUS"`, `event_subtype: "FIXTURE_HEALTH"`, `billable: false`
- Does **not** call `Actor.charge` / does **not** emit fabricated leads
- Leaves a **non-empty** default dataset well under 5 minutes

`emitFixtureDemoEvents: true` is only for unit/local demos of typed lead shapes; leave it **false** on Store and in production.

### Pricing (single PPE tier)

| Event | Price | When charged |
|-------|-------|--------------|
| `trade-lead-delivered` | **$0.02** ($20 / 1,000) | Each unique trade-matched newly-validated lead pushed to the dataset |
| `apify-actor-start` | **$0.00005** | Actor start (platform) |

**Never charged:** `RUN_STATUS`, non-matches, duplicates (`event_uid`), radius/trade filtered rows, fixture Store-health runs, invalid trade ids.

No volume-discount tiers (see factory ADR 0010).

### Output fields (lead rows)

`authority`, `organisation_entity`, `reference`, `application_entity`, `address`, `postcode`, `proposal_description`, `matched_trades`, `decision_status`, `decision_type`, `applicant`, `agent` (when published), `entry_date`, `decision_date`, `latitude`, `longitude`, `distance_miles`, `source_url`, `event_uid`, `summary`, `billable`.

### Attribution / licence

Contains public sector information licensed under the [Open Government Licence v3.0](https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).\
Source: [planning.data.gov.uk](https://www.planning.data.gov.uk/) (MHCLG Planning Data).\
© Crown copyright and database right.

### Features

- Endpoint: `https://www.planning.data.gov.uk/entity.json?dataset=planning-application`
- Built-in keyword taxonomy + `customTradeKeywords` extension
- Optional geometry bbox pre-filter + haversine refine
- High-water checkpoint + seen `event_uid`s in KVS
- `sourceMode: fixture | live`
- Always-on non-billable `RUN_STATUS` (`HEALTH_OK` | `MONITOR_IDLE` | `CATCHUP_COMPLETE` | `FIXTURE_HEALTH`)

### Example input (production)

```json
{
  "sourceMode": "live",
  "trades": ["scaffold", "roof"],
  "originPostcode": "M1 1AE",
  "radiusMiles": 20,
  "lookbackDays": 7,
  "maxRunSeconds": 90,
  "maxEvents": 200,
  "resumeFromCheckpoint": true
}
```

# Actor input Schema

## `trades` (type: `array`):

Trade taxonomy ids to match against proposal descriptions. Built-in: solar\_pv\_battery, roof, scaffold, extension\_loft, hvac, glazing. Use "all" or leave default for every built-in trade. Custom ids require customTradeKeywords.

## `originPostcode` (type: `string`):

UK postcode used as the centre of an optional radius filter. Geocoded via public postcodes.io in live mode. Leave empty for nationwide (API coverage) matching.

## `radiusMiles` (type: `number`):

When originPostcode is set, keep only applications whose published point (or geocoded address postcode) falls within this radius. 0 / empty = no radius filter.

## `organisationEntityIds` (type: `array`):

Optional planning.data.gov.uk organisation-entity ids to restrict the live query (LPA / local authority entities). Empty = all organisations present in the API.

## `customTradeKeywords` (type: `object`):

Map of trade id → extra keyword/phrase list (case-insensitive substrings). Extends or adds trades beyond the built-in taxonomy.

## `lookbackDays` (type: `integer`):

When no checkpoint exists, search entry-date this many days back from today (UTC). Subsequent runs resume from stored high-water.

## `maxRunSeconds` (type: `integer`):

Wall-clock budget for this scheduled catch-up.

## `maxEvents` (type: `integer`):

Stop after this many unique trade-matched leads are delivered (does not include RUN\_STATUS).

## `resumeFromCheckpoint` (type: `boolean`):

If true, resume entry-date high-water and seen event\_uids from the default Key-Value Store.

## `sourceMode` (type: `string`):

fixture = local/CI / Apify Store daily auto-test default (no network; emits RUN\_STATUS only unless emitFixtureDemoEvents=true). live = official planning.data.gov.uk HTTP API.

## `emitFixtureDemoEvents` (type: `boolean`):

When sourceMode=fixture, if true push fabricated fixture leads (unit/local demos only). Default false so Store daily auto-tests never emit fake leads or charge trade-lead-delivered. Production: leave false and use sourceMode=live.

## `maxTotalChargeUsd` (type: `number`):

Optional soft budget hint for delivered leads. Platform ACTOR\_MAX\_TOTAL\_CHARGE\_USD also applies when set.

## `requestPaceMs` (type: `integer`):

Minimum delay between outbound HTTP requests (planning.data.gov.uk and postcodes.io).

## Actor input object example

```json
{
  "trades": [
    "solar_pv_battery",
    "roof",
    "scaffold",
    "extension_loft",
    "hvac",
    "glazing"
  ],
  "originPostcode": "",
  "radiusMiles": 0,
  "organisationEntityIds": [],
  "customTradeKeywords": {},
  "lookbackDays": 14,
  "maxRunSeconds": 55,
  "maxEvents": 500,
  "resumeFromCheckpoint": true,
  "sourceMode": "fixture",
  "emitFixtureDemoEvents": false,
  "requestPaceMs": 250
}
```

# Actor output Schema

## `OUTPUT` (type: `string`):

JSON summary: delivered, charged, runStatus, checkpoint, stats

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("plym-actor-factory/uk-planning-trade-lead-radar").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("plym-actor-factory/uk-planning-trade-lead-radar").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call plym-actor-factory/uk-planning-trade-lead-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plym-actor-factory/uk-planning-trade-lead-radar"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/IyKhqPhlfA5zUvIDS/builds/nEbb3fxBKpeaVNNxu/openapi.json
