# NSW Planning Applications Monitor (Council DA Tracker JSON) (`steadfetch/au-planning-application-monitor`) Actor

Extract development and planning applications from NSW Planning Portal: applicant, location, description, status and decision date. Monitor mode pushes only new or changed applications. For construction research, site analysis and planning compliance. HTTP only, no browser, pay per delivered row.

- **URL**: https://apify.com/steadfetch/au-planning-application-monitor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Business, Other
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 application delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## steadfetch/au-planning-application-monitor

Returns development applications and planning proposals from the NSW Planning Portal Application Tracker as clean JSON, with a monitor mode that delivers only new and changed applications.

### What it does

- Reads the public Application Tracker API behind https://www.planningportal.nsw.gov.au/map. It is one unified feed for councils across New South Wales: no per-council pages to scrape and no browser.
- Returns one row per application with reference, council, site address, works description, application type, status and lodgement/determination dates.
- Filters by council, address (including partial addresses through the portal address lookup) and lodgement date window.
- Monitor mode keeps a per-user snapshot of the applications already seen and returns only new applications and rows whose status, description or dates changed. An unchanged check returns zero rows.
- Never extracts applicant or owner names: those fields are not requested and not present in the output schema.

### Coverage and source

- Source: NSW Planning Portal Application Tracker, `https://api.apps1.nsw.gov.au/eplanning/data/v0/DAApplicationTracker`.
- Coverage: 131 council display names and about 650,000 applications in the catalogue observed on 2026-10-26. The dataset includes development applications, complying development certificates and modifications; use `query` with a council or address to narrow it.
- v1 covers New South Wales only. Other Australian states are out of scope.
- Public access only: no login, no paywall, no CAPTCHA, no proxy. The API returns JSON over HTTP, so there is no JavaScript rendering.

### Minimum input

This is the smallest real input and the exact `exampleRunInput` of the Actor:

```json
{
  "mode": "extract",
  "query": "Calvert Street Marrickville",
  "councils": ["Inner West Council"],
  "maxResults": 10,
  "strictSchema": true
}
```

With defaults plus a query like `"Inner West Council"` it returns up to 1,000 applications.

### How to use

1. Run the Actor from the Apify Console, the API or the CLI:

   ```bash
   apify call steadfetch/au-planning-application-monitor --input='{"mode":"extract","query":"Inner West Council","maxResults":100}'
   ```

2. Read the default dataset (JSON, CSV or API).

3. For monitoring, schedule the Actor and switch `mode` to `monitor`. Add `dateFrom` (for example the last 30 days) so each check scans a bounded, recent window.

#### Input fields

| Field | Type | Required | Default | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `mode` | string | Yes | `extract` | `extract` returns every matching application. `monitor` returns only new or changed applications since the previous run. | `"monitor"` |
| `query` | string | Yes | - | Council display name or an address / address fragment. Address fragments are resolved through the portal's public address lookup. | `"Calvert Street Marrickville"` |
| `councils` | array | No | `[]` | Optional council display names to narrow the search. | `["Inner West Council"]` |
| `dateFrom` | string | No | - | Inclusive lodgement date lower bound, `YYYY-MM-DD`. Recommended in monitor mode. | `"2026-07-01"` |
| `dateTo` | string | No | - | Inclusive lodgement date upper bound, `YYYY-MM-DD`. | `"2026-09-30"` |
| `maxResults` | integer | No | 1000 | Maximum applications returned in this run (1-10000). Each returned row is one billable result. | `100` |
| `strictSchema` | boolean | No | true | Fail the run when a row does not match the output schema. Keep enabled. | `true` |

### Output

#### Example output

```json
{
  "url": "https://www.planningportal.nsw.gov.au/map",
  "title": "PAN-652178 - 10 CALVERT STREET MARRICKVILLE 2204",
  "scrapedAt": "2026-10-26T09:00:00.000Z",
  "applicationReference": "PAN-652178",
  "council": "Inner West Council",
  "state": "NSW",
  "address": "10 CALVERT STREET MARRICKVILLE 2204",
  "description": "Demolition,Subdivision,Erection of a new structure,Semi-detached dwelling",
  "applicationType": "Development Application",
  "status": "pending_court_appeal",
  "statusRaw": "Pending Court Appeal",
  "dateLodged": "2026-06-30",
  "dateDetermined": null,
  "sourceUrl": "https://www.planningportal.nsw.gov.au/map",
  "planningPortalId": "PAN-652178",
  "firstSeenAt": "2026-10-26T09:00:00.000Z",
  "lastCheckedAt": "2026-10-26T09:00:00.000Z",
  "changes": ["new"]
}
```

