Planning Applications API Docs

Use PlanWire as a UK planning applications API for council planning records, postcode planning data, radius search, current applications, decisions, source links, freshness metadata, signed webhooks and MCP workflows.

Base URL: https://api.planwire.io  ·  Version: v1

From sample key to a working integration

Keep your API key on your server. Free keys return evaluation samples; verify live coverage and missing fields before selecting a paid plan. A successful sample request confirms the response shape, not full territory coverage.

Get a sample key · Check your councils · Integration walkthrough · Customer workflows · MCP server

Run the tested Node.js council-search example. No dependencies are required. It keeps authentication in a server-side header, limits the sample to five records and handles empty responses and errors. This is a PlanWire reference example, not customer code.

Prefer Python? Search UK planning applications with the tested Python example, or open the public Postman collection for search, record lookup, coverage and webhook recovery (JSON download). Keep credentials local and unshared; webhook recovery requires an existing subscription owned by your paid key.

Common evaluator requests

These are the request shapes most teams test first when comparing UK planning data APIs.

Council planning applications
curl "https://api.planwire.io/v1/applications?council=adur&page=1&limit=5" \
  -H "X-API-Key: YOUR_KEY"
National date-window search
curl "https://api.planwire.io/v1/applications?date_from=2026-08-28&date_to=2026-08-28&limit=100" \
  -H "X-API-Key: YOUR_KEY"
Postcode planning data
curl "https://api.planwire.io/v1/applications?postcode=SW1A&status=Approved&limit=25" \
  -H "X-API-Key: YOUR_KEY"
Radius search
curl "https://api.planwire.io/v1/applications/nearby?lat=51.4975&lng=-0.1278&radius_km=1" \
  -H "X-API-Key: YOUR_KEY"

Authentication

New sample keys are delivered to your signup email. Existing keys continue to work and cannot be retrieved by submitting an email address. Sign in to your account to regenerate a key. This immediately invalidates the old key; update all integrations.

To rotate programmatically, send POST /v1/account/rotate-key with your current API key and JSON body {"confirm":true}. The response contains the new key once. This account operation does not consume request credits. If the response is lost, contact hello@planwire.io from your registered email for recovery; do not blindly retry.

Pass your API key in the X-API-Key header. You can also use Authorization: Bearer YOUR_KEY.

Request headers
X-API-Key: pw_live_a1b2c3d4e5f6...

Errors

All errors return JSON with an error field and an appropriate HTTP status code.

Status Meaning
400Bad request — missing or invalid parameters
401Unauthorised — missing or invalid API key
402Payment required — fresh data or broad coverage requires a paid plan
404Not found — resource does not exist
429Rate limit exceeded — upgrade your plan
500Server error — contact support

Webhook deliveries do not count against the request allowance. API reads, lookups, exports, and backfill requests do.

Pagination

All list endpoints return a meta object. Use ?page=2&limit=50 to paginate. Paid plans can request up to 100 records per page. Free keys are capped at 10 records per page and older sample data.

"meta": {
  "total":  4821,  // total matching records
  "page":   1,     // current page (1-indexed)
  "limit":  25,    // records per page
  "pages":  242    // total pages
}

Freshness & dates

An application's freshness.recordCheckedAt is its last successful individual council-page check. It can advance even when the status or decision has not changed. A null value means no successful individual check has been recorded by this mechanism, not that the council has never been visited.

recordCheckFailedAt
Time of the last failed individual check, or null. Retained after a later success: compare it with recordCheckedAt to determine which happened most recently.
recordCheckError
Machine-readable failure code, or null. Examples include source_timeout, source_blocked, source_rate_limited and detail_authority_mismatch. Cleared on a successful check.
lastSeenAt
Time this stored record was last written. It is not proof that its council page was re-read.
lastScrapedAt
Council-wide source-check time, not the check time for this individual application. sourceCheckStatus also describes the council source, not an individual refresh result.

These fields appear in application responses. To request an individual source check, use Refresh application. A failed check does not erase a previously known decision.

PlanWire exposes source-check freshness and application-date freshness separately. lastScrapedAt means the source was checked or refreshed. It should not be treated on its own as proof that new application records were ingested.

For data recency, use latestApplicationDate on councils and dates.applicationDate on applications. Application lists are sorted by applicationDate desc, firstSeenAt desc by default.

"freshness": {
  "sourceCheckStatus": "fresh",
  "dataStatus":        "stale",
  "lastScrapedAt":     "2026-05-01T09:20:00.000Z",
  "latestApplicationDate": "2026-03-12"
}

