Client API Reference¶
The Client API is the public bulk-sync feed used by our partner Relive to sync
Foundational Data's building intelligence into their own database. It is deployed as its
own Vercel project (client-api/), reads only the read-only client_buildings view via a
dedicated DATABASE_URL_RO, and is gated by a per-consumer X-API-Key.
Coverage
Currently serves the Miami metro (Miami-Dade, Broward, Palm Beach counties). More metros are planned under the same contract; a metro not yet covered simply has no properties in the feed — there is no separate "unsupported metro" error.
Intended usage
This API is meant to be called from a backend sync job on your side, on a daily/weekly cadence (see Operational notes). It is not meant to be called from client-side / browser code — your API key would be exposed.
Base path: /v1
Authentication¶
Every request requires an X-API-Key header.
GET /v1/properties?limit=100 HTTP/1.1
Host: api.foundationaldata.example
X-API-Key: fd_live_xxxxxxxxxxxxxxxx
A missing or invalid key returns 401. Keys are provisioned per-consumer manually —
contact the Foundational Data team to have a key issued or rotated. There is no
self-service key management endpoint.
Versioning¶
- Fields are append-only within
v1: a field already shipped is never renamed, removed, or retyped. - Enum vocabularies (e.g.
lead_tier,locator_friendly) only ever gain new values, and only with advance notice. - Any breaking change ships as a new
/v2base path, with an announced migration window during which/v1keeps working unchanged.
Consumers should treat unrecognized fields and unrecognized enum values as forward-compatible no-ops rather than errors.
Resource shape¶
Every property is one nested document:
{
"property": {
"...building fields": "...",
"floorplans": [
{
"...floorplan fields": "...",
"units": [
{ "...unit fields": "..." }
]
}
]
}
}
All values are strings unless otherwise noted (last_updated is ISO 8601 UTC, e.g.
2026-08-01T12:00:00.000Z). An empty string "" means "unknown" throughout the
contract — it is never a guessed or filler value. Treat "" and absence of data
identically.
See ../data-model/warehouse-schema.md for the
underlying table/column definitions and
../data-model/glossary.md for terms like lead_tier,
building_key, and grain.
GET /v1/properties¶
Paginated list of all served properties.
Query parameters¶
| Param | Type | Default | Notes |
|---|---|---|---|
limit |
int | 100 |
Page-size upper bound, max 500. Not a quota — a page can be shorter than limit and still not be the last page (also bounded by a response-size budget). Keep paging until next_cursor is null. |
cursor |
string | — | Opaque continuation token from the previous page's next_cursor. Never construct or parse it yourself — pass it back verbatim. A stale or invalid cursor returns 400; treat that as "restart sync from the beginning." |
updated_since |
ISO 8601 UTC | — | Returns properties where any grain (building/floorplan/unit) changed after this timestamp. This is a re-verification cursor, not a change cursor — last_updated advances on every refresh cycle regardless of whether anything actually changed. Use price-history if you specifically need "what moved." |
include |
comma-separated | full doc | include=floorplans,units (default, explicit) · include=floorplans (no nested units, ~0.45x response size) · include= (building fields only, no floorplans key at all, ~0.08x size). include=units without floorplans returns 400. |
Response 200¶
{
"properties": [
{ "...property fields": "..." }
],
"next_cursor": "opaque-string-or-null"
}
next_cursor: null is the only end-of-data signal. Pagination is keyset-based and
stable under concurrent writes — no offset drift, no skipped or duplicated rows across a
paging run.
Caching¶
Every 200 response carries a strong ETag. Send it back as If-None-Match on the next
call to the same URL; an unchanged page returns 304 Not Modified with no body.
Cache-Control: private, no-cache
no-cache means always revalidate — never cache a response blindly without checking the
ETag first.
Errors¶
| Status | When |
|---|---|
401 |
Missing or invalid X-API-Key. |
400 |
Stale/invalid cursor, or include=units without floorplans. |
GET /v1/properties/{property_id}¶
Single property by id, in the same nested document shape as the list endpoint.
URL-encode the id
Property ids can contain | and spaces, e.g. 2155 northwest 7 avenue|33127. The
caller is responsible for URL-encoding the id segment.
Response¶
| Status | When |
|---|---|
200 |
Property found; body is { "property": { ... } }. |
401 |
Missing or invalid X-API-Key. |
404 |
Not served (unknown id, or the property has been dropped from coverage). |
GET /v1/properties/{id}/price-history¶
One rent trend line for a building.
Query parameters¶
| Param | Type | Default | Notes |
|---|---|---|---|
days |
int | 180 |
Window length, max 730. Out-of-range values are clamped, never rejected. |
Response 200¶
{
"id": "2155 northwest 7 avenue|33127",
"window_days": 180,
"points": [
{ "date": "2026-07-01", "rent": 1850, "rent_min": 1600, "rent_max": 2100, "plans": 3 }
],
"first_observed": "2026-01-15T00:00:00.000Z",
"last_observed": "2026-07-01T00:00:00.000Z",
"truncated": false
}
| Field | Meaning |
|---|---|
rent |
Blended asking rent across bedroom counts. |
rent_min / rent_max |
Cheapest / priciest bedroom count on that date. |
plans |
How many distinct bedroom counts are represented. A plans change means the unit mix changed, not necessarily that price did. |
truncated |
true means the window hit an internal row cap — shorten days and re-request. |
Step function, not a line
The series is a step function — a point only appears where something moved. Chart it with step interpolation, not linear interpolation, between points.
Prices are concession-adjusted where determinable, matching the floorplans[].price
field on the main resource.
Errors¶
| Status | When |
|---|---|
200, points: [] |
Valid response — "we serve this building, no priced history in this window." Not an error. |
401 |
Missing or invalid X-API-Key. |
404 |
The id isn't served at all. |
POST /v1/properties/{id}/refresh¶
Requests a faster refresh cadence for one property (e.g. because an end user is actively viewing its listing).
Request body¶
{
"action": "priority",
"ttl_days": 30,
"reason": "agent is actively viewing this listing"
}
| Field | Type | Required | Notes |
|---|---|---|---|
action |
string | no | "priority" (default) or "standard" (releases the property early, back to the standard cadence). |
ttl_days |
int | no | 1–90. Priority expires automatically rather than staying elevated forever. |
reason |
string | no | Free text, ≤200 characters. |
Response 200¶
{ "tier": "priority", "expires_at": "2026-09-03T00:00:00Z", "expedited": true }
expedited: true means a genuine promotion was just scheduled (typically within roughly
30 minutes); calling this again on a property already on priority does not re-expedite
it — expedited will be false on the repeat call.
Cadence is a target, not a guarantee
Standard-tier properties re-price roughly weekly; priority-tier, roughly daily. Both are operational targets, not SLAs.
Errors¶
| Status | When |
|---|---|
401 |
Missing or invalid X-API-Key. |
404 |
The id isn't served at all. |
409 |
The property isn't yet enrolled in refresh tracking (retry shortly), or the priority list is currently full. |
Field reference (selected)¶
The full column-level contract lives in
../data-model/warehouse-schema.md. The fields below
are the ones most likely to need explanation.
Schools
school_elementary/school_middle/school_high— address-boundary assignments from the latest official source.school_status—complete/partial/unavailable.school_source_url— supports manual verification.nearby_schools— up to 6 nearby public/charter/private alternatives (proximity only, no enrollment-rule guarantee), each withschool_id, name, school_type, grades, address, distance_miles, data_year.
Coordinates
latitude/longitude— WGS84 decimal-degree strings from a US Census Bureau geocoder. An empty value means unresolved — never a fallback ZIP/city centroid or0,0. Skip the marker rather than plotting a wrong location.
Empty floorplans
floorplans: []is normal ("no unit data yet"), not an error or a removal signal — the building-level fields (contacts, commission,listing_url,listing_price) are still the deliverable for such properties.
Inferred units
inferred: "yes"means the source page proved at least one unit of that floorplan exists/is available without publishing full details —unit_name/sqft/rentare empty on that unit row; use the floorplan's ownrentinstead. The unit count is a floor, not an exact figure, and is replaced automatically once real data arrives.
Pricing
rentis always base rent only.displayed_total_monthly_costis a fees-inclusive total, populated only where the source publishes one.price(derived convenience field) prefersrent, falling back to the declared total.pricing_semantics—base_rentortotal_monthly_cost— tells you which source field populatedprice. Exactly one ofrent/displayed_total_monthly_costis ever non-empty per row.- Floorplans also carry
price_max.
Room listings
listing_type: "room"marks a co-living/room-for-rent listing.bedsis forced to"1";baths/square_feetmay describe the shared unit as a whole — don't fold these into whole-apartment aggregate stats.
Amenities
- Three additive grains — property / floorplan / unit — see
../data-model/amenities-and-grains.mdor../data-model/glossary.md. Controlled vocabulary, append-only per the versioning policy above. Absence at a grain never means "confirmed doesn't have."
Images
photos— property gallery, hero image first, capped at 12,[]= none.photo_url— equalsphotos[0], kept for backward compatibility.floorplans[].image_url— a floorplan diagram, not a photo.- All image URLs are absolute URLs to the original source/CDN — hotlink them, don't re-host.
Commission verdict
locator_friendly—Yes/Likely/No/Unknown.locator_commission,locator_confidence(high/med/low),locator_evidence,commission_basis,commission_source,mls_coop_commission.- See
../data-model/amenities-and-grains.mdfor the confidence-ladder definitions.
Contacts
contact_name,email,phone— the best verified contact for the building. Empty means none trusted yet.
Lead tier
lead_tier—hql(fully qualified) /phql(one confirmation short) /candidate(inventory observed, contact not yet graded). See../data-model/glossary.mdfor the funnel definitions.
Operational notes¶
- Freshness: standard-tier buildings and their pricing refresh roughly weekly;
priority-tier, roughly daily.
last_updatedis authoritative for "when was this last verified," not "when did this change" — see theupdated_sincenote above. - Rate guidance: the API is sized for one daily delta poll plus one weekly full sync
per consumer. Stay under roughly 2 requests/second with
limit=500. Contact the team before running a backfill or a load test. - Recommended sync pattern:
- Use
include=for a cheap weekly building-level reconciliation pass. - Fetch the full document (
include=floorplans,units) only where pricing detail is actually needed. - Send
If-None-Matchon every request to take advantage of304responses.
- Use
Do not treat this as a scraping bypass
This feed exists so partners don't need to independently discover or scrape the same inventory. Requesting far above the rate guidance, or attempting to reconstruct coverage outside your contracted metro(s), is out of scope for this key — contact the team first.
Related documentation¶
- Warehouse schema — full column definitions behind every field in this document.
- Glossary —
lead_tier, grain,building_key, and other terms used throughout. - Deployment — how
client-api/is deployed and where it fits relative tointernal-api/. - Environment variables —
DATABASE_URL_ROand related configuration.