Skip to main content
GET
Permits (listing endpoint)
Returns paginated permit rows filtered by jurisdiction + date range. The response example below is generated from the live OpenAPI spec at api.permitcore.io/openapi.json — re-pulled before each docs deploy, never hand-written.

Authentication

Authorization: Bearer <your-api-key> — see Authentication.

Query parameters

These are the parameters /v1/permits actually accepts, per the live OpenAPI spec. There is no cohort parameter on /v1/permits — buyer-segment filtering lives on the /v1/permits/target variant via target_segment.

Response shape

The example below is generated from the PermitResponse schema in openapi.json. The enrichment fields (cohort, dq_flags, top-level PLUTO-derived fields, description_llm_enriched) are now on the wire as of the 2026-06-04 promotion. The nested pluto DQ-status object is list-endpoint NULL by design — see the list-vs-detail asymmetry note below.
The pagination object above is abbreviated for readability. See Pagination for the full, canonical envelope both this endpoint and /v1/permits/target return.

Pagination

/v1/permits and /v1/permits/target return the same response envelope:
  • total_count is capped at 1,000 on selective-filter queries, and jurisdiction-only queries use a fast row estimate. When total_count_is_estimate or has_more is true, the true total exceeds total_count — render it as "1,000+" if you surface it directly.
  • Offset paging (both endpoints): pass page + per_page.
  • Keyset / cursor paging (/v1/permits only): pass the prior response’s pagination.next_cursor back as the cursor query parameter to page deep or across jurisdictions without an OFFSET scan. When cursor is supplied, page is ignored; the cursor is bound to the request’s order_by/order_dir, so keep those stable while paging. next_cursor is null on the last page. /v1/permits/target is offset-only (page/per_page).

What’s on the wire

The list-endpoint shape carries 58 fields organized as:
  • Identityid, jurisdiction_slug, source_permit_id, canonical_uid, filing_id
  • Addressaddress_full, address_street, address_city, address_state, address_zip, parcel_apn
  • Geocode metadatalat, lng, geocode_confidence, geocode_source
  • Type + classificationpermit_type_raw, permit_type_normalized, work_class, building_subtype, is_new_construction, is_alteration, is_demolition
  • Scopeunits_residential, units_commercial, units_total, square_footage_total, valuation_usd
  • PLUTO-derived (denormalized)year_built, building_class, zoning_district, lot_area
  • Datesapplied_date, issued_date, finaled_date, expired_date
  • Status + descriptioncurrent_status, status_raw, description_raw, description_extracted, description_llm_enriched
  • Peopleapplicant_name, applicant_role, contractor_name, contractor_license, architect_name, engineer_name, owner_name
  • Source link + timestampssource_url, first_seen_at, last_seen_at, last_changed_at
  • Enrichmentcohort, dq_flags, pluto
Most fields are nullable — coverage varies by jurisdiction and by upstream record completeness.

Enrichment field semantics

  • cohort — the buyer-targeting bucket the row classifies into, one of the 23 canonical segments. null = row not in the cohort matview OR in the matview but unsegmented. Identical taxonomy to target_segment on /v1/permits/target. See Concepts → Cohorts.
  • dq_flags — 12-key boolean object from the bounds-rule registry. All 12 keys are always present when this field is non-null. Semantic note: false means “the rule has not flagged this value,” NOT “the value was independently verified clean.” NULL silver flag values are coerced to false per the SD-039 rule. See Data freshness → field-level DQ flags for the full flag set + semantics.
  • description_llm_enriched — LLM-extracted feature object (e.g. scope_summary, unit_count, square_footage, tenant_improvement_flag). Distinct column from description_extracted (which is adapter-populated, per-jurisdiction shape). Coverage today: Austin + Chicago commercial_new permits only (~17,861 prod rows). Broader cross-metro LLM extraction is on the roadmap.

List-vs-detail asymmetry: the nested pluto object

The nested pluto object (PLUTO parcel-data presence + per-flag DQ status — {is_placeholder, oob}) is ALWAYS null on /v1/permits by design. The PLUTO LEFT JOIN required to populate the DQ-status object would add ~75ms p95 to the list query, which disqualifies it for the browse path. To get the populated pluto object, query the per-permit detail endpoint:
See Get a permit by canonical_uid. Note that the top-level PLUTO-derived fields (year_built, building_class, zoning_district, lot_area, units_total) are denormalized into the silver layer and DO populate on this endpoint when known. Only the nested pluto DQ-status object is detail-only.

The /v1/permits/target variant

/v1/permits/target returns the same response envelope but exposes a curated commercial/multifamily/ADU slice, filtered by target_segment=<segment>. This is the cohort-classified slice that the site’s recency surfaces query. It also adds geo filters (state, metro_cbsa), value bounds (value_min/value_max), and enrichment filters (climate_zone, roof_area_min, hvac_tonnage_min). It is offset-only (no cursor). target_segment accepts a comma-separated list of the 23 canonical segment slugs (see Cohorts). Behavior:
  • Valid segment with zero rows in window: 200 OK with an empty items array — no 404. Use pagination.total_count to confirm the empty result.
  • Invalid slug (not in the 23-segment enum): 422 Unprocessable Entity (invalid-target-segment) with a problem+json body enumerating the accepted whitelist.
  • Newer cohort caveat: multifamily_mep is filterable and populated but not yet certified to the ≥99% precision bar of the other 22 segments — currently 3 metros (primarily Austin), largely historical. See the note on Cohorts. The other 22 are the established, certified set.
/v1/permits/target also has a separate cohort parameter — but it is a per-manufacturer cohort (an A5 YAML basename such as roofing-manufacturer), not a buyer segment. An unknown value returns 400 (unknown-cohort). Use target_segment for segment filtering; use cohort only for manufacturer-cohort targeting.

Quotas

Quotas are tier-based and apply to every request against this endpoint. See Rate limits for the matrix + the X-RateLimit-* response headers + the 429 problem+json shape.

Example request