GET /v1/jurisdictions/{slug}/cohorts/distribution
curl --request GET \
--url https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distributionimport requests
url = "https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution', 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/jurisdictions/{slug}/cohorts/distribution",
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/jurisdictions/{slug}/cohorts/distribution"
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/jurisdictions/{slug}/cohorts/distribution")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution")
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{
"jurisdiction_slug": "<string>",
"jurisdiction_display_name": "<string>",
"as_of_utc": "<string>",
"total_target_permits": 123,
"cohort_distribution": {}
}API Reference
GET /v1/jurisdictions/{slug}/cohorts/distribution
Per-jurisdiction breakdown across the canonical 23 cohorts. Cached 1 hour.
GET
/
v1
/
jurisdictions
/
{slug}
/
cohorts
/
distribution
GET /v1/jurisdictions/{slug}/cohorts/distribution
curl --request GET \
--url https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distributionimport requests
url = "https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution', 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/jurisdictions/{slug}/cohorts/distribution",
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/jurisdictions/{slug}/cohorts/distribution"
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/jurisdictions/{slug}/cohorts/distribution")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.permitcore.io/v1/jurisdictions/{slug}/cohorts/distribution")
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{
"jurisdiction_slug": "<string>",
"jurisdiction_display_name": "<string>",
"as_of_utc": "<string>",
"total_target_permits": 123,
"cohort_distribution": {}
}Returns the canonical cohort permit distribution for a single jurisdiction.
The response carries one key per canonical segment (23 today) — segments with
no permits are zero-filled. Counts reflect the rolling-window total permits
classified to each segment for that jurisdiction as of the
(Counts above are illustrative; newer segments are shown zero-filled here.)
The What is
The data-model flow:
The relationship in math:
as_of_utc timestamp.
Authentication
Authorization: Bearer <your-api-key> — see Authentication.
Path parameters
string
required
Lowercase jurisdiction slug. Examples:
nyc, la, houston, dc. See
Concepts → Jurisdictions for the coverage list.
Returns 404 for unknown slugs and for slugs in DEPLOY-PENDING status.Response
string
Echoes the path parameter for round-trip clarity.
string
Human-readable metro name, e.g.
"New York City, NY".string
ISO 8601 UTC timestamp marking when the underlying data was last computed.
number
Count of buyer-targeted permits for this jurisdiction — lifetime
accumulation, no time window. See the
What is
total_target_permits?
section below for the matview filter definition. The sum of
cohort_distribution values is less than total_target_permits
by the implicit NULL-segment count (permits that pass the matview
filter but didn’t match any of the segment CASE branches).object
Map of
{ segment_slug: integer_count } for all canonical segments (23
today). One key per segment; zero-filled when a segment has no permits.Example request
curl https://api.permitcore.io/v1/jurisdictions/nyc/cohorts/distribution \
-H "Authorization: Bearer $PERMITCORE_API_KEY" \
-H "Accept: application/json"
const res = await fetch(
"https://api.permitcore.io/v1/jurisdictions/nyc/cohorts/distribution",
{
headers: {
Authorization: `Bearer ${process.env.PERMITCORE_API_KEY}`,
Accept: "application/json",
},
},
);
const data = await res.json();
import os, httpx
r = httpx.get(
"https://api.permitcore.io/v1/jurisdictions/nyc/cohorts/distribution",
headers={"Authorization": f"Bearer {os.environ['PERMITCORE_API_KEY']}"},
)
r.raise_for_status()
data = r.json()
Example response
{
"jurisdiction_slug": "nyc",
"jurisdiction_display_name": "New York City, NY",
"as_of_utc": "2026-05-24T02:00:00Z",
"total_target_permits": 3218856,
"cohort_distribution": {
"adu_qualifying": 380,
"civic": 1820,
"commercial_alteration": 119800,
"commercial_demolition": 50100,
"commercial_hvac": 0,
"commercial_mep": 27200,
"commercial_new": 38400,
"commercial_pool": 3920,
"commercial_shell_only": 1200,
"commercial_signage": 18900,
"industrial_new": 2840,
"multifamily_alteration": 2474522,
"multifamily_mep": 0,
"multifamily_new": 73200,
"residential_alteration": 14820,
"residential_demolition": 9840,
"residential_foundation_only": 248,
"residential_mep": 8400,
"residential_new_sf": 6240,
"residential_pool": 0,
"solar_residential": 0,
"storage_residential": 0,
"temporary_construction_support": 146420
}
}
cohort_distribution values sum to 2,998,250 — the remaining
220,606 permits (matview total 3,218,856 − cohort sum) fall in the
implicit NULL segment: rows that pass the matview filter but didn’t
match any of the segment CASE branches in the classifier. See the
explainer below for the data-model flow.
What is total_target_permits?
total_target_permits is the count of buyer-targeted commercial,
multifamily, and qualifying-ADU permits for this jurisdiction. It is a
lifetime accumulation — no time window applied — of permits matching
PermitCore’s matview criteria:
work_classin (commercial,mixed,industrial), OR- ≥ 5 residential units, OR
- explicit multifamily flag, OR
- qualifying ADU
| Total | NYC value | Source |
|---|---|---|
| Full silver permit table | ~7.63M | All permits indexed; all work types |
total_target_permits (matview slice) | ~3.22M | Buyer-targeted subset |
Silver permits table (NYC: ~7.63M; all types, all time)
│
▼ matview WHERE filter
Buyer-targeted slice (NYC: ~3.22M; = total_target_permits)
│
▼ cohort CASE classification
23 canonical cohorts + 1 NULL bucket (implicit)
│
▼ aggregation per request
cohort_distribution response field
sum(cohort_distribution) + null_segment = total_target_permits. The NULL bucket isn’t exposed in the response
shape; subtract the cohort sum from the total if you need to know its
size for a given jurisdiction.
When not to use total_target_permits:
- For a metro’s full permit count across all work types (residential single-family, miscellaneous trades, etc.), the matview total understates by ~57% on NYC and roughly similar on other large metros. Use the metro page on permitcore.io for the lifetime silver count.
- For time-bounded counts (e.g., “permits issued in 2025”), this
endpoint is the wrong tool — use
/v1/permitswith theissued_after=filter. (Upper-bounduntil=filter is on the roadmap, not yet available.)
Errors
Errors are RFC 9457application/problem+json — branch on the type slug (see Errors):
| HTTP | type slug | Cause |
|---|---|---|
401 | missing-credentials | No Authorization header |
401 | invalid-credentials | Key malformed / revoked |
403 | missing-scope | Key lacks query scope |
404 | unknown-jurisdiction | Unknown slug OR DEPLOY-PENDING metro |
429 | monthly-requests-quota-exceeded | Monthly quota exhausted (see Rate limits) |
500 | internal-error | Upstream pipeline issue; retry after backoff |
Caching
The upstream API responds withCache-Control: max-age=3600. The
PermitCore site consumes this endpoint via Next.js ISR with
revalidate: 3600, so site-rendered pages lag the underlying pipeline
by at most 1 hour beyond the pipeline’s own nightly refresh.
For your own caching, the 1-hour value is a safe upper bound — pipeline
data only changes once per 24 hours per metro.
Related
- Concepts → Cohorts — what each cohort covers
- Permits endpoint — paginate the rows behind each count.