{"openapi":"3.1.0","info":{"title":"PermitStack","description":"\n## PermitStack Building Permit API\n\nAccess 120M+ building permits across 8,000+ U.S. cities in 48 states and DC (902 active data sources including counties and statewide feeds, plus 75 historical archives), drawn from official city and county permit systems and open-data portals and re-ingested nightly for most sources.\n\n### Getting started\n1. Sign up at [permit-stack.com](https://permit-stack.com/#pricing) for a free API key (100 req/day)\n2. Pass your key as `X-API-Key` header on every request\n3. See the `/v1/permits/search` endpoint to get started\n\n### Rate limits\nTier       | Requests/min | Requests/day\n-----------|--------------|-------------\nFree       | 30           | 100\nIndie      | 30           | 1,000\nHobbyist   | 30           | 2,500\nDeveloper  | 60           | 10,000\nBusiness   | 200          | 100,000\nScale      | 500          | 500,000\n\nFree keys cover the last 30 days of permits; every paid tier has full historical access.\nHigher volume or custom terms: support@permit-stack.com\n\n### Support\nsupport@permit-stack.com\n","termsOfService":"https://permit-stack.com/terms.html","contact":{"name":"PermitStack Support","url":"https://permit-stack.com/","email":"support@permit-stack.com"},"license":{"name":"Commercial -- see Terms","url":"https://permit-stack.com/terms.html"},"version":"1.0.0"},"servers":[{"url":"https://api.permit-stack.com","description":"Production"}],"paths":{"/health":{"get":{"tags":["Health"],"summary":"Health Check","operationId":"health_check","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/stats":{"get":{"tags":["Health"],"summary":"Public Stats","description":"Public stats endpoint — no auth required.\nReturns aggregate coverage numbers for marketing/transparency.\nCached server-side for 30 minutes.","operationId":"public_stats","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/v1/permits/search":{"get":{"tags":["Permits"],"summary":"Search Permits","description":"Search building permits by location, date, category, contractor and more.\n\nFilters combine with AND. Supply at least one narrowing filter -- a city, ZIP,\njurisdiction, date range or lat/lng radius -- for a query that returns quickly.\n\nResults are paginated and ordered newest-first on the permit's best available date\n(`date_issued`, falling back to `date_filed`). `total` is exact up to 10,000; beyond\nthat it stops counting and returns `total: 10000` with `total_capped: true`, meaning\n\"10,000 or more\". If the count cannot finish in time the response carries\n`total: null` with `total_unknown: true` -- an unknown total is reported as unknown\nand never as a number. Rows are unaffected either way; page through them normally.\n\n`category=hvac` matches HVAC and MECHANICAL together, because AC and furnace work is\nfiled under either depending on the city. Unknown parameter names are rejected with a\n400 rather than ignored, so a typo can never silently return unfiltered results.\n\nFree-tier keys are limited to a recent window; paid tiers have full history.","operationId":"search_permits","security":[{"ApiKey":[]}],"parameters":[{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP code","title":"Zip Code"},"description":"5-digit ZIP code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (e.g. 'Wake County', 'Tacoma') or partial match","title":"Jurisdiction"},"description":"Jurisdiction name (e.g. 'Wake County', 'Tacoma') or partial match"},{"name":"address","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Street address, partial + case-insensitive (e.g. '1600 Pennsylvania') — indexed, fast","title":"Address"},"description":"Street address, partial + case-insensitive (e.g. '1600 Pennsylvania') — indexed, fast"},{"name":"lat","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Latitude for radius search","title":"Lat"},"description":"Latitude for radius search"},{"name":"lng","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Longitude for radius search","title":"Lng"},"description":"Longitude for radius search"},{"name":"radius_miles","in":"query","required":false,"schema":{"type":"number","maximum":50,"description":"Radius in miles (used with lat/lng)","default":5.0,"title":"Radius Miles"},"description":"Radius in miles (used with lat/lng)"},{"name":"fields","in":"query","required":false,"schema":{"type":"string","description":"'summary' (default) or 'full'. With 'full' each result is a PermitDetail rather than a PermitSummary -- the same nine extra columns GET /v1/permits/{id} returns (record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license, created_at) -- so you do not fetch them one permit at a time. See the PermitDetail schema for their types; the declared response schema here is PermitSummary, which is the default shape. Developer plan and above.","default":"summary","title":"Fields"},"description":"'summary' (default) or 'full'. With 'full' each result is a PermitDetail rather than a PermitSummary -- the same nine extra columns GET /v1/permits/{id} returns (record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license, created_at) -- so you do not fetch them one permit at a time. See the PermitDetail schema for their types; the declared response schema here is PermitSummary, which is the default shape. Developer plan and above."},{"name":"bbox","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Map-viewport bounding box 'minLng,minLat,maxLng,maxLat'. Returns permits whose location falls inside the box (geocoded permits only).","title":"Bbox"},"description":"Map-viewport bounding box 'minLng,minLat,maxLng,maxLat'. Returns permits whose location falls inside the box (geocoded permits only)."},{"name":"polygon","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A drawn area as a GeoJSON Polygon geometry (URL-encoded), e.g. {\"type\":\"Polygon\",\"coordinates\":[[[lng,lat],...]]}. Returns permits inside the polygon (geocoded permits only).","title":"Polygon"},"description":"A drawn area as a GeoJSON Polygon geometry (URL-encoded), e.g. {\"type\":\"Polygon\",\"coordinates\":[[[lng,lat],...]]}. Returns permits inside the polygon (geocoded permits only)."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Permit category (e.g. solar, SOLAR, roofing, hvac — case insensitive)","title":"Category"},"description":"Permit category (e.g. solar, SOLAR, roofing, hvac — case insensitive)"},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/PermitStatus"},{"type":"null"}],"description":"Permit status (e.g. issued, filed, final)","title":"Status"},"description":"Permit status (e.g. issued, filed, final)"},{"name":"property_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/PropertyType"},{"type":"null"}],"description":"Property type (e.g. residential, commercial)","title":"Property Type"},"description":"Property type (e.g. residential, commercial)"},{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by tag","title":"Tag"},"description":"Filter by tag"},{"name":"record_kind","in":"query","required":false,"schema":{"type":"string","description":"Record kind: 'permit' (default, building permits only), 'contractor', 'tag', 'non_building', 'admin', 'program' (incentive-program records counted through another dataset, e.g. NYSERDA NY-Sun), or 'all'","default":"permit","title":"Record Kind"},"description":"Record kind: 'permit' (default, building permits only), 'contractor', 'tag', 'non_building', 'admin', 'program' (incentive-program records counted through another dataset, e.g. NYSERDA NY-Sun), or 'all'"},{"name":"filed_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Filed on or after this date","title":"Filed After"},"description":"Filed on or after this date"},{"name":"filed_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Filed on or before this date","title":"Filed Before"},"description":"Filed on or before this date"},{"name":"issued_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Issued on or after this date","title":"Issued After"},"description":"Issued on or after this date"},{"name":"issued_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Issued on or before this date","title":"Issued Before"},"description":"Issued on or before this date"},{"name":"date_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"On or after this date, matched against whichever date a record has (issued, else filed). Use this when a source populates only one of issued/filed — e.g. issued-only feeds vs filed-only feeds.","title":"Date After"},"description":"On or after this date, matched against whichever date a record has (issued, else filed). Use this when a source populates only one of issued/filed — e.g. issued-only feeds vs filed-only feeds."},{"name":"date_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"On or before this date, matched against whichever date a record has (issued, else filed).","title":"Date Before"},"description":"On or before this date, matched against whichever date a record has (issued, else filed)."},{"name":"parcel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Parcel number / APN / folio (formatting ignored). Returns permits on that parcel where the source publishes one.","title":"Parcel"},"description":"Parcel number / APN / folio (formatting ignored). Returns permits on that parcel where the source publishes one."},{"name":"min_value","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum estimated value","title":"Min Value"},"description":"Minimum estimated value"},{"name":"max_value","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Maximum estimated value","title":"Max Value"},"description":"Maximum estimated value"},{"name":"min_solar_kw","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum extracted solar system size (kW DC)","title":"Min Solar Kw"},"description":"Minimum extracted solar system size (kW DC)"},{"name":"max_solar_kw","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Maximum extracted solar system size (kW DC)","title":"Max Solar Kw"},"description":"Maximum extracted solar system size (kW DC)"},{"name":"min_sqft","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"description":"Minimum square footage mentioned (from enrichment)","title":"Min Sqft"},"description":"Minimum square footage mentioned (from enrichment)"},{"name":"has_enrichment","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"Only permits that have (true) or lack (false) LLM enrichment","title":"Has Enrichment"},"description":"Only permits that have (true) or lack (false) LLM enrichment"},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Substring match on the enriched work scope","title":"Scope"},"description":"Substring match on the enriched work scope"},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Case-insensitive substring match across description, address, and permit number. Terms under 3 characters are matched but not counted: `total` is null with total_unknown=true (a 1-2 character term has no trigram index and the count would cost tens of seconds).","title":"Q"},"description":"Case-insensitive substring match across description, address, and permit number. Terms under 3 characters are matched but not counted: `total` is null with total_unknown=true (a 1-2 character term has no trigram index and the count would cost tens of seconds)."},{"name":"permit_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact permit number as the issuing jurisdiction assigned it, case-insensitive (surrounding spaces ignored). Narrower and much faster than q= for a known number. The same number can exist in more than one jurisdiction, so add city or state to tell them apart.","title":"Permit Number"},"description":"Exact permit number as the issuing jurisdiction assigned it, case-insensitive (surrounding spaces ignored). Narrower and much faster than q= for a known number. The same number can exist in more than one jurisdiction, so add city or state to tell them apart."},{"name":"contractor_name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Contractor name (partial match)","title":"Contractor Name"},"description":"Contractor name (partial match)"},{"name":"owner_filed","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true = owner-filed permits only (no contractor on record but an owner name is present — often a DIY/homeowner lead, though for feeds that don't capture contractors the owner may be a builder or institution); false = permits that have a contractor","title":"Owner Filed"},"description":"true = owner-filed permits only (no contractor on record but an owner name is present — often a DIY/homeowner lead, though for feeds that don't capture contractors the owner may be a builder or institution); false = permits that have a contractor"},{"name":"has_owner_address","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true = only permits that carry a property-owner mailing address; false = only those that do not. Coverage varies sharply by jurisdiction (we hold the county assessor roll for some and not others), so this is a targeting filter rather than a defect: it returns exactly the rows that are actionable. The address itself is visible on Business and above.","title":"Has Owner Address"},"description":"true = only permits that carry a property-owner mailing address; false = only those that do not. Coverage varies sharply by jurisdiction (we hold the county assessor roll for some and not others), so this is a targeting filter rather than a defect: it returns exactly the rows that are actionable. The address itself is visible on Business and above."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Results per page. Above your plan's maximum this is clamped, not rejected; the response echoes the per_page actually applied.","default":25,"title":"Per Page"},"description":"Results per page. Above your plan's maximum this is clamped, not rejected; the response echoes the per_page actually applied."},{"name":"keyword","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alias of `q`.","deprecated":true,"title":"Keyword"},"description":"Alias of `q`.","deprecated":true},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_after`.","deprecated":true,"title":"Date From"},"description":"Alias of `date_after`.","deprecated":true},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_before`.","deprecated":true,"title":"Date To"},"description":"Alias of `date_before`.","deprecated":true},{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_after`.","deprecated":true,"title":"Start Date"},"description":"Alias of `date_after`.","deprecated":true},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_before`.","deprecated":true,"title":"End Date"},"description":"Alias of `date_before`.","deprecated":true},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Alias of `per_page`. Clamped to your plan's maximum, not rejected.","deprecated":true,"title":"Limit"},"description":"Alias of `per_page`. Clamped to your plan's maximum, not rejected.","deprecated":true},{"name":"count_only","in":"query","required":false,"schema":{"type":"boolean","description":"Return only the total for these filters -- no permit records. Skips the row fetch entirely, so it is markedly cheaper for both of us when you are sizing a query rather than reading it. `results` comes back empty and `total_capped` / `total_unknown` mean exactly what they always do.","default":false,"title":"Count Only"},"description":"Return only the total for these filters -- no permit records. Skips the row fetch entirely, so it is markedly cheaper for both of us when you are sizing a query rather than reading it. `results` comes back empty and `total_capped` / `total_unknown` mean exactly what they always do."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/permits/export":{"get":{"tags":["Permits"],"summary":"Export Permits","description":"Export permits matching filters as CSV. Tier-gated row limits.","operationId":"export_permits","security":[{"ApiKey":[]}],"parameters":[{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip Code"}},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"}},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/PermitStatus"},{"type":"null"}],"title":"Status"}},{"name":"property_type","in":"query","required":false,"schema":{"anyOf":[{"$ref":"#/components/schemas/PropertyType"},{"type":"null"}],"title":"Property Type"}},{"name":"tag","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tag"}},{"name":"record_kind","in":"query","required":false,"schema":{"type":"string","default":"permit","title":"Record Kind"}},{"name":"filed_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Filed After"}},{"name":"filed_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Filed Before"}},{"name":"issued_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Issued After"}},{"name":"issued_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Issued Before"}},{"name":"date_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"On or after this date, matched against whichever date a record has (issued, else filed).","title":"Date After"},"description":"On or after this date, matched against whichever date a record has (issued, else filed)."},{"name":"date_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"On or before this date, matched against whichever date a record has (issued, else filed).","title":"Date Before"},"description":"On or before this date, matched against whichever date a record has (issued, else filed)."},{"name":"parcel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Parcel number / APN / folio (formatting ignored).","title":"Parcel"},"description":"Parcel number / APN / folio (formatting ignored)."},{"name":"min_value","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Value"}},{"name":"max_value","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Value"}},{"name":"min_solar_kw","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Solar Kw"}},{"name":"max_solar_kw","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Max Solar Kw"}},{"name":"min_sqft","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Sqft"}},{"name":"has_enrichment","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Enrichment"}},{"name":"scope","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Q"}},{"name":"contractor_name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name"}},{"name":"owner_filed","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"description":"true = owner-filed permits only (no contractor on record, owner name present — often DIY/homeowner leads, though for feeds without contractor capture the owner may be a builder/institution); false = permits that have a contractor","title":"Owner Filed"},"description":"true = owner-filed permits only (no contractor on record, owner name present — often DIY/homeowner leads, though for feeds without contractor capture the owner may be a builder/institution); false = permits that have a contractor"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"A jurisdiction's id (from /v1/jurisdictions) or its name, partial and case-insensitive -- the same matching as /v1/permits/search. Lets a full load be partitioned along the coverage list.","title":"Jurisdiction"},"description":"A jurisdiction's id (from /v1/jurisdictions) or its name, partial and case-insensitive -- the same matching as /v1/permits/search. Lets a full load be partitioned along the coverage list."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":1,"description":"Rows to return, up to your plan's export maximum (a larger value is refused with 403, never silently lowered). If more rows match than `limit`, the response header X-Permitstack-Truncated is `true`: narrow the filter or split the date range and export again.","default":1000,"title":"Limit"},"description":"Rows to return, up to your plan's export maximum (a larger value is refused with 403, never silently lowered). If more rows match than `limit`, the response header X-Permitstack-Truncated is `true`: narrow the filter or split the date range and export again."},{"name":"keyword","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alias of `q`.","deprecated":true,"title":"Keyword"},"description":"Alias of `q`.","deprecated":true},{"name":"date_from","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_after`.","deprecated":true,"title":"Date From"},"description":"Alias of `date_after`.","deprecated":true},{"name":"date_to","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_before`.","deprecated":true,"title":"Date To"},"description":"Alias of `date_before`.","deprecated":true},{"name":"start_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_after`.","deprecated":true,"title":"Start Date"},"description":"Alias of `date_after`.","deprecated":true},{"name":"end_date","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"Alias of `date_before`.","deprecated":true,"title":"End Date"},"description":"Alias of `date_before`.","deprecated":true}],"responses":{"200":{"description":"Successful Response","content":{"text/csv":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/permits/sync":{"get":{"tags":["Permits"],"summary":"Sync Permits","description":"Incremental data feed (change-data-capture). Page the dataset ordered by\n(updated_at, id). Omit `cursor` for the initial full load; persist `next_cursor` and pass\nit back after each nightly ingest to receive only new + changed permits. Upsert-only:\npermits we delete (duplicate copies removed in cleanups) are listed separately by\nGET /v1/permits/sync/deletions, which pages the same way.\n\n**Keeping a mirror exact.** Upsert every record from this feed, and drop every\n`permit_id` that GET /v1/permits/sync/deletions returns when called with the SAME\n`record_kind`, `state` and `category` you pass here. A permit can leave a filtered\nfeed without being deleted -- re-classified (roofing -> hvac), re-located to another\nstate, re-labelled as a non-building record, or its source switched off -- and the\ndeletions feed then lists it for every filter it no longer matches, and never for one\nit still matches. A permit that moves INTO your filter arrives here with a new\n`updated_at`. Membership changes are recorded from 2026-10-25; category and state moves\nbefore that date were not, so a mirror built with `category=` or `state=` before then\nshould re-pull once (or sync unfiltered with `record_kind=all` and filter locally).","operationId":"sync_permits","security":[{"ApiKey":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous response's `next_cursor`. Omit to start the initial full sync from the beginning.","title":"Cursor"},"description":"Opaque cursor from the previous response's `next_cursor`. Omit to start the initial full sync from the beginning."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Alternative start point: an ISO-8601 UTC timestamp; returns permits with updated_at >= since. Ignored when `cursor` is supplied.","title":"Since"},"description":"Alternative start point: an ISO-8601 UTC timestamp; returns permits with updated_at >= since. Ignored when `cursor` is supplied."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50000,"minimum":1,"description":"Max permits per page (cursor page size, up to 50,000).","default":5000,"title":"Limit"},"description":"Max permits per page (cursor page size, up to 50,000)."},{"name":"record_kind","in":"query","required":false,"schema":{"type":"string","description":"'permit' (default), a specific record_kind, or 'all'.","default":"permit","title":"Record Kind"},"description":"'permit' (default), a specific record_kind, or 'all'."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional 2-letter state filter to scope the feed.","title":"State"},"description":"Optional 2-letter state filter to scope the feed."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional category filter (e.g. solar, roofing).","title":"Category"},"description":"Optional category filter (e.g. solar, roofing)."},{"name":"fields","in":"query","required":false,"schema":{"type":"string","description":"'summary' (default) or 'full'. 'full' adds record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license and created_at to every record, so a mirror does not have to fetch them one permit at a time. OPT-IN: the default payload is unchanged apart from `contractor_id`, added on 2026-09-10 to every permit surface; widening a feed under a consumer with a strict schema is otherwise avoided.","default":"summary","title":"Fields"},"description":"'summary' (default) or 'full'. 'full' adds record_kind, date_expired, fee_amount, stories, units, square_footage, applicant_name, contractor_license and created_at to every record, so a mirror does not have to fetch them one permit at a time. OPT-IN: the default payload is unchanged apart from `contractor_id`, added on 2026-09-10 to every permit surface; widening a feed under a consumer with a strict schema is otherwise avoided."}],"responses":{"200":{"description":"A page of permits in (updated_at, id) order. Keep `next_cursor` and pass it back as `cursor` to resume; `has_more` is false once you are caught up. Each record in `results` has the /v1/permits/search fields (fields=summary) plus the extras listed under `fields=full`. Page size is `limit`, up to 50,000.","content":{"application/json":{"schema":{},"example":{"results":[{"id":"0f6a8f3e-5b1d-4d53-9d0e-2f1b5c8a7e21","permit_number":"BP-2026-01234","status":"ISSUED","category":"ROOFING","tags":["roofing"],"property_type":"COMMERCIAL","address_street":"100 MAIN ST","address_city":"AUSTIN","address_state":"TX","address_zip":"78701","parcel_id":"0201010101","description_raw":"Re-roof, 12,000 sq ft TPO","estimated_value":185000.0,"date_filed":"2026-09-01","date_issued":"2026-09-10","contractor_name":"ACME ROOFING LLC","contractor_id":"6c1d2e3f-4a5b-6c7d-8e9f-0a1b2c3d4e5f","jurisdiction_name":"Austin","latitude":30.2672,"longitude":-97.7431}],"count":1,"has_more":true,"next_cursor":"2026-09-30T03:08:28.999929+00:00|0f6a8f3e-5b1d-4d53-9d0e-2f1b5c8a7e21"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/permits/sync/deletions":{"get":{"tags":["Permits"],"summary":"Sync Deletions","description":"Permits to drop from a mirror built with /v1/permits/sync.\n\n/v1/permits/sync is upsert-only, so a permit we delete (almost always a duplicate copy of a\npermit that still exists under another id) would otherwise live on in a mirror and 404 on\n/v1/permits/{id}. Drop every `permit_id` listed here. Same cursor contract as /sync: omit\n`cursor` (or pass `since`) to start, then pass back `next_cursor`. When the deleted copy's\npermit still exists under another id, `replaced_by` names it. Every delete is recorded by a\ndatabase trigger, so this list covers all deletions from 2026-09-24 on, whatever removed them.\n\n**Call it with the same `record_kind`, `state` and `category` as your /v1/permits/sync\ncalls.** It also lists permits that LEFT that feed without being deleted (`kind =\n\"left_feed\"`, the permit still exists): we stopped serving the source\n(`reason = \"jurisdiction_deactivated\"`), it was re-labelled as a non-building\nrecord (`left_permit_set:<kind>`, or `record_kind_changed:<kind>` for a\nrecord that was not a permit), or -- from 2026-10-25 -- it was re-classified or moved to\nanother state or source (`category_changed:OLD->NEW`,\n`state_changed:OLD->NEW`, `jurisdiction_changed`; for a move,\n`jurisdiction_id` is the source it LEFT and `moved_to_jurisdiction_id` the one it joined).\n`reason` names the latest change, except that a re-label or a stopped source still in\nforce keeps its reason through later changes. Each entry is judged against the permit as it is\nNOW: one that still matches your filters is never listed for you, and one that comes back\nis re-sent by /v1/permits/sync with a new `updated_at`.\n\nA page holds at most `limit` results. To reach them it examines records past ones that do\nnot concern you (up to 250,000 per page), so after a large\nre-classification a page can still hold fewer than `limit` results, even none, while\n`has_more` is true. Keep paging until `has_more` is false; `next_cursor` always advances\npast everything examined.","operationId":"sync_deletions","security":[{"ApiKey":[]}],"parameters":[{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous response's `next_cursor`.","title":"Cursor"},"description":"Opaque cursor from the previous response's `next_cursor`."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"ISO-8601 UTC timestamp; returns deletions with deleted_at >= since. Ignored when `cursor` is supplied. Omit both to page every recorded deletion.","title":"Since"},"description":"ISO-8601 UTC timestamp; returns deletions with deleted_at >= since. Ignored when `cursor` is supplied. Omit both to page every recorded deletion."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":50000,"minimum":1,"description":"Max results per page (up to 50,000). A page can return fewer -- even none -- and still have `has_more`: keep paging while `has_more` is true.","default":5000,"title":"Limit"},"description":"Max results per page (up to 50,000). A page can return fewer -- even none -- and still have `has_more`: keep paging while `has_more` is true."},{"name":"record_kind","in":"query","required":false,"schema":{"type":"string","description":"Pass the same value you pass to /v1/permits/sync: 'permit' (default), a specific record_kind, or 'all'. A permit re-labelled as a non-building record is listed for record_kind=permit and not for record_kind=all, where it is still in the feed.","default":"permit","title":"Record Kind"},"description":"Pass the same value you pass to /v1/permits/sync: 'permit' (default), a specific record_kind, or 'all'. A permit re-labelled as a non-building record is listed for record_kind=permit and not for record_kind=all, where it is still in the feed."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass the same 2-letter state you pass to /v1/permits/sync. Lists permits that left that state's feed, including ones whose own address_state differs from their jurisdiction's.","title":"State"},"description":"Pass the same 2-letter state you pass to /v1/permits/sync. Lists permits that left that state's feed, including ones whose own address_state differs from their jurisdiction's."},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Pass the same category you pass to /v1/permits/sync. Lists permits re-classified out of it (e.g. roofing -> hvac) as well as deletions.","title":"Category"},"description":"Pass the same category you pass to /v1/permits/sync. Lists permits re-classified out of it (e.g. roofing -> hvac) as well as deletions."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/permits/events":{"get":{"tags":["Permits"],"summary":"List Permit Events","description":"What CHANGED, instead of re-reading everything: permit transitions\n(new_permit, status_change, issued, completed, expired) built from the daily\ningest diff, filterable by city, state, category and jurisdiction.\n\n**This is the endpoint to poll on a schedule, and `cursor` is how to poll it.**\nSave the `next_cursor` from each response and pass it back as `cursor` on the\nnext call; you then receive only what you have not already seen, in the order it\nwas detected, with no duplicates and no gap. Page until a response comes back\nshorter than `per_page`, save that last `next_cursor`, and resume there tomorrow.\n\n**WHERE YOU START MATTERS, AND THE FIRST CALL DOES NOT START AT THE BEGINNING.**\nA call with NO cursor returns the NEWEST events and its `next_cursor` is the\nfrontier -- a watermark at \"now\". Pass it straight back and you correctly get 0\nresults, because you have just declared yourself caught up. That is exactly what\na poller wants: from here on you receive everything new. It is NOT a backfill,\nand it will not walk history for you -- there are 75,544 events behind that\nwatermark for the Tampa filter below alone.\n\nTo start somewhere else, build the cursor yourself: it is\n`<ISO-8601 timestamp>|<event id>`, and `|0` is a valid id floor.\n\n    # start polling from now (typical):\n    GET /v1/permits/events?city=Tampa&state=FL&category=roofing\n        &event_type=new_permit                       -> save next_cursor\n    # ...then, from the next call onward:\n    GET ...&cursor=2026-09-08T04%3A09%3A16.381111%2B00%3A00%7C51823904\n\n    # start from a chosen point instead (backfill the last 30 days, then keep polling):\n    GET ...&cursor=2026-08-11T00%3A00%3A00%2B00%3A00%7C0\n\nOne call answers what a paged re-pull of /v1/permits/search costs hundreds of.\nMeasured 2026-09-09, that Tampa query returns **38 records in a single call**;\nthe same question asked by re-pulling search pages 1-100 twelve times a day\ncosts ~9,600 requests and runs into the daily cap.\n\n`detected_after` still works and is fine for an ad-hoc look at a window, but it\nis the wrong tool for a poller: it is INCLUSIVE, and one ingest batch stamps\nthousands of events with a single identical `detected_at` (measured 2026-09-10:\n3,688 timestamps carry over 1,000 events each, the largest 1,370,561). A poller\nthat stores the newest `detected_at` and passes it back therefore re-receives\nthat whole batch every time. `cursor` breaks the tie on the event id and does not.\n\nSupplying a `cursor` returns events oldest-unseen first, which is what lets a\nbatch be paged safely; without one you get the newest first, for browsing.\n`cursor` and `page` are alternatives -- passing both is a 400 rather than a\nsilently skipped page.\n\nIngest runs once a day, so a daily poll is enough; `detected_at` is when WE saw\nthe change, not when the city recorded it. For push instead of poll, register a\nwebhook (developer tier and above) and skip the polling entirely.","operationId":"list_permit_events","security":[{"ApiKey":[]}],"parameters":[{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Filter by event type: new_permit, status_change, issued, completed, expired (the permit's expiration date has passed without it being finished; emitted when that date arrives)","title":"Event Type"},"description":"Filter by event type: new_permit, status_change, issued, completed, expired (the permit's expiration date has passed without it being finished; emitted when that date arrives)"},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Permit category (e.g. solar, battery)","title":"Category"},"description":"Permit category (e.g. solar, battery)"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (partial)","title":"Jurisdiction"},"description":"Jurisdiction name (partial)"},{"name":"permit_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"Events for a single permit id","title":"Permit Id"},"description":"Events for a single permit id"},{"name":"detected_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only events detected on/after this UTC timestamp (ISO 8601). Inclusive, so a batch sharing one timestamp is re-delivered; prefer `cursor` for polling.","title":"Detected After"},"description":"Only events detected on/after this UTC timestamp (ISO 8601). Inclusive, so a batch sharing one timestamp is re-delivered; prefer `cursor` for polling."},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Opaque cursor from the previous response's `next_cursor`. Resumes exactly where you stopped, with no duplicates and no gap. Omit on the first call.","title":"Cursor"},"description":"Opaque cursor from the previous response's `next_cursor`. Resumes exactly where you stopped, with no duplicates and no gap. Omit on the first call."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":50,"title":"Per Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"Alias of `per_page`. Clamped to 100, not rejected.","deprecated":true,"title":"Limit"},"description":"Alias of `per_page`. Clamped to 100, not rejected.","deprecated":true}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitEventsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/permits/{permit_id}":{"get":{"tags":["Permits"],"summary":"Get Permit","description":"Get full details for a single permit.","operationId":"get_permit","security":[{"ApiKey":[]}],"parameters":[{"name":"permit_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Permit Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/permits/address/{address}":{"get":{"tags":["Permits"],"summary":"Get Permits By Address","description":"Get all permits for a specific address (partial match).","operationId":"get_permits_by_address","security":[{"ApiKey":[]}],"parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","title":"Address"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}},{"name":"record_kind","in":"query","required":false,"schema":{"type":"string","description":"'permit' (default), a specific record_kind, or 'all'.","default":"permit","title":"Record Kind"},"description":"'permit' (default), a specific record_kind, or 'all'."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/permits/stats/coverage":{"get":{"tags":["Permits"],"summary":"Get Coverage Stats","description":"Coverage statistics: total permits, every jurisdiction we hold, and the counties they fall in.\n\nEach jurisdiction carries `data_through` (the newest permit we actually hold, measured),\nplus `county` / `county_fips` (the county most of its permits fall in) and `counties`\n(the full measured mix with a `share` of the sample per county, since some cities straddle\na county line). `counties` at the top level is the same data keyed by county, listing the\njurisdictions whose permits fall in each one. A null county means not measured, which is\nthe case for statewide feeds that publish no coordinates.","operationId":"get_coverage_stats","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}},"security":[{"ApiKey":[]}]}},"/v1/plays":{"get":{"tags":["Permits"],"summary":"List Plays","description":"List available trade plays and their parameters.","operationId":"list_plays","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}},"security":[{"ApiKey":[]}]}},"/v1/plays/system-age":{"get":{"tags":["Permits"],"summary":"System Age","description":"Addresses whose LATEST permit in the trade is min–max years old with nothing\nsince — the system is statistically due for replacement. Only trades with a real\nlifecycle are supported (roofing, hvac, mechanical, solar=repower, pool).","operationId":"system_age","security":[{"ApiKey":[]}],"parameters":[{"name":"trade","in":"query","required":true,"schema":{"type":"string","description":"roofing | hvac | mechanical | solar | pool","title":"Trade"},"description":"roofing | hvac | mechanical | solar | pool"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP","title":"Zip Code"},"description":"5-digit ZIP"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (partial)","title":"Jurisdiction"},"description":"Jurisdiction name (partial)"},{"name":"min_age_years","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":40,"minimum":1},{"type":"null"}],"description":"Override the trade's default minimum age","title":"Min Age Years"},"description":"Override the trade's default minimum age"},{"name":"max_age_years","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":60,"minimum":1},{"type":"null"}],"description":"Override the trade's default maximum age","title":"Max Age Years"},"description":"Override the trade's default maximum age"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/plays/battery-retrofit-candidates":{"get":{"tags":["Permits"],"summary":"Battery Retrofit Candidates","description":"Solar permits aged between min/max years whose address has NO battery permit\non file — the storage-retrofit lead list. Requires a location filter.","operationId":"battery_retrofit_candidates","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP","title":"Zip Code"},"description":"5-digit ZIP"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (partial)","title":"Jurisdiction"},"description":"Jurisdiction name (partial)"},{"name":"min_age_years","in":"query","required":false,"schema":{"type":"number","maximum":30,"minimum":0,"description":"Minimum age of the PV permit, in years","default":2,"title":"Min Age Years"},"description":"Minimum age of the PV permit, in years"},{"name":"max_age_years","in":"query","required":false,"schema":{"type":"number","maximum":40,"minimum":0,"description":"Maximum age of the PV permit, in years","default":7,"title":"Max Age Years"},"description":"Maximum age of the PV permit, in years"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/plays/reroof-due":{"get":{"tags":["Permits"],"summary":"Reroof Due","description":"Roofing permits aged min-max years whose address has NO newer roofing permit —\nhomes whose roof is statistically due for replacement. Alias of\n/v1/plays/system-age?trade=roofing (kept as the discoverable roofing-first name).","operationId":"reroof_due","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP","title":"Zip Code"},"description":"5-digit ZIP"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (partial)","title":"Jurisdiction"},"description":"Jurisdiction name (partial)"},{"name":"min_age_years","in":"query","required":false,"schema":{"type":"number","maximum":40,"minimum":1,"description":"Minimum age of the roofing permit, in years","default":12,"title":"Min Age Years"},"description":"Minimum age of the roofing permit, in years"},{"name":"max_age_years","in":"query","required":false,"schema":{"type":"number","maximum":60,"minimum":1,"description":"Maximum age of the roofing permit, in years","default":25,"title":"Max Age Years"},"description":"Maximum age of the roofing permit, in years"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/plays/orphan-recovery":{"get":{"tags":["Permits"],"summary":"Orphan Recovery","description":"PV permits whose installer (contractor, applicant, or owner) matches `installer`\nin a territory — a named competitor's customer base. Requires a location filter.","operationId":"orphan_recovery","security":[{"ApiKey":[]}],"parameters":[{"name":"installer","in":"query","required":true,"schema":{"type":"string","minLength":2,"description":"Installer name to match (contractor/applicant/owner)","title":"Installer"},"description":"Installer name to match (contractor/applicant/owner)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP","title":"Zip Code"},"description":"5-digit ZIP"},{"name":"jurisdiction","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Jurisdiction name (partial)","title":"Jurisdiction"},"description":"Jurisdiction name (partial)"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PermitSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/contractors/search":{"get":{"tags":["Contractors"],"summary":"Search Contractors","description":"Search contractors by name, location, or specialty, ranked by activity score.","operationId":"search_contractors","security":[{"ApiKey":[]}],"parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Contractor name (partial match)","title":"Name"},"description":"Contractor name (partial match)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name","title":"City"},"description":"City name"},{"name":"specialty","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Specialty tag (e.g. solar, roofing, hvac)","title":"Specialty"},"description":"Specialty tag (e.g. solar, roofing, hvac)"},{"name":"license_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Exact state licence number, e.g. 'CBC1262595'. Matched exactly, and also tried uppercased -- so any capitalisation works for the licences stored uppercase, which is 99.96% of them. Combine with license_state when the same number is issued in more than one state.","title":"License Number"},"description":"Exact state licence number, e.g. 'CBC1262595'. Matched exactly, and also tried uppercased -- so any capitalisation works for the licences stored uppercase, which is 99.96% of them. Combine with license_state when the same number is issued in more than one state."},{"name":"license_state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state that ISSUED the licence. Not the same as `state`, which is where the contractor pulls permits.","title":"License State"},"description":"2-letter state that ISSUED the licence. Not the same as `state`, which is where the contractor pulls permits."},{"name":"min_permits","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"description":"Minimum total permits","title":"Min Permits"},"description":"Minimum total permits"},{"name":"min_score","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":100,"minimum":0},{"type":"null"}],"description":"Minimum contractor activity score (0-100)","title":"Min Score"},"description":"Minimum contractor activity score (0-100)"},{"name":"sort","in":"query","required":false,"schema":{"type":"string","description":"Sort order: 'score' (default), 'permits', or 'recent'","default":"score","title":"Sort"},"description":"Sort order: 'score' (default), 'permits', or 'recent'"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContractorSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/contractors/{contractor_id}":{"get":{"tags":["Contractors"],"summary":"Get Contractor","description":"Get a contractor's full profile with permit stats.\n\n`phone`, `email` and the street `address` are contractor contact fields gated to the\nDeveloper plan and up (see /v1/billing/plans for current pricing); on free/indie/hobbyist\nthey return null and `locked_fields` names them. `name`, `license_number`, `specialties`,\n`city`/`state`/`zip_code`, and the stats are available on all plans.","operationId":"get_contractor","security":[{"ApiKey":[]}],"parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContractorProfile"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/contractors/{contractor_id}/permits":{"get":{"tags":["Contractors"],"summary":"Get Contractor Permits","description":"Get all permits associated with a contractor.\n\nFree keys created after the grandfathering cutoff (free_new) see only their recent\nwindow, exactly as on /v1/permits/search and /v1/property/history, and the page is\ncapped at their tier's max_per_page. Paid and grandfathered (free_legacy) keys see the\ncontractor's full history. The window is stated in the X-PermitStack-Date-Window-*\nheaders, the same additive signal property history uses.","operationId":"get_contractor_permits","security":[{"ApiKey":[]}],"parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/contractors/{contractor_id}/licenses":{"get":{"tags":["Licensing"],"summary":"Get Contractor Licenses","description":"Licences the public licence boards publish for one contractor.\n\nA licence is linked by the contractor's licence number in the board's issuing state (the\nnames must also agree -- licence-number spaces collide across boards -- except in\nWashington L&I's roster, where a number on one row is enough), or by the contractor's exact\nnormalised name in its state where that name is unique in the board's roster. A\nregistration (record_kind \"registration\") is linked by name only. No fuzzy matching. Bond,\ninsurance, workers' comp and discipline are Licensing Pro fields. Contact data is not part\nof this dataset.","operationId":"get_contractor_licenses","security":[{"ApiKey":[]}],"parameters":[{"name":"contractor_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Contractor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContractorLicensesResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/licenses/lookup":{"get":{"tags":["Licensing"],"summary":"Lookup License","description":"One licence number in one issuing state, as every roster we hold for that state\npublishes it, plus the contractors in our permit data carrying that licence number.","operationId":"lookup_license","security":[{"ApiKey":[]}],"parameters":[{"name":"license_number","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":40,"description":"Licence number as issued (spaces, dashes and case are ignored)","title":"License Number"},"description":"Licence number as issued (spaces, dashes and case are ignored)"},{"name":"state","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2,"description":"2-letter issuing state","title":"State"},"description":"2-letter issuing state"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LicenseLookupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/certificates/search":{"get":{"tags":["Certificates of Occupancy"],"summary":"Search Certificates","description":"Search certificates of occupancy (and, with kind=completion, certificates of completion).\n\nEach result carries `co_date_basis`, which says what its date means, and every response lists\nthe sources it drew from with their measured `data_through`, `dated_share` and (where the\nsource publishes a total) `completeness`. Add-on dataset: included in Business and Scale,\navailable as an add-on on Developer.","operationId":"search_certificates","security":[{"ApiKey":[]}],"parameters":[{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name (matches the address city or the issuing jurisdiction)","title":"City"},"description":"City name (matches the address city or the issuing jurisdiction)"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"},{"name":"zip_code","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"5-digit ZIP","title":"Zip Code"},"description":"5-digit ZIP"},{"name":"jurisdiction_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"description":"A jurisdiction id from /v1/jurisdictions","title":"Jurisdiction Id"},"description":"A jurisdiction id from /v1/jurisdictions"},{"name":"address","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Street address, partial, case-insensitive","title":"Address"},"description":"Street address, partial, case-insensitive"},{"name":"parcel","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Parcel / APN / folio / BBL (formatting ignored)","title":"Parcel"},"description":"Parcel / APN / folio / BBL (formatting ignored)"},{"name":"lat","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lat"}},{"name":"lng","in":"query","required":false,"schema":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lng"}},{"name":"radius_miles","in":"query","required":false,"schema":{"anyOf":[{"type":"number","maximum":50,"exclusiveMinimum":0},{"type":"null"}],"description":"Radius in miles around lat/lng (default 1)","title":"Radius Miles"},"description":"Radius in miles around lat/lng (default 1)"},{"name":"kind","in":"query","required":false,"schema":{"enum":["occupancy","completion","all"],"type":"string","description":"'occupancy' (default), 'completion' (certificates of completion: close-outs of work that creates no occupancy, e.g. roofs, pools), or 'all'","default":"occupancy","title":"Kind"},"description":"'occupancy' (default), 'completion' (certificates of completion: close-outs of work that creates no occupancy, e.g. roofs, pools), or 'all'"},{"name":"co_type","in":"query","required":false,"schema":{"anyOf":[{"enum":["final","temporary","partial","unknown"],"type":"string"},{"type":"null"}],"title":"Co Type"}},{"name":"certificate_status","in":"query","required":false,"schema":{"anyOf":[{"enum":["issued","expired","pending","void","unknown","all"],"type":"string"},{"type":"null"}],"description":"Default: issued + expired (both were issued). 'pending' = CO applications not yet issued.","title":"Certificate Status"},"description":"Default: issued + expired (both were issued). 'pending' = CO applications not yet issued."},{"name":"renewals","in":"query","required":false,"schema":{"enum":["exclude","include","only"],"type":"string","description":"Temporary-certificate RENEWALS (NYC re-issues a TCO every 90 days). Default 'exclude': finals and first-time TCOs only.","default":"exclude","title":"Renewals"},"description":"Temporary-certificate RENEWALS (NYC re-issues a TCO every 90 days). Default 'exclude': finals and first-time TCOs only."},{"name":"issued_after","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"CO date on or after (undated certificates never match)","title":"Issued After"},"description":"CO date on or after (undated certificates never match)"},{"name":"issued_before","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"description":"CO date on or before","title":"Issued Before"},"description":"CO date on or before"},{"name":"min_units","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"title":"Min Units"}},{"name":"occupancy_class","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Building-code occupancy group as published, e.g. R-3, B","title":"Occupancy Class"},"description":"Building-code occupancy group as published, e.g. R-3, B"},{"name":"permit_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"The building permit the certificate was issued on","title":"Permit Number"},"description":"The building permit the certificate was issued on"},{"name":"co_number","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Co Number"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":25,"title":"Per Page"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificateSearchResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/certificates/coverage":{"get":{"tags":["Certificates of Occupancy"],"summary":"Certificate Coverage","description":"Which places we hold certificates for, how current and how complete. Once the add-on is on\nsale this is UNGATED (any key), so a buyer can check before adding the dataset, like\n/v1/jurisdictions; before that it answers the pre-launch tiers only, like every route here. One row\nper (source, place); a statewide source (CA HCD) lists each city and county it reports for.\n`records` counts canonical issued certificates of occupancy, renewals excluded: exactly what a\ndefault search returns. `completion_records` counts certificates of completion (kind=completion).","operationId":"certificate_coverage","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"2-letter state code","title":"State"},"description":"2-letter state code"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/certificates/{certificate_id}":{"get":{"tags":["Certificates of Occupancy"],"summary":"Get Certificate","description":"One certificate, with the PII-stripped source record. A non-canonical id (a duplicate of\nanother source's row) answers with the canonical one, so a stored id never dead-ends.","operationId":"get_certificate","security":[{"ApiKey":[]}],"parameters":[{"name":"certificate_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Certificate Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CertificateDetail"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/property/history":{"get":{"tags":["Property History"],"summary":"Get Property History","description":"Complete construction history + property profile for an address.\n\nMatches every permit whose street address contains the supplied address;\npass `city`/`state`/`zip` to disambiguate the same street number across\nmetros. Returns a derived profile (permit timeline, category breakdown,\ncontractors, and underwriting signals such as solar/roof/pool age) plus\nthe paginated permit records.\n\nAn address with **no** permits returns `found: false` with an empty\nprofile (HTTP 200) — a clean property is a valid, useful answer, not an\nerror.","operationId":"get_property_history","security":[{"ApiKey":[]}],"parameters":[{"name":"address","in":"query","required":true,"schema":{"type":"string","minLength":3,"description":"Street address to look up (e.g. '123 Main St')","title":"Address"},"description":"Street address to look up (e.g. '123 Main St')"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Optional city to disambiguate identical street addresses","title":"City"},"description":"Optional city to disambiguate identical street addresses"},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}],"description":"Optional 2-letter state code","title":"State"},"description":"Optional 2-letter state code"},{"name":"zip","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":3,"maxLength":10},{"type":"null"}],"description":"Optional ZIP (prefix-matched)","title":"Zip"},"description":"Optional ZIP (prefix-matched)"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Per Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":200,"minimum":1},{"type":"null"}],"description":"Alias of per_page.","title":"Limit"},"description":"Alias of per_page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/property/by-parcel":{"get":{"tags":["Property History"],"summary":"Get Property By Parcel","description":"Complete construction history + property profile for a parcel / APN.\n\nLooks up every permit whose source parcel number matches `parcel`, with\nformatting ignored, so an APN from a county appraiser matches regardless of how\nthe permit feed punctuates it. Pass `state` to disambiguate the same parcel\nnumber used by different counties. Returns the same profile shape as /history\n(timeline, category breakdown, contractors, and roof/solar/HVAC age signals).\n\nParcel coverage spans the county / appraiser / GIS sources that publish a parcel\nid; a handful of Accela-sourced jurisdictions omit parcel in their public export\nand won't match here (use /property/history by address for those).\n\nA parcel with **no** permits returns `found: false` (HTTP 200), not an error.","operationId":"get_property_by_parcel","security":[{"ApiKey":[]}],"parameters":[{"name":"parcel","in":"query","required":true,"schema":{"type":"string","minLength":3,"description":"Parcel number / APN / folio. Formatting is ignored — dashes, dots and spaces are stripped before matching.","title":"Parcel"},"description":"Parcel number / APN / folio. Formatting is ignored — dashes, dots and spaces are stripped before matching."},{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}],"description":"Optional 2-letter state to disambiguate the same parcel number across counties","title":"State"},"description":"Optional 2-letter state to disambiguate the same parcel number across counties"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","maximum":200,"minimum":1,"default":50,"title":"Per Page"}},{"name":"limit","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":200,"minimum":1},{"type":"null"}],"description":"Alias of per_page.","title":"Limit"},"description":"Alias of per_page."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PropertyHistoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/webhooks/":{"get":{"tags":["Webhooks"],"summary":"List Webhooks","description":"List all your registered webhooks.","operationId":"list_webhooks","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}},"security":[{"ApiKey":[]}]},"post":{"tags":["Webhooks"],"summary":"Create Webhook","description":"Register a webhook to be notified when new permits match your filters.\n\nAvailable on the Developer plan and above (see /v1/billing/plans for current\npricing). Maximum 10 webhooks per API key.\nWhen a new permit matches your filters, we'll POST the permit data as JSON to your URL.\nSet contractor_name to track a specific company — you'll get a POST within minutes\nof any permit they pull appearing in our data (competitor tracking).","operationId":"create_webhook","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}},"security":[{"ApiKey":[]}]}},"/v1/webhooks/{webhook_id}":{"delete":{"tags":["Webhooks"],"summary":"Delete Webhook","description":"Delete a webhook.","operationId":"delete_webhook","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}},"patch":{"tags":["Webhooks"],"summary":"Update Webhook","description":"Re-activate (or deactivate) one of your webhooks.\n\nRe-activating resumes delivery from now: permits ingested while the webhook was paused\nare not replayed (use /v1/permits/search or /v1/permits/sync to backfill a gap), so a\nnewly fixed endpoint is not hit with the whole backlog at once. The failure count resets\nto zero.","operationId":"update_webhook","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"is_active","in":"query","required":true,"schema":{"type":"boolean","description":"true re-activates a webhook the failure breaker switched off.","title":"Is Active"},"description":"true re-activates a webhook the failure breaker switched off."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/webhooks/{webhook_id}/test":{"post":{"tags":["Webhooks"],"summary":"Test Webhook","description":"Send a test event to a webhook URL.\n\nUseful for verifying your webhook endpoint is reachable and signature \nvalidation works. Sends a fake permit payload with event=permit.test.","operationId":"test_webhook","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/webhooks/{webhook_id}/deliveries":{"get":{"tags":["Webhooks"],"summary":"Get Webhook Deliveries","description":"Why your webhook is or is not being delivered, seen from our side of the connection.\n\nOnly FAILED attempts are recorded individually; successful deliveries are counted\n(fire_count, last_fired_at). An empty `recent_failures` list is therefore good news, not\nmissing data. `consecutive_failures` is what the automatic pause acts on: when it reaches\nthe limit the webhook is deactivated, so it is the field to watch.","operationId":"get_webhook_deliveries","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"description":"How many recent failed attempts to return.","default":20,"title":"Limit"},"description":"How many recent failed attempts to return."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/webhooks/{webhook_id}/rotate-secret":{"post":{"tags":["Webhooks"],"summary":"Rotate Webhook Secret","description":"Issue a NEW signing secret for this webhook. The old one stops working immediately.\n\nEvery delivery after this call is signed with the new secret, so update your verifier\nfirst. Creating a webhook again with the same settings returns the existing secret; this\nendpoint is the only way to rotate it.","operationId":"rotate_webhook_secret","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/webhooks/{webhook_id}/secret":{"get":{"tags":["Webhooks"],"summary":"Get Webhook Secret","description":"Retrieve your webhook signing secret. Use this to validate signatures.\n\nThe X-PermitStack-Signature header on incoming webhook requests is the\nHMAC-SHA256 of the request body using this secret. Verify before \nprocessing to ensure the request came from PermitStack.","operationId":"get_webhook_secret","security":[{"ApiKey":[]}],"parameters":[{"name":"webhook_id","in":"path","required":true,"schema":{"type":"string","title":"Webhook Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}}}},"/v1/jurisdictions":{"get":{"tags":["Permits"],"summary":"List Covered Jurisdictions","description":"Every jurisdiction we cover, with how current and how complete each one is.\n\n`data_through` is the newest permit we actually hold, measured -- not when we last\npolled the source. `freshness_label` and `completeness_label` are the same sentences the\nexport picker and the delivered manifest use, so this can never disagree with them.\n\n`last_refreshed` is the UTC date we last finished reading the source, whether or not it\nhad anything new: a refresh date, never a currency claim (that is `data_through`).\n`previous_names` lists the names the same jurisdiction `id` was published under before,\noldest first, each with the date it stopped being used. The id never changes on a rename.","operationId":"list_covered_jurisdictions","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}}},"security":[{"ApiKey":[]}]}},"/v1/metrics/monthly":{"get":{"tags":["Metrics","Metrics"],"summary":"Metrics Monthly","description":"Monthly permit counts and total valuation for a city/state/category.\n\nAvailable on the Developer plan and above (see /v1/billing/plans for current pricing).\n\nServed from a nightly rollup, so it answers in milliseconds where the equivalent\nsearch-and-count takes seconds. At least one of `state` or `city` is required: an\nunfiltered national series would be a different (and much larger) product.","operationId":"metrics_monthly","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}],"description":"Two-letter state code, e.g. FL","title":"State"},"description":"Two-letter state code, e.g. FL"},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"City name; case-insensitive","title":"City"},"description":"City name; case-insensitive"},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Permit category, e.g. ROOFING","title":"Category"},"description":"Permit category, e.g. ROOFING"},{"name":"months","in":"query","required":false,"schema":{"type":"integer","maximum":120,"minimum":1,"description":"How many trailing months to return","default":24,"title":"Months"},"description":"How many trailing months to return"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/metrics/current":{"get":{"tags":["Metrics","Metrics"],"summary":"Metrics Current","description":"Category breakdown over a trailing window — the 'what is happening here now' view.\n\nAvailable on the Developer plan and above (see /v1/billing/plans for current pricing).","operationId":"metrics_current","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":2,"maxLength":2},{"type":"null"}],"title":"State"}},{"name":"city","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"}},{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":730,"minimum":28,"description":"Trailing window. Rounded to whole months by the rollup.","default":90,"title":"Days"},"description":"Trailing window. Rounded to whole months by the rollup."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}},"/v1/metrics/cities":{"get":{"tags":["Metrics","Metrics"],"summary":"Metrics Cities","description":"Busiest cities in a state — the ranking customers were building by hand.\n\nAvailable on the Developer plan and above (see /v1/billing/plans for current pricing).","operationId":"metrics_cities","security":[{"ApiKey":[]}],"parameters":[{"name":"state","in":"query","required":true,"schema":{"type":"string","minLength":2,"maxLength":2,"title":"State"}},{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"months","in":"query","required":false,"schema":{"type":"integer","maximum":120,"minimum":1,"default":12,"title":"Months"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":50,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"401":{"description":"Missing or invalid API key. Pass a key as the `X-API-Key` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Invalid API key"}}}},"429":{"description":"Rate limit exceeded -- either the per-minute burst or the daily cap for your plan. Retry after the window resets; the daily cap resets at UTC midnight.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorDetail"},"example":{"detail":"Rate limit exceeded: 30 requests per minute"}}}},"403":{"description":"Your plan does not include this endpoint, or an option you passed. The body is machine-readable: `error` is `feature_locked`, `feature` names the gate, `upgrade_url` links to the cheapest plan that unlocks it, and `current_tier` is the plan you are on.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureLocked"},"example":{"detail":{"error":"feature_locked","feature":"full_fields","message":"fields=full is available on Developer ($79/mo) and above.","upgrade_url":"https://permit-stack.com/?plan=developer&feature=full_fields#pricing","current_tier":"Indie"}}}}}}}}},"components":{"securitySchemes":{"ApiKey":{"type":"apiKey","description":"Your PermitStack API key. Get one at permit-stack.com/#pricing","in":"header","name":"X-API-Key"}},"schemas":{"CertificateDetail":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"co_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Co Number"},"kind":{"type":"string","title":"Kind"},"co_type":{"type":"string","title":"Co Type"},"certificate_status":{"type":"string","title":"Certificate Status"},"is_renewal":{"type":"boolean","title":"Is Renewal","default":false},"co_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Co Date"},"co_date_basis":{"type":"string","title":"Co Date Basis"},"application_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Application Date"},"expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Expiration Date"},"address_street":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Street"},"address_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address City"},"address_state":{"type":"string","title":"Address State"},"address_zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Zip"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude"},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude"},"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id"},"bin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bin"},"occupancy_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Occupancy Class"},"occupancy_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Occupancy Description"},"use_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Description"},"job_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Type"},"units":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units"},"units_existing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units Existing"},"square_footage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Square Footage"},"stories":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Stories"},"linked_permit_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Linked Permit Id"},"linked_permit_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linked Permit Number"},"contractor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name"},"contractor_license":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor License"},"jurisdiction_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Jurisdiction Id"},"source":{"type":"string","title":"Source"},"source_id":{"type":"string","format":"uuid","title":"Source Id"},"origin":{"type":"string","title":"Origin"},"co_type_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Co Type Raw"},"status_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Raw"},"address_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Raw"},"source_record":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Source Record"},"updated_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Updated At"}},"type":"object","required":["id","kind","co_type","certificate_status","co_date_basis","address_state","source","source_id","origin"],"title":"CertificateDetail"},"CertificateSearchResponse":{"properties":{"results":{"items":{"$ref":"#/components/schemas/CertificateSummary"},"type":"array","title":"Results"},"total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total"},"total_capped":{"type":"boolean","title":"Total Capped","default":false},"total_unknown":{"type":"boolean","title":"Total Unknown","default":false},"page":{"type":"integer","title":"Page"},"per_page":{"type":"integer","title":"Per Page"},"sources":{"items":{"$ref":"#/components/schemas/SourceCoverage"},"type":"array","title":"Sources"}},"type":"object","required":["results","total","page","per_page","sources"],"title":"CertificateSearchResponse"},"CertificateSummary":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"co_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Co Number"},"kind":{"type":"string","title":"Kind"},"co_type":{"type":"string","title":"Co Type"},"certificate_status":{"type":"string","title":"Certificate Status"},"is_renewal":{"type":"boolean","title":"Is Renewal","default":false},"co_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Co Date"},"co_date_basis":{"type":"string","title":"Co Date Basis"},"application_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Application Date"},"expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Expiration Date"},"address_street":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Street"},"address_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address City"},"address_state":{"type":"string","title":"Address State"},"address_zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Zip"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude"},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude"},"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id"},"bin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bin"},"occupancy_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Occupancy Class"},"occupancy_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Occupancy Description"},"use_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Use Description"},"job_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Job Type"},"units":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units"},"units_existing":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units Existing"},"square_footage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Square Footage"},"stories":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Stories"},"linked_permit_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Linked Permit Id"},"linked_permit_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Linked Permit Number"},"contractor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name"},"contractor_license":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor License"},"jurisdiction_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Jurisdiction Id"},"source":{"type":"string","title":"Source"},"source_id":{"type":"string","format":"uuid","title":"Source Id"},"origin":{"type":"string","title":"Origin"}},"type":"object","required":["id","kind","co_type","certificate_status","co_date_basis","address_state","source","source_id","origin"],"title":"CertificateSummary"},"ContractorLicensesResponse":{"properties":{"access":{"type":"string","title":"Access"},"licenses":{"items":{"$ref":"#/components/schemas/LicenseRecord"},"type":"array","title":"Licenses"},"total":{"type":"integer","title":"Total"},"locked_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Locked Fields"},"upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Upgrade Url"},"contractor_id":{"type":"string","format":"uuid","title":"Contractor Id"},"contractor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["access","licenses","total","contractor_id"],"title":"ContractorLicensesResponse"},"ContractorProfile":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"license_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License Number"},"license_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License State"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"total_permits":{"type":"integer","title":"Total Permits"},"first_permit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Permit Date"},"last_permit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Last Permit Date"},"specialties":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Specialties"},"score":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Score"},"is_business":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Business"},"phone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"contact_locked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Contact Locked"},"locked_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Locked Fields"},"upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Upgrade Url"},"withheld_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Withheld Fields"},"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address"},"zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip Code"},"recent_categories":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Recent Categories"},"avg_project_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Avg Project Value"}},"type":"object","required":["id","name","license_number","license_state","city","state","total_permits","first_permit_date","last_permit_date","specialties","phone","email","address","zip_code"],"title":"ContractorProfile"},"ContractorSearchResponse":{"properties":{"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"per_page":{"type":"integer","title":"Per Page"},"results":{"items":{"$ref":"#/components/schemas/ContractorSummary"},"type":"array","title":"Results"},"specialties_matched":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Specialties Matched"}},"type":"object","required":["total","page","per_page","results"],"title":"ContractorSearchResponse"},"ContractorSummary":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"type":"string","title":"Name"},"license_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License Number"},"license_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License State"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"total_permits":{"type":"integer","title":"Total Permits"},"first_permit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Permit Date"},"last_permit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Last Permit Date"},"specialties":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Specialties"},"score":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Score"},"is_business":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Business"}},"type":"object","required":["id","name","license_number","license_state","city","state","total_permits","first_permit_date","last_permit_date","specialties"],"title":"ContractorSummary"},"CoverageConfidence":{"properties":{"confidence":{"type":"string","title":"Confidence"},"covered":{"type":"boolean","title":"Covered"},"jurisdiction":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction"},"data_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Status"},"data_through":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Through"},"freshness":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Freshness"},"jurisdiction_permit_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Jurisdiction Permit Count"},"note":{"type":"string","title":"Note"},"tier_window_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tier Window Days"},"tier_window_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier Window From"},"capture_ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Capture Ratio"},"capture_checked_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Capture Checked At"},"capture_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Capture Note"}},"type":"object","required":["confidence","covered","note"],"title":"CoverageConfidence","description":"Honest read on whether a `found: false` is meaningful or just missing data.\n\nThe wedge competitors won't ship: a 'no permit on record' is only useful if we\nactually cover that jurisdiction. We say so explicitly so an automated (e.g.\nunderwriting) workflow can tell UNKNOWN apart from genuinely-no-work."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"Hint":{"properties":{"code":{"type":"string","title":"Code"},"message":{"type":"string","title":"Message"},"docs_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Docs Url"}},"type":"object","required":["code","message"],"title":"Hint","description":"Advisory, additive, never an error. Tells a caller that a cheaper endpoint answers\nthe question their request pattern implies."},"JurisdictionCoverage":{"properties":{"jurisdiction":{"type":"string","title":"Jurisdiction"},"data_through":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Through"},"status":{"type":"string","title":"Status","default":"not_measured"},"results_on_page":{"type":"integer","title":"Results On Page","default":0},"with_contractor":{"type":"integer","title":"With Contractor","default":0}},"type":"object","required":["jurisdiction"],"title":"JurisdictionCoverage","description":"How current one jurisdiction's data is, for the jurisdictions on THIS page of results.\n\nLets you tell \"this source has not yet published the window you asked about\" apart from\n\"this data is missing\": `data_through` is the newest permit we hold for the jurisdiction,\nand `with_contractor` shows how many rows on this page carry a contractor name."},"LicenseLookupResponse":{"properties":{"access":{"type":"string","title":"Access"},"licenses":{"items":{"$ref":"#/components/schemas/LicenseRecord"},"type":"array","title":"Licenses"},"total":{"type":"integer","title":"Total"},"locked_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Locked Fields"},"upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Upgrade Url"},"license_number":{"type":"string","title":"License Number"},"state":{"type":"string","title":"State"},"contractors":{"items":{"$ref":"#/components/schemas/LinkedContractor"},"type":"array","title":"Contractors"}},"type":"object","required":["access","licenses","total","license_number","state","contractors"],"title":"LicenseLookupResponse"},"LicenseRecord":{"properties":{"source":{"type":"string","title":"Source"},"record_kind":{"type":"string","title":"Record Kind","default":"license"},"record_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Record Note"},"issuing_state":{"type":"string","title":"Issuing State"},"license_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License Number"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"license_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"License Type"},"classification":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Classification"},"business_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Business Type"},"is_individual":{"type":"boolean","title":"Is Individual"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"status_normalized":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Normalized"},"status_derived":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Status Derived"},"status_detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Detail"},"in_force":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"In Force"},"issue_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Issue Date"},"effective_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Effective Date"},"expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Expiration Date"},"inactive_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Inactive Date"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip"},"source_updated_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Source Updated Date"},"as_of":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"As Of"},"match":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Match"},"bond_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bond Company"},"bond_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Bond Amount"},"bond_effective_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Bond Effective Date"},"bond_expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Bond Expiration Date"},"bond_cancellation_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Bond Cancellation Date"},"insurance_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Insurance Company"},"insurance_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Insurance Amount"},"insurance_expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Insurance Expiration Date"},"workers_comp_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workers Comp Type"},"workers_comp_company":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workers Comp Company"},"workers_comp_expiration_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Workers Comp Expiration Date"},"discipline_flag":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Discipline Flag"},"discipline_detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Discipline Detail"}},"type":"object","required":["source","issuing_state","is_individual"],"title":"LicenseRecord"},"LinkedContractor":{"properties":{"id":{"type":"string","format":"uuid","title":"Id"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"total_permits":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total Permits"}},"type":"object","required":["id"],"title":"LinkedContractor"},"PermitDetail":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Stable PermitStack id. Pass to GET /v1/permits/{id}.","examples":["f4be62ed-b522-42a3-907b-b4ab4ec1a102"]},"permit_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permit Number","description":"The permit number as the issuing jurisdiction publishes it. Not unique across jurisdictions, and not always present.","examples":["R2608-181"]},"status":{"type":"string","enum":["FILED","ISSUED","IN_PROGRESS","FINAL","EXPIRED","CANCELLED","REVOKED","UNKNOWN","INTERCONNECTED"],"title":"Status","description":"One of FILED, ISSUED, IN_PROGRESS, FINAL, EXPIRED, CANCELLED, REVOKED, INTERCONNECTED, UNKNOWN -- normalised by us from each source's own vocabulary. UNKNOWN means the source published no status, never that the permit is inactive. INTERCONNECTED comes from the California NEM solar feed.","examples":["ISSUED"]},"category":{"type":"string","enum":["NEW_CONSTRUCTION","RENOVATION","DEMOLITION","ELECTRICAL","PLUMBING","MECHANICAL","ROOFING","SOLAR","BATTERY","EV_CHARGER","HVAC","FIRE_ALARM","SIGN","FENCE","POOL","FOUNDATION","ADDITION","INTERIOR_REMODEL","GRADING","OTHER"],"title":"Category","description":"Trade/work classification, UPPERCASE. One of: NEW_CONSTRUCTION, RENOVATION, DEMOLITION, ELECTRICAL, PLUMBING, MECHANICAL, ROOFING, SOLAR, BATTERY, EV_CHARGER, HVAC, FIRE_ALARM, SIGN, FENCE, POOL, FOUNDATION, ADDITION, INTERIOR_REMODEL, GRADING, OTHER. Derived by us from the permit type and description, not published by the city. NOTE when FILTERING: ?category=hvac matches HVAC *and* MECHANICAL, and ?category=mechanical does the same, because HVAC work is routinely filed as MECHANICAL -- they are one market and a bare equality would hide half of it.","examples":["ROOFING"]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Free-form keywords extracted from the description (e.g. 'pool', 'reroof'). Additive; do not rely on a fixed vocabulary.","examples":[["pool"]]},"property_type":{"type":"string","title":"Property Type","description":"RESIDENTIAL, COMMERCIAL, INDUSTRIAL, MIXED_USE or UNKNOWN. UNKNOWN is common -- many feeds publish nothing that implies a property type.","examples":["RESIDENTIAL"]},"address_street":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Street","description":"Street address as published. Stored verbatim, so spacing can be irregular; our address matching normalises whitespace for you.","examples":["288 Mosaic Blvd"]},"address_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address City","description":"City. Null where the feed publishes none -- notably county-wide feeds. Read null as unknown, never as 'not in a city'.","examples":["Daytona Beach"]},"address_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address State","description":"2-letter state. Null on 6.6M permits (7.3%) whose feed never populates it; those rows are still returned by ?state= searches, which admit them on independent evidence of the jurisdiction.","examples":["FL"]},"address_zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Zip","description":"5-digit ZIP where published.","examples":["32124"]},"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id","description":"Assessor parcel number / APN exactly as the SOURCE publishes it -- formatting varies by county and is not normalised. ~41% populated estate-wide and entirely dependent on whether the source publishes one, so read a null as 'this feed does not carry a parcel', never as 'this property has none'. Filterable via ?parcel=.","examples":["521804000160"]},"description_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description Raw","description":"The scope of work exactly as the jurisdiction published it. The single most useful field for lead qualification, and the input our category classifier reads.","examples":["RE-ROOF: remove existing shingles, install 39 sq architectural shingles"]},"estimated_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Value","description":"Declared job value in USD, as published. Frequently null and occasionally nominal -- treat 0 and 1 as 'not stated'.","examples":[24500.0]},"date_filed":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Filed","description":"Application/filing date. Null where the source publishes only an issue date.","examples":["2026-08-14"]},"date_issued":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Issued","description":"Issue date. Null on ~25% of permits estate-wide -- whole feeds publish only a filed date -- which is why date_after/date_before filter on COALESCE(date_issued, date_filed) and issued_after does not.","examples":["2026-08-21"]},"date_completed":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Completed","description":"Completion/final date where the source publishes one. Usually null.","examples":[null]},"approval_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Approval Days","description":"Calendar days from date_filed to date_issued (null unless both dates present)"},"construction_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Construction Days","description":"Calendar days from date_issued to date_completed (null unless both dates present)"},"contractor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name","description":"Contractor of record, cleaned and de-duplicated by us. Null where the feed publishes no contractor -- which is whole jurisdictions, not scattered rows: 45% of permits estate-wide carry one. A page of nulls usually means the source does not publish contractors, not that our data is missing; the `jurisdiction_coverage` block on a search response tells you which.","examples":["GLZ Industries LLC"]},"contractor_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contractor Id","description":"Opaque id of the contractor on this permit. Pass it to GET /v1/contractors/{id} for the full record; null when the source publishes no contractor for this permit."},"owner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Name","description":"Owner of record. contractor_name null + owner_name set means no contractor was recorded -- often a homeowner who pulled the permit themselves, which is a sales lead. Caveat: on feeds that capture no contractor at all, EVERY permit looks owner-filed and the owner may be a builder, not a homeowner. Use the ?owner_filed= filter rather than inferring this yourself.","examples":["SMITH JOHN A"]},"owner_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Address","description":"Property-owner MAILING address. Business plan ($149/mo) and above; null on every other plan, which is a gate and not an absence of data. Joined from county assessor rolls by parcel, so coverage is bimodal -- near-complete in the jurisdictions whose roll we have loaded, absent in those we have not. Never read a null as 'this property has no owner on record'.","examples":[null]},"jurisdiction_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction Name","description":"The PermitStack data source this permit came from -- a city, county or statewide feed, which is not always the permit's own city.","examples":["Daytona Beach"]},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude","description":"WGS84 latitude. Null where the source publishes no coordinates and we could not geocode it; such permits are invisible to radius, bbox and polygon search.","examples":[29.1872]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude","description":"WGS84 longitude.","examples":[-81.0559]},"location_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Source","description":"Where the coordinates came from. 'source' = published by the city with the permit record, as good as the city's own data. 'geocoded' = derived by us from the site address (TIGER, address-centroid quality), which for a structure set back from the road, such as an antenna mast, marks the property address rather than the structure. 'derived' = set by us by another method (typically a parcel-centroid join), so treat it as ours, not the city's. 'source' is judged per feed: in a feed that publishes coordinates, a minority of rows whose record lacked them may have been filled by our geocoder before 2026-09-21, when it began logging successes, and those read 'source'. Null when latitude/longitude are null.","examples":["source"]},"enrichment":{"anyOf":[{"$ref":"#/components/schemas/PermitEnrichment"},{"type":"null"}],"description":"Structured detail parsed from the description (e.g. solar kW) where we could extract it. Null for most permits."},"record_kind":{"type":"string","title":"Record Kind","description":"permit | contractor | tag | non_building | admin. Some portals mix non-permit records (contractor registrations, right-of-way, administrative) into the same feed; we store them and label them rather than counting them as permits. Search and export return record_kind='permit' only unless you ask otherwise.","default":"permit","examples":["permit"]},"date_expired":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Expired","description":"Expiry date where published. Deliberately NOT null-checked against the future the way the other dates are -- an expiry legitimately lies ahead.","examples":["2027-02-21"]},"fee_amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Fee Amount","description":"Permit/inspection fee. Frequently null — most open-data feeds do not publish fee data."},"stories":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Stories","description":"Building stories, where published.","examples":[1]},"units":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units","description":"Dwelling/tenant units, where published.","examples":[1]},"square_footage":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Square Footage","description":"Project square footage, where published. Usually null.","examples":[2100.0]},"applicant_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Applicant Name","description":"Whoever filed the application. On most feeds this is the OWNER; on a few it is the contractor, and for those tenants we map it to contractor_name instead. Never assume which one it is from this field alone.","examples":["WINDOW WORLD OF DAYTONA"]},"contractor_license":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor License","description":"Contractor's licence number as published or as matched against a state licence-board roster. A free public identifier; contact details remain gated to /v1/contractors/{id} on Developer+.","examples":["CBC1262595"]},"created_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Created At","description":"When PermitStack first ingested this record (ISO 8601). NOT a date the city published -- use date_filed/date_issued for that.","examples":["2026-08-22T04:11:03.882174+00:00"]}},"type":"object","required":["id","permit_number","status","category","tags","property_type","address_street","address_city","address_state","address_zip","description_raw","estimated_value","date_filed","date_issued","date_completed","date_expired","stories","units","square_footage","applicant_name"],"title":"PermitDetail","description":"PermitSummary plus the nine columns that only a single-permit lookup used to carry.\nAlso returned inline by search and sync with ?fields=full (Developer and above)."},"PermitEnrichment":{"properties":{"scope":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scope"},"work_summary":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Work Summary"},"solar_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Solar Kw"},"sqft":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Sqft"},"units":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Units"},"is_residential":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Residential"},"is_commercial":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Commercial"},"materials":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Materials"}},"type":"object","title":"PermitEnrichment","description":"Curated subset of the offline LLM enrichment (permits.enriched_data).\n\nPresent only on the ~1-2% of permits that have been enriched so far\n(prioritized toward SOLAR/ROOFING/NEW_CONSTRUCTION). null otherwise."},"PermitEventOut":{"properties":{"id":{"type":"integer","title":"Id"},"permit_id":{"type":"string","format":"uuid","title":"Permit Id"},"event_type":{"type":"string","title":"Event Type"},"old_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Old Value"},"new_value":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"New Value"},"occurred_on":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Occurred On"},"detected_at":{"type":"string","format":"date-time","title":"Detected At"},"permit_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permit Number"},"address_street":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Street"},"address_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address City"},"address_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address State"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"jurisdiction_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction Name"},"contractor_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contractor Id","description":"Stable id of the contractor of record, joinable to /v1/contractors/{id}. Null where the source publishes no contractor."},"property_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Property Type","description":"RESIDENTIAL, COMMERCIAL, MIXED_USE, ... or UNKNOWN, as on /v1/permits/search."},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description","description":"The permit's scope of work as the jurisdiction published it (description_raw on search)."}},"type":"object","required":["id","permit_id","event_type","detected_at"],"title":"PermitEventOut"},"PermitEventsResponse":{"properties":{"total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total"},"page":{"type":"integer","title":"Page"},"per_page":{"type":"integer","title":"Per Page"},"results":{"items":{"$ref":"#/components/schemas/PermitEventOut"},"type":"array","title":"Results"},"total_capped":{"type":"boolean","title":"Total Capped","default":false},"total_unknown":{"type":"boolean","title":"Total Unknown","default":false},"hints":{"items":{"$ref":"#/components/schemas/Hint"},"type":"array","title":"Hints","default":[]},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor","description":"Pass back as `cursor` to receive only events you have not seen yet, with no duplicates and no gap. Null when this page cannot define a resume point (i.e. a `page` beyond the first, where the newest row on the page is not the newest overall)."}},"type":"object","required":["total","page","per_page","results"],"title":"PermitEventsResponse"},"PermitSearchResponse":{"properties":{"total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total"},"page":{"type":"integer","title":"Page"},"per_page":{"type":"integer","title":"Per Page"},"results":{"items":{"$ref":"#/components/schemas/PermitSummary"},"type":"array","title":"Results"},"hints":{"items":{"$ref":"#/components/schemas/Hint"},"type":"array","title":"Hints","default":[]},"coverage_confidence":{"anyOf":[{"$ref":"#/components/schemas/CoverageConfidence"},{"type":"null"}]},"tier_window_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tier Window Days"},"tier_window_from":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier Window From"},"tier_window_clamped":{"type":"boolean","title":"Tier Window Clamped","default":false},"tier_window_upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tier Window Upgrade Url"},"locked_fields":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Locked Fields"},"locked_upgrade_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Locked Upgrade Url"},"coverage":{"items":{"$ref":"#/components/schemas/JurisdictionCoverage"},"type":"array","title":"Coverage","default":[]},"total_capped":{"type":"boolean","title":"Total Capped","default":false},"total_unknown":{"type":"boolean","title":"Total Unknown","default":false}},"type":"object","required":["total","page","per_page","results"],"title":"PermitSearchResponse"},"PermitStatus":{"type":"string","enum":["filed","issued","in_progress","final","expired","cancelled","revoked","unknown","interconnected"],"title":"PermitStatus"},"PermitSummary":{"properties":{"id":{"type":"string","format":"uuid","title":"Id","description":"Stable PermitStack id. Pass to GET /v1/permits/{id}.","examples":["f4be62ed-b522-42a3-907b-b4ab4ec1a102"]},"permit_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Permit Number","description":"The permit number as the issuing jurisdiction publishes it. Not unique across jurisdictions, and not always present.","examples":["R2608-181"]},"status":{"type":"string","enum":["FILED","ISSUED","IN_PROGRESS","FINAL","EXPIRED","CANCELLED","REVOKED","UNKNOWN","INTERCONNECTED"],"title":"Status","description":"One of FILED, ISSUED, IN_PROGRESS, FINAL, EXPIRED, CANCELLED, REVOKED, INTERCONNECTED, UNKNOWN -- normalised by us from each source's own vocabulary. UNKNOWN means the source published no status, never that the permit is inactive. INTERCONNECTED comes from the California NEM solar feed.","examples":["ISSUED"]},"category":{"type":"string","enum":["NEW_CONSTRUCTION","RENOVATION","DEMOLITION","ELECTRICAL","PLUMBING","MECHANICAL","ROOFING","SOLAR","BATTERY","EV_CHARGER","HVAC","FIRE_ALARM","SIGN","FENCE","POOL","FOUNDATION","ADDITION","INTERIOR_REMODEL","GRADING","OTHER"],"title":"Category","description":"Trade/work classification, UPPERCASE. One of: NEW_CONSTRUCTION, RENOVATION, DEMOLITION, ELECTRICAL, PLUMBING, MECHANICAL, ROOFING, SOLAR, BATTERY, EV_CHARGER, HVAC, FIRE_ALARM, SIGN, FENCE, POOL, FOUNDATION, ADDITION, INTERIOR_REMODEL, GRADING, OTHER. Derived by us from the permit type and description, not published by the city. NOTE when FILTERING: ?category=hvac matches HVAC *and* MECHANICAL, and ?category=mechanical does the same, because HVAC work is routinely filed as MECHANICAL -- they are one market and a bare equality would hide half of it.","examples":["ROOFING"]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Tags","description":"Free-form keywords extracted from the description (e.g. 'pool', 'reroof'). Additive; do not rely on a fixed vocabulary.","examples":[["pool"]]},"property_type":{"type":"string","title":"Property Type","description":"RESIDENTIAL, COMMERCIAL, INDUSTRIAL, MIXED_USE or UNKNOWN. UNKNOWN is common -- many feeds publish nothing that implies a property type.","examples":["RESIDENTIAL"]},"address_street":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Street","description":"Street address as published. Stored verbatim, so spacing can be irregular; our address matching normalises whitespace for you.","examples":["288 Mosaic Blvd"]},"address_city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address City","description":"City. Null where the feed publishes none -- notably county-wide feeds. Read null as unknown, never as 'not in a city'.","examples":["Daytona Beach"]},"address_state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address State","description":"2-letter state. Null on 6.6M permits (7.3%) whose feed never populates it; those rows are still returned by ?state= searches, which admit them on independent evidence of the jurisdiction.","examples":["FL"]},"address_zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Zip","description":"5-digit ZIP where published.","examples":["32124"]},"parcel_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel Id","description":"Assessor parcel number / APN exactly as the SOURCE publishes it -- formatting varies by county and is not normalised. ~41% populated estate-wide and entirely dependent on whether the source publishes one, so read a null as 'this feed does not carry a parcel', never as 'this property has none'. Filterable via ?parcel=.","examples":["521804000160"]},"description_raw":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description Raw","description":"The scope of work exactly as the jurisdiction published it. The single most useful field for lead qualification, and the input our category classifier reads.","examples":["RE-ROOF: remove existing shingles, install 39 sq architectural shingles"]},"estimated_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Estimated Value","description":"Declared job value in USD, as published. Frequently null and occasionally nominal -- treat 0 and 1 as 'not stated'.","examples":[24500.0]},"date_filed":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Filed","description":"Application/filing date. Null where the source publishes only an issue date.","examples":["2026-08-14"]},"date_issued":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Issued","description":"Issue date. Null on ~25% of permits estate-wide -- whole feeds publish only a filed date -- which is why date_after/date_before filter on COALESCE(date_issued, date_filed) and issued_after does not.","examples":["2026-08-21"]},"date_completed":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Date Completed","description":"Completion/final date where the source publishes one. Usually null.","examples":[null]},"approval_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Approval Days","description":"Calendar days from date_filed to date_issued (null unless both dates present)"},"construction_days":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Construction Days","description":"Calendar days from date_issued to date_completed (null unless both dates present)"},"contractor_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Contractor Name","description":"Contractor of record, cleaned and de-duplicated by us. Null where the feed publishes no contractor -- which is whole jurisdictions, not scattered rows: 45% of permits estate-wide carry one. A page of nulls usually means the source does not publish contractors, not that our data is missing; the `jurisdiction_coverage` block on a search response tells you which.","examples":["GLZ Industries LLC"]},"contractor_id":{"anyOf":[{"type":"string","format":"uuid"},{"type":"null"}],"title":"Contractor Id","description":"Opaque id of the contractor on this permit. Pass it to GET /v1/contractors/{id} for the full record; null when the source publishes no contractor for this permit."},"owner_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Name","description":"Owner of record. contractor_name null + owner_name set means no contractor was recorded -- often a homeowner who pulled the permit themselves, which is a sales lead. Caveat: on feeds that capture no contractor at all, EVERY permit looks owner-filed and the owner may be a builder, not a homeowner. Use the ?owner_filed= filter rather than inferring this yourself.","examples":["SMITH JOHN A"]},"owner_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Address","description":"Property-owner MAILING address. Business plan ($149/mo) and above; null on every other plan, which is a gate and not an absence of data. Joined from county assessor rolls by parcel, so coverage is bimodal -- near-complete in the jurisdictions whose roll we have loaded, absent in those we have not. Never read a null as 'this property has no owner on record'.","examples":[null]},"jurisdiction_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Jurisdiction Name","description":"The PermitStack data source this permit came from -- a city, county or statewide feed, which is not always the permit's own city.","examples":["Daytona Beach"]},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude","description":"WGS84 latitude. Null where the source publishes no coordinates and we could not geocode it; such permits are invisible to radius, bbox and polygon search.","examples":[29.1872]},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude","description":"WGS84 longitude.","examples":[-81.0559]},"location_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Location Source","description":"Where the coordinates came from. 'source' = published by the city with the permit record, as good as the city's own data. 'geocoded' = derived by us from the site address (TIGER, address-centroid quality), which for a structure set back from the road, such as an antenna mast, marks the property address rather than the structure. 'derived' = set by us by another method (typically a parcel-centroid join), so treat it as ours, not the city's. 'source' is judged per feed: in a feed that publishes coordinates, a minority of rows whose record lacked them may have been filled by our geocoder before 2026-09-21, when it began logging successes, and those read 'source'. Null when latitude/longitude are null.","examples":["source"]},"enrichment":{"anyOf":[{"$ref":"#/components/schemas/PermitEnrichment"},{"type":"null"}],"description":"Structured detail parsed from the description (e.g. solar kW) where we could extract it. Null for most permits."}},"type":"object","required":["id","permit_number","status","category","tags","property_type","address_street","address_city","address_state","address_zip","description_raw","estimated_value","date_filed","date_issued","date_completed"],"title":"PermitSummary","description":"One building permit."},"PropertyHistoryResponse":{"properties":{"query":{"$ref":"#/components/schemas/PropertyQuery"},"found":{"type":"boolean","title":"Found"},"total_matches":{"type":"integer","title":"Total Matches"},"truncated":{"type":"boolean","title":"Truncated"},"distinct_addresses":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distinct Addresses"},"distinct_jurisdictions":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Distinct Jurisdictions"},"fan_out_warning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Fan Out Warning"},"summary":{"$ref":"#/components/schemas/PropertySummary"},"coverage":{"anyOf":[{"$ref":"#/components/schemas/CoverageConfidence"},{"type":"null"}]},"data_currency":{"anyOf":[{"items":{"$ref":"#/components/schemas/JurisdictionCoverage"},"type":"array"},{"type":"null"}],"title":"Data Currency"},"page":{"type":"integer","title":"Page"},"per_page":{"type":"integer","title":"Per Page"},"has_more":{"type":"boolean","title":"Has More"},"permits":{"items":{"$ref":"#/components/schemas/PermitSummary"},"type":"array","title":"Permits"}},"type":"object","required":["query","found","total_matches","truncated","summary","page","per_page","has_more","permits"],"title":"PropertyHistoryResponse"},"PropertyQuery":{"properties":{"address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address"},"parcel":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parcel"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"zip":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip"}},"type":"object","title":"PropertyQuery"},"PropertySignals":{"properties":{"has_solar":{"type":"boolean","title":"Has Solar","default":false},"solar_kw":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Solar Kw"},"last_solar_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Solar Date"},"has_battery":{"type":"boolean","title":"Has Battery","default":false},"last_battery_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Battery Date"},"has_roofing":{"type":"boolean","title":"Has Roofing","default":false},"last_roofing_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Roofing Date"},"has_pool":{"type":"boolean","title":"Has Pool","default":false},"last_pool_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Pool Date"},"has_addition":{"type":"boolean","title":"Has Addition","default":false},"has_new_construction":{"type":"boolean","title":"Has New Construction","default":false},"has_electrical":{"type":"boolean","title":"Has Electrical","default":false},"has_hvac":{"type":"boolean","title":"Has Hvac","default":false},"last_hvac_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Hvac Date"},"has_plumbing":{"type":"boolean","title":"Has Plumbing","default":false},"has_demolition":{"type":"boolean","title":"Has Demolition","default":false},"last_activity_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Activity Date"}},"type":"object","title":"PropertySignals","description":"Boolean/scalar rollups useful for underwriting, valuation and diligence.\n\nDerived from permit category + tags + offline enrichment across every\npermit matched at the address. Dates are the most-recent permit of that\nkind (ISO yyyy-mm-dd) so you can reason about roof/solar/pool age."},"PropertySummary":{"properties":{"total_permits":{"type":"integer","title":"Total Permits"},"first_permit_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Permit Date"},"last_permit_date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Permit Date"},"years_of_history":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Years Of History"},"total_estimated_value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Total Estimated Value"},"categories":{"additionalProperties":{"type":"integer"},"type":"object","title":"Categories"},"jurisdictions":{"items":{"type":"string"},"type":"array","title":"Jurisdictions"},"contractors":{"items":{"type":"string"},"type":"array","title":"Contractors"},"signals":{"$ref":"#/components/schemas/PropertySignals"}},"type":"object","required":["total_permits","categories","jurisdictions","contractors","signals"],"title":"PropertySummary"},"PropertyType":{"type":"string","enum":["residential","commercial","industrial","mixed_use","institutional","unknown"],"title":"PropertyType"},"SourceCoverage":{"properties":{"source_id":{"type":"string","format":"uuid","title":"Source Id"},"name":{"type":"string","title":"Name"},"state":{"type":"string","title":"State"},"data_through":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Data Through"},"freshness":{"type":"string","title":"Freshness"},"records":{"type":"integer","title":"Records"},"completion_records":{"type":"integer","title":"Completion Records","default":0},"dated_share":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Dated Share"},"completeness":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Completeness"},"cadence":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cadence"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","required":["source_id","name","state","freshness","records"],"title":"SourceCoverage"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WebhookCreate":{"properties":{"url":{"type":"string","maxLength":2083,"minLength":1,"format":"uri","title":"Url"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"zip_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Zip Code"},"keyword":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Keyword"},"contractor_name":{"anyOf":[{"type":"string","maxLength":255,"minLength":3},{"type":"null"}],"title":"Contractor Name","description":"Fire only for permits pulled by contractors whose name contains this text (case-insensitive) — track a competitor or partner as their new permits arrive."}},"type":"object","required":["url"],"title":"WebhookCreate"},"ErrorDetail":{"type":"object","title":"ErrorDetail","properties":{"detail":{"type":"string"}}},"FeatureLocked":{"type":"object","title":"FeatureLocked","properties":{"detail":{"type":"object","properties":{"error":{"type":"string","enum":["feature_locked"]},"feature":{"type":"string"},"message":{"type":"string"},"upgrade_url":{"type":"string","format":"uri"},"current_tier":{"type":"string"}}}}}}},"tags":[{"name":"Permits","description":"Search and retrieve building permits"},{"name":"Contractors","description":"Search contractors and see their permit history"},{"name":"Property History","description":"Get permit history for a specific address"},{"name":"Webhooks","description":"New and changed permits pushed to your endpoint about 60 seconds after we ingest them; most sources are ingested nightly. Developer plan ($79/mo) and above."},{"name":"Health","description":"Service health checks"},{"name":"Metrics","description":"Pre-aggregated permit counts and valuation from a nightly rollup. Use these instead of paging through search results to count them: they answer 'how many' and 'is it trending' in one millisecond-scale call. Developer plan and above."},{"name":"Licensing","description":"Contractor licence records as public state licence boards publish them: licence number, type, class, status and dates. Look one up by number and state, or list the licences linked to a contractor. Contractor licensing add-on, $19/mo on Developer; included in Business and Scale. Bond, liability insurance, workers' comp and discipline fields need Contractor licensing Pro (only some boards publish them): Contractor licensing Pro add-on, $29/mo on Developer or Business; included in Scale. No phone, email or street address is returned here."},{"name":"Certificates of Occupancy","description":"Certificates of occupancy as cities, counties and state agencies publish them: certificate number, type, status and date, address, parcel, occupancy class, units and the linked permit where we hold it. /v1/certificates/coverage works with any API key, so you can check a place before buying. Certificates of occupancy add-on, $29/mo on Developer; included in Business and Scale."}],"x-speakeasy-timeout":120000}