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 free PlanWire API key and Node.js or Python.
Step 1: Get your API key
Sign up at planwire.io/#signup. The free tier gives you 500 calls/day — plenty for a monitoring script running hourly or daily.
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&days=7" \ -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&days=7" \ -H "X-API-Key: your_api_key"
The days=7 parameter restricts results to applications received or updated in the last 7 days. Tune this to match your cron frequency — use days=1 if you're running daily.
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'; // ── Configure your query here ──────────────────────────────────── const QUERY = new URLSearchParams({ q: 'Bellway', // competitor name council_id: 'leeds', // or remove for nationwide days: '1', 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 { 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 offset parameter:
async function fetchAll() { const all = []; let offset = 0; while (true) { QUERY.set('offset', offset); const res = await fetch(`https://api.planwire.io/v1/applications?${QUERY}`, { headers: { 'X-API-Key': API_KEY } }); const { applications, total } = await res.json(); all.push(...applications); if (all.length >= total || applications.length === 0) break; offset += applications.length; } 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 a wider days window 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.