Rate limits

Limits reset at midnight UTC. Exceeding the limit returns 429. Free access remains 25 credits/day, up to 10 historical application samples per response and the first 50 results per query, with application dates more than 30 days old. Undated records are not available on free.

Free responses include application identity, location, description and receipt/validation dates. They omit current status, decisions, documents, source-check timestamps, UPRN and enrichment; the access object explains these omissions. They are samples, not historical snapshots or a monitoring feed. Status, decision, UPRN and enrichment filters require a paid plan. Current intelligence (including commencement signals and LPA performance), property flags, changed flags, market evidence, refresh and webhook recovery return HTTP 402 on free keys without reserving request credits. Paid-plan limits are unchanged. Live evaluations and exports are paid, with an agreed scope and no automatic billing.

Plan Requests/day Price
Free25£0
Developer1,000£49/mo + VAT
Production10,000£199/mo + VAT
Business50,000£499/mo + VAT
EnterpriseCustomContact us

GET /v1/applications

List planning applications. Parameters can be combined for council, postcode, date-window, status, decision and keyword workflows. Each application includes council, source, freshness, UPRN property identity where available, decision first-seen, and document metadata so you can verify where the record came from and join it to property datasets.

Query Parameters

Parameter
Type
Description
council
string
Council ID (e.g. adur, camden)
postcode
string
Exact full postcode or exact outward-code filter. Use postcode_prefix for broad prefix scans.
uprn
string
Filter to records attached to one Unique Property Reference Number.
uprn_in
string
Comma-separated list of up to 100 UPRNs for portfolio or internal asset matching.
status
string
Approved · Refused · Pending · Withdrawn
type
string
Application type substring match
decision
string
Decision outcome substring match (e.g. Approved, Refused)
q
string
Full-text search across address + description
date_from
date
Filter by application date from YYYY-MM-DD, using the best available received or validated date.
date_to
date
Filter by application date to YYYY-MM-DD, using the best available received or validated date.
validated_from
date
Inclusive council validatedDate lower bound (YYYY-MM-DD). No received-date fallback; records without a validation date are excluded.
validated_to
date
Inclusive council validatedDate upper bound. Can be used alone or with validated_from and existing filters.
changed_since
datetime
Paid plans: records written at or after this timezone-qualified timestamp, including new records and older applications updated later. Uses updatedAt, not council publication time or a decision-only event log.
changed_before
datetime
Exclusive upper bound, requiring changed_since. Defaults to the first request time, returned as meta.changedBefore and retained in the cursor. Future bounds are rejected.
cursor
string
Use meta.nextCursor for the next changed-since page, with identical filters. Do not use page offsets for changed-since polling.
page
integer
Page number, default 1. Validated-date-only searches use page; changed-since searches use cursor.
limit
integer
Results per page, default 25. Paid max 100; free max 10.

Validation collection and reconciliation

Collect an inclusive validation window with validated_from and validated_to. These searches sort by validatedDate and id ascending. They do not infer dates missing from council records. Existing date_from/date_to behaviour is unchanged; combining date filters intersects them.

curl -G 'https://api.planwire.io/v1/applications' \
  -H 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'council=dartford' \
  --data-urlencode 'validated_from=2026-08-17' \
  --data-urlencode 'validated_to=2026-10-11' \
  --data-urlencode 'limit=100'

For ongoing reconciliation, request changed_since without a validation window to include decisions on older applications. Follow meta.nextCursor until null, keeping the same filters. The order is updatedAt then id ascending; meta.hasMore indicates another page. Date-window responses omit exact totals.

curl -G 'https://api.planwire.io/v1/applications' \
  -H 'X-API-Key: YOUR_API_KEY' \
  --data-urlencode 'council=gravesham' \
  --data-urlencode 'changed_since=2026-10-11T00:00:00Z' \
  --data-urlencode 'limit=100'

Only advance your checkpoint after every page succeeds. Start the next poll slightly before the prior meta.changedBefore and upsert by id to tolerate overlapping results, in-flight writes and replica lag. Choose an overlap suitable for your workflow and periodically reconcile tracked records. This is a live record view, not a frozen snapshot or an exactly-once change stream: records can move to a later window during a read; deletions are not emitted; maintenance writes can appear without a business-field change. Free keys retain their existing historical sample limits and cannot use changed-since monitoring.

