Build on deed-verified condo data
One REST API and one MCP server over CURVE's registry-verified condominium record: every recorded sale, each unit's resale chain, owners of record, condo documents, analytics and your own MLS. Connect an AI assistant in a minute, or call it directly.
Quickstart
From a CURVE account to your first call.
- Have a paid CURVE subscription, or a seat on oneThe API authenticates as you, so it carries your account's market entitlements. Access comes with your own paid CURVE subscription or a team seat on someone else's. Access granted any other way — a free trial, a promotional Pro preview, a referral reward — works in the web app but not here: those tokens authenticate fine and then answer 403 api_requires_paid_plan on every route.
- Create a personal access tokenAccount → API & Connections → Personal access tokens. Pick the scopes you need, pick an expiry, and copy the secret — it starts with curve_pat_ and is shown exactly once.
- Make your first callEvery response is an envelope: a `data` payload plus `meta`. List endpoints add `next_cursor`. This one returns the markets your account is entitled to — the ids you substitute for {market} everywhere else.Terminal
curl -s "https://curvereports.com/api/v1/markets" \ -H "Authorization: Bearer $CURVE_TOKEN" - Read the machine-readable specThe full OpenAPI 3.1 document is served at https://curvereports.com/api/v1/openapi.json. Point a client generator at it rather than hand-writing types — it is generated from the same route registry this page is, and it declares every operation's path and query parameters, error codes, and — for every write operation — its JSON request body.
What comes back
{
"data": [
{
"id": "boston",
"name": "Boston",
"live": true,
"capabilities": { ... },
"building_count": ...,
"dataset_version": "...",
"completed_at": "..."
}
],
"meta": {}
}Every success is an envelope: data plus meta. List endpoints add next_cursor — pass it back as ?cursor= to page forward, and size pages with ?limit= (default 100, max 500). A next_cursor of null means you have the whole set.
Base URL https://curvereports.com/api/v1. Full specification: https://curvereports.com/api/v1/openapi.json. Every route is documented there with the same summaries you see below, because both are generated from one registry.
Authentication
Two ways in, one verifier behind them. Both produce a bearer token that carries YOUR account’s market entitlements and plan limits — the API never sees data your account cannot.
Personal access tokens
For your own scripts, notebooks and back-office jobs. Create one at Account → API & Connections.
- Created at Account → API & Connections → Personal access tokens. The secret starts with `curve_pat_` and is displayed exactly once.
- Scopes are chosen per token. Every scope is pre-selected EXCEPT owners:export — bulk owner export is opt-in even on a token you mint for yourself.
- Expiry is your choice: never, 30, 90 or 365 days. Revoking a token kills it immediately.
- Send it as `Authorization: Bearer curve_pat_...`. Never put it in a query string, a client bundle, or a public repository.
OAuth 2.1 with PKCE
For an application connecting on a user’s behalf — including every MCP client. Nobody copies a token: the client discovers CURVE from the metadata documents below, the user reviews every requested permission before approving, and the grant is revocable at any time from Account → API & Connections → Connected apps.
Scopes
A token carries the scopes it was granted and nothing more. A call missing one answers 403 insufficient_scope, and the WWW-Authenticate header repeats the scopes the route required.
| Scope | Grants | Personal token | OAuth, no scope requested |
|---|---|---|---|
| data:read | Read market data: buildings, sales, units, registry events, analytics, developer intelligence, and dataset downloads for your entitled markets. | Pre-selected | Granted |
| docs:read | Read condo documents (master deeds, amendments, plans) and their OCR'd page text for your entitled markets. | Pre-selected | Granted |
| owners:read | View owner-of-record lists and owner intelligence briefs, under your account's owner-list access. | Pre-selected | Must be requested by name |
| owners:export | Export full owner lists (counts against your account's rolling export quota). | Off by default | Must be requested by name |
| ai:ask | Ask Curve AI questions and receive grounded, cited answers. | Pre-selected | Granted |
| workflow:read | Read your shares, embeds, trackers, follows, pins, CMA sheets, and their engagement data. | Pre-selected | Granted |
| workflow:write | Create and manage shares, embeds, trackers, follows, pins, and CMA sheets; send share emails. | Pre-selected | Must be requested by name |
| reports:generate | Generate branded building report PDFs. | Pre-selected | Granted |
| mls:read | Use your connected MLS/TAN to read your listings and run live searches. | Pre-selected | Must be requested by name |
Connect an AI assistant
CURVE runs an MCP server, so an assistant can query the record directly rather than being pasted screenshots of it.
- Endpoint: https://curvereports.com/api/mcp
- Transport: Streamable HTTP, MCP specification revision 2026-07-28.
- The MCP tools and the REST endpoints run behind the same verifier, the same scopes and the same quota classes — anything a tool can do, a token with that scope can do over REST. REST is the wider surface: datasets, the Daily Bulletin, market-wide events, /v1/me and /v1/usage have no tool.
- CORS is open, and MCP-Protocol-Version, Mcp-Method and Mcp-Name are all allowed on preflight, so a browser-hosted client can connect.
- Every tool carries all four MCP annotations — readOnlyHint, destructiveHint, idempotentHint and openWorldHint — so a client can confirm only what genuinely needs confirming. The reads are annotated read-only; a write that only adds something declares destructiveHint false; and the handful that take something away, send real email, replace stored rows or spend a permanent allowance are the ones that ask. In particular, get_owner_list is a pure read and unlock_owner_list is the separate tool that spends an included-building slot.
One click
These editors install an MCP server from a link. The link carries the CURVE address; the editor runs the OAuth approval. There is nothing to paste and no token to mint.
Or have an assistant do it
Paste this into an assistant that can run shell commands — Claude Code, Codex CLI, Cursor's agent. It names both add commands, so the assistant runs the one that matches itself and then tells you how to authorize. An assistant that cannot register a server says so rather than improvising.
Connect me to CURVE, a real-estate market-data MCP server at https://curvereports.com/api/mcp.
If you can run shell commands, run the one that matches you and nothing else:
- Claude Code: claude mcp add --transport http curve https://curvereports.com/api/mcp
- Codex CLI: codex mcp add curve --url https://curvereports.com/api/mcp
It uses OAuth, so after adding it tell me the exact next step to authorize it (in Claude Code that is /mcp, then pick curve and choose Authenticate; in Codex that is codex mcp login curve). Do not ask me for a token or a password — there isn't one.
If you cannot run shell commands, say so plainly and tell me where to add it in my client's settings instead. Do not try to open a browser or sign in on my behalf.Or add it by hand
Every client, including the ones with no one-click install.
claude.ai
- Settings → Connectors → Add custom connector.
- Paste the CURVE MCP URL: https://curvereports.com/api/mcp
- Click Connect. Claude discovers CURVE's authorization server, registers itself, and sends you to CURVE's authorization screen.
- Sign in to CURVE if you are not already, review the requested permissions, and Approve or Cancel. No token is copied by hand.
- The connection appears under Account → API & Connections → Connected apps, where you can revoke it at any time.
Claude Code
- Run the command below in any project.
- Claude Code opens the CURVE authorization screen in your browser on first use; approve it once.
- Ask something like "what did units at Millennium Tower sell for last year?" to confirm the tools are live.
claude mcp add --transport http curve https://curvereports.com/api/mcpCodex CLI
- Run both commands below in any terminal.
- The second one opens the CURVE authorization screen in your browser; approve it once. Codex then holds an OAuth grant, so there is no token to mint and no environment variable to keep in sync.
- The connection appears under Account → API & Connections → Connected apps, where you can revoke it at any time.
codex mcp add curve --url https://curvereports.com/api/mcp
codex mcp login curveChatGPT
- ChatGPT calls these apps. Full MCP, including CURVE's write actions, is available on ChatGPT web for Business, Enterprise and Edu; Pro supports read/fetch tools only.
- Enable Developer mode. Business admins/owners can start at Workspace Settings → Apps → Create. Enterprise/Edu admins grant access under Workspace Settings → Permissions & Roles → Connected Data; an authorized user then enables it at Settings → Apps → Advanced settings.
- From Workspace Settings → Apps → Create (admin/owner) or Settings → Apps → Create (authorized user), add https://curvereports.com/api/mcp as the server URL with Streamable HTTP and OAuth.
- Press Scan Tools. That is the step that runs OAuth, so a failure there is the CURVE authorization screen, not the URL.
- Enable the CURVE connector in the composer for the conversations where you want it.
Any other MCP client
- Transport is Streamable HTTP at https://curvereports.com/api/mcp — one endpoint, POST for every request. The server is stateless, so there is no GET server stream to open (GET answers 405), no stdio binary to install and no SSE-only fallback.
- Authentication is a bearer token: either an OAuth 2.1 access token obtained through the discovery documents listed under Authentication, or a personal access token sent as Authorization: Bearer curve_pat_....
- An unauthenticated request answers 401 with a WWW-Authenticate header naming the protected-resource metadata document, which is where a compliant client starts discovery.
- Call tools/list for the live tool catalogue: it is the authoritative list, and each tool's description names the scope it needs, its caps and its refusals.
Endpoint reference
All 72 REST endpoints, with the scopes and quota class each one requires. This table and the OpenAPI document are generated from the same route registry, so they cannot disagree.
Account & usage
Who this token is, what it may see, and how much of its quota is left.
Condo documents
Master deeds, amendments, plans and their OCR'd page text, plus short-lived signed PDF URLs.
Owners & entity resolution
Owner-of-record lists, deed chains, beneficial owners and CSV export — behind the platform's fail-closed owner-list access.
Your MLS & TAN connections
Your own connected listing sources: saved workspaces, live search, refresh and job polling. Third-party licensed data, never CURVE's corpus.
Curve AI
The grounded, cited answer pipeline the CURVE product runs, over HTTP.
Report PDFs
Branded building report rendering — a two-phase start-then-poll contract.
Analytics
The analytics query engine and saved views: one computed value per requested compute key.
Developer intelligence
Sellout, absorption, pricing trajectory, floor premium and resale cohorts, per building and market-wide.
Datasets & bulk download
Version-addressed, content-hashed dataset chunks for pulling a market's corpus down whole.
CMA & comparables
The CMA Builder pipeline: ranked comparable sales and persisted comp sheets.
Shares & embeds
Share links, their email fan-out and engagement, plus embedded widgets and their placement counters.
Trackers, follows & pins
Saved client searches, followed units and pinned buildings.
Rankings & Daily Bulletin
Registry-backed building superlatives and the day-keyed recorded-sales bundle.
Markets, buildings, sales & units
The core corpus: every market you are entitled to, its buildings, and the recorded sales, units and registry events beneath them.
Field semantics
The definitions behind the numbers. Recomputing a CURVE figure from raw rows without these will produce a number that is close, wrong, and hard to explain.
Every field name is snake_case
Field names on the core market-data, owners and documents payloads are `snake_case` on both surfaces — REST bodies and MCP tool results alike. Several of those payloads are CURVE's own internal records, and some of those are camelCase inside the product; the API renames them at its serialization boundary, which is why `get_market_stats` answers `avg_price` and not `avgPrice`, an owner row carries `unit_id` / `held_years`, a condo document carries `doc_key` / `book_page`, and a combo sale carries `combo_display`. The CMA comparables envelope joined them: `get_comps` and the comps route answer `algorithm_version`, `as_of_date`, `cutoff_date`, `eligible_count`, `verified_pool_count` and `generated_at`. Two places do NOT follow that rule yet and are being converged: the computed ANALYTICS shapes — developer intelligence, the peer benchmark board, saved views, and the comparable ROWS nested inside a comps response (`fitScore`, `matchBand`, `reasonCodes`) — still publish their internal camelCase field names, and a handful of MCP tool ARGUMENTS are camelCase (`docKey`, `unitId`, `widgetType`). For those, read the schema: the OpenAPI document for a REST shape, each tool's own input schema for an argument.
Medians are trailing-12-month, and separate from all-time counts
A building's or market's headline `medianPrice` / `medianPsf` is computed over the trailing 12 months of qualifying sales, while sale counts and totals in the same payload are all-time. They answer different questions on purpose: "what is this trading at now" versus "how much has ever traded here". A median over a window with too few sales is still reported — check the accompanying count before quoting it.
Gain, loss and flat are three buckets, not two
`gain_loss_pct` is a sale's price against the same unit's prior recorded sale. Every gain/loss computation across CURVE buckets it three ways: gain is > +1%, loss is < −1%, and anything within ±1% inclusive is FLAT — neither. If you re-bucket on the raw field with a simple `> 0` test your gain rate will not match CURVE's, and the gap is exactly the flat band. `gain_loss_pct` is null when there is no prior sale to compare against.
Affordable-restricted sales are excluded from market-rate analytics
A sale carrying `is_affordable_sale: true` is under an affordable-housing price restriction (`affordable_restriction_source` says how that was inferred). Those rows are excluded from every market-rate calculation — averages, medians, gain/loss rates and any gain basis — because a deed-restricted price is not a market price. They are still RETURNED on the sales endpoints: the flag is on the row so you can include or exclude them deliberately, and the default analytics answer excludes them.
Financing coverage is per-market, and absence is stated rather than guessed
`financing_status` / `has_mortgage` describe whether a purchase was cash-like or mortgage-financed, derived from the recorded instruments. Coverage exists today in Boston, Austin. In Miami, Atlanta, New York, San Francisco, Los Angeles the registry feed does not support it, so financing filters are suppressed and the `cash_vs_mortgage` compute key answers `{"error":"not_available_for_market"}` rather than returning an empty or half-populated breakdown. A sale whose instruments are ambiguous is classed "Unknown", and the Unknown group is always returned — including at zero count — so absence is never inferred from a missing key.
Not-found and not-entitled are the same answer
An unknown market id and a real market your account is not entitled to both return an identical `404 not_found`. The same holds for another user's share, embed, tracker or MLS job. This is deliberate: an error that distinguishes the two is an existence oracle. Do not branch on 404 to infer that something exists.
Every price is a recorded price
CURVE publishes no automated valuations, asking prices or listing estimates. A `sale_price` is a price a unit actually recorded at in the public registry, reconciled to the building and unit. Nominal and non-arm's-length transfers are classified and separated so a $1 family transfer never lands in a median.
Your MLS connection
The /v1/mls/* routes read YOUR connected MLS account. This is third-party licensed data reached with your own sign-in — not part of CURVE’s registry corpus, and not available to anyone else. TAN signals are not served here; they reach only the Curve AI answer pipeline, on a turn that carries mls:read.
Working with the MLS routes
- Call GET /v1/mls/connection first. It is the one MLS route that never refuses; every other one answers 409 mls_not_connected or 409 mls_reauth_required, each carrying a connect_url your user can open.
- Live search is serialized behind your MLS sign-in's live-session lock. Branch on the returned status — completed, busy, unavailable, failed — and on busy, retry in about a minute rather than parallelizing.
- mls:read is never granted to a third-party OAuth client by omission. It IS pre-selected on a personal token you mint for yourself, because you are the MLS licensee acting on your own behalf; an application acting for you is not, and must request it by name.
Errors
Every non-2xx body is `{error, message?}`. Branch on `error` — it is the stable machine-readable code; `message` is human-facing prose and may change. Per-endpoint codes (owner_list_access_required, owner_export_quota_exceeded, embed_upgrade_required, market_access_required, building_filter_required, render_failed and the rest) are documented on the endpoint that raises them, in the endpoint reference and in the OpenAPI document. Every 429 carries `retry_after_seconds` in the body and a `Retry-After` header, whichever ledger it came from — so you can back off uniformly without branching on the code first.
| Status | Code | What it means |
|---|---|---|
| 401 | invalid_token | Missing, malformed, revoked, expired, or audience-mismatched bearer token. The WWW-Authenticate header carries the RFC 9728 resource_metadata pointer that starts OAuth discovery. |
| 403 | api_requires_paid_plan | The token is valid but the account has no paid CURVE subscription and no team seat on one. Trial, promotional-preview and referral-reward access do not carry API access, even though they unlock the web app. |
| 403 | insufficient_scope | The token is missing a required scope. The message names the missing ones and WWW-Authenticate repeats the required set. |
| 403 | account_suspended | The account is suspended. Tokens stop working immediately. |
| 403 | password_reset_required | The account must reset its password in the web app before its tokens work again. |
| 404 | not_found | The resource does not exist, or exists and is not yours / not entitled. Identical either way, deliberately. |
| 409 | mls_not_connected | No usable MLS sign-in on this account. Carries connect_url — show it to your user. |
| 409 | mls_reauth_required | The stored MLS credentials expired. Same connect_url. |
| 429 | rate_limited | A quota class is exhausted, or — on /v1/ask and ask_curve only — CURVE's platform-wide daily AI spend cap has been reached, which the message names and which resets at 00:00 UTC. Either way the body carries retry_after_seconds and the Retry-After header repeats it. No partial result is returned. |
Quotas
Metered per account, per quota class, in UTC windows. Owner accounts are unmetered. These are the launch defaults for each plan.
| Quota class | Plus / day | Plus / min | Pro / day | Pro / min | On counter failure | Endpoints |
|---|---|---|---|---|---|---|
| general | 10,000 | 60 | 50,000 | 300 | serves (fail-open) | 44 |
| ask | 25 | — | 200 | — | refuses (fail-closed) | 1 |
| mls_live | 10 | — | 40 | — | refuses (fail-closed) | 2 |
| dataset_chunks | 200 | — | 2,000 | — | refuses (fail-closed) | 1 |
| workflow_write | 120 | — | 600 | — | refuses (fail-closed) | 19 |
| owners_read | 300 | — | 1,500 | — | refuses (fail-closed) | 2 |
| owners_export | 30 | — | 120 | — | refuses (fail-closed) | 1 |
| share_email | 1,000 | — | 2,000 | — | refuses (fail-closed) | 1 |
| reports_generate | 30 | — | 120 | — | refuses (fail-closed) | 1 |
- Windows are UTC. Every metered response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds) for the day window; a 429 adds Retry-After.
- fail mode `closed` means that if CURVE cannot count your usage, the request is refused rather than served. `general` is the only fail-open class — a read that is very occasionally over-served is a better failure than a platform outage. Everything that touches personal data, writes, spends money or calls a third party fails closed.
- GET /v1/usage reports your live counters and your plan's limits, including any environment override in effect. Read it rather than hardcoding the numbers on this page.
- Some endpoints sit behind a SECOND, tighter ledger that is the real binding limit — owner exports against the account's rolling 24h export ledger, share emails against the platform's hourly send limiter. The quota class above those is an outer circuit breaker, not the number you will hit first.
Changelog
v1 is stable: fields are added, never removed or repurposed, and a breaking change would ship as a new version prefix.
- Every tool now advertises all four MCP annotations rather than readOnlyHint alone. destructiveHint defaults to true in the spec whenever readOnlyHint is false, so every write was being presented to clients as destructive — including the ones that only add a row. Adding a share link, an embed, a tracker search, a saved view, a CMA sheet, a unit follow or a building pin now declares destructiveHint false; revoke_share, send_share_email and refresh_mls keep the destructive stance.
- search_mls, generate_report and export_owner_list are annotated read-only: they read (the MLS, a cached render, a building already unlocked) and change nothing in the account's data. Their quota classes are unchanged — an expensive call is rationed by its ledger, not by a confirmation prompt.
- MCP only: get_owner_list no longer takes a claim argument and can never spend an included-building slot, so it is a pure read; a preview result now carries unlock_hint (in place of claim_hint) naming the tool that unlocks. The new unlock_owner_list tool is where the permanent unlock lives, and it returns the full list for the building it unlocks. The REST route is unchanged — GET /v1/markets/{market}/buildings/{slug}/owners still takes ?claim=true and still answers claim_hint.
- No scope or entitlement changed. owners:read, owners:export and the fail-closed owner-list gate behave exactly as before.
- Core market-data, owners and documents fields that still carried CURVE's internal camelCase are now snake_case on REST and MCP alike: get_market_stats (avg_price, avg_ppsf, cash_share, prior_coverage, latest_sale, comparable_count, financing_known_count, same_owner, non_arms), the owner list and unit owner payloads (building_name, total_rows, portfolio_available, typical_hold, hold_verdict, deed_chain, beneficial_owners, entity_resolution and every owner row field), condo documents (doc_key, book_page, document_type, page_count, page_number and the extraction summary), building completeness (sales_complete, closed_sale_row_count, expected_sale_count), the owner CSV export result, and a sale's combo_display / timeline_display.
- Report polling now advertises a realistic retry_after_ms: a cold render measured 115 seconds, so the previous 1.8s hint invited about 64 pointless polls.
- Conditional requests are evaluated after request validation, so a malformed request answers 400 with the code that names the problem instead of an empty 304.
- The two 429s that were missing it — owner_export_quota_exceeded and share_email_rate_limited — now carry retry_after_seconds like every other rate-limit body.
- Added /v1/mls/* — connection status, saved workspace, live search, refresh and job polling — behind the new `mls:read` scope and the account's live-MLS lock.
- Added POST /v1/ask: the CURVE product's grounded, cited answer pipeline over HTTP, behind `ai:ask`. It obeys the same platform-wide daily AI spend cap the product's own chat does.
- Added the matching MCP tools, this documentation page, and /llms.txt.
- Added OAuth 2.1 with PKCE, dynamic client registration, client ID metadata documents, and the RFC 8414 / RFC 9728 discovery documents, so an MCP client can connect without a token being copied by hand.
- Added the consent screen and the Connected apps panel, where any authorization can be revoked.
- Rate-limit responses now carry retry_after_seconds in the body, not only in the Retry-After header.
- Added the Streamable HTTP MCP server at /api/mcp, on the same verifier, scopes and quota classes as REST.
- Added condo documents and their OCR'd page text, owner-of-record lists and CSV export, CMA comparables and sheets.
- Added shares, share emails and engagement, embeds, trackers, unit follows, building pins and branded report PDFs.
- Added buildings, sales, units, registry events, rankings, the Daily Bulletin, dataset manifests and chunk downloads.
- Added the analytics query engine and saved views, and per-building / market-wide developer intelligence.
- Personal access tokens, the single bearer verifier, scopes, per-plan quota classes, the response envelope and the OpenAPI document at /api/v1/openapi.json.
Machine-readable index for agents: /llms.txt. Data sourcing and reconciliation: how CURVE sources condo data.
Get a token
API access comes with a paid CURVE subscription, your own or a team seat on one. Mint a personal access token in Account → API & Connections, or connect an AI assistant with one click.
Open API & Connections