#### Output fields

| Field | Type | Description |
| --- | --- | --- |
| `url` | string | Public source URL (common envelope, required). |
| `title` | string or null | `reference - address`, for readable tables. |
| `scrapedAt` | date-time | Run timestamp in UTC (common envelope, required). |
| `applicationReference` | string | Portal application number (`PAN-...` or `CDC-...`). |
| `council` | string | Council display name. |
| `state` | string | Always `NSW` in v1. |
| `address` | string | Site address as published by the portal. |
| `description` | string or null | Works applied for, comma separated as published. |
| `applicationType` | string or null | Development Application, Complying Development Certificate Application, Modification Application, Review of determination. |
| `status` | string | Normalized snake_case status (`under_assessment`, `approved`, `determined`, ...). |
| `statusRaw` | string or null | Status exactly as published by the source. |
| `dateLodged` | date or null | Lodgement date (`YYYY-MM-DD`). |
| `dateDetermined` | date or null | Determination date, or null while undecided. |
| `sourceUrl` | string | Public source URL. |
| `planningPortalId` | string | Same as `applicationReference`. |
| `firstSeenAt` | date-time | First run that delivered the application (preserved in monitor mode). |
| `lastCheckedAt` | date-time | Run that observed the current values. |
| `changes` | array | `new`, or the changed fields (`status`, `description`, `determination_date`, `lodgement_date`, `address`). |

The portal does not publish each council's own DA number (for example `DA/2026/0548`) in this feed; the stable, state-wide identifier is the portal application number. `statusRaw` and the portal reference let you join a row back to the council tracker when you need the local number.

### Monitor mode

- State is stored per Apify user and per normalized query in the named key-value store `au-planning-application-monitor-state`, under a key that includes the user id. One user never sees another user's watches.
- A run delivers only `new` applications and rows whose status, description, lodgement date, determination date or address changed. Unchanged rows are not pushed.
- New or changed rows beyond `maxResults` stay pending and are delivered by the next run.
- The store keeps the most recent 20,000 applications per watched query; older, least recently seen entries are evicted first.
- A `monitor-check` event is charged once per watched query per run, even when nothing changed. Results are charged separately only when a row is delivered.

### Pricing

Pay per event, charged automatically by the Apify platform:

- `apify-actor-start`: 0.00005 USD per run (Apify-recommended default, includes the first 5 seconds of compute).
- `apify-default-dataset-item`: **1.50 USD per 1,000 applications** (0.0015 USD per row), charged only for validated rows saved to the dataset.
- `monitor-check`: **0.20 USD per 1,000 checks** (0.0002 USD per check), charged once per watched query in a monitor run, including checks where nothing changed.

#### Cost per 1,000

Measured locally on 2026-10-26 over the committed 358-row Inner West fixture: mapping plus schema validation took about **9 ms per 1,000 rows** against the 343 KB saved API response. At 256 MB and Apify's reference of 0.20 USD per GB-hour, parsing compute is about **0.00000013 USD per 1,000 rows**; including HTTP transfer, retries and run overhead the budget is **well under 0.01 USD per 1,000 results**. At 1.50 USD per 1,000 the gross margin on results is above 99 percent.

A monitor check is one run that fetches up to `maxResults` rows and diffs them: at 256 MB a typical 5-10 second check costs about **0.07-0.14 USD per 1,000 checks** at the same compute reference, so the 0.20 USD per 1,000 check fee covers its compute and the result fee carries the margin.

**Works on the Apify free plan**: about **3,300 applications** for the 5 USD monthly credit at 0.0015 USD per result (plus the run-start fee and any monitor checks).