Example
curl "https://api.planwire.io/v1/applications?council=adur&q=extension&status=Approved&limit=5" \
  -H "X-API-Key: YOUR_KEY"

Operational record fields

propertyIdentity object

UPRN and match metadata where the authority supplies a source-native UPRN, PlanWire can attach one from an approved official match index, or a record has a conservative OS Open UPRN coordinate match. Use it to join planning records to EPC, property, price-paid and internal asset tables.

propertyContext object

Optional single-record enrichment joined by UPRN or application geometry where available: EPC facts, safe HMLR price-paid summaries, and constraints such as conservation areas, listed buildings, flood zones, green belt, Article 4, TPOs and brownfield land.

decisionFirstSeenAt datetime | null

Immutable timestamp for when PlanWire first observed a decision date or usable decision outcome on this record. Use it to separate publication availability from later row updates.

documents array

Document links and titles where the authority source supplies document metadata. Empty when the source does not expose documents through the checked route.

freshness object

Includes source-check status, source age, application-date status, first-seen, last-seen, decision date, and decision-first-seen fields for audit workflows.

GET /v1/applications/nearby

Find applications within a radius using PostGIS spatial indexing. Extremely fast even over millions of records.

Parameter
Type
Description
lat *
float
Latitude (WGS84)
lng *
float
Longitude (WGS84)
radius_km
float
Search radius in km, default 1. Paid max 50; free max 0.5.
limit
integer
Max results. Paid max 100; free max 10.
Example — 1km around Westminster
curl "https://api.planwire.io/v1/applications/nearby?lat=51.4975&lng=-0.1278&radius_km=1" \
  -H "X-API-Key: YOUR_KEY"
GET /v1/applications/:id

Fetch a single planning application by its UUID. Includes source metadata, document links where available, and current light planning context: EA flood zone plus Land Registry sold prices for the postcode.

# Take the id from a search response and fetch the full record
curl "https://api.planwire.io/v1/applications/$APPLICATION_ID" \
  -H "X-API-Key: YOUR_KEY"

Application ids are internal and can change if a record is re-ingested from its council. If you need a stable handle, use the council reference lookup below — a council's own reference does not change, so it is the right thing to store against your own records.

Light Planning Context Fields

floodRisk object | null

EA Flood Map for Planning zone. zone is one of low, 2, 3a, 3b. Cached 30 days. Null on first call, populated within ~10s.

nearbyPrices object | null

Land Registry price paid data for the same postcode over the last 2 years. Returns avgPrice (pence), count, postcode. Null if no sales in period or on first call.

documents array

Source document titles and URLs where available. Constraint datasets such as listed-building, conservation-area, green-belt and Article 4 flags are not exposed until backed by loaded source datasets.

Application object

Core application fields and nested paths. See the OpenAPI schema for the full endpoint contract. Field names are camelCase in JSON responses. Fields marked | null are absent or null where the source authority does not publish them — coverage varies by council, so treat any single field as optional.

Field Type Notes
idstringInternal PlanWire UUID for fetching this stored record. For durable customer records, store councilId plus reference and use the reference lookup.
councilIdstringPlanWire council slug, e.g. leeds.
council.namestringFull authority name.
referencestringThe authority's own application reference.
urlstring | nullLink to the record on the council portal.
addressstring | nullSite address as published.
postcodestring | nullSite postcode where available.
uprnstring | nullUnique Property Reference Number where source-native, officially matched, or matched from OS Open UPRN coordinates.
uprnMatchMethodstring | nullsource_native, official_address_match, os_open_coordinate, or manual.
uprnMatchConfidencenumber | null0 to 1 confidence score for the UPRN match.
uprnMatchedAttimestamp | nullWhen the UPRN was attached to the record.
propertyContextobjectOptional enrichment on single-application endpoints: EPC, HMLR price-paid summary and constraint matches where available.
latnumber | nullLatitude, WGS84.
lngnumber | nullLongitude, WGS84.
descriptionstring | nullProposal text as published by the authority.
applicationTypestring | nullAuthority's application type label.
statusstring | nullAuthority's status label.
decisionstring | nullDecision text as published.
decisionOutcomestringNormalised outcome: approved, refused, withdrawn, other, or unknown when the source text cannot be classified.
receivedDatedate | nullDate the authority received the application.
validatedDatedate | nullDate the application was validated.
decisionDatedate | nullDate the decision was issued.
applicationDatedate | nullBest available application date: receivedDate, falling back to validatedDate.
applicationDateSourcestring | nullWhich field applicationDate came from: receivedDate or validatedDate.
applicantNamestring | nullApplicant as published. Sparsely populated — present on a minority of records.
agentNamestring | nullAgent as published. Sparsely populated.
documentsarrayObjects of { title, url }. Empty for most records.
source.typestringPortal family the record was sourced from, e.g. idox.
source.lastScrapedAttimestamp | nullWhen PlanWire last checked this council's source.
freshness.sourceAgeHoursnumber | nullHours since the source check; null when the check time is unavailable.
createdAttimestampWhen PlanWire first recorded this application.
updatedAttimestampWhen PlanWire last changed this record.
councilobjectAuthority identifier and name: council.id and council.name.
sourceobjectPortal type, label and source.lastScrapedAt. A source check is not proof of new records.
freshnessobjectSeparate sourceCheckStatus and dataStatus signals, source age and observation dates.
datesobjectApplication dates and their provenance, including dates.decisionFirstSeenAt where known.
propertyIdentityobjectUPRN and match method/confidence; inspect whether the join is available before using it.
POST /v1/property/flags

