Skip to main content
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_body
The per-permit detail endpoint. Returns a single fully enriched 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}
The 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:
CharacterWhere it appearsEncoded 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
Worked examples:
# 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
Skipping the encode is the most common integration mistake: an unencoded / 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 to PermitResponse on /v1/permits — 53 fields including the enrichment set (cohort, dq_flags, top-level PLUTO-derived fields, description_llm_enriched). The differences on this endpoint are:
  1. pluto is populated when the row’s parcel_apn matches a PLUTO bbl row (NYC only today). For NYC rows where the parcel does not match a PLUTO row, AND for all non-NYC rows, pluto remains null.
  2. The response body is the single row object — not wrapped in items: [...] — and there is no pagination envelope.
{
  "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
  }
}
Elided fields above resolve from the same 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_placeholderyear_built flagged as a placeholder in PLUTO.
  • oobyear_built flagged as out-of-bounds in PLUTO.
Per the SD-039 semantic, both default to 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_apnbbl match.

Errors

All error responses follow RFC 9457 problem+json — see Errors for the envelope.

400 Malformed 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://api.permitcore.io/errors/malformed-canonical-uid",
  "title": "Malformed 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://api.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"
JavaScript / TypeScript:
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();
Python:
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()