NYC property data, with evidence built into the response.
Search published parcels, retrieve normalized records, and preserve the source trail. Every response identifies its revision, coverage, warnings, and provenance.
The API is in preview. We are still reviewing each source's terms for delivery through an
API, so responses return parcel identifiers and coverage, and mark every other field
withheld
until that review is done. A search that returns parcels counts toward your monthly
allowance; a property response with every field withheld does not.
Start here
Make your first request
Create a test key, keep it in an environment variable, and search by street address, BBL, or LotContext property ID. Search responses are never presented as proof that a record does or does not exist outside published coverage.
curl --get "https://lotcontext.com/api/v1/properties" \
--data-urlencode "query=100 Broadway" \
--data-urlencode "limit=5" \
--header "Authorization: Bearer $LOTCONTEXT_API_KEY"
const url = new URL("https://lotcontext.com/api/v1/properties");
url.search = new URLSearchParams({ query: "100 Broadway", limit: "5" });
const response = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.LOTCONTEXT_API_KEY}` }
});
const payload = await response.json();
import os
import requests
response = requests.get(
"https://lotcontext.com/api/v1/properties",
params={"query": "100 Broadway", "limit": 5},
headers={"Authorization": f"Bearer {os.environ['LOTCONTEXT_API_KEY']}"},
timeout=15,
)
payload = response.json()
response =
Req.get!("https://lotcontext.com/api/v1/properties",
params: [query: "100 Broadway", limit: 5],
auth: {:bearer, System.fetch_env!("LOTCONTEXT_API_KEY")}
)
payload = response.body
require "net/http"
require "json"
uri = URI("https://lotcontext.com/api/v1/properties")
uri.query = URI.encode_www_form(query: "100 Broadway", limit: 5)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{ENV.fetch("LOTCONTEXT_API_KEY")}"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(request) }
payload = JSON.parse(response.body)
your HTTPS endpoint
Available
Signed change webhooks
Configure endpoints in the developer console. LotContext signs the exact body, sends a stable delivery ID as Idempotency-Key, retries failed deliveries with exponential backoff, and keeps a replayable delivery log.
X-LotContext-Signature
t=unix_seconds,v1=hex_hmac. Compute HMAC-SHA256 over
timestamp.body
with the signing secret.
Idempotency-KeyPersist this value before side effects. Replays retain the same key.
property.changedpermit.publishedapplication.publishedviolation.status_changeddocument.recorded
Security
Authentication
Create keys in the
developer console
and send one as Authorization: Bearer lc_….
A key is shown once; LotContext stores only its hash. Use environment variables in
applications and never place keys in URLs or source control.
lc_test_…
Test mode. Reads the same published data and shares the monthly allowance.
lc_live_…
Live mode. Successful responses that deliver data use one unit.
Revoked or expired keys stop authenticating immediately.
/api/v1/properties
Available
Search properties
Returns published parcels matching the query and filters. Results contain compact parcel
summaries; use the returned id with the detail
endpoint for the complete evidence-backed dossier.
Query parameters
-
querystring - Street address, 10-digit BBL, or LotContext property ID. Minimum 3 characters.
-
cursorstring -
Opaque continuation cursor from
meta.next_cursor. A cursor is bound to its filters and publication revision. -
limitinteger - Maximum results to return. Defaults to 400 and is capped at 400.
-
boroughinteger - NYC borough code, 1 through 5. Optional.
-
formatenum -
json(default) returns the standard envelope.geojsonreturns a GeoJSON FeatureCollection with the same revision and warnings metadata.
Response contract
data[]Matched parcels and normalized summary fields.
metaRequest ID, API version, result count, and applied limit.
warnings[]Coverage, licensing, staleness, or fixture qualifications.
linksStable paths to the property dossier and related collections.
/api/v1/changes
Available
Publication change feed
Consume committed canonical changes in revision order with a key carrying changes:read. Save
meta.next_cursor
after processing a page, then send it
back to continue or poll. Filter by property, resource type, or change type without
exposing collector endpoints or provider health.
revisionMonotonic publication boundary.
fields[]Normalized fields changed in this publication.
publicationSource, release, and publish time.
Reference
Endpoint index
| Method | Path | Returns | Status |
|---|---|---|---|
| GET | /properties?query={address-or-BBL} | Published parcel matches | Available |
| GET | /properties/{id} | Complete property dossier | Available |
| GET | /properties/{id}/violations | Normalized violation records | Available |
| GET | /properties/{id}/permits | Normalized permit records | Available |
| GET | /properties/{id}/applications | Normalized application records | Available |
| GET | /properties/{id}/transactions | Recorded-document summaries | Available |
| GET | /properties/{id}/timeline | Chronological activity | Available |
| GET | /properties/{id}/reported-ownership | Source-qualified parties | Available |
| GET | /changes | Committed canonical publication changes | Available |
| GET | /meta/coverage | Published corpus coverage and freshness | Available |
API lifecycle
Changelog
-
September 22, 2026 · v1
Webhook delivery and coverage metadata
Added signed idempotent webhooks, delivery logs and replay,
/meta/coverage, GeoJSON search output, and Ruby/Elixir clients. -
September 20, 2026 · v1
Initial public contract
Published property search, dossier sections, revision-bound cursors, change feed, conditional requests, test keys, and OpenAPI 3.1.
Data semantics
Coverage is part of the payload
A null
never stands in for “no records.” Each dossier
section reports an explicit state so an unavailable source cannot be confused with a
completed search that found nothing.
availableno_matching_recordsnot_coveredstaleconflictingtemporarily_unavailablewithheld
status: "withheld"
with a source_rights_pending
warning. Those responses are not billed.
Decimals are strings, areas carry explicit units, timestamps are ISO 8601 UTC, and
meta.data_revision
identifies the publication reflected by the response.
Successful responses include ETag, X-Data-Revision, and rate-limit headers. Send
If-None-Match
to receive an unbilled 304
when the publication is unchanged.
Failure modes
Errors
Every error uses the same envelope: {"error": {"code": …, "message": …, "request_id": …}}.
Include the request ID when contacting support.
- 400
- invalid_request
- A parameter is malformed or outside its supported range.
- 401
- invalid_credentials
- The key is missing, malformed, revoked, or expired.
- 404
- not_found
- No published property has that identifier.
- 429
- quota_exceeded
- The monthly allowance or per-minute rate limit was reached.
Usage
Allowances
Every account starts on Free. Paid tiers are not yet on sale; these are draft limits for testing and may change before launch. Limits are hard caps, with no surprise overage charges.
| Tier | Monthly units | Active keys | Availability |
|---|---|---|---|
| Free | 100 | 2 | Available |
| Developer | 5,000 | 5 | Draft |
| Pro | 50,000 | 10 | Draft |
| Business | 200,000 | 25 | Draft |