Skip to main content
GET
/
v1
/
permits
Permits (listing endpoint)
curl --request GET \
  --url https://api.permitcore.io/v1/permits
import requests

url = "https://api.permitcore.io/v1/permits"

response = requests.get(url)

print(response.text)
const options = {method: 'GET'};

fetch('https://api.permitcore.io/v1/permits', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));
<?php

$curl = curl_init();

curl_setopt_array($curl, [
CURLOPT_URL => "https://api.permitcore.io/v1/permits",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
]);

$response = curl_exec($curl);
$err = curl_error($curl);

curl_close($curl);

if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}
package main

import (
"fmt"
"net/http"
"io"
)

func main() {

url := "https://api.permitcore.io/v1/permits"

req, _ := http.NewRequest("GET", url, nil)

res, _ := http.DefaultClient.Do(req)

defer res.Body.Close()
body, _ := io.ReadAll(res.Body)

fmt.Println(string(body))

}
HttpResponse<String> response = Unirest.get("https://api.permitcore.io/v1/permits")
.asString();
require 'uri'
require 'net/http'

url = URI("https://api.permitcore.io/v1/permits")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Get.new(url)

response = http.request(request)
puts response.read_body
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

ParamTypeNotes
jurisdictionstring (slug)Required. Single metro slug, e.g. nyc, la.
target_segmentstring (slug)Optional. Curated cohort filter for the /v1/permits/target variant — see the variant section below. (The plain cohort= parameter on this endpoint arrives with the next promotion.)
issued_afterISO 8601 dateOptional. Lower bound for issued_date (YYYY-MM-DD).
per_pageintegerOptional. Page size; max 500.
pageintegerOptional. 1-indexed page number.

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.
{
  "items": [
    {
      "id": 4218034,
      "jurisdiction_slug": "nyc",
      "source_permit_id": "BT-202601090",
      "canonical_uid": "1:BT-202601090",
      "filing_id": null,

      "address_full": "82 ASP 1ST FLOOR, Manhattan, NY 10009",
      "address_street": "82 ASP",
      "address_city": "Manhattan",
      "address_state": "NY",
      "address_zip": "10009",
      "parcel_apn": "1004920019",
      "lat": 40.7271,
      "lng": -73.9836,
      "geocode_confidence": 0.92,
      "geocode_source": "city_geocoder_v2",

      "permit_type_raw": "BUILDING/EW",
      "permit_type_normalized": "alteration",
      "work_class": "alteration_minor",
      "building_subtype": "tenant_improvement",
      "is_new_construction": false,
      "is_alteration": true,
      "is_demolition": false,

      "units_residential": null,
      "units_commercial": null,
      "units_total": null,
      "square_footage_total": null,
      "valuation_usd": 18400,

      "year_built": 1925,
      "building_class": "O4",
      "zoning_district": "C6-2",
      "lot_area": 2540,

      "applied_date": "2026-05-20",
      "issued_date": "2026-05-27",
      "finaled_date": null,
      "expired_date": null,

      "current_status": "issued",
      "status_raw": "ISSUED-PARTIAL",
      "description_raw": "Interior reno @ unit 3R; partition + finishes; no MEP scope",
      "description_extracted": null,
      "description_llm_enriched": null,

      "applicant_name": null,
      "applicant_role": null,
      "contractor_name": "MIDTOWN BUILD CO INC",
      "contractor_license": "GC-0148921",
      "architect_name": null,
      "engineer_name": null,
      "owner_name": null,

      "source_url": "https://www.nyc.gov/site/buildings/permit/BT-202601090",
      "first_seen_at": "2026-05-27T03:14:22Z",
      "last_seen_at": "2026-06-04T03:11:08Z",
      "last_changed_at": "2026-05-27T03:14:22Z",

      "cohort": "commercial_alteration",
      "dq_flags": {
        "issued_date_is_parse_bug": false,
        "issued_date_is_placeholder": false,
        "issued_date_is_future_dated": false,
        "valuation_usd_is_placeholder": false,
        "valuation_usd_is_oob": false,
        "units_residential_is_placeholder": false,
        "units_total_is_placeholder": false,
        "units_residential_is_oob": false,
        "units_total_is_oob": false,
        "units_commercial_is_placeholder": false,
        "geocode_is_sentinel": false,
        "geocode_is_oob": false
      },
      "pluto": null
    }
  ],
  "pagination": {
    "total_count": 1000,
    "next_page": 2,
    "has_more": true
  }
}
Note on total_count: capped at 1,000 on selective-filter queries. When pagination.has_more is true, the true total exceeds the cap — render as "1,000+" if you surface this number directly.

What’s on the wire

The list-endpoint shape carries 53 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 18 canonical cohorts. 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:
GET /v1/permits/by-canonical-uid/{canonical_uid}
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 accepts the same parameters but applies a curated commercial/multifamily/ADU filter via target_segment=<cohort>. This is the cohort-classified slice that the site’s recency surfaces query. target_segment accepts all 18 canonical cohort slugs. Behavior:
  • Valid cohort 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 18-cohort enum): 422 Unprocessable Entity with a problem+json body enumerating the accepted whitelist.

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

curl "https://api.permitcore.io/v1/permits?jurisdiction=nyc&issued_after=2026-05-01&per_page=25" \
  -H "Authorization: Bearer $PERMITCORE_API_KEY" \
  -H "Accept: application/json"