Planning flags for a batch of postcodes, for filtering a property index before deciding what is worth opening. Returns whether each postcode has any planning history, whether any of it was refused, and when it was last decided — without returning the underlying records.

Metered per property, not per request. A batch of 400 postcodes costs 400 against your daily allowance. Duplicate postcodes are collapsed before charging, so you are not billed twice for the same answer.

Request Body

Field
Type
Description
postcodes *
string[]
Full UK postcodes. Partial codes such as SW1 are rejected rather than widened to a sector.

Batch size by plan

Plan Postcodes per request
Free10
Developer100
Production500
Business500
Flags for a batch of postcodes
curl -X POST "https://api.planwire.io/v1/property/flags" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "postcodes": ["SW1A 1AA", "LS10 1BS", "M1 1AE"]
  }'

Response

Every requested postcode gets a row, including ones with no planning history — that is the answer that lets you skip a property. hasRefusal reads the normalised decision_outcome, not council decision text, so it means the same thing in every authority.

{
  "data": [
    {
      "postcode":            "SW1A 1AA",
      "hasApplication":      true,
      "hasRefusal":          false,
      "lastDecisionDate":    "2025-11-04",
      "lastApplicationDate": "2025-08-19",
      "applicationCount":    12
    }
  ],
  "meta": {
    "requested":            3,
    "charged":              3,   // charged per postcode, after duplicates collapse
    "duplicatesCollapsed":  0,
    "invalidCount":         0,
    "callsToday":           431
  }
}
GET /v1/applications/ref/:council/:reference

Fetch an application using the council's own reference number (e.g. 24/01234/FUL). Single-application reads also include optional property context, flood risk and nearby sold prices where available.

Both this lookup and GET by ID read PlanWire's stored planning record; neither re-checks the council's planning page. Use the returned id with Refresh application when a supported record needs checking.

curl "https://api.planwire.io/v1/applications/ref/adur/AWDM%2F0158%2F25" \
  -H "X-API-Key: YOUR_KEY"

Refresh application

POST /v1/applications/:id/refresh

Queue a council-page check for one stored application using your existing paid API key. No request body is required. Take the UUID from a recent council/reference lookup; the UUID is an internal record identifier.

curl -X POST \
  "https://api.planwire.io/v1/applications/$APPLICATION_ID/refresh" \
  -H "X-API-Key: YOUR_KEY"

202 Accepted means queued, not refreshed. Illustrative response (UUID values abbreviated):

{
  "data": {
    "id": "QUEUE_ID",
    "status": "pending",
    "createdAt": "2026-09-21T12:30:21.000Z",
    "applicationId": "APPLICATION_ID",
    "alreadyQueued": false
  }
}

data.id identifies the queue entry, not the application. Repeating a request while it is pending or running returns the same queue entry with alreadyQueued: true. Use a refresh batch when you need durable progress and per-reference outcomes.

Check the result

Save the application's current freshness fields before submitting. After allowing time for processing, read the application again by UUID or council/reference. A newer recordCheckedAt confirms a successful source check; a newer recordCheckFailedAt records a failure. Inspect recordCheckError and the relative timestamps. If neither has advanced, completion is not yet confirmed. Do not keep POSTing or poll rapidly; subsequent API reads consume requests.

The server scheduler checks for queued work every minute, with bounded sequential processing and customer requests prioritised over automatic revisits. This is not a one-minute completion guarantee: queue load, source health holds and council response times can delay or prevent a check. Failed jobs are retained; the individual endpoint does not automatically retry a failed source check.

