Every endpoint is one authenticated HTTP call. Resolve your records to canonical entities, read the market signal and the underwriting verdict, rank where to look next, and put it all on a map. Every example below is a real production response.
pk_sandbox_…, production keys pk_live_….X-API-Key (or Authorization: Bearer — both work):export PG_KEY="pk_sandbox_..." curl "https://developer.permitgranted.ai/api/partner/v1/rankings?ahj=jersey-city-nj&limit=5" \ -H "X-API-Key: $PG_KEY"
Base URL: https://developer.permitgranted.ai/api/partner/v1. Rate limits + monthly quota are per plan.
Missing/invalid keys return 401. A call outside your plan's endpoints or geographies returns 403 with the exact missing entitlement (e.g. {"dimension":"geography","requested":"us-nj","allowed":["us-nj-sample"]}) — so your code can react precisely. Exceeding quota returns 429 with Retry-After. Owner names and personal data resolve through per-jurisdiction privacy rules — an uncleared jurisdiction returns data with those fields withheld, never a violation.
There is one real dataset — no synthetic sandbox. Every key returns real production data, scoped to your plan's geographies (Explore starts with a New Jersey sample; stepping past it returns a 403 naming the exact entitlement to add). The environment is about intent, not a different dataset:
pk_sandbox_… — development keys. For building and evaluating: real data, your sample scope, safe to experiment with.pk_live_… — production keys. Your real traffic, full entitled scope.Secrets are shown once, at creation — we store only a fingerprint, so copy or download the key then. Lost one? Rotate it in the console. Going to production is widening your entitlement (scope + quota), not switching datasets.
/resolveResolve your identifier — a parcel_id, block+lot+ahj, tract geoid, or exact situs address — to a canonical entity URI.
This is the front door: your systems know addresses and tax keys; the graph speaks canonical URIs (pg:parcel:{uuid}, pg:market:{geoid}). Resolve once, store the URI, and every other endpoint takes it. Matching is deterministic today (exact keys); fuzzy address matching is on the roadmap — prefer block+lot+ahj or parcel_id for guaranteed hits.
curl -X POST https://developer.permitgranted.ai/api/partner/v1/resolve \
-H "X-API-Key: $PG_KEY" \
-H "Content-Type: application/json" \
-d '{"ahj":"jersey-city-nj","block":"7902","lot":"16"}'{ "match": { "uri": "pg:parcel:03EDA726-317B-4695-BDF7-0B6E48ED5669",
"score": 1, "decision": "match" },
"candidates": [] }/parcels/{id}The parcel dossier: identity, geography (county/AHJ/census tract), a centroid, and the underwriting verdict with its confidence.
One call answers "should I look harder at this parcel?" — verdict.pencils is the engine's buy-right read (does a project pencil under current zoning + market inputs) and confidence tells you how much data stands behind it. Responses are JSON-LD: @id is the canonical URI you can store and re-fetch; external_ids carries the joins back to YOUR keys (tract geoid, normalized address, lat/lon) — the lat/lon is a map-ready centroid.
curl https://developer.permitgranted.ai/api/partner/v1/parcels/03EDA726-317B-4695-BDF7-0B6E48ED5669 \ -H "X-API-Key: $PG_KEY"
{ "@context": ".../context/v1.jsonld",
"@id": "pg:parcel:03EDA726-...", "@type": "Parcel",
"external_ids": { "census_tract_geoid": "34017001900",
"address_normalized": "719 NEWARK AVE.",
"lat": 40.73494, "lon": -74.061621 },
"geography": { "country": "US", "admin1": "NJ", "admin2": "Hudson",
"ahj": "jersey-city-nj", "tract": "34017001900" },
"verdict": { "pencils": false, "confidence": 0.82 } }/markets/{id}A census tract's market signal — the same "heating" read published in the NJ Wave Report, machine-readable.
signal.class (heating / warming / neutral / cooling) is price-excess vs the statewide tide over a trailing 3-year window — one canonical definition everywhere, thresholds pinned by tests. excess_pct is %/yr above the state; volume_led=true with price_moved=false is the EARLIEST read (activity before price); confidence reflects sale-count depth. The id after pg:market: IS the Census tract GEOID — join it to public tract geometry to map it.
curl https://developer.permitgranted.ai/api/partner/v1/markets/34017006500 \ -H "X-API-Key: $PG_KEY"
{ "@id": "pg:market:34017006500", "@type": "Market",
"geography": { "country": "US", "admin1": "NJ" },
"signal": { "class": "heating", "excess_pct": 48.15,
"volume_led": false, "price_moved": true,
"confidence": "med", "as_of": 2026 },
"forward": { "forward_score": 100, ... } }/parcels/{id}/underwritingThe ADVANCED underwriting view: the full 5-strategy table with dollar metrics, plus the input assumptions. Build and Platform plans.
Where /parcels answers "does it pencil?", this answers "how, by which strategy, and on what assumptions" — per strategy (ground-up, value-add, buy-hold, flip, land-bank): pencils, developer spread, yield-on-cost, TDC, residual land value, the for-sale vs rental exit read, and a plain-English headline. assumptions carries the inputs (cost/SF, cap rate, rent, product type, approval odds) so you can trust-but-verify. The methodology itself — weights, thresholds, calibration tables — is never exposed.
curl https://developer.permitgranted.ai/api/partner/v1/parcels/E47CE4A5-.../underwriting \ -H "X-API-Key: $PG_KEY"
{ "@type": "ParcelUnderwriting", "strategy_version": "strat@v2-...",
"confidence": 1,
"strategies": { "ground_up": { "pencils": true,
"headline": "For-sale exit: 41.3% developer margin @ $863/SF ...",
"metric": { "dev_spread_bps": 82, "yield_on_cost_pct": 6.6,
"tdc": 101027932, "residual_land_value": 25009187,
"for_sale_margin_pct": 41.3, "pencil_exit": "for_sale" } },
"buy_hold": { "available": false, "note": "no existing building to rent" }, ... },
"assumptions": { "cost_per_sf": 350, "cap_rate": 0.055,
"product_type": "podium_midrise", "approval_odds_pct": 78.6, ... } }/parcels/{id}/assemblageAssemblage-cluster membership: adjacent parcels under a common owner relation — the combined-lot origination signal. Platform plan.
Deal origination: parcels that could be bought TOGETHER. Each cluster carries the member parcel URIs, combined area, the owner-RELATION TYPE (e.g. shared_mailing), absentee + tenure signals, and a confidence. Owner identities are never in the payload — relation structure only, with identity resolution governed by per-jurisdiction privacy rules.
curl https://developer.permitgranted.ai/api/partner/v1/parcels/7A1B5C21-.../assemblage \ -H "X-API-Key: $PG_KEY"
{ "@type": "ParcelAssemblage", "cluster_count": 1,
"clusters": [ { "members": [ "pg:parcel:7A1B5C21-...", "pg:parcel:E401FF75-...",
"pg:parcel:F33834AA-..." ],
"member_count": 3, "combined_area_m2": 5109,
"owner_relation": "shared_mailing", "absentee": true,
"min_tenure_years": 40, "confidence": 0.9 } ] }/entities/{uri}Generic fetch by canonical URI — dispatches to the right view (parcel or market) from the URI type.
When your pipeline stores heterogeneous URIs from /resolve, this saves you routing logic: pg:parcel:… returns the parcel dossier, pg:market:… the market view. Same payloads as the typed endpoints.
curl https://developer.permitgranted.ai/api/partner/v1/entities/pg:market:34017006500 \ -H "X-API-Key: $PG_KEY"
// identical to GET /markets/34017006500
/rankingsA jurisdiction's tracts ranked by market signal — the movers list.
The "where should I even look?" call. Pass ?ahj= (e.g. jersey-city-nj) and get its tracts ordered by signal strength, each in the same Market shape as /markets/{id}. Because each carries its tract GEOID and signal.class, this is the one call you need to render a heating choropleth for a whole jurisdiction. Statewide cross-AHJ rankings land with the baked ranking table (roadmap).
curl "https://developer.permitgranted.ai/api/partner/v1/rankings?ahj=jersey-city-nj&limit=20" \ -H "X-API-Key: $PG_KEY"
{ "markets": [ { "@id": "pg:market:34017006500", "@type": "Market",
"signal": { "class": "heating", "excess_pct": 48.15, ... } },
... ],
"count": 20, "next_cursor": null }You can render our data on a map today, with no extra endpoints. Two joins carry it:
/parcels/{id} returns external_ids.lat/lon — a map-ready centroid. Drop a marker or cluster.pg:market: is the tract GEOID (e.g. 34017006500). Join it to public TIGER/Line tract geometry and fill by signal.class.So one /rankings call gives you a whole jurisdiction's tracts, each with a GEOID and a signal class — enough to paint the same heating choropleth we ship:
# 1) every ranked tract carries its GEOID + signal class
curl "https://developer.permitgranted.ai/api/partner/v1/rankings?ahj=jersey-city-nj&limit=200" -H "X-API-Key: $PG_KEY"
# → markets[].: { "@id": "pg:market:34017006500", signal: { class: "heating" } }
# 2) GEOID = the tract id → join to public tract geometry, fill by signal.class
# heating → crimson · warming → gold · cooling → blue
# parcels give you points directly:
curl "https://developer.permitgranted.ai/api/partner/v1/parcels/03EDA726-..." -H "X-API-Key: $PG_KEY"
# → external_ids: { lat: 40.73494, lon: -74.061621 }When you want the exact lot polygons and the styled layers without the public-geometry join: parcel + tract GeoJSON and first-party vector tiles are designed and next up. No fake shapes here until they're callable.
/parcels/{id}/geometryComing soonParcel footprint as GeoJSON — the polygon, not just the centroid.
When a marker isn't enough: render the exact lot boundary, compute area, or clip against a district. Sourced from the same parcel geometry behind our map.
/markets/{id}/geometryComing soonCensus tract boundary as GeoJSON.
Skip the TIGER/Line join — get the tract polygon inline with its signal so you can fill a choropleth from one response.
/tiles/{layer}/{z}/{x}/{y}.mvtComing soonFirst-party Mapbox Vector Tiles for the parcel + signal layers.
Render the exact map we ship — parcels styled by verdict, tracts by signal — as a GPU vector layer in MapLibre or Mapbox, without hosting your own tile server.
Traversal, evidence, and keeping your copy current. Designed and specced (the OpenAPI document covers them); not yet callable. Ask for early access and we'll prioritize with you.
/entities/{uri}/edgesComing soonTyped relationships — ownership, assemblage membership, jurisdiction.
Graph traversal: from a parcel to who controls it, what assemblage it could join, which market it sits in. The ownership graph exists internally (shell-cluster + assemblage detection); this exposes it under per-jurisdiction privacy rules.
/comparables/{id}Coming soonComparable-period evidence pack — historical analogs with outcomes, failures included.
The substantiation layer: "tracts that looked like this one went on to outperform N of M times." Honest by construction — fizzled analogs are counted, not hidden.
/changesComing soonChange feed since a cursor — keep your copy of the graph in sync.
Instead of re-pulling, poll the diff: entities/properties changed since your last cursor. The nightly bake is the write cadence.
/webhooksComing soonPush notifications on graph events — e.g. a tract turning heating.
The be-early alert: register a URL, get signed events when signals flip in your entitled geographies.