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.
| Endpoint | Kind | Records | Returns |
|---|---|---|---|
| /api/v1/index.jsonService index | Utility | — | 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.jsonListings | Collection | 8,737 | Every 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 listing | Single 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.jsonBuildings | Collection | 274 | Every published building, with its live availability count and entry price. |
| /api/v1/buildings/{slug}.jsonOne building | Single record | — | The full projection, plus every unit we track in that building. |
| /api/v1/neighborhoods.jsonNeighborhoods | Collection | 57 | Every sector, with building and listing counts that agree with the directory pages. |
| /api/v1/neighborhoods/{slug}.jsonOne neighborhood | Single record | — | The full projection, plus the sector’s buildings and current inventory. |
| /api/v1/agents.jsonAdvisors | Collection | 3 | The public roster. Compensation, licensing and internal production are not on the projected type at all. |
| /api/v1/agents/{slug}.jsonOne advisor | Single record | — | Profile, coverage and the listings they represent. |
| /api/v1/articles.jsonEditorial | Collection | 3 | Published articles and guides, newest first. |
| /api/v1/articles/{slug}.jsonOne article | Single record | — | Including the markdown body. |
| /api/v1/home.jsonHome page composition | Utility | — | 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.jsonCounters | Utility | — | 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 index | Utility | — | 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
@widthsuffix before the extension.