The price sits below the only comparable Australian actor seen in the Store on 2026-10-26 (100 USD per 1,000 with 2 users) and is in line with the 2-5 USD per 1,000 band of the other actors in the niche, with the monitor delta as the differentiator.

### Legal and data protection

- Only public data: no login, no paywall, no CAPTCHA and no account is used or bypassed.
- Only application metadata is extracted. Applicant and owner names are not in the feed and are not requested. The output schema is closed (`additionalProperties: false`), so a personal field cannot slip in silently.
- Requests are spaced out and retried with backoff; the API is a public NSW Government service.
- If a filter target cannot be reached, it is written to the run key-value store under `skipped` with the reason; the rest of the run continues.

### FAQ

**Does it need an API key?** No. The NSW Planning Portal Application Tracker API is public and keyless.

**Why not scrape each council's DA tracker?** The portal feed already unifies 131 NSW councils in one schema, which is more coverage with fewer requests and fewer breakages. Council-specific trackers can be added later as an enrichment source if a buyer needs the local DA number.

**Does it return personal data?** No. It returns the application, the site address, the works and the status. Applicant/owner names are excluded by design.

**What is the difference between `status` and `statusRaw`?** `statusRaw` is exactly what the portal publishes (`Under Assessment`); `status` is the stable snake_case value (`under_assessment`) for filters and joins.

**How do I monitor only new applications?** Use `mode: "monitor"` with a `dateFrom` covering the window you care about and schedule the Actor. The second run returns only what changed.

**Are the dates always present?** No. The portal sometimes has no lodgement or determination date; those fields are null rather than guessed.

### Support

Report bugs or request features through the Apify Store page (Issues tab) or the Steadfetch actors repository. Include the run ID and the input used.

### Maintenance

The Actor is **tested daily** with the stored example input, and its fixtures, schemas and pricing run in the repository CI on every push. The source API and the council list can change; the daily run and the monitor `skipped` list surface breakages instead of hiding them.

# Actor input Schema

## `mode` (type: `string`):

extract: return every application matching the query. monitor: compare the query with the previous run for this Apify user and return only new applications and rows whose status, description or dates changed.

## `query` (type: `string`):

A council display name (for example "Inner West Council") or an address or address fragment (for example "Calvert Street Marrickville" or "10 Calvert Street Marrickville 2204"). Address fragments are resolved through the portal's public address lookup.

## `councils` (type: `array`):

Optional council display names to narrow the search, for example \["Inner West Council"]. Leave empty to search every NSW council covered by the portal.

## `dateFrom` (type: `string`):

Optional inclusive lower bound on the lodgement date. Recommended for monitor mode so each check scans a bounded, recent window.

## `dateTo` (type: `string`):

Optional inclusive upper bound on the lodgement date.

## `maxResults` (type: `integer`):

Maximum number of applications returned in this run. Each returned row is one billable result. In monitor mode the scan and the delivered changes are capped at this number.

## `strictSchema` (type: `boolean`):

Fail the run when an item does not match the output schema. Keep enabled: Steadfetch never publishes Actors without output-schema validation.

## Actor input object example

```json
{
  "mode": "extract",
  "query": "Sydney",
  "councils": [
    "city-of-sydney",
    "inner-west"
  ],
  "dateFrom": "2026-01-01",
  "dateTo": "2026-12-31",
  "maxResults": 100,
  "strictSchema": true
}
```

# Actor output Schema

## `results` (type: `string`):

Dataset items validated against the item schema in dataset_schema.json (fields).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "extract",
    "query": "Calvert Street Marrickville",
    "councils": [
        "Inner West Council"
    ],
    "maxResults": 10,
    "strictSchema": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/au-planning-application-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "extract",
    "query": "Calvert Street Marrickville",
    "councils": ["Inner West Council"],
    "maxResults": 10,
    "strictSchema": True,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/au-planning-application-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "extract",
  "query": "Calvert Street Marrickville",
  "councils": [
    "Inner West Council"
  ],
  "maxResults": 10,
  "strictSchema": true
}' |
apify call steadfetch/au-planning-application-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/au-planning-application-monitor"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/mDKSyiau9wU1SjyiK/builds/ltjKRQZD1tJbG6VCZ/openapi.json
