Skip to content

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 /v2 base path, with an announced migration window during which /v1 keeps 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 cursorlast_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_statuscomplete / 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 with school_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 or 0,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 / rent are empty on that unit row; use the floorplan's own rent instead. The unit count is a floor, not an exact figure, and is replaced automatically once real data arrives.

Pricing

  • rent is always base rent only.
  • displayed_total_monthly_cost is a fees-inclusive total, populated only where the source publishes one.
  • price (derived convenience field) prefers rent, falling back to the declared total.
  • pricing_semanticsbase_rent or total_monthly_cost — tells you which source field populated price. Exactly one of rent / displayed_total_monthly_cost is ever non-empty per row.
  • Floorplans also carry price_max.

Room listings

  • listing_type: "room" marks a co-living/room-for-rent listing. beds is forced to "1"; baths / square_feet may describe the shared unit as a whole — don't fold these into whole-apartment aggregate stats.

Amenities

Images

  • photos — property gallery, hero image first, capped at 12, [] = none.
  • photo_url — equals photos[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_friendlyYes / 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.md for the confidence-ladder definitions.

Contacts

  • contact_name, email, phone — the best verified contact for the building. Empty means none trusted yet.

Lead tier

  • lead_tierhql (fully qualified) / phql (one confirmation short) / candidate (inventory observed, contact not yet graded). See ../data-model/glossary.md for the funnel definitions.

Operational notes

  • Freshness: standard-tier buildings and their pricing refresh roughly weekly; priority-tier, roughly daily. last_updated is authoritative for "when was this last verified," not "when did this change" — see the updated_since note 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:
    1. Use include= for a cheap weekly building-level reconciliation pass.
    2. Fetch the full document (include=floorplans,units) only where pricing detail is actually needed.
    3. Send If-None-Match on every request to take advantage of 304 responses.

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.

  • Warehouse schema — full column definitions behind every field in this document.
  • Glossarylead_tier, grain, building_key, and other terms used throughout.
  • Deployment — how client-api/ is deployed and where it fits relative to internal-api/.
  • Environment variablesDATABASE_URL_RO and related configuration.