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

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 sandbox, instant API key. Paid plans unlock fresh monitoring data.

Get an API key →