Availability and limits

  • Available on paid plans, including Developer. Free keys receive HTTP 402; the free allowance is unchanged.
  • Supported routes are active, live IDOX council detail routes and Stratford-on-Avon's native application detail route. Coverage in the feed does not imply refresh support. Unsupported routes receive HTTP 409. Inactive or unavailable councils also receive HTTP 409.
  • The initial worker checks status and decision fields. It can enrich an undecided record but preserves an existing known decision. It does not refresh documents or reconstruct full stage history.
  • A one-hour cooldown applies per application across callers after a refresh request, including a failed attempt. Pending/running work is deduplicated first. During the cooldown, a new request returns HTTP 429 with code refresh_cooldown and Retry-After: 3600.
  • Refresh POSTs, including authenticated duplicate or cooldown requests, and subsequent GETs use the normal API request allowance. The background council check is not an additional customer API request. Normal daily and burst limits still apply; not every 429 is a refresh cooldown.

Responses

HTTPMeaning
202Queued or already pending/running.
400 / 401Invalid UUID / missing or invalid authentication.
402 / 404Paid plan required / no stored application with this ID.
409refresh_unsupported or refresh_unavailable: this record's council route is not available for refresh.
429Refresh cooldown or normal API rate limit. Inspect the code and Retry-After header.
503refresh_unavailable when disabled, or temporarily_unavailable for a transient queue conflict.

See the OpenAPI specification for the request and response schemas.

Refresh a batch

POST /v1/applications/refresh-batches

Submit up to 50,000 stored council/reference pairs for asynchronous reconciliation. The batch is durable, customer-isolated and idempotent. Only one batch may be active per API key.

curl -X POST \
  "https://api.planwire.io/v1/applications/refresh-batches" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Idempotency-Key: reconciliation-2026-10-19" \
  -H "Content-Type: application/json" \
  --data '{"applications":[
    {"council":"southampton","reference":"26/00784/FUL"},
    {"council":"bristol","reference":"26/12543/F"}
  ]}'

HTTP 202 confirms storage, not completion. Reuse the same idempotency key and body safely after a client timeout. Reusing the key with a different body returns HTTP 409.

Read progress

GET /v1/applications/refresh-batches/:batchId
curl \
  "https://api.planwire.io/v1/applications/refresh-batches/$BATCH_ID?limit=100&page=1" \
  -H "X-API-Key: YOUR_KEY"

The response reports total, completed and failed items plus paged per-reference states: pending, queued, running, done, failed, skipped or not_found. A terminal completed_with_errors batch includes explicit failure reasons for records that could not be checked.

Source protection

A large accepted batch does not create a source-request burst. At most three batch records per minute enter the existing globally bounded queue. Council health holds, shared-source pacing, URL validation and per-record cooldowns remain in force. Completion time therefore depends on source availability and queue load.

Supplier insights Pilot

Add include=supplier to application list, nearby, ID or council-reference requests to receive an optional supplierInsights object. Existing authentication, request metering, response limits and free/paid data access still apply. No extra database searches or external AI calls are made.

curl "https://api.planwire.io/v1/applications?postcode=LS1&include=supplier&limit=25" \
  -H "X-API-Key: YOUR_API_KEY"
  • classification: conservative description rules for new housing, extensions, industrial development, demolition and refurbishment. Each match includes an exact evidence span and source URL where available. Multiple categories can apply; unmatched descriptions stay unclassified.
  • quantities: explicit numeric dwelling mentions and labelled floor-area mentions. Ranges, approximate values, upper bounds and existing/historical contexts are retained. Word-only quantities are not extracted. Mentions are not project totals, net additions or material quantities.
  • history: explicit labelled references in the description, resolved only against records in the returned response and the current council. Reference formats outside the pilot parser remain unlinked. Same-address and UPRN matches are not merged. Recorded dates are not a complete revision history.
  • territory: source postcode/council matches or the existing PostGIS distance from a nearby search. This is geographic relevance, not buying intent. Classifications are not server-side search filters.

Planning permission does not establish that construction has started or that a supplier order exists. The pilot schema is versioned as pilot-1; verify classifications and source evidence before operational use.

Open supplier territory evaluation
GET /v1/councils

List council source records and freshness metadata. Paid plans receive full coverage; free keys receive a limited preview.

