PlanningTrack API · Free beta
Quickstart
Search planning applications, retrieve council evidence, and keep your product updated through one authenticated API.
Application coverage is incomplete. Start by checking your target councils. Address searches match published text; they do not establish verified property history.
1. Create your account and key
Sign in with an email code, then create a named key in API keys. Copy it when it appears; you cannot reveal it later.
Set PLANNINGTRACK_API_KEY in your server’s environment. Never put it in browser code, a mobile app, a URL, or source control.
2. Check collection coverage
Use GET /v1/coverage?authority=local-authority-oxo to inspect collection evidence. Directory entries, summary counts, and full detailed records describe different things.
3. Search applications
The example alongside this guide searches collected Oxford records. Use q to add an address, postcode, reference, or proposal. Empty results do not prove that no applications exist.
4. Retrieve a returned application
Take an id from items and URL-encode it in GET /v1/applications/{id}. Display sourceUrl, lastCheckedAt, and section availability with your results.
Run a complete example
These examples check coverage, search, and retrieve an actual returned ID. They use up to three successful requests and include bounded retries and a pagination helper.
// Node 24. Set PLANNINGTRACK_API_KEY in your server environment.
const base = process.env.PLANNINGTRACK_API_BASE || 'https://api.planningtrack.co.uk';
const key = process.env.PLANNINGTRACK_API_KEY;
if (!key) throw new Error('Set PLANNINGTRACK_API_KEY');
export async function request(path) {
for (let attempt = 0; attempt < 4; attempt++) {
let response;
try {
response = await fetch(base + path, {
headers: { Authorization: 'Bearer ' + key },
signal: AbortSignal.timeout(15000),
});
} catch (error) {
if (attempt === 3) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt));
continue;
}
const body = await response.json();
if (response.ok) return body;
const retryable = response.status === 429 || response.status >= 500;
const delay = Number(response.headers.get('retry-after') || 2 ** attempt);
if (!retryable || body.error?.code === 'quota_exceeded' || attempt === 3 || !Number.isFinite(delay) || delay > 60) {
throw new Error(JSON.stringify({ status: response.status, ...body.error,
requestId: response.headers.get('x-request-id') }));
}
await new Promise(resolve => setTimeout(resolve, (Math.max(1, delay) + Math.random()) * 1000));
}
}
export async function* applications(filters) {
let cursor;
do {
const params = new URLSearchParams({ ...filters, limit: '100' });
if (cursor) params.set('cursor', cursor);
const page = await request('/v1/applications?' + params);
yield* page.items;
cursor = page.nextCursor;
} while (cursor);
}
// Quickstart uses up to three successful requests; importing helpers makes none.
if (globalThis._importMeta_.main) {
const authority = encodeURIComponent(process.env.PLANNINGTRACK_AUTHORITY || 'local-authority-oxo');
const coverage = await request('/v1/coverage?authority=' + authority);
console.log('Collection evidence:', coverage);
const page = await request('/v1/applications?authority=' + authority + '&limit=1');
if (page.items.length) {
const application = await request('/v1/applications/' + encodeURIComponent(page.items[0].id));
console.log(application.reference, application.proposal, application.sourceUrl);
} else {
console.log('No collected records matched. This does not establish no planning history.');
}
}