Developers

API Reference

Read-only JSON projections of the published directory — the same shapes the website itself renders from.

This page is also reachable at /api.

Every response is wrapped

{
  "api_version": "1.0",
  "generated_at": "2026-08-03T09:35:00.000Z",
  "count": 8737,
  "data": [ … ]
}

count appears on collections only. api_version is bumped when a field is removed or changes meaning — adding one is not a breaking change, so clients must ignore unknown properties.

Endpoints

Collections return summaries; items return the full projection.

EndpointKindRecordsReturns
/api/v1/index.jsonService indexUtility—Every collection, its size and the template for a single record. Start here — a client should be able to discover the API without reading this page.
/api/v1/properties.jsonListingsCollection8,737Every published listing in the summary shape. Unpaginated — the whole directory in one request rather than the several round trips a paginated client would make.
/api/v1/properties/{slug}.jsonOne listingSingle record—The full projection, plus the derived value comparison and similar listings — the same numbers the page renders, so a client need not reimplement the arithmetic.
/api/v1/buildings.jsonBuildingsCollection274Every published building, with its live availability count and entry price.
/api/v1/buildings/{slug}.jsonOne buildingSingle record—The full projection, plus every unit we track in that building.
/api/v1/neighborhoods.jsonNeighborhoodsCollection57Every sector, with building and listing counts that agree with the directory pages.
/api/v1/neighborhoods/{slug}.jsonOne neighborhoodSingle record—The full projection, plus the sector’s buildings and current inventory.
/api/v1/agents.jsonAdvisorsCollection3The public roster. Compensation, licensing and internal production are not on the projected type at all.
/api/v1/agents/{slug}.jsonOne advisorSingle record—Profile, coverage and the listings they represent.
/api/v1/articles.jsonEditorialCollection3Published articles and guides, newest first.
/api/v1/articles/{slug}.jsonOne articleSingle record—Including the markdown body.
/api/v1/home.jsonHome page compositionUtility—Everything the home page renders, in one response — featured rail, spotlight tower with its layout ladder, that tower’s sector, newest listings, featured sectors, counters and the citywide market averages. The selection rules are editorial and live on the server, so a client cannot drift from them.
/api/v1/stats.jsonCountersUtility—The same function the trust bar renders from, so a dashboard built on this endpoint cannot disagree with the site’s own footer.
/api/v1/search.jsonSearch indexUtility—One row per record across all types, with a pre-lowercased haystack so a client need not normalise on every keystroke.

Single-record paths take the same slug the page uses — /properties/{slug} on the site is /api/v1/properties/{slug}.json here. Every page also declares its own data with <link rel="alternate" type="application/json">.

What is guaranteed

No internal data

Generated from the same projections the website uses. Floor prices, owner contacts, full addresses, internal notes, private documents and agent compensation are absent by construction — and an audit greps the built output for the real values.

Coordinates are approximate

Stored at neighborhood precision for owner privacy. Do not treat them as a doorstep.

Withheld prices are null

`price_visible: false` means the price is not for publication, and `price` is null rather than a number you should not show. Honour it.

Nullability is real

A field typed `number | null` is null on some records. Do not default it to zero — that is how an average becomes wrong rather than absent.

Errors

RFC 9457 problem details. One error handler, not one per endpoint.

Every failure returns application/problem+json with a type, a title and a status. Match on type — it is a stable URI, and following it in a browser reaches the page describing that error. Validation failures add an errors array with a JSON Pointer per field, so a 422 becomes a message beside the right input rather than a toast.

{
  "type": "https://casanova.realty/problems/validation-failed",
  "title": "Validation failed",
  "status": 422,
  "detail": "1 field failed validation.",
  "instance": "/api/v1/properties/luxor-500-3402",
  "errors": [
    {
      "pointer": "/pricing/sale_price_cents",
      "detail": "must be an integer"
    }
  ]
}

What the service adds

The files here are static. The service that replaces them does the things a static build cannot — all of them now built, none of them yet live on this host:

  • Keyset pagination and filtering — built. The query names match the site’s own filter names.
  • ?updated_since= for incremental sync — built. Every record carries updated_at.
  • ETag and If-None-Match — built, weak comparison per RFC 9110.
  • Write endpoints with If-Match and Idempotency-Key — built.
  • Server-side privacy stripping — built. Every response is projected through an allowlist and checked again on the way out.
  • Rate limiting — built. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.

CORS and caching

Access-Control-Allow-Origin: *
The payload is public by definition.
Cache-Control: public, max-age=0, must-revalidate
Matching the HTML. The directory changes, and a stale listing is worse than a round-trip.
Photography
Image URLs are root-relative. Seven derivative widths exist per photograph — 160, 240, 320, 480, 640, 960 and 1280 — via an @width suffix before the extension.