Blog ·Tutorial·April 2026

How to Monitor a Competitor's Planning Applications with a Cron Job

Track every planning application submitted by a competitor — by developer name, address keyword, or postcode — and get an alert the moment they make a move.

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:

  1. Queries the PlanWire API for new applications matching your criteria
  2. Compares against a local set of already-seen application IDs
  3. Sends a Slack or email notification for anything new
  4. 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:

bash · test your query first
# 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

JavaScript · monitor.js
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:

bash · crontab -e
# 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>&1

Or 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:

JavaScript · paginate until done
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.

Start monitoring

Free tier, instant API key. 500 calls/day included — more than enough for a daily monitor.

Get your free API key →