{
  "openapi": "3.1.0",
  "info": {
    "title": "PlanWire — UK Planning Data Infrastructure API",
    "version": "1.0.0",
    "description": "Production-grade UK planning data infrastructure for developers. Search 19M+ applications across 375 live planning authorities by keyword, location, status, or type. Includes source metadata, freshness signals, document metadata where available, source health, spatial radius search, webhook alerts, and free API keys.",
    "contact": {
      "name": "PlanWire",
      "url": "https://planwire.io",
      "email": "hello@planwire.io"
    },
    "license": {
      "name": "Commercial",
      "url": "https://planwire.io/terms"
    },
    "x-logo": {
      "url": "https://planwire.io/logo.png"
    }
  },
  "servers": [
    {
      "url": "https://api.planwire.io",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Get an API key at https://planwire.io. Free keys retain 25 credits/day, 10 historical samples per response and the first 50 results per query. Applications must have a known date more than 30 days old. Samples omit current status/decisions, documents, source-check timestamps, UPRN and enrichment, with an access object explaining omissions. Current intelligence, property enrichment, API monitoring and live evaluations require a paid plan. Paid-only endpoint rejections do not reserve request credits."
      }
    },
    "schemas": {
      "Application": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Internal PlanWire UUID for fetching this stored record. For durable customer records, store councilId plus reference and use the reference lookup endpoint."
          },
          "councilId": {
            "type": "string",
            "example": "camden"
          },
          "reference": {
            "type": "string",
            "example": "2025/0234/P"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "postcode": {
            "type": "string",
            "nullable": true
          },
          "uprn": {
            "type": "string",
            "nullable": true,
            "description": "Unique Property Reference Number where source-native, officially matched, or matched from OS Open UPRN coordinates."
          },
          "uprnMatchMethod": {
            "type": "string",
            "nullable": true,
            "enum": [
              "source_native",
              "official_address_match",
              "os_open_coordinate",
              "manual"
            ],
            "description": "How the UPRN was attached to the record."
          },
          "uprnMatchConfidence": {
            "type": "number",
            "nullable": true,
            "minimum": 0,
            "maximum": 1,
            "description": "0 to 1 confidence score for the UPRN match."
          },
          "uprnMatchedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the UPRN was attached to the record."
          },
          "lat": {
            "type": "number",
            "nullable": true
          },
          "lng": {
            "type": "number",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "applicationType": {
            "type": "string",
            "example": "Householder",
            "nullable": true
          },
          "status": {
            "type": "string",
            "example": "Pending",
            "nullable": true
          },
          "decision": {
            "type": "string",
            "nullable": true
          },
          "decisionOutcome": {
            "type": "string",
            "enum": [
              "approved",
              "refused",
              "withdrawn",
              "other",
              "unknown"
            ],
            "description": "PlanWire-normalised decision bucket derived from the council decision text, falling back to status when needed."
          },
          "receivedDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "validatedDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "decisionDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "decisionFirstSeenAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When PlanWire first observed a decision date or usable decision outcome for this record. Null for decision facts observed before this field existed."
          },
          "applicationDate": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Best available council application date: receivedDate or validatedDate."
          },
          "applicationDateSource": {
            "type": "string",
            "enum": [
              "receivedDate",
              "validatedDate"
            ],
            "nullable": true
          },
          "applicantName": {
            "type": "string",
            "nullable": true
          },
          "agentName": {
            "type": "string",
            "nullable": true
          },
          "documents": {
            "type": "array",
            "description": "Source document links and metadata where the authority source supplies them.",
            "items": {
              "type": "object",
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "additionalProperties": true
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "sourceCheckedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Top-level representation of freshness.recordCheckedAt. Prefer the freshness object for individual-check results."
          },
          "sourceCheckFailedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Top-level representation of freshness.recordCheckFailedAt; retained after a later successful check."
          },
          "sourceCheckError": {
            "type": "string",
            "nullable": true,
            "description": "Top-level representation of freshness.recordCheckError; cleared on a successful check."
          },
          "council": {
            "type": "object",
            "description": "The local planning authority that published the application.",
            "properties": {
              "id": {
                "type": "string",
                "example": "hounslow"
              },
              "name": {
                "type": "string",
                "example": "London Borough of Hounslow"
              }
            }
          },
          "source": {
            "type": "object",
            "description": "Where the record came from, and when that source was last checked.",
            "properties": {
              "type": {
                "type": "string",
                "example": "assure",
                "description": "Portal software the council runs."
              },
              "label": {
                "type": "string",
                "example": "Assure"
              },
              "lastScrapedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Latest source check timestamp. Kept for API compatibility."
              }
            }
          },
          "propertyIdentity": {
            "type": "object",
            "description": "UPRN property identity and match provenance. Use this to join PlanWire planning records to EPC, price-paid, property and internal asset datasets.",
            "properties": {
              "uprn": {
                "type": "string",
                "nullable": true,
                "description": "Unique Property Reference Number where source-native, officially matched, or matched from OS Open UPRN coordinates."
              },
              "uprnMatchMethod": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "source_native",
                  "official_address_match",
                  "os_open_coordinate",
                  "manual"
                ]
              },
              "uprnMatchConfidence": {
                "type": "number",
                "nullable": true,
                "minimum": 0,
                "maximum": 1
              },
              "uprnMatchedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          },
          "propertyContext": {
            "type": "object",
            "description": "Optional property enrichment for single-application reads. Returned where PlanWire has a safe UPRN/property join or precomputed constraint match. Treat each nested dataset as optional.",
            "properties": {
              "joinKey": {
                "type": "object",
                "description": "Whether this planning record has a UPRN available for joins to property datasets.",
                "properties": {
                  "uprn": {
                    "type": "string",
                    "nullable": true
                  },
                  "available": {
                    "type": "boolean"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              },
              "epc": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Latest EPC facts joined by UPRN where available from the England/Wales EPC dataset.",
                "properties": {
                  "currentEnergyRating": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "potentialEnergyRating": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "floorArea": {
                    "type": [
                      "number",
                      "null"
                    ]
                  },
                  "builtForm": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "propertyType": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "inspectionDate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "lodgementDate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "source": {
                    "type": "string"
                  },
                  "sourceUpdatedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  }
                }
              },
              "soldPrices": {
                "type": [
                  "object",
                  "null"
                ],
                "description": "Safe derived HMLR price-paid summary joined by UPRN where available.",
                "properties": {
                  "latestSalePrice": {
                    "type": [
                      "integer",
                      "null"
                    ]
                  },
                  "latestSaleDate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "saleCount": {
                    "type": "integer"
                  },
                  "firstSaleDate": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date"
                  },
                  "source": {
                    "type": "string"
                  },
                  "sourceAttribution": {
                    "type": "string"
                  },
                  "refreshedAt": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "date-time"
                  }
                }
              },
              "constraints": {
                "type": "array",
                "description": "Precomputed planning/property constraints intersecting or near the application or its UPRN, where available.",
                "items": {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "conservation_area",
                        "listed_building",
                        "flood_zone",
                        "green_belt",
                        "article_4",
                        "tree_preservation_order",
                        "brownfield_land"
                      ]
                    },
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "source": {
                      "type": "string"
                    },
                    "sourceReference": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "relation": {
                      "type": "string",
                      "enum": [
                        "intersects",
                        "within",
                        "nearby"
                      ]
                    },
                    "distanceM": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "matchedAt": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "freshness": {
            "type": "object",
            "description": "Individual record checks, council-wide source checks and application dates are separate signals. recordCheckedAt is the last successful individual detail-page check. sourceCheckStatus and lastScrapedAt are council-wide; they do not prove this application's status is current. dataStatus describes the age of the application's date, not capture completeness.",
            "properties": {
              "sourceCheckStatus": {
                "type": "string",
                "enum": [
                  "fresh",
                  "stale",
                  "unknown"
                ],
                "description": "fresh when the source was checked within 72 hours."
              },
              "sourceAgeHours": {
                "type": "number",
                "nullable": true
              },
              "dataStatus": {
                "type": "string",
                "enum": [
                  "fresh",
                  "stale",
                  "unknown"
                ],
                "description": "fresh when the application itself is 30 days old or less."
              },
              "applicationAgeDays": {
                "type": "integer",
                "nullable": true
              },
              "applicationDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "applicationDateSource": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "receivedDate",
                  "validatedDate",
                  null
                ],
                "description": "Which council field applicationDate was taken from."
              },
              "firstSeenAt": {
                "type": "string",
                "format": "date-time"
              },
              "lastSeenAt": {
                "type": "string",
                "format": "date-time",
                "description": "Last database write to this record. Not a council-page check; use recordCheckedAt."
              },
              "recordCheckedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Last successful check of this application's own council detail page. Advances even if its decision is unchanged. Null means this mechanism has not recorded a successful individual check. Initial refresh checks status/decision fields, not documents or full stage history."
              },
              "recordCheckFailedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Last failed individual check. Retained after later success; compare with recordCheckedAt to determine which happened most recently."
              },
              "recordCheckError": {
                "type": "string",
                "nullable": true,
                "description": "Machine-readable individual-check failure code, cleared on success. Examples: source_timeout, source_blocked, source_rate_limited, detail_reference_mismatch, detail_authority_mismatch. Not an exhaustive list.",
                "example": "source_timeout"
              },
              "lastScrapedAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "Council-wide source check timestamp, not an individual application check."
              },
              "decisionDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "decisionFirstSeenAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true,
                "description": "When PlanWire first observed a decision date or usable decision outcome for this record."
              },
              "note": {
                "type": "string"
              }
            }
          },
          "dates": {
            "type": "object",
            "description": "Every date held for the application, with its provenance.",
            "properties": {
              "applicationDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "applicationDateSource": {
                "type": "string",
                "nullable": true
              },
              "receivedDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "validatedDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "decisionDate": {
                "type": "string",
                "format": "date",
                "nullable": true
              },
              "decisionFirstSeenAt": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              },
              "firstSeenAt": {
                "type": "string",
                "format": "date-time"
              },
              "lastSeenAt": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "floodRisk": {
            "nullable": true,
            "description": "EA Flood Map for Planning zone. Populated on first fetch, cached 30 days. Only present on single-application endpoints (GET /applications/:id and /applications/ref/:council/:reference).",
            "type": "object",
            "properties": {
              "zone": {
                "type": "string",
                "enum": [
                  "low",
                  "1",
                  "2",
                  "3a",
                  "3b"
                ],
                "description": "Flood zone per EA planning policy. low = <0.1% annual probability, 2 = 0.1–1%, 3a = >1%, 3b = functional floodplain."
              },
              "description": {
                "type": "string"
              }
            }
          },
          "nearbyPrices": {
            "nullable": true,
            "description": "Land Registry price paid data for the same postcode, last 2 years. Populated on first fetch, cached 30 days. Only present on single-application endpoints.",
            "type": "object",
            "properties": {
              "avgPrice": {
                "type": "integer",
                "description": "Average sold price in pence"
              },
              "count": {
                "type": "integer",
                "description": "Number of sales used to calculate the average"
              },
              "postcode": {
                "type": "string"
              },
              "periodYears": {
                "type": "integer",
                "example": 2
              }
            }
          },
          "supplierInsights": {
            "$ref": "#/components/schemas/SupplierInsights"
          }
        }
      },
      "Council": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "portal_type": {
            "type": "string",
            "enum": [
              "idox",
              "northgate",
              "uniform",
              "other"
            ]
          },
          "portal_url": {
            "type": "string",
            "format": "uri"
          },
          "active": {
            "type": "boolean"
          },
          "last_scraped_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Latest source check timestamp. Kept for API compatibility."
          },
          "latestApplicationDate": {
            "type": "string",
            "format": "date",
            "nullable": true,
            "description": "Newest receivedDate/validatedDate currently available for this council."
          },
          "latestReceivedDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "latestValidatedDate": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "latestSeenAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "freshness": {
            "type": "object",
            "properties": {
              "sourceCheckStatus": {
                "type": "string",
                "enum": [
                  "fresh",
                  "stale",
                  "unknown"
                ]
              },
              "dataStatus": {
                "type": "string",
                "enum": [
                  "fresh",
                  "stale",
                  "unknown"
                ]
              },
              "sourceAgeHours": {
                "type": "number",
                "nullable": true
              },
              "latestApplicationAgeDays": {
                "type": "integer",
                "nullable": true
              },
              "note": {
                "type": "string"
              }
            }
          }
        }
      },
      "Webhook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "secret": {
            "type": "string"
          },
          "filters": {
            "type": "object",
            "properties": {
              "councilId": {
                "type": "string"
              },
              "postcodePrefix": {
                "type": "string"
              },
              "applicationType": {
                "type": "string"
              },
              "status": {
                "type": "string"
              }
            }
          },
          "active": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "Meta": {
        "type": "object",
        "properties": {
          "total": {
            "type": "integer",
            "nullable": true
          },
          "page": {
            "type": "integer",
            "nullable": true,
            "description": "Null for cursor-based changed-since results."
          },
          "limit": {
            "type": "integer"
          },
          "maxLimit": {
            "type": "integer",
            "example": 100,
            "description": "Maximum records per page for this key. Paid plans can request up to 100; free keys are capped lower."
          },
          "pages": {
            "type": "integer",
            "nullable": true
          },
          "hasMore": {
            "type": "boolean",
            "description": "Present for validation and changed-since window searches."
          },
          "nextCursor": {
            "type": "string",
            "nullable": true,
            "description": "Next changed-since page, or null when finished. Keep selection filters unchanged. Validation-only windows use page instead."
          },
          "changedSince": {
            "type": "string",
            "format": "date-time"
          },
          "changedBefore": {
            "type": "string",
            "format": "date-time",
            "description": "Exclusive polling-window upper bound, preserved by the cursor."
          },
          "sort": {
            "type": "string",
            "example": "applicationDate desc, firstSeenAt desc"
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        }
      },
      "ApplicationRefreshAccepted": {
        "type": "object",
        "required": [
          "data"
        ],
        "properties": {
          "data": {
            "type": "object",
            "required": [
              "id",
              "status",
              "createdAt",
              "applicationId",
              "alreadyQueued"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid",
                "description": "Queue entry ID, not an application ID. No public queue-status endpoint is available in this release."
              },
              "status": {
                "type": "string",
                "enum": [
                  "pending",
                  "running"
                ]
              },
              "createdAt": {
                "type": "string",
                "format": "date-time"
              },
              "applicationId": {
                "type": "string",
                "format": "uuid"
              },
              "alreadyQueued": {
                "type": "boolean",
                "description": "True when pending/running work is reused rather than submitting another council check."
              }
            }
          }
        }
      },
      "ApplicationRefreshError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Present for refresh-specific failures; authentication and ordinary quota errors may omit it."
          },
          "retryAfterSeconds": {
            "type": "integer",
            "description": "3600 for refresh_cooldown. Ordinary rate limits may have a different retry interval."
          }
        }
      },
      "SupplierInsights": {
        "type": "object",
        "description": "Opt-in supplier evaluation metadata. Computed from records already permitted by the underlying endpoint. References are not duplicate-project assertions; classification is inferred and quantities are source mentions only.",
        "required": [
          "version",
          "classification",
          "quantities",
          "history",
          "territory",
          "caveat"
        ],
        "properties": {
          "version": {
            "type": "string",
            "enum": [
              "pilot-1"
            ]
          },
          "classification": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "rule_matches",
                  "unclassified"
                ]
              },
              "applicationRole": {
                "type": "string",
                "enum": [
                  "follow_up",
                  "proposal_or_unspecified"
                ],
                "description": "Follow-up descriptions are flagged so work types referring to an earlier proposal are not counted as new developments."
              },
              "roleEvidence": {
                "allOf": [
                  {
                    "$ref": "#/components/schemas/SupplierEvidence"
                  }
                ],
                "nullable": true
              },
              "categories": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "category": {
                      "type": "string",
                      "enum": [
                        "new_housing",
                        "extension",
                        "industrial",
                        "demolition",
                        "refurbishment"
                      ]
                    },
                    "rule": {
                      "type": "string"
                    },
                    "basis": {
                      "type": "string",
                      "enum": [
                        "description_rule"
                      ]
                    },
                    "evidence": {
                      "$ref": "#/components/schemas/SupplierEvidence"
                    }
                  }
                }
              },
              "note": {
                "type": "string"
              }
            }
          },
          "quantities": {
            "type": "object",
            "properties": {
              "dwellingMentions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SupplierQuantityMention"
                }
              },
              "floorAreaMentions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SupplierQuantityMention"
                }
              },
              "note": {
                "type": "string"
              }
            }
          },
          "history": {
            "type": "object",
            "properties": {
              "recordKey": {
                "type": "string",
                "description": "Council ID and reference; identifies a record, not a development."
              },
              "scope": {
                "type": "string",
                "enum": [
                  "current_record_and_explicit_references"
                ]
              },
              "references": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string"
                    },
                    "councilId": {
                      "type": "string"
                    },
                    "reference": {
                      "type": "string"
                    },
                    "relationship": {
                      "type": "string",
                      "enum": [
                        "explicit_reference"
                      ]
                    },
                    "resolution": {
                      "type": "string",
                      "enum": [
                        "in_response",
                        "not_resolved"
                      ]
                    },
                    "applicationId": {
                      "type": "string",
                      "nullable": true
                    },
                    "apiPath": {
                      "type": "string",
                      "nullable": true
                    },
                    "evidence": {
                      "$ref": "#/components/schemas/SupplierEvidence"
                    }
                  }
                }
              },
              "timeline": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "field": {
                      "type": "string"
                    },
                    "date": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "sourceUrl": {
                      "type": "string",
                      "nullable": true
                    },
                    "basis": {
                      "type": "string",
                      "enum": [
                        "council_record",
                        "planwire_observation"
                      ]
                    }
                  }
                }
              },
              "siteContext": {
                "type": "object",
                "nullable": true,
                "properties": {
                  "uprn": {
                    "type": "string"
                  },
                  "matchMethod": {
                    "type": "string",
                    "nullable": true
                  },
                  "note": {
                    "type": "string"
                  }
                }
              },
              "note": {
                "type": "string"
              }
            }
          },
          "territory": {
            "type": "object",
            "properties": {
              "status": {
                "type": "string",
                "enum": [
                  "matched",
                  "not_established"
                ]
              },
              "reasons": {
                "type": "array",
                "items": {
                  "type": "object",
                  "description": "Council/postcode reasons contain requested and actual values. Radius reasons contain centre, radiusKm, distanceM and database_geography_distance basis."
                }
              },
              "note": {
                "type": "string"
              }
            }
          },
          "caveat": {
            "type": "string"
          }
        }
      },
      "SupplierEvidence": {
        "type": "object",
        "required": [
          "field",
          "text",
          "start",
          "end",
          "sourceUrl"
        ],
        "properties": {
          "field": {
            "type": "string",
            "example": "description"
          },
          "text": {
            "type": "string",
            "description": "Exact substring of the source field."
          },
          "start": {
            "type": "integer",
            "description": "Zero-based UTF-16 offset, inclusive."
          },
          "end": {
            "type": "integer",
            "description": "Zero-based UTF-16 offset, exclusive."
          },
          "sourceUrl": {
            "type": "string",
            "format": "uri",
            "nullable": true
          }
        }
      },
      "SupplierQuantityMention": {
        "type": "object",
        "properties": {
          "value": {
            "type": "number",
            "nullable": true,
            "description": "Literal numeric mention, null for a range. Not a project total."
          },
          "range": {
            "type": "object",
            "nullable": true,
            "properties": {
              "min": {
                "type": "number"
              },
              "max": {
                "type": "number"
              }
            }
          },
          "qualifier": {
            "type": "string",
            "enum": [
              "stated",
              "range",
              "up_to",
              "approximate"
            ]
          },
          "unit": {
            "type": "string",
            "enum": [
              "dwellings",
              "sq_m",
              "sq_ft"
            ]
          },
          "context": {
            "type": "string",
            "enum": [
              "proposed_mention",
              "existing_or_historical_or_mixed",
              "negated_mention",
              "unspecified"
            ]
          },
          "evidence": {
            "$ref": "#/components/schemas/SupplierEvidence"
          }
        }
      }
    },
    "parameters": {
      "SupplierInclude": {
        "name": "include",
        "in": "query",
        "description": "Set to supplier to add pilot-1 supplierInsights: description-rule classifications, literal quantities, explicit references and geographic reasons. No demand or construction-start inference. Existing auth, metering and data-access limits apply.",
        "schema": {
          "type": "string",
          "example": "supplier"
        }
      }
    }
  },
  "paths": {
    "/v1/account/rotate-key": {
      "post": {
        "summary": "Regenerate your API key",
        "description": "Requires the current API key. Immediately invalidates it and returns the replacement once. Update every integration. Does not consume request credits. Contact support from your registered email if the response is lost; do not blindly retry.",
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": ["confirm"], "properties": { "confirm": { "type": "boolean", "enum": [true] } } } } } },
        "responses": {
          "200": { "description": "Replacement credential; response is not cacheable.", "content": { "application/json": { "schema": { "type": "object", "properties": { "key": { "type": "string" }, "message": { "type": "string" } } } } } },
          "400": { "description": "Explicit confirmation is required." },
          "401": { "description": "Missing or invalid API key." },
          "409": { "description": "The key changed concurrently; authenticate again." },
          "429": { "description": "Burst limit exceeded." }
        }
      }
    },
    "/v1/applications": {
      "get": {
        "summary": "List planning applications",
        "description": "Search and filter UK planning applications. Supports keyword search, council filtering, exact postcode or outward-code lookup, broad postcode_prefix lookup, status, application type, and date ranges. Results are sorted by applicationDate desc, then firstSeenAt desc. Free keys return historical application identity/location/description and receipt/validation dates only, not current state, documents, check timestamps or enrichment. The access object documents omitted fields. Status, decision, UPRN and include filters require a paid plan; broad harvest-style searches are restricted on free.",
        "operationId": "listApplications",
        "tags": [
          "Applications"
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Full-text search across address, description, reference, and postcode",
            "schema": {
              "type": "string"
            },
            "example": "rear extension"
          },
          {
            "name": "council",
            "in": "query",
            "description": "Filter by council ID (e.g. 'camden', 'oxford')",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "council_id",
            "in": "query",
            "description": "Alias for council. Filter by council ID (e.g. 'camden', 'argyll-bute')",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postcode",
            "in": "query",
            "description": "Exact full postcode or exact outward code. For example, postcode=LE2 matches applications whose own outward code is LE2; it does not match LE20. Use postcode_prefix for broad prefix scans.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "postcode_prefix",
            "in": "query",
            "description": "Broad postcode prefix search. For exact full postcode or exact outward-code matching, use postcode.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uprn",
            "in": "query",
            "description": "Filter to records attached to one Unique Property Reference Number.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "uprn_in",
            "in": "query",
            "description": "Comma-separated list of up to 100 UPRNs for portfolio or internal asset matching.",
            "schema": {
              "type": "string"
            },
            "example": "100021234567,100021234568"
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter by status: Pending, Approved, Refused, Withdrawn",
            "schema": {
              "type": "string",
              "enum": [
                "Pending",
                "Approved",
                "Refused",
                "Withdrawn"
              ]
            }
          },
          {
            "name": "type",
            "in": "query",
            "description": "Filter by application type (e.g. Householder, Full, Listed Building)",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "date_from",
            "in": "query",
            "description": "Filter by applicationDate from (receivedDate/validatedDate, YYYY-MM-DD)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "date_to",
            "in": "query",
            "description": "Filter by applicationDate to (receivedDate/validatedDate, YYYY-MM-DD)",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "validated_from",
            "in": "query",
            "description": "Inclusive council validatedDate lower bound. No received-date fallback; missing validation dates are excluded. Intersects other filters. Validation-only searches sort by validatedDate and id ascending and use page pagination.",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "validated_to",
            "in": "query",
            "description": "Inclusive council validatedDate upper bound, on or after validated_from when both are provided.",
            "schema": { "type": "string", "format": "date" }
          },
          {
            "name": "changed_since",
            "in": "query",
            "description": "Paid plans: inclusive stored-record updatedAt lower bound, including new records and updates to old or undated applications. ISO timestamp with timezone, up to six fractional digits. Not council publication time, a decision-only log, a deletion feed or an exactly-once stream. Sorts by updatedAt and id ascending. Follow meta.nextCursor with the same filters until null; do not use page offsets. Overlap subsequent polling windows and upsert by id to account for in-flight writes and replica lag. Maintenance writes may be included.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "changed_before",
            "in": "query",
            "description": "Exclusive stored-record updatedAt upper bound. Requires changed_since; must be later than it and not in the future. Defaults to first request time; meta.changedBefore and cursors retain that bound. Only advance the polling checkpoint after all pages succeed; overlap the next window and periodically reconcile tracked records.",
            "schema": { "type": "string", "format": "date-time" }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "Opaque meta.nextCursor from changed-since results. Repeat the same selection filters. Cannot be used without changed_since or with page other than 1. Date-window responses have meta.hasMore and omit exact totals; change-window responses also have meta.changedSince, meta.changedBefore and meta.nextCursor (null when finished).",
            "schema": { "type": "string", "maxLength": 2048 }
          },
          {
            "name": "page",
            "in": "query",
            "description": "Page number for ordinary or validation-only searches. Changed-since searches require cursor pagination instead.",
            "schema": {
              "type": "integer",
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Paid max 100 per page; free keys are capped at 10.",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/SupplierInclude"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Application"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "example": {
                  "data": [
                    {
                      "id": "dcf25a92-b0fb-4755-998c-c6a6d9f53ff7",
                      "councilId": "hounslow",
                      "reference": "P/2026/1742",
                      "url": "https://planningandbuilding.hounslow.gov.uk/NECSWS/ES/Presentation/Planning/OnlinePlanning/OnlinePlanningOverview?applicationNumber=P%2F2026%2F1742",
                      "address": "66 Whitestile Road, Brentford, TW8 9NL",
                      "postcode": "TW8 9NL",
                      "uprn": null,
                      "uprnMatchMethod": null,
                      "uprnMatchConfidence": null,
                      "uprnMatchedAt": null,
                      "lat": 51.493303,
                      "lng": -0.308509,
                      "description": "Certificate of lawfulness for proposed erection of a rear roof extension with a Juliet balcony and windows to match.",
                      "applicationType": "Lawful Development Certificate",
                      "status": "FINAL DECISION",
                      "decision": "GRANT",
                      "decisionOutcome": "approved",
                      "applicationDate": "2026-06-22",
                      "applicationDateSource": "validatedDate",
                      "receivedDate": "2026-06-11",
                      "validatedDate": "2026-06-22",
                      "decisionDate": "2026-08-14",
                      "decisionFirstSeenAt": "2026-08-15T08:42:42.631Z",
                      "createdAt": "2026-06-22T16:55:43.755Z",
                      "updatedAt": "2026-08-15T08:42:42.631Z",
                      "sourceCheckedAt": null,
                      "sourceCheckFailedAt": null,
                      "sourceCheckError": null,
                      "council": {
                        "id": "hounslow",
                        "name": "London Borough of Hounslow"
                      },
                      "source": {
                        "type": "assure",
                        "label": "Assure",
                        "lastScrapedAt": "2026-08-17T07:53:32.183Z"
                      },
                      "propertyIdentity": {
                        "uprn": null,
                        "uprnMatchMethod": null,
                        "uprnMatchConfidence": null,
                        "uprnMatchedAt": null
                      },
                      "freshness": {
                        "sourceCheckStatus": "fresh",
                        "sourceAgeHours": 2.1,
                        "dataStatus": "stale",
                        "applicationAgeDays": 56,
                        "applicationDate": "2026-06-22",
                        "applicationDateSource": "validatedDate",
                        "firstSeenAt": "2026-06-22T16:55:43.755Z",
                        "lastSeenAt": "2026-08-15T08:42:42.631Z",
                        "recordCheckedAt": null,
                        "recordCheckFailedAt": null,
                        "recordCheckError": null,
                        "lastScrapedAt": "2026-08-17T07:53:32.183Z",
                        "decisionDate": "2026-08-14",
                        "decisionFirstSeenAt": "2026-08-15T08:42:42.631Z",
                        "note": "sourceCheckStatus tracks when PlanWire last checked the council source. dataStatus tracks the application date on this record."
                      },
                      "dates": {
                        "applicationDate": "2026-06-22",
                        "applicationDateSource": "validatedDate",
                        "receivedDate": "2026-06-11",
                        "validatedDate": "2026-06-22",
                        "decisionDate": "2026-08-14",
                        "decisionFirstSeenAt": "2026-08-15T08:42:42.631Z",
                        "firstSeenAt": "2026-06-22T16:55:43.755Z",
                        "lastSeenAt": "2026-08-15T08:42:42.631Z"
                      }
                    }
                  ],
                  "meta": {
                    "total": 18300000,
                    "page": 1,
                    "limit": 25,
                    "pages": 613504,
                    "maxLimit": 100,
                    "dataAccess": "production",
                    "sort": "applicationDate desc, firstSeenAt desc"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unauthorized",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/applications/nearby": {
      "get": {
        "summary": "Spatial radius search",
        "description": "Find planning applications within a given radius of a lat/lng coordinate. Uses PostGIS for accurate distance calculations. Free keys are limited to smaller radii and older sample records.",
        "operationId": "nearbyApplications",
        "tags": [
          "Applications"
        ],
        "parameters": [
          {
            "name": "lat",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "example": 51.5074
          },
          {
            "name": "lng",
            "in": "query",
            "required": true,
            "schema": {
              "type": "number"
            },
            "example": -0.1278
          },
          {
            "name": "radius_km",
            "in": "query",
            "schema": {
              "type": "number",
              "default": 1
            },
            "description": "Search radius in kilometres. Paid plans can use larger radii; free keys are capped at 0.5km."
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Paid max 100 per page; free keys are capped at 10.",
            "schema": {
              "type": "integer",
              "default": 25,
              "maximum": 100
            }
          },
          {
            "$ref": "#/components/parameters/SupplierInclude"
          }
        ],
        "responses": {
          "200": {
            "description": "Success — results include distance_m field",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "allOf": [
                          {
                            "$ref": "#/components/schemas/Application"
                          },
                          {
                            "type": "object",
                            "properties": {
                              "distance_m": {
                                "type": "number"
                              }
                            }
                          }
                        ]
                      }
                    },
                    "meta": {
                      "type": "object",
                      "properties": {
                        "lat": {
                          "type": "number"
                        },
                        "lng": {
                          "type": "number"
                        },
                        "radius_km": {
                          "type": "number"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/applications/{id}": {
      "get": {
        "summary": "Get application by ID",
        "description": "Read the stored planning record. This does not re-check the council's planning page. Use POST /v1/applications/{id}/refresh to queue a supported individual check, then inspect freshness on a subsequent read.",
        "operationId": "getApplication",
        "tags": [
          "Applications"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/SupplierInclude"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Application"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found"
          }
        }
      }
    },
    "/v1/applications/refresh-batches": {
      "post": {
        "summary": "Create an asynchronous application refresh batch",
        "operationId": "createApplicationRefreshBatch",
        "tags": ["Applications"],
        "description": "Paid customers can submit up to 50,000 council/reference pairs for guarded reconciliation. References are deduplicated, the request requires an Idempotency-Key header, and each API key may have one active batch. Acceptance does not create an immediate source-request burst: the dispatcher admits at most three records per minute into the existing globally bounded refresh queue and continues to respect council health holds, shared-source pacing and per-record cooldowns.",
        "externalDocs": {
          "description": "Batch refresh workflow",
          "url": "https://planwire.io/docs#refresh-batch"
        },
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "description": "A caller-generated key, unique to this logical batch. Reusing it with the same body returns the original batch; a different body returns 409.",
            "schema": { "type": "string", "maxLength": 200 }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["applications"],
                "properties": {
                  "applications": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 50000,
                    "items": {
                      "type": "object",
                      "required": ["council", "reference"],
                      "properties": {
                        "council": { "type": "string", "description": "PlanWire council id." },
                        "reference": { "type": "string", "maxLength": 200 }
                      }
                    }
                  }
                }
              },
              "example": {
                "applications": [
                  { "council": "southampton", "reference": "26/00784/FUL" },
                  { "council": "bristol", "reference": "26/12543/F" }
                ]
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Batch accepted. Use the Location header or returned id to read progress." },
          "200": { "description": "Idempotent replay of the same accepted batch." },
          "400": { "description": "Invalid batch or missing/invalid Idempotency-Key." },
          "401": { "description": "Missing or invalid API key." },
          "402": { "description": "Paid plan required." },
          "409": { "description": "Idempotency conflict or another batch is already active for this API key." },
          "503": { "description": "Application refresh is disabled." }
        }
      }
    },
    "/v1/applications/refresh-batches/{batchId}": {
      "get": {
        "summary": "Read an application refresh batch",
        "operationId": "getApplicationRefreshBatch",
        "tags": ["Applications"],
        "description": "Returns customer-isolated progress plus paged per-reference outcomes. completed_with_errors means every item reached a terminal state but one or more failed, were unsupported, or were not found.",
        "parameters": [
          { "name": "batchId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "default": 1 } },
          { "name": "limit", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 250, "default": 100 } }
        ],
        "responses": {
          "200": { "description": "Batch progress and per-reference results." },
          "400": { "description": "Invalid batch UUID." },
          "401": { "description": "Missing or invalid API key." },
          "402": { "description": "Paid plan required." },
          "404": { "description": "Batch not found for this API key." }
        }
      }
    },
    "/v1/applications/{id}/refresh": {
      "post": {
        "summary": "Queue an individual application refresh",
        "operationId": "refreshApplication",
        "tags": [
          "Applications"
        ],
        "description": "Paid plans, including Developer, can queue a source check for a stored application. No request body is required. Supported routes are active, live IDOX council detail routes and Stratford-on-Avon's native application detail route; unsupported routes return 409. A 202 response confirms queue acceptance, not source-check completion. Pending/running requests are deduplicated per application across callers. After a recent request, including a failed attempt, new requests are subject to a one-hour cooldown; pending/running work is reused first. Refresh POSTs, including authenticated duplicate/cooldown requests, and subsequent GETs consume the normal API allowance. The background council check is not an additional customer request. The server scheduler checks queued work every minute with bounded sequential processing, but source health holds and load can delay or prevent completion; no one-minute completion guarantee applies. Failed checks are retained, not automatically retried by the individual endpoint. Save the prior freshness fields, then read the application again: a newer recordCheckedAt confirms success, a newer recordCheckFailedAt records failure, and unchanged timestamps do not establish completion. Use the batch endpoints when queue progress and per-reference outcomes are required. Initial refresh enriches status/decision for undecided records and preserves known decisions; it does not refresh documents or reconstruct full stage history.",
        "externalDocs": {
          "description": "Refresh workflow and freshness fields",
          "url": "https://planwire.io/docs#refresh-application"
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Current application UUID from a recent ID or council/reference lookup, not a queue entry ID.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Queued or already pending/running. No source result is implied.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshAccepted"
                },
                "example": {
                  "data": {
                    "id": "10000000-0000-4000-8000-000000000001",
                    "status": "pending",
                    "createdAt": "2026-09-21T12:30:21.000Z",
                    "applicationId": "10000000-0000-4000-8000-000000000002",
                    "alreadyQueued": false
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid application UUID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          },
          "402": {
            "description": "Paid plan required. Free allowance is unchanged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          },
          "404": {
            "description": "No stored application with this ID.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          },
          "409": {
            "description": "refresh_unsupported for an unsupported detail route, or refresh_unavailable for an inactive/unavailable council.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          },
          "429": {
            "description": "Refresh cooldown or ordinary API quota/burst limit. Inspect code and Retry-After; not every 429 is refresh_cooldown.",
            "headers": {
              "Retry-After": {
                "description": "3600 seconds for refresh_cooldown; ordinary API rate limits may use a different value or omit this header.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                },
                "example": {
                  "code": "refresh_cooldown",
                  "error": "A refresh was recently attempted for this application. Retry after the cooldown.",
                  "retryAfterSeconds": 3600
                }
              }
            }
          },
          "503": {
            "description": "refresh_unavailable when disabled, or temporarily_unavailable for a transient queue conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplicationRefreshError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/councils": {
      "get": {
        "summary": "List councils",
        "description": "Returns monitored council coverage. Paid keys receive the full council catalogue; free keys receive a limited redacted preview.",
        "operationId": "listCouncils",
        "tags": [
          "Councils"
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Council"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "summary": "List webhooks",
        "operationId": "listWebhooks",
        "tags": [
          "Webhooks"
        ],
        "responses": {
          "200": {
            "description": "Success"
          }
        }
      },
      "post": {
        "summary": "Create a webhook",
        "description": "Register a URL to receive POST requests when new planning applications match your filter. Each delivery is HMAC-SHA256 signed with your webhook secret. 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.",
        "operationId": "createWebhook",
        "tags": [
          "Webhooks"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "filters": {
                    "type": "object",
                    "properties": {
                      "councilId": {
                        "type": "string"
                      },
                      "postcodePrefix": {
                        "type": "string"
                      },
                      "applicationType": {
                        "type": "string"
                      },
                      "status": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Webhook created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Webhook"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/missed": {
      "get": {
        "summary": "List missed webhook deliveries",
        "description": "Returns failed attempts since the supplied ISO date. Durable events retry for up to 24 hours. Use the events cursor endpoint for complete recorded payload recovery; a failed attempt may subsequently have succeeded.",
        "operationId": "listMissedWebhookDeliveries",
        "tags": [
          "Webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "ISO timestamp. Defaults to seven days ago."
          }
        ],
        "responses": {
          "200": {
            "description": "Failed delivery attempts and matching application references"
          },
          "404": {
            "description": "Webhook not found"
          }
        }
      }
    },
    "/v1/webhooks/{id}/events": {
      "get": {
        "summary": "Recover recorded subscription events",
        "operationId": "recoverWebhookEvents",
        "tags": [
          "Webhooks"
        ],
        "description": "Returns durable payloads in committed cursor order. Starts at durable capture for this subscription, not historical council changes. Persist nextCursor after storing the full page and deduplicate by payload.id. Recovery requests count against the API allowance; pushed deliveries do not. See /docs#webhooks-recovery for the runnable cache example.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[0-9]{1,18}$",
              "default": "0"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 500,
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "data contains payload, cursor, status and attempts; meta contains nextCursor and hasMore"
          },
          "400": {
            "description": "Invalid cursor or limit"
          },
          "404": {
            "description": "Webhook not owned by this key"
          },
          "503": {
            "description": "Durable recovery not yet enabled"
          }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "operationId": "healthCheck",
        "tags": [
          "System"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "db": {
                      "type": "string"
                    },
                    "timestamp": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/applications/ref/{council}/{reference}": {
      "get": {
        "summary": "Get application by council and reference",
        "description": "Fetch the stored record using a council ID and URL-encoded council reference. This does not re-check the council's planning page. Use the returned application id with POST /v1/applications/{id}/refresh for a supported queued source check, then inspect freshness on a subsequent read. include=supplier adds pilot supplier insights. This is a metered request with the same free/paid restrictions as application ID lookup.",
        "operationId": "getApplicationByReference",
        "tags": [
          "Applications"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/SupplierInclude"
          },
          {
            "name": "council",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "reference",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Encode embedded slashes, for example 24%2F00123%2FFUL."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Application"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "API key required or invalid"
          },
          "402": {
            "description": "Record is outside the key's data access"
          },
          "404": {
            "description": "Application not found"
          },
          "429": {
            "description": "Request limit reached"
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "Applications",
      "description": "Search and retrieve planning applications"
    },
    {
      "name": "Councils",
      "description": "Browse available local authorities"
    },
    {
      "name": "Webhooks",
      "description": "Real-time delivery when new applications match your filter"
    },
    {
      "name": "System",
      "description": "Health and status endpoints"
    }
  ]
}
