Get a permit by canonical_uid
curl --request GET \
--url https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}import requests
url = "https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}', 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/by-canonical-uid/{canonical_uid}",
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/by-canonical-uid/{canonical_uid}"
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/by-canonical-uid/{canonical_uid}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}")
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_bodyAPI Reference
Get a permit by canonical_uid
Per-permit detail endpoint. Returns one fully enriched row by its stable cross-jurisdictional canonical_uid — including the nested PLUTO presence + DQ object.
GET
/
v1
/
permits
/
by-canonical-uid
/
{canonical_uid}
Get a permit by canonical_uid
curl --request GET \
--url https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}import requests
url = "https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}"
response = requests.get(url)
print(response.text)const options = {method: 'GET'};
fetch('https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}', 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/by-canonical-uid/{canonical_uid}",
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/by-canonical-uid/{canonical_uid}"
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/by-canonical-uid/{canonical_uid}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.permitcore.io/v1/permits/by-canonical-uid/{canonical_uid}")
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_bodyThe per-permit detail endpoint. Returns a single fully enriched
The
The Encoding requirement — clients MUST
Every client MUST percent-encode the
Worked examples:
Skipping the encode is the most common integration mistake: an
unencoded
Elided fields above resolve from the same The nested
Returned when the path segment can be parsed but doesn’t conform to
the
Returned when the
Returned when the
JavaScript / TypeScript:
Python:
PermitResponse row by its stable cross-jurisdictional canonical_uid.
The shape returned is the same PermitResponse schema as
/v1/permits, with one difference: the
nested pluto object (PLUTO parcel-data presence + DQ status) is
populated here, NULL on the list endpoint — see the
asymmetry note.
Authentication
Authorization: Bearer <your-api-key> — see Authentication.
The canonical_uid path parameter
canonical_uid is the stable cross-jurisdictional permit identifier:
{jurisdiction_id}:{source_permit_id}
source_permit_id portion routinely contains characters that are
reserved or unsafe in a URL path segment.
Encoding requirement — clients MUST encodeURIComponent
Every client MUST percent-encode the canonical_uid before placing
it in the path (JavaScript: encodeURIComponent(canonical_uid);
Python: urllib.parse.quote(canonical_uid, safe="")). Four hostile
shape classes appear in real canonical_uid values:
| Character | Where it appears | Encoded form |
|---|---|---|
: | Jurisdiction separator — every canonical_uid | %3A |
| | NYC scaffold/class separator (e.g. Suspended Scaffold) | %7C |
(space) | Austin (e.g. 2026-066944 PP), Orlando | %20 |
/ | Orlando (e.g. BLD23/00099), fort_worth, philadelphia | %2F |
# atlanta — colon only
canonical_uid = "259:BT-202601090"
encoded = "259%3ABT-202601090"
URL = https://api.permitcore.io/v1/permits/by-canonical-uid/259%3ABT-202601090
# nyc — colon + pipe + space
canonical_uid = "1:982522764|Sidewalk Shed"
encoded = "1%3A982522764%7CSidewalk%20Shed"
URL = https://api.permitcore.io/v1/permits/by-canonical-uid/1%3A982522764%7CSidewalk%20Shed
# orlando — colon + slash sub-path
canonical_uid = "182:BLD23/00099"
encoded = "182%3ABLD23%2F00099"
URL = https://api.permitcore.io/v1/permits/by-canonical-uid/182%3ABLD23%2F00099
# austin — colon + numeric + space + suffix
canonical_uid = "85:2026-066944 PP"
encoded = "85%3A2026-066944%20PP"
URL = https://api.permitcore.io/v1/permits/by-canonical-uid/85%3A2026-066944%20PP
/ will be parsed as a path-segment break, and an unencoded
| or space is invalid in a URL path segment per RFC 3986. The server
returns 400 Malformed canonical_uid for the shapes it can detect at
parse time, and falls through to 404 for shapes that produce a
valid-looking but non-matching key.
Response shape
Identical toPermitResponse on /v1/permits — 58 fields including the enrichment set (cohort, dq_flags, top-level PLUTO-derived fields, description_llm_enriched). The differences on this endpoint are:
plutois populated when the row’sparcel_apnmatches a PLUTObblrow (NYC only today). For NYC rows where the parcel does not match a PLUTO row, AND for all non-NYC rows,plutoremainsnull.- The response body is the single row object — not wrapped in
items: [...]— and there is nopaginationenvelope.
{
"id": 4218034,
"jurisdiction_slug": "nyc",
"source_permit_id": "BT-202601090",
"canonical_uid": "1:BT-202601090",
"address_full": "82 ASP 1ST FLOOR, Manhattan, NY 10009",
"parcel_apn": "1004920019",
"lat": 40.7271,
"lng": -73.9836,
"permit_type_normalized": "alteration",
"work_class": "alteration_minor",
"year_built": 1925,
"building_class": "O4",
"zoning_district": "C6-2",
"lot_area": 2540,
"units_total": null,
"issued_date": "2026-05-27",
"current_status": "issued",
"description_raw": "Interior reno @ unit 3R; partition + finishes; no MEP scope",
"contractor_name": "MIDTOWN BUILD CO INC",
"source_url": "https://www.nyc.gov/site/buildings/permit/BT-202601090",
"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": {
"is_placeholder": false,
"oob": false
}
}
PermitResponse schema — see the listing endpoint’s response shape for the full field list.
The nested pluto object semantics
pluto reports the PLUTO join + per-flag DQ status, not the PLUTO
values themselves (those are denormalized into top-level fields like
year_built, building_class).
is_placeholder—year_builtflagged as a placeholder in PLUTO.oob—year_builtflagged as out-of-bounds in PLUTO.
false when the join landed
but the upstream PLUTO DQ flag was NULL (the “rule has not flagged
this value” meaning). pluto: null (the field itself) means the join
didn’t land at all — either non-NYC, or no parcel_apn ↔ bbl match.
Errors
All error responses follow RFC 9457 problem+json — see Errors for the envelope.400 Invalid canonical_uid
Returned when the path segment can be parsed but doesn’t conform to
the {jurisdiction_id}:{source_permit_id} shape (e.g. missing the :
separator, or jurisdiction_id is non-numeric).
{
"type": "https://docs.permitcore.io/errors/invalid-canonical-uid",
"title": "Invalid canonical_uid",
"status": 400,
"detail": "Expected {jurisdiction_id}:{source_permit_id}; got 'abc-xyz'."
}
404 Permit not found
Returned when the canonical_uid is well-formed but no matching permit
exists.
{
"type": "https://docs.permitcore.io/errors/permit-not-found",
"title": "Permit not found",
"status": 404,
"detail": "No permit matched canonical_uid '999:UNKNOWN-9999'."
}
422 Validation Error
Returned when the canonical_uid is path-parameter-invalid (rare —
typically caught at the framework layer before the endpoint runs).
Quotas
Quotas are tier-based and apply to every successful request against this endpoint. See Rate limits for the matrix- the
X-RateLimit-*response headers + the 429 problem+json shape.
Example request
# canonical_uid: "1:BT-202601090" — pre-encoded
curl "https://api.permitcore.io/v1/permits/by-canonical-uid/1%3ABT-202601090" \
-H "Authorization: Bearer $PERMITCORE_API_KEY" \
-H "Accept: application/json"
const canonicalUid = "1:982522764|Sidewalk Shed"; // NYC scaffold-class shape
const url = `https://api.permitcore.io/v1/permits/by-canonical-uid/${encodeURIComponent(canonicalUid)}`;
const res = await fetch(url, {
headers: {
Accept: "application/json",
Authorization: `Bearer ${process.env.PERMITCORE_API_KEY}`,
},
});
if (res.status === 404) return null;
if (!res.ok) throw new Error(`PD-7 lookup failed: ${res.status}`);
const permit = await res.json();
import os, urllib.parse, requests
canonical_uid = "182:BLD23/00099" # orlando slash shape
url = f"https://api.permitcore.io/v1/permits/by-canonical-uid/{urllib.parse.quote(canonical_uid, safe='')}"
r = requests.get(url, headers={
"Authorization": f"Bearer {os.environ['PERMITCORE_API_KEY']}",
"Accept": "application/json",
})
if r.status_code == 404:
permit = None
else:
r.raise_for_status()
permit = r.json()
Related
- Permits (listing endpoint) — the paginated browse path.
- Data freshness → field-level DQ flags — the 12-key
dq_flagsregistry + the SD-039 semantic. - Cohorts — the 18-cohort taxonomy that
cohortdraws from.