Why this matters
Planning applications are public record. Every time a developer, housebuilder, or architect submits an application to a council, it becomes searchable — you just need the right tooling to catch it quickly.
Common use cases: a housebuilder wants to know when a rival submits in their target areas. A planning consultant tracks clients' sites for unauthorised applications. An architect monitors a prolific local developer to understand their design approach. In all cases the goal is the same: know first, act fast.
The approach
You'll build a small script that:
- Queries the PlanWire API for new applications matching your criteria
- Compares against a local set of already-seen application IDs
- Sends a Slack or email notification for anything new
- Runs on a cron schedule (daily, or every few hours)
Total setup time: about 20 minutes. You'll need a PlanWire API key and Node.js or Python.
Step 1: Get an API key
Sign up at planwire.io/#signup. The free sandbox lets you test query shape; paid plans unlock fresh monitoring data for hourly or daily workflows.
Step 2: Find the right query
PlanWire supports several ways to identify applications from a specific developer or in a specific area:
- By applicant name: use the
qparameter to search applicant or agent name in the description - By postcode prefix:
postcode_prefix=SW1to watch a specific area - By council:
council_id=wandsworthto watch an entire LPA - By bbox: a lat/lng bounding box around your target geography
# Search by applicant/agent name in a specific council curl "https://api.planwire.io/v1/applications?q=Bellway&council_id=leeds&limit=10&date_from=2026-06-01" \ -H "X-API-Key: your_api_key" # Or by postcode prefix across all councils curl "https://api.planwire.io/v1/applications?postcode_prefix=LS1&limit=10&date_from=2026-06-01" \ -H "X-API-Key: your_api_key"
The date_from=2026-06-01 parameter restricts this example to applications received or updated from 1 June 2026 onwards. For daily monitoring, compute yesterday's date before each request and pass it as date_from.
Step 3: The monitoring script
import fs from 'node:fs'; const API_KEY = process.env.PLANWIRE_API_KEY; const SLACK_URL = process.env.SLACK_WEBHOOK_URL; const SEEN_FILE = './seen-ids.json'; const since = new Date(Date.now() - 24 * 60 * 60 * 1000) .toISOString() .slice(0, 10); // ── Configure your query here ──────────────────────────────────── const QUERY = new URLSearchParams({ q: 'Bellway', // competitor name council_id: 'leeds', // or remove for nationwide date_from: since, limit: '50', }); // ───────────────────────────────────────────────────────────────── async function run() { const seen = loadSeen(); const res = await fetch( `https://api.planwire.io/v1/applications?${QUERY}`, { headers: { 'X-API-Key': API_KEY } } ); const { data: applications } = await res.json(); const fresh = applications.filter(a => !seen.has(a.id)); for (const app of fresh) { console.log(`New: ${app.reference} — ${app.address}`); seen.add(app.id); await notify(app); } saveSeen(seen); console.log(`Done. ${fresh.length} new applications found.`); } async function notify(app) { if (!SLACK_URL) return; await fetch(SLACK_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `*New competitor application*\n` + `*Ref:* ${app.reference}\n` + `*Address:* ${app.address}\n` + `*Type:* ${app.application_type}\n` + `*Status:* ${app.status}\n` + `*Council:* ${app.council_id}\n` + (app.url ? `<${app.url}|View on council portal>` : ''), }), }); } function loadSeen() { try { return new Set(JSON.parse(fs.readFileSync(SEEN_FILE, 'utf8'))); } catch { return new Set(); } } function saveSeen(seen) { fs.writeFileSync(SEEN_FILE, JSON.stringify([...seen])); } run().catch(console.error);
Step 4: Schedule it with cron
On Linux/Mac, add a cron entry to run the script daily at 7am:
# Run every day at 07:00
0 7 * * * PLANWIRE_API_KEY=your_key SLACK_WEBHOOK_URL=https://hooks.slack.com/... \
/usr/bin/node /path/to/monitor.js >> /var/log/planwire-monitor.log 2>&1Or deploy it to Railway / Fly.io as a cron service if you want it running in the cloud. Railway's cron syntax is the same.
Handling pagination
If your competitor is prolific, a single request may not return all new applications. Add pagination using the page parameter:
async function fetchAll() { const all = []; let page = 1; while (true) { QUERY.set('page', String(page)); const res = await fetch(`https://api.planwire.io/v1/applications?${QUERY}`, { headers: { 'X-API-Key': API_KEY } }); const { data: applications, meta } = await res.json(); all.push(...applications); if (page >= meta.pages || applications.length === 0) break; page += 1; } return all; }
Smarter matching: track decision outcomes too
Knowing a competitor submitted isn't the whole picture — knowing whether they got approved or refused is more valuable. Add a second query filtered by status=Approved or status=Refused with an older date_from to catch decisions on applications you already know about.
Alternatives to polling
If you'd rather not manage a cron job at all, PlanWire webhooks do the same thing server-side — you register a filter and PlanWire pushes new matches to your endpoint in real time. See the webhook tutorial for the setup guide.