# InvestViews API — full documentation

> Every guide on https://docs.investviews.ai, concatenated. The machine-readable API contract is at
> https://api.investviews.ai/public/v1/openapi.yaml

---

<div class="hero" markdown="1">

<div class="eyebrow">Public API</div>

# Real-estate market statistics

> Real-estate market statistics for Europe, the Middle East and beyond, by H3 cell or by place name.

Median and average price, price per m², area and listing counts across residential and
commercial listings — addressed either by H3 cell or by a place name this API resolves
for you.
{: .lead }

[Get started &rarr;](https://docs.investviews.ai/quickstart.html){: .button }
[Browse the reference](https://docs.investviews.ai/reference.html){: .button .secondary }

</div>

## Start here

<div class="card-grid">
  <a class="card" href="quickstart.html">
    <h3>Quickstart</h3>
    <p>From no token to your first market figure, in five minutes.</p>
  </a>
  <a class="card" href="authentication.html">
    <h3>Authentication</h3>
    <p>How tokens work, and what each refusal means.</p>
  </a>
  <a class="card" href="reference.html">
    <h3>API Reference</h3>
    <p>Every endpoint, every parameter, with a live playground.</p>
  </a>
  <a class="card" href="reference.html#tag/stats/GET/stats/current">
    <h3>Current stats <span class="card-tag">GET</span></h3>
    <p>Jump straight to <code>/stats/current</code> in the reference.</p>
  </a>
</div>

## What you can build

The API answers four kinds of question.

**Market figures.** [`/stats/current`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/current) returns
median and average price, price per m², area and listing count for every H3 cell in a
territory. Name that territory in one of three ways: by cell, by place, or by a circle
around a point.

**Places.** You have a name; the API has ids.
[`/geo/search`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/search) turns "Valencia" into a `geo_id`
you can query. [`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo) walks the tree when you do not
have a name yet, and [`/geo/lookup`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/lookup) answers the
reverse question: what is this cell called?

**Coverage.** [`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage) says which countries
this API can answer for and how current each market's inventory is — before you spend a
metered call finding out.

**Spend.** [`/usage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/usage) reports what this token has
spent and what is left, per endpoint group. It costs nothing to ask.

!!! note "Discovery is free, on purpose"
    Everything above except `/stats/current` is uncounted. Read [Quota](https://docs.investviews.ai/quota.html) for why
    that is a design decision rather than an oversight.

## Before you cite a number

Three rules decide whether a figure means what you think it means. Each has its own
section in [Data](https://docs.investviews.ai/data.html), and all three are worth reading before the first integration
goes out.

1. **A figure describes one complete period, not today.** `as_of` can be up to 34 days old.
2. **A row with `estimated: true` carries borrowed figures**, pooled from neighbouring cells.
3. **Price filters are always USD**, whatever display `currency` you asked for.

---

# Quickstart

> Make your first InvestViews API call in under five minutes.

Every request goes to one host:

```
https://api.investviews.ai/public/v1
```

By the end of this page you will have a token, resolved a place name into an id, pulled
market figures for it, and checked what it cost.

## 1. Get a token

Tokens are created on the **API tokens** page of your InvestViews account. A token carries a
prefix that tells you what it can do:

| Prefix | What it is |
|---|---|
| `iv_live_rw_` | read/write |
| `iv_live_ro_` | read-only |

Every endpoint in this API is a read, so either kind works here. See
[Authentication](https://docs.investviews.ai/authentication.html) for what each refusal means.

!!! note "Before you run anything below"
    Replace `{TOKEN}` with your own token. Keep it out of source control, out of URLs, and
    out of anything that gets logged — it is a bearer credential, so whoever holds it is you.

## 2. Make a first call that costs nothing

[`/ping`](https://docs.investviews.ai/reference.html#tag/service/GET/ping) needs no token at all. Use it to check you
can reach the API before you debug anything harder.

```bash
curl -s https://api.investviews.ai/public/v1/ping
```

```json
{
  "status": "ok",
  "version": "v1"
}
```

Now try an authenticated call that is still free —
[`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage) tells you which countries this API
can answer for:

```bash
curl -s https://api.investviews.ai/public/v1/coverage \
  -H "Authorization: Bearer {TOKEN}"
```

If that returns `401`, the token is wrong. If it returns `402`, the token is fine and the
subscription is not — those are different problems, and
[Authentication](https://docs.investviews.ai/authentication.html) explains why they are different responses.

## 3. Turn a place name into something you can query

You know "Valencia". The API works in ids.
[`/geo/search`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/search) turns one into the other:

```bash
curl -s "https://api.investviews.ai/public/v1/geo/search?q=valencia" \
  -H "Authorization: Bearer {TOKEN}"
```

Each result carries a `geo_id`, its `level` (`region`, `province`, `city`, `macrozone` or
`microzone`), and `prices_available` — whether `/stats/current` can actually answer for it.

Four things worth knowing now rather than later:

- **`prices_available: false` means stop.** There is nothing in that place. A metered call
  on it answers `200` with an empty `stats` array and still costs you quota.
- **Names are not unique.** 13.6% of neighbourhood names repeat inside their own country, so
  a search for a common name returns several places. `ancestors` — an array of objects, fine
  to coarse, each carrying its own `geo_id` — is how you tell them apart. Match on `geo_id`,
  never on `name`.
- **The result gives you `hexes_url`, not the cells themselves.** A city is thousands of
  cells at resolution 8, and inlining them would spend your whole context on a lookup. Pass
  the `geo_id` straight to `/stats/current`; follow `hexes_url` only when you genuinely need
  the cell list.
- **If you do not have a name to search for, start at
  [`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo)** and walk down: it lists countries, then the
  children of whatever you name as `parent`. [Geography](https://docs.investviews.ai/geography.html) walks that whole
  cascade, from nothing to a metered call.

## 4. Ask for the market

[`/stats/current`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/current) is the metered endpoint —
the one that costs quota. Name the territory in exactly **one** of three ways:

| Selector | Use it when |
|---|---|
| `geo_id=` | you resolved a place in step 3 |
| `h3=` | you already hold cell ids, all at one resolution |
| `lat=` + `lng=` + `radius_km=` | you want a circle around a point |

Sending two selectors is refused, not resolved — a request naming both a place and a point
has two different answers and no way for the API to know which one you meant.

```bash
curl -s "https://api.investviews.ai/public/v1/stats/current?geo_id=R14727511&currency=EUR" \
  -H "Authorization: Bearer {TOKEN}"
```

```json
{
  "as_of": "2026-08-16",
  "currency": "EUR",
  "resolution": 8,
  "territory": {
    "selector": "geo_id",
    "place": { "name": "Russafa", "level": "microzone", "country": "es" }
  },
  "filters": {
    "ad_sub_type": "buy",
    "units": { "size": "m2", "price": "USD" }
  },
  "stats": [
    {
      "h3": "613498079267520511",
      "estimated": false,
      "median_price": 249000,
      "median_price_per_sqm": 2790,
      "median_area": 88.0,
      "name": "València, Eixample, Russafa"
    }
  ],
  "snapshot": {
    "as_of": "2026-07-01",
    "built": "monthly",
    "window_months": 1,
    "outlier_trim_pct": 2
  }
}
```

## 5. Read the answer correctly

This is the step people skip, and it is the one that produces wrong numbers in a report.

**`filters` is what was actually applied, defaults included.** You did not send
`ad_sub_type` above, and it defaulted to `buy` — sale prices, not rents. Cite the population
the response describes, not the one you meant to ask for.

**The figures describe one complete period, not today.** The `snapshot` block says how they
were built: one calendar month at resolutions 4–6, a rolling three-month window at 7–8. The
table is built monthly over complete periods only, so `as_of` can be up to 34 days old. The
sentence to write is "in July 2026, over that month" — never "the median price today".

**`"estimated": true` means the figures are borrowed.** Too few listings fell in that cell,
so they are pooled from its six immediate neighbours, about 1.2 km out. Report those as an
estimate for the area, never as that cell's own market.

**Price filters are USD.** `currency=EUR` changed how the figures are *displayed*. It did
not change which listings were selected. `min_price_usd` and `max_price_usd` filter USD bins
whatever `currency` says, and the bare `min_price` / `max_price` are refused rather than
ignored — a filter that was silently dropped would hand you a complete, plausible,
*unfiltered* answer that you would then report as a filtered one.

[Data](https://docs.investviews.ai/data.html) covers all of this properly.

## 6. Check what it cost

```bash
curl -s https://api.investviews.ai/public/v1/usage \
  -H "Authorization: Bearer {TOKEN}"
```

Every response also carries the answer in its headers:

| Header | Meaning |
|---|---|
| `X-Quota-Group` | which budget this request spent from |
| `X-Quota-Limit` | the allowance — or the word `uncounted` |
| `X-Quota-Used` | spent this cycle, absent on an uncounted group |
| `X-RateLimit-Remaining` | requests left this minute |

Only `/stats/current` and `/stats/history` are request-metered. Everything you did in steps
2 and 3 was free. [Quota](https://docs.investviews.ai/quota.html) explains why.

## 7. Where to go next

- **[Geography](https://docs.investviews.ai/geography.html)** — what H3 resolutions 4–8 mean on the ground, and the
  three ways into the data.
- **[Data](https://docs.investviews.ai/data.html)** — periods, windows, trimming, estimated cells, and how current a
  market is.
- **[Errors](https://docs.investviews.ai/errors.html)** — the error shape, every code, and which ones are worth retrying.
- **[API Reference](https://docs.investviews.ai/reference.html)** — every parameter, with a playground that sends real
  requests.

---

# Authentication

> How InvestViews API tokens work, and what each failure means.

Every request carries a bearer token in the `Authorization` header:

```
Authorization: Bearer iv_live_ro_...
```

```bash
curl -s https://api.investviews.ai/public/v1/coverage \
  -H "Authorization: Bearer {TOKEN}"
```

There is no OAuth flow, no signed request and no handshake. The token in the header is the
credential.

Two endpoints need no token at all: [`/ping`](https://docs.investviews.ai/reference.html#tag/service/GET/ping) and
[`/openapi.yaml`](https://docs.investviews.ai/reference.html#tag/service/GET/openapi.yaml). You can fetch the whole
contract before you have an account.

## Two kinds of token

| Prefix | Capability |
|---|---|
| `iv_live_rw_` | read/write |
| `iv_live_ro_` | read-only |

Every endpoint in this contract is a read, so **either kind works for everything documented
here**. A read-only token is refused only on a write endpoint, with `403 read_only_token`.

If you are handing a token to something that only ever reads — a dashboard, a scheduled
report, an agent — use the read-only one. It cannot become a write credential later by
accident.

## What each refusal means

Token state maps to the response. These are three different problems and they need three
different fixes, which is why they are three different codes.

| State | Response | What to do |
|---|---|---|
| header absent, malformed, unknown, or revoked | `401 invalid_token` | fix or re-issue the token |
| the subscription that owns the token lapsed | `402 subscription_inactive` | fix the billing, not the token |
| this cycle's allowance for the group is spent | `402 quota_exhausted` | wait for the reset, or upgrade |
| a read-only token on a write endpoint | `403 read_only_token` | use a read/write token |

!!! note "Why a lapsed plan is 402 and not 401"
    A `401` says "your credential is not usable" and sends an engineer to re-issue a token.
    But the token was never the problem — it is still yours, and a new one will fail exactly
    the same way. `402` says the account cannot pay for the call, which is a billing
    problem with a billing fix. Collapsing the two would send people to the wrong place
    every time.

Both `402`s are payment states, but they are still distinct: `subscription_inactive` means
the plan lapsed, `quota_exhausted` means the plan is fine and this cycle's requests are
spent. See [Quota](https://docs.investviews.ai/quota.html).

## Revocation takes effect immediately

Tokens are created and revoked on the **API tokens** page of your InvestViews account, which
also lists every token you hold and what it has spent.

The token row is re-read on **every** request and never cached. Revoke a token and the very
next call using it is refused — there is no propagation delay to wait out, and no window in
which a leaked token still works.

A revoked token stays in that list, marked revoked, rather than disappearing: it is the
record that a rotation happened.

The same fact costs you something: a token cannot be validated once and trusted for a batch.
Every request stands alone.

## Handling a token safely

- **Put it in the header, never the URL.** Query strings end up in access logs, browser
  history, proxy logs and `Referer` headers. A header does not.
- **Do not commit it.** Read it from the environment or a secret store.
- **Give each consumer its own token.** Then revoking one does not take down the others, and
  [`/usage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/usage) tells you what each is spending. Name it
  after the app or agent when you create it, so the one to revoke is obvious later.
- **Prefer read-only wherever it fits.**
- **Copy it when it is shown.** The plaintext appears exactly once, on the API tokens page,
  at the moment you create it. It is never shown again and cannot be recovered — if you lose
  it, revoke that token and create another.

!!! warning "The API playground uses a real token"
    The [reference page](https://docs.investviews.ai/reference.html) sends real requests against production. A token
    pasted there spends real quota against real data. It is not routed through any third
    party — the playground talks to `api.investviews.ai` directly — but it is a live call.

## Next

- [Quota](https://docs.investviews.ai/quota.html) — which calls cost anything, and how to see what is left.
- [Errors](https://docs.investviews.ai/errors.html) — every error code, and which are worth retrying.

---

# Quota

> Which calls cost quota, which are free, and how to check what is left.

Requests are metered per endpoint **group**, not per endpoint. Every response tells you
which group it spent from, in `X-Quota-Group`.

| Group | Endpoints | Request-metered |
|---|---|---|
| `current` | `GET /stats/current` | **yes** |
| `history` | `GET /stats/history` *(not yet shipped)* | **yes** |
| `metadata` | `/ping`, `/openapi.yaml`, `/geo*`, `/coverage`, `/meta/filters`, `/usage` | no — **uncounted** |
| `reports` | `POST /reports` *(not yet shipped)* | no — charged against your report allowance instead |

In practice: **only the stats endpoints cost requests.** Resolving a place, walking the
geography, checking coverage, reading the filter vocabulary and asking what you have spent
are all free.

## Why discovery is free

This is a design decision, not an oversight.

Billing an agent for resolving a place before it can query one teaches it to guess place ids
instead of looking them up — and a guessed id produces a confidently wrong answer rather
than an error. The same applies to "can I still afford to call anything": a client that
learns checking costs something stops checking.

So discovery, self-description and usage reporting are all uncounted. The expensive thing is
the answer, not finding out how to ask for it.

!!! warning "Uncounted is not unlimited"
    A free group still has a per-minute burst limit. Exceed it and you get
    `429 rate_limited` with a `Retry-After` header. Uncounted means "does not draw down your
    cycle allowance", not "call it as fast as you like".

## Reading the headers

Every response carries them.

| Header | Meaning |
|---|---|
| `X-Quota-Group` | which budget this request spent from |
| `X-Quota-Limit` | requests allowed this cycle — or the literal word `uncounted` |
| `X-Quota-Used` | spent this cycle, including this request. **Absent** on an uncounted group |
| `X-Quota-Reset` | when the cycle rolls, ISO 8601 UTC |
| `X-RateLimit-Limit` | requests per minute for this group |
| `X-RateLimit-Remaining` | requests left in this minute |
| `Retry-After` | seconds until the burst window rolls. Sent on `429` only |

Two of those are deliberately awkward, and both for the same reason.

**`X-Quota-Limit` is the word `uncounted`, not a number.** A client that parses it as an
integer fails visibly instead of quietly believing a ceiling nobody set.

**`X-Quota-Used` is absent on an uncounted group, not `0`.** Zero would read as "you have
spent none of a real allowance", which is a different claim from "there is no allowance
here to spend".

**`X-Quota-Reset` is the end of your subscription cycle**, not the first of next month. It
is absent on an account whose balance is not anchored yet — absent rather than invented.

## Checking your balance

[`/usage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/usage) reports the token and one entry per group.
It costs nothing, so call it before a long run and after a burst.

The same figures are on the **API tokens** page of your InvestViews account — allowance used
of limit per group, the reset date, and a 30-day chart of requests and refusals. Use the
endpoint from code, the page when you just want to look.

```bash
curl -s https://api.investviews.ai/public/v1/usage \
  -H "Authorization: Bearer {TOKEN}"
```

Three things about that response are worth knowing.

**`used` and `remaining` include usage not yet committed to the billing row.** Counting
happens in a fast store and is folded into the account balance periodically. Reading the
balance alone would show an account that just burned 400 requests as having spent none of
them — exactly the moment you are most likely to look. These figures subtract the
uncommitted delta, so they are the ones to trust.

**The `metadata` group reports `null` limits, not `0`.** It is uncounted, and `0` would read
as "no allowance left", the opposite of the truth. The honest shape is `metered: false` with
a real `rate_per_min`.

**`degraded: true` means the metering store was unreachable.** The figures are then the
committed balance alone, missing this cycle's uncommitted usage. The API fails **open** in
that state — your requests are served, not refused.

## When you run out

`402 quota_exhausted` means this cycle's request allowance for that group is spent. It is
not a credential problem: the token is fine, and [Authentication](https://docs.investviews.ai/authentication.html)
explains why that is a different code from `subscription_inactive`.

Uncounted groups keep working. You can still resolve places, check coverage and read
`/usage` after the metered allowance is gone — which is exactly when you need to.

## Spending less

- **Ask for the territory, not the cells.** One `/stats/current?geo_id=` call covers a whole
  place. Fetching the hex list first and then querying by `h3=` costs the same metered call
  plus your context.
- **Use the free endpoints to aim.** [`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage)
  tells you whether a market has data before you spend a metered call finding out.
- **Cache the answer, not the question.** A period is built monthly, so re-asking for the
  same territory within a month returns the same figures. `as_of` tells you when it would
  change.

---

# Data

> What the figures describe, how current they are, and when they are estimated.

Everything on this page exists to answer one question: **does this number mean what you
think it means?**

Read it before the first integration goes out. Every rule below has produced a wrong report
somewhere.

## The figures describe one period, not today

"Current" means the newest period that has been **built**. It is never a live price.

| Resolution | One period is |
|---|---|
| 4–6 | one calendar month |
| 7–8 | a rolling three-month window |

The window widens at the fine resolutions because a single month is too thin to fill a cell
that small — there would not be enough listings in it to say anything.

The table is rebuilt **monthly**, and a period is published only once it is **complete**. So
`as_of` can be up to **34 days old**. Between the 1st and the 5th of a month, the newest
built period is the month *before* last.

Every response carries a `snapshot` block stating exactly how its figures were built, read
from the pipeline's own constants:

```json
"snapshot": {
  "as_of": "2026-07-01",
  "built": "monthly",
  "max_as_of_age_days": 34,
  "window_months": 1,
  "outlier_trim_pct": 2,
  "min_listings_by_res": { "4": 5, "5": 6, "6": 8, "7": 10, "8": 10 }
}
```

The response also states `period`, `window_start`, `window_end` and `fx_date`.

!!! warning "Write the period into the sentence"
    The sentence to write is *"in February 2026, over 1 December to 28 February"*. Not
    *"the median price"*, and never *"the median price today"*. Cite `period` over
    `window_start`..`window_end` — those fields exist so you do not have to guess.

**Read the fields, not this page.** `outlier_trim_pct` has already moved once, from 5 to 2.
Anything you hard-code from prose here can go stale; the `snapshot` block cannot.

## Outliers are trimmed

Each country × property type × rooms group is trimmed to the 2nd–98th percentile band before
the figures are computed. A palace and a parking space do not drag the median of a
neighbourhood.

The exact quantile is in `snapshot.outlier_trim_pct`.

## `estimated: true` means the figures are borrowed

A row in `stats` can carry `"estimated": true`. When it does, **the figures are not that
cell's own market**.

Too few listings fell in the cell, so the values are pooled from its six immediate
neighbours — roughly 1.2 km out — by kRing smoothing.

```json
{ "h3": "613498079269617663", "estimated": true, "median_price": 238000 }
```

Report that as an estimate *for the area*. Never as that cell's own figure. The per-
resolution floor below which a cell is withheld or smoothed is in
`snapshot.min_listings_by_res`.

A cell with too few listings and no usable neighbours is **absent from the response
entirely**. An absent cell is not a cell worth zero.

## Price filters are always USD

`currency` is a **display** setting. It converts the figures on the way out. It does not
change which listings were selected.

| Parameter | Effect |
|---|---|
| `min_price_usd`, `max_price_usd` | filter the underlying USD bins |
| `min_price`, `max_price` | **refused**, not ignored |
| `currency` | display only |

The bare names are refused deliberately. A filter that was silently dropped would hand you a
complete, plausible, **unfiltered** answer that you would then report as a filtered one —
the worst possible failure, because nothing about it looks wrong.

Every response restates the unit in `filters.units`, and echoes `filters` **as applied,
defaults included**. A filter you omitted still has a value: `ad_sub_type` defaults to `buy`,
so you get sale prices, not rents, unless you say otherwise. Cite the population the response
describes, not the one you thought you asked for.

## Coverage: which markets can be answered

[`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage) is computed live, and it costs
nothing to call. Use it before spending a metered request finding out a market is empty.

Two fields matter most.

**`prices_available`** — whether [`/stats/current`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/current)
can answer for that country at all.

**`last_ad_parsed_at`** — how current that market's *inventory* is: when the country last
ingested listings. This is about the raw listings, not about the built statistics.

!!! note "A null `last_ad_parsed_at` does not mean an empty market"
    It is null when no source targets that market directly. The API may still hold data for
    it. "We have no ingestion job pointed here" and "there is nothing here" are different
    statements, and the field only makes the first one.

A country can also be present in `/coverage` but missing from
[`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo). That combination —
`prices_available: true, searchable: false` — means the API holds listings there but cannot
name places inside it, so you address it by coordinates rather than by name. **Check
`/coverage` before concluding a market does not exist.**

[Geography](https://docs.investviews.ai/geography.html) has the full response shape, and the four states those two
booleans make between them.

!!! warning "`prices_available` is not one field, it is two"
    On a `/coverage` **country** row it means *some source ingested listings for this market
    in the last 365 days*. On a **place** from `/geo`, `/geo/search` or `/geo/lookup` it
    means *this place holds listings*. Same name, different grain. Do not carry a value from
    one into the other.

## History

[`/stats/history`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/history) is the same query over the
same table; the only difference is whether you name a period. The newest point of a history
series and `/stats/current` are identical by construction — not merely consistent.

`snapshot.earliest_period` tells you how far back the table goes.

## Next

- [Geography](https://docs.investviews.ai/geography.html) — what resolutions 4–8 mean on the ground.
- [Quota](https://docs.investviews.ai/quota.html) — which of these calls cost anything.

---

# Geography

> Addressing the world by H3 cell or by named place.

This API addresses the world in two ways: by **H3 cell** and by **named place**. Most work
starts with a place and ends with figures.

Everything under `/geo` is **free and uncounted**. Everything under `/stats` is **metered**.
That line is deliberate, and it decides how this page is organised: use the free calls to
find out exactly what you can ask for, then spend one metered call on the right question.

!!! note "Discovery never returns a price"
    No `/geo` response contains a median, an average or any other figure. It tells you
    *whether* a place can be answered, and *how far* its data reaches. The numbers come from
    [`/stats/current`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/current) and nowhere else.

## What an H3 cell is

H3 divides the globe into hexagons. Each hexagon has an id, and each id encodes its own
size — its **resolution**. A coarse cell covers a region; a fine one covers a few streets.

Hexagons rather than squares, because every neighbour of a hexagon is the same distance
away. That matters here: when a cell has too few listings, its figures are pooled from its
six neighbours, and with squares those neighbours would sit at two different distances.

This API holds data at **resolutions 4 to 8**. Outside that range a request is refused
rather than answered emptily.

| Resolution | Average cell area | Roughly |
|---|---|---|
| 4 | ~1,770 km² | a province |
| 5 | ~253 km² | a large city and its surroundings |
| 6 | ~36 km² | a city district |
| 7 | ~5.2 km² | a neighbourhood |
| 8 | ~0.74 km² | a few streets |

Resolution 8 is the default. Two things follow from an id encoding its own resolution:

- **You cannot mix resolutions in one request.** All cells in an `h3=` list must be at the
  same resolution, because merging two would double-count the area a coarse cell shares with
  the fine cells inside it.
- **`res=` is refused alongside `h3=`.** The cell already answered that question, and
  accepting a second opinion would only create a way to disagree.

## Three ways in

| You have | Use |
|---|---|
| nothing — no name at all | [`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo), and walk down |
| a name | [`/geo/search`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/search) |
| coordinates or a cell id | [`/geo/lookup`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/lookup) |

The first one is the important one, and it is the one most clients skip. If you do not have
a name, **do not guess one**. A guessed name that misses returns `unknown_place`, and
`unknown_place` looks the same whether the place does not exist, the spelling is wrong, or
the whole country is not covered. Walking down the tree cannot produce any of those three,
because every id you send came out of the previous response.

## Nothing to search for: walk down the tree

[`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo) with no parameters is the root. From there, each
result carries a `geo_id` you pass back as `parent` to go one level deeper.

The whole walk, from nothing to a metered call:

| # | Call | Answers |
|---|---|---|
| 1 | `GET /geo` | which countries have a named place tree |
| 2 | `GET /geo?parent=es` | that country's top level |
| 3 | `GET /geo?parent=R349043&level=city` | its cities |
| 4 | `GET /geo?parent=R344953` | what sits under that city |
| 5 | `GET /stats/current?geo_id=…` | **the figures.** This one is metered |

### 1. The root

```bash
curl -s https://api.investviews.ai/public/v1/geo \
  -H "Authorization: Bearer {TOKEN}"
```

```json
{
  "results": [
    { "kind": "country", "geo_id": "es", "name": "ES", "level": "country",
      "country": "es", "prices_available": true },
    { "kind": "country", "geo_id": "pl", "name": "PL", "level": "country",
      "country": "pl", "prices_available": true }
  ]
}
```

A country row is deliberately thin: an id, a name, and whether there is anything behind it.
There is no `ancestors` and no `hexes_url`, because a country has no ancestors and its cell
list would run to millions of entries.

### 2. One country down

The `geo_id` of a country is its ISO 3166-1 alpha-2 code, lower case. Pass it as `parent`.

```bash
curl -s "https://api.investviews.ai/public/v1/geo?parent=es" \
  -H "Authorization: Bearer {TOKEN}"
```

```json
{
  "parent": { "geo_id": "es", "name": "ES", "level": "country", "country": "es" },
  "results": [
    {
      "kind": "zone",
      "geo_id": "R349043",
      "name": "Comunitat Valenciana",
      "level": "region",
      "country": "es",
      "ancestors": [
        { "geo_id": "es", "name": "es", "level": "country" }
      ],
      "h3_res": 8,
      "hexes_url": "/public/v1/geo/R349043/hexes",
      "current_period": "2026-07-01",
      "prices_available": true,
      "reports_available": true
    }
  ]
}
```

Every response to a `parent=` request echoes a `parent` object. You never have to keep your
own record of where you are in the tree.

### 3. Filter the step to one level

`level=` filters the children you get back. It does **not** change how far down you go.

```bash
curl -s "https://api.investviews.ai/public/v1/geo?parent=R349043&level=city" \
  -H "Authorization: Bearer {TOKEN}"
#   → geo_id "R344953", name "València", level "city"
```

The five levels, coarse to fine, are `region`, `province`, `city`, `macrozone`,
`microzone`. A `macrozone` is a district; a `microzone` is a neighbourhood.

!!! note "Children are the next non-empty level, which differs by branch"
    Levels are skipped, not shifted. A city whose province is blank hangs directly off its
    region. So `parent=<region>&level=province` can be empty while `parent=<region>` returns
    cities. **Ask for children and read the `level` you got back**, rather than assuming
    which one you landed on.

### 4. Keep going until the results are empty

```bash
curl -s "https://api.investviews.ai/public/v1/geo?parent=R344953" \
  -H "Authorization: Bearer {TOKEN}"
#   → geo_id "R4231821", name "Eixample", level "macrozone"

curl -s "https://api.investviews.ai/public/v1/geo?parent=R4231821" \
  -H "Authorization: Bearer {TOKEN}"
#   → geo_id "R14727511", name "Russafa", level "microzone"
```

An empty `results` array means you have reached a leaf. That is a real answer, not a
failure — stop there and use the id you already hold.

### 5. Spend the metered call

```bash
curl -s "https://api.investviews.ai/public/v1/stats/current?geo_id=R14727511&currency=EUR" \
  -H "Authorization: Bearer {TOKEN}"
```

Steps 1 to 4 cost nothing. Only step 5 draws down quota.

!!! note "Browsing never errors on a branch that is not there"
    An unresolvable `parent`, a page past the end, or a malformed parameter all return an
    empty `results` array with `200`. Exploring is meant to be safe, so a wrong turn costs
    one free call and tells you to turn back.

Paging is `page=` (1-based) with `limit=` (default 50, maximum 100). A `page` beyond 10,000
returns `[]` rather than repeating the last page.

## You have a name: search

[`/geo/search`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/search) resolves it.

```bash
curl -s "https://api.investviews.ai/public/v1/geo/search?q=russafa&country=es" \
  -H "Authorization: Bearer {TOKEN}"
```

Matching is trigram-ranked, so a near miss still resolves, and diacritics and casing are
handled. A query under two characters returns an empty result rather than scanning the whole
tree. `limit` defaults to 10 and is capped at 50.

Results are ranked **zones**, in the same shape the cascade returns. If the tree misses
entirely, the request falls back to a geocoder and returns a **point** instead, which is a
different shape — check `kind` and handle both.

!!! warning "`country=` is a filter that is honoured, not a hint"
    An unknown country code is refused with `invalid_parameter`. A valid code that is simply
    the wrong one returns nothing, because the filter is applied rather than dropped. If a
    search you expected to work comes back empty, drop `country` and `level` before
    concluding the place does not exist — a contradicting filter is the commonest reason a
    real place misses.

## Names are not unique

**13.6% of neighbourhood names repeat inside their own country.** That is not an edge case;
it is one name in seven. Measured across the tree: 12,233 of 89,965 `(country, name)` pairs
at `microzone` level cover two or more different parent cities. At `macrozone` level it is
10.0%. Names repeat across borders too, though far less often: 344 city names and 554
microzone names are in use in two or more countries.

So a search for a common name returning several results is **normal and correct**. The API
is not being vague. It is telling you that the name alone does not identify a place.

**`ancestors` is how you tell them apart.** It is an array of objects, ordered fine to
coarse, ending at the country:

```json
"ancestors": [
  { "geo_id": "R344953", "name": "València",             "level": "city" },
  { "geo_id": "R349043",   "name": "Comunitat Valenciana", "level": "region" },
  { "geo_id": "es",      "name": "es",                   "level": "country" }
]
```

Two rules for using it:

- **Every entry carries a `geo_id` you can spend directly.** If a search lands you one level
  too fine, you do not need another lookup — the parent's id is already in your hand. Send
  it to `/stats/current`, or back to `/geo?parent=`.
- **Never match on `name` alone. Match on `geo_id`.** A name is a label for a person to
  read; `geo_id` is the identity. Deduplicating a result set by name silently merges two
  different markets into one.

When a search for `centro` returns eleven results, the answer is not "take the first one".
Read `ancestors` on each, find the city you meant, and use that row's `geo_id`.

## `prices_available: false` means stop

Every zone, country and point result carries `prices_available`. It is a boolean, and it is
the cheapest thing in this API.

!!! warning "Do not spend a metered call on a place that reports `prices_available: false`"
    There is nothing there. `/stats/current` will answer `200` with an empty `stats` array,
    you will have paid for it, and an empty array reads exactly like "this market has
    collapsed" to anything downstream. Check the boolean first; it costs nothing.

`prices_available: true` is **necessary, not sufficient**. It says listings exist in that
place. Each period still applies its own minimum-listing floor on top, so a very thin place
can pass this check and still return no rows. It is a filter that saves wasted calls, not a
guarantee.

!!! note "The same field name means two things, in two places"
    On a **place** (`/geo`, `/geo/search`, `/geo/lookup`) it means *this place holds
    listings*. On a **country row from [`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage)**
    it means *some source ingested listings for this market in the last 365 days*. Both are
    honest; they answer at different grains. Do not carry a value from one into the other.

`reports_available` is the same idea for the report pipeline: whether that country's reports
can be generated at all.

## `current_period` — whether it is worth asking *right now*

`current_period` names **the window [`/stats/current`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/current)
would answer from for this place**. It is `null` when that endpoint holds nothing for the place in
the window it is currently serving.

!!! warning "`null` means *nothing in the current period* — NOT *nothing ever*"
    A place reporting `current_period: null` may still hold older windows that
    [`/stats/history`](https://docs.investviews.ai/reference.html#tag/stats/GET/stats/history) will serve happily. The field is
    called `current_period` and not `latest_period` for exactly this reason: answering "how far back
    does this place go" means confirming absence across every stored period, which costs roughly
    thirty times what a metered call costs — far too much for a free endpoint. So this field answers
    the cheap question only, and answers it honestly.

Read it beside `prices_available` and the two give you a three-state signal:

| `prices_available` | `current_period` | What to do |
|---|---|---|
| `true` | a date | Ask `/stats/current` — this is the window it will answer from |
| `true` | `null` | Listings exist, but too few cleared this period's floor. `/stats/history` may still answer |
| `false` | `null` | Dead end. Do not spend a metered call |

It is a **date label, not a price.** Nothing on `/geo` is a price, a median or a listing count —
every figure lives behind the metered `/stats` endpoints. What discovery tells you is whether
asking is worth it, never what the answer is.

A period label is the **first day of the month the period is named for**. What that label covers —
one calendar month at resolutions 4–6, a rolling three-month window at 7–8 — is on the
[Data](https://docs.investviews.ai/data.html) page, and every `/stats` response restates it in its own `snapshot` block.

## Your country is not in `/geo`

`GET /geo` lists countries that have a **named place tree**. A country where this API holds
listings but has never imported geographic names is **absent from that list entirely**.

That is the one gap the cascade cannot express, because absence looks the same as "we have
never heard of that country". [`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage) is
where the difference is answered. It is free, and it is the call to make before concluding a
market does not exist.

```bash
curl -s "https://api.investviews.ai/public/v1/coverage?country=ae" \
  -H "Authorization: Bearer {TOKEN}"
```

```json
{
  "window_days": 365,
  "freshness": "These flags are computed live. …",
  "coverage": "This list is the markets this API serves. …",
  "countries": [
    {
      "country": "ae",
      "h3_resolutions": [4, 5, 6, 7, 8],
      "last_ad_parsed_at": "2026-09-08T04:11:07Z",
      "searchable": false,
      "prices_available": true,
      "reports_available": false
    }
  ]
}
```

Two booleans, four states, and each one calls for a different next move:

| `prices_available` | `searchable` | What it means | What to do |
|---|---|---|---|
| `true` | `true` | the normal case | search or browse by name |
| `true` | `false` | listings exist, but no name in that country resolves | address it by `lat`/`lng`/`radius_km`, or by `h3=` |
| `false` | `true` | names resolve, no recent listings | pick another market |
| `false` | `false` | nothing usable here | pick another market |

The second row is the one worth knowing about. The United Arab Emirates is the standing
example: around 1.86 million listings and zero named zones. `/geo/search?q=dubai` cannot
resolve, `GET /geo` does not list `ae`, and the market is there all the same.

**A country missing from `/coverage` too** is a market this API does not serve. Listings
from it may still exist behind the scenes — portals list across borders — but no name will
resolve there. Omitting it is the honest answer; enumerating every country on earth to say
"not this one" is not an option.

`last_ad_parsed_at` is when that market last ingested listings, and it is `null` when no
source targets the market directly. It is about the **raw inventory**, not about the built
statistics — see [Data](https://docs.investviews.ai/data.html) for the difference.

Three failures sit on this same distinction, and they are worth reading together:

| Code | Says | Next move |
|---|---|---|
| `unknown_place` | that name did not resolve **inside a market we do cover** | check `did_you_mean`, or browse with `/geo` |
| `not_covered` | we hold **no geographic data for that country** | pick another market — retrying cannot succeed |
| `invalid_parameter` on `country` | that is not an ISO 3166-1 alpha-2 code | fix the code |

## You have a cell: look it up

[`/geo/lookup`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/lookup) is the reverse: given a cell or a
coordinate, say what it is called.

```bash
curl -s "https://api.investviews.ai/public/v1/geo/lookup?h3=8839540ad1fffff" \
  -H "Authorization: Bearer {TOKEN}"

curl -s "https://api.investviews.ai/public/v1/geo/lookup?lat=39.46&lng=-0.37&res=8" \
  -H "Authorization: Bearer {TOKEN}"
```

The cell id may be decimal (`613498079267520511`) or a canonical H3 string
(`8839540ad1fffff`) — both name the same cell. With `h3=` the resolution comes from the cell
itself, never from a parameter. With `lat`/`lng` there is no cell yet, so `res=` picks one,
and it defaults to 8.

This returns the point object **directly**, not wrapped in `results`: there is exactly one
answer for a cell. Its `zones` array holds the named zones containing that cell, each with
its own `geo_id` and `ancestors` — that is what turns a point into something you can query.
A bare point is a dead end.

## A place is not its cells

A search or cascade result gives you `hexes_url`, **not** the hex list. That is deliberate.
A city is thousands of cells at resolution 8, and inlining them would spend an agent's
entire context on a discovery call.

So there are two paths, and most of the time you want the first:

| You want | Do this |
|---|---|
| figures for a place | pass `geo_id` straight to `/stats/current` |
| the actual cell ids | follow `hexes_url` |

[`/geo/{geo_id}/hexes`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo/{geo_id}/hexes) is paginated.
Follow the cursor the response gives you rather than constructing one — a cursor the API did
not issue for that query is refused with `400 invalid_cursor`.

A **country** `geo_id` is refused here, with `invalid_parameter` and a message naming the two
things to do instead. A country is millions of cells; query it directly with
`/stats/current?geo_id=es`, or drill into it with `/geo?parent=es`.

!!! warning "Below a zone's own resolution, cells extend past the boundary"
    Ask for a place at a resolution coarser than the registry stores it at, and the cells you
    get are parent roll-ups that reach **beyond the place boundary**. The response says so in
    `territory.rolled_up_from` and `territory.rollup_note`. Read those before reporting the
    figures as belonging to that place alone.

## Worked example

"I want prices for a neighbourhood in Valencia." You do not know its name.

```bash
# 1. which countries have a tree  (free)
curl -s https://api.investviews.ai/public/v1/geo \
  -H "Authorization: Bearer {TOKEN}"
#    → "es"

# 2. Spain's top level  (free)
curl -s "https://api.investviews.ai/public/v1/geo?parent=es" \
  -H "Authorization: Bearer {TOKEN}"
#    → R349043  "Comunitat Valenciana"  region

# 3. its cities  (free)
curl -s "https://api.investviews.ai/public/v1/geo?parent=R349043&level=city" \
  -H "Authorization: Bearer {TOKEN}"
#    → R344953  "València"  city

# 4. down to the neighbourhoods  (free)
curl -s "https://api.investviews.ai/public/v1/geo?parent=R344953" \
  -H "Authorization: Bearer {TOKEN}"
#    → R4231821  "Eixample"  macrozone
curl -s "https://api.investviews.ai/public/v1/geo?parent=R4231821" \
  -H "Authorization: Bearer {TOKEN}"
#    → R14727511  "Russafa"  microzone
#      prices_available true, current_period "2026-07-01"

# 5. ask for the market  (metered — the only call that costs)
curl -s "https://api.investviews.ai/public/v1/stats/current?geo_id=R14727511&currency=EUR" \
  -H "Authorization: Bearer {TOKEN}"
```

Every id in that sequence came out of the previous response. Nothing was guessed, nothing
was built by hand, and the only call that cost anything was the last one.

If you *do* already know the name, steps 1 to 4 collapse into one
`/geo/search?q=russafa&country=es` — but read `ancestors` on each result before picking one,
because "Russafa" is exactly the kind of name that repeats.

## Addressing a point instead

When you have coordinates rather than a name, `/stats/current` takes a circle:

```bash
curl -s "https://api.investviews.ai/public/v1/stats/current?lat=39.46&lng=-0.37&radius_km=3" \
  -H "Authorization: Bearer {TOKEN}"
```

All three of `lat`, `lng` and `radius_km` are required together, and the radius is in
kilometres, at most 500. A radius question is a local question — address a whole country
with `geo_id`, which knows the real boundary.

This is also the way into a market whose `searchable` flag is `false`: the names are
missing, the listings are not.

## Next

- [Data](https://docs.investviews.ai/data.html) — what the figures for those cells actually describe.
- [Errors](https://docs.investviews.ai/errors.html) — `too_many_hexes`, `unknown_place`, `not_covered` and the rest.
- [Quota](https://docs.investviews.ai/quota.html) — why every call on this page except the last one is free.

---

# Errors

> The error envelope, what each code means, and which are worth retrying.

Every failure on this API has the same shape — including a wrong path, a wrong method and an
unparseable body. There is one envelope to handle, not one per endpoint.

## The envelope

```json
{
  "error": "invalid_parameter",
  "message": "radius_km must be 500 or less.",
  "docs_url": "https://docs.investviews.ai/errors.html",
  "parameter": "radius_km",
  "request_id": "req_01JQ8Z..."
}
```

| Field | Always present | What it is |
|---|---|---|
| `error` | yes | the machine-readable code. **Branch on this** |
| `message` | yes | a human sentence. **Never branch on this** |
| `docs_url` | yes | where to read more |
| `parameter` | no | which parameter was at fault, on the 400s that name one |
| `request_id` | no | our handle on this failure. Quote it to support |

Other fields appear where they help — `limit`, `resets_at`, `upgrade_url`, `did_you_mean`.
They are **additive**: treat unknown keys as forward compatibility, not as an error in your
client.

!!! warning "Branch on `error`, never on `message`"
    `message` is written for a person and can be reworded at any time. A client that matches
    on its text breaks on a copy edit, and breaks silently — it stops recognising a
    condition it used to handle.

## Every code

### Credentials and billing

| Code | Status | Meaning |
|---|---|---|
| `invalid_token` | 401 | no usable credential: header absent, malformed, unknown, or revoked |
| `subscription_inactive` | 402 | the subscription that owns this token is not active |
| `quota_exhausted` | 402 | this cycle's request allowance for the group is spent |
| `read_only_token` | 403 | a read-only token was used on a write endpoint |

Three different problems with three different fixes. See
[Authentication](https://docs.investviews.ai/authentication.html) for why a lapsed plan is `402` rather than `401`, and
[Quota](https://docs.investviews.ai/quota.html) for what to do when the allowance runs out.

### The request

| Code | Status | Meaning |
|---|---|---|
| `missing_parameter` | 400 | a required parameter was not sent |
| `invalid_parameter` | 400 | a parameter was sent but its value is unusable |
| `invalid_cursor` | 400 | the pagination cursor is not one we issued for this query |
| `malformed_request` | 4xx | the request could not be processed as sent (method, body, headers) |
| `too_many_hexes` | 422 | the territory exceeds the per-request cell budget |

`invalid_cursor` usually means a cursor was constructed by hand or reused across a different
query. Follow the cursor the response gives you.

`too_many_hexes` means the territory is too large to answer in one request. Ask for a
coarser resolution, or split the territory.

### Geography

| Code | Status | Meaning |
|---|---|---|
| `unknown_place` | 404 | no place, cell or key matched — inside a market we do serve |
| `not_covered` | 404 | this API holds no geographic data for that country |
| `resolution_not_in_plan` | 403 | the requested H3 resolution is finer than the plan allows |

`unknown_place` and `not_covered` are both `404` and they mean different things. The first
says "we serve this country, but that name is not in it" — check the spelling, or browse
with [`/geo`](https://docs.investviews.ai/reference.html#tag/geo/GET/geo). The second says "we do not serve that
country at all" — check
[`/coverage`](https://docs.investviews.ai/reference.html#tag/metadata/GET/coverage) before concluding a market does not
exist, because a market can hold listings without being searchable by name.

### Limits and faults

| Code | Status | Meaning |
|---|---|---|
| `rate_limited` | 429 | too many requests this minute |
| `not_found` | 404 | no such resource or path |
| `internal_error` | 500 | a fault on our side |
| `reports_unavailable_for_country` | 422 | the report pipeline does not serve that country |

## What to retry

Only two of these are worth retrying. The rest describe something about the request that
will be just as wrong the second time.

| Code | Retry? | How |
|---|---|---|
| `rate_limited` | **yes** | wait `Retry-After` seconds, then retry |
| `internal_error` | **yes** | back off and retry. Quote `request_id` if it persists |
| `quota_exhausted` | not until the cycle rolls | `X-Quota-Reset` says when |
| everything else | **no** | fix the request, the token, or the billing |

!!! warning "Honour `Retry-After` rather than guessing"
    A `429` carries `Retry-After` in seconds. Backing off by a number you invented is how a
    client that hit the limit once keeps hitting it.

## Rate limits

Separate from quota. Quota is your allowance for the billing cycle; the rate limit is how
fast you may spend it.

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | requests per minute for this group. On every response |
| `X-RateLimit-Remaining` | requests left in this minute |
| `Retry-After` | seconds until the window rolls. On `429` only |

The limit applies to **uncounted groups too**. Free does not mean unmetered in time — see
[Quota](https://docs.investviews.ai/quota.html).

## Handling failures well

- **Switch on `error`.** Treat an unrecognised code the way you would treat a `500`: log it
  with the `request_id` and move on, rather than crashing.
- **Log `request_id` on every failure.** It is the only handle support has on your specific
  request.
- **Do not retry a `400`.** Nothing about the request will have changed.
- **Fail loudly on `quota_exhausted`.** Silently returning empty results makes a billing
  problem look like an empty market, and someone downstream will report the zero.

---

# Terms

> What you may do with InvestViews API data, what you may not, and what ends access.

These terms cover use of the InvestViews API. They are deliberately short, and each one exists
for a reason stated beside it.

## What you may do

**Use the figures in your own product, analysis or research.** Build a valuation tool on them,
put them in a report you sell, feed them to an agent that answers questions for your customers.
That is what the API is for.

**Cache them.** Every response tells you which period it describes and when it was built, so you
can store an answer and reuse it rather than asking twice. Nothing here requires you to re-request
data you already hold.

## What you may not do

### Redistribute or resell the aggregates themselves

You may sell **what you build** with the data. You may not sell **the data**.

The line is whether a customer of yours could substitute your product for an InvestViews
subscription. A market report priced off our medians is a product. A feed, an export, a bulk
download or an API of your own that re-serves our figures is a substitute, and is not permitted.

⚠️ This includes indirect redistribution: publishing a dataset, committing figures to a public
repository, or exposing an endpoint that proxies ours to callers who are not you.

### Circumvent the quota

Your allowance is per account. Splitting a workload across multiple accounts, sharing a token
between parties, or rotating tokens to reset a counter are all the same act: taking more than the
plan sold you.

⚠️ **This is the one term that ends access immediately rather than after a conversation.** Quota is
how the service is paid for, so circumventing it is not a billing dispute — it is a decision not to
pay. Everything else here we will contact you about first.

## Attribution

**If you display our figures publicly, say where they came from.** A visible "Data: InvestViews"
with a link to `investviews.ai` is enough. It is not required for internal use, for analysis you do
not publish, or for figures that inform a decision without being shown.

The reason is narrow and practical: a median price per square metre with no source is unfalsifiable.
Attribution lets a reader judge the number, and lets us correct it if it is wrong.

## Accuracy, and what the figures are not

The API reports what listings say, aggregated. It does not report transactions, valuations or
appraisals.

- A median asking price is not a median sale price.
- Coverage varies by country and by level — a place with few listings produces a thin figure, and
  the API tells you so rather than hiding it.
- Periods are closed windows. The newest complete period can be days old; the response states which
  window it describes.

**Do not present our figures as a valuation, an appraisal or investment advice**, and do not
represent them as transaction data. If your product implies any of those, the implication is yours
and not ours.

## Availability

There is no uptime commitment on the API today. Quota and rate limits may change with notice;
the endpoint contract will not change without a version.

## What happens when a term is broken

| Term | What happens |
|---|---|
| Quota circumvention | access terminated, immediately |
| Redistribution or resale | we contact you; access suspended if it continues |
| Missing attribution | we contact you; no suspension for a first instance |
| Misrepresenting the figures | we contact you |

Tokens can be revoked individually, so a single misbehaving integration does not have to cost you
the account.

## Questions

If you are unsure whether something you want to build is redistribution, ask before you build it.
The answer is usually yes-you-can, and it is far cheaper to hear that first.

---