Response
{
  "data": [{
    "id":               "adu",
    "name":             "Adur District Council",
    "portalType":       "official",
    "applicationCount": 874
  }]
}
GET /v1/councils/:id

Get a single council's details including the date of the most recent application.

curl "https://api.planwire.io/v1/councils/adu" \
  -H "X-API-Key: YOUR_KEY"

GET /v1/webhooks

List all webhooks registered to your API key.

POST /v1/webhooks

Subscribe to planning application events. You'll receive an HTTP POST to your URL whenever a matching application is new or updated. Developer keys can create one active webhook scoped to a council or postcode prefix. Production supports up to 100 webhooks, including national coverage; Business supports national webhook workflows subject to fair use.

Request Body

Field
Type
Description
url *
string
HTTPS endpoint to POST events to
filters.councilId
string
Only fire for this council
filters.postcodePrefix
string
Only fire for postcodes starting with (e.g. SW1)
filters.status
string
Only fire for this status value
filters.applicationType
string
Only fire for this application type
Create webhook
curl -X POST "https://api.planwire.io/v1/webhooks" \
  -H "X-API-Key: YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/hooks/planning",
    "filters": {
      "postcodePrefix": "SW1",
      "status": "Approved"
    }
  }'

Webhook Payload

Requests are signed with HMAC-SHA256 — verify using the X-PlanWire-Signature header.

{
  "event":       "application.new",   // or "application.updated"
  "timestamp":   "2025-03-19T14:23:01Z",
  "id": "stable-event-uuid",
  "cursor": "12345",
  "changedFields": ["documents"],
  "data": { /* captured application fields, including councilId, reference and documents */ }
}

The application.updated event is emitted when PlanWire stores a changed record, including document links or titles when the authority source supplies a changed non-empty document list.

Verifying signatures

Compute HMAC-SHA256(secret, rawBody) and compare to the X-PlanWire-Signature header value (after stripping the sha256= prefix).

Node.js / Bun
import { createHmac } from 'crypto';

function verify(secret, rawBody, sigHeader) {
  const expected = 'sha256=' + createHmac('sha256', secret)
    .update(rawBody).digest('hex');
  return sigHeader === expected;
}
Python
import hmac, hashlib

def verify(secret, raw_body, sig_header):
    expected = 'sha256=' + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(sig_header, expected)
DELETE /v1/webhooks/:id

Remove a webhook by its UUID.

POST /v1/webhooks/:id/test

Send a test event to verify your endpoint is reachable and your signature verification works.

curl -X POST "https://api.planwire.io/v1/webhooks/YOUR_WEBHOOK_ID/test" \
  -H "X-API-Key: YOUR_KEY"
GET /v1/webhooks/:id/deliveries

View the last 50 delivery attempts for a webhook — useful for debugging failed deliveries.

Response
{
  "data": [{
    "id":          "a3f2...",
    "event":       "application.new",
    "statusCode":  200,
    "success":     true,
    "createdAt":   "2025-03-19T14:23:01Z"
  }]
}
GET /v1/webhooks/:id/missed

List failed delivery attempts since an ISO date. This is an attempt log, not the recovery cursor. Durable events retry with increasing delays for up to 24 hours and remain recoverable after the retry window. Use the events endpoint below to recover complete recorded payloads.

Example
curl "https://api.planwire.io/v1/webhooks/YOUR_WEBHOOK_ID/missed?since=2026-09-01T00:00:00Z" \
  -H "X-API-Key: YOUR_KEY"

Recoverable planning monitoring

GET /v1/webhooks/:id/events?after=0&limit=500 returns captured events in committed cursor order. Store the entire page, then persist meta.nextCursor. Continue while meta.hasMore is true. Deduplicate by event ID; a webhook arriving is not proof that earlier events were received.

Document-only changes emit application.updated once captured. The stream begins with durable capture for your subscription; it cannot recreate earlier missed changes or guarantee that every council document is available. Events are retained for at least 30 days; deleting the webhook removes its recovery stream. Recovery API requests count against your allowance; webhook deliveries do not.

Scoped evaluation and recovery guide · Runnable Bun receiver · Persistent cache and signature verification

Developer includes one council/postcode-scoped webhook. For a scoped paid evaluation or export, send your councils or postcode areas and intended usage to hello@planwire.io. No free live-data offers or automatic billing.

Use sourceHealth on the councils endpoint to distinguish the latest audit status from application freshness. An empty flood result is unavailable screening, not proof of low risk.

Ready to build?

Get an API key →