# Property API — AI Context

Norwegian property data: cadastres (matrikkelenheter), buildings, dwelling units (boenheter), addresses, ownership and transaction history. Backed by **PropCloud** (NMA-id routes) and **v2-core-db** (integer cadastreId panel routes).

## Quick start

```http
GET /property/search?q=Storgata 1 Oslo
→ array of address matches with property_id_nma (e.g. "0301-1-2-0-0")

GET /property/addresses/0301-1-2-0-0       # property summary
GET /property/buildings/0301-1-2-0-0       # buildings on the property
GET /property/units/0301-1-2-0-0           # dwelling units (boenheter)
GET /property/owners/0301-1-2-0-0          # current owners
GET /property/transactions/0301-1-2-0-0    # transaction/price history
GET /property/cadastres/0301-1-2-0-0       # full summary (area, geometry)
```

All endpoints require `Authorization: Bearer <token>` with scope `property:read`.

## Two ID systems

| ID format            | Example         | Used by                                   |
|----------------------|-----------------|-------------------------------------------|
| **NMA string**       | `0301-1-2-0-0`  | PropCloud routes (most of `/property`) |
| **Integer cadastreId** | `12345678`    | v2-core-db routes (`/property/panel/*`) |

The NMA format is `kommunenr-gnr-bnr-fnr-snr`. Integer IDs come from `console.placepoint.no/map/cadastres/{id}` URLs. **There is no automatic bridge between them** — pick the right route based on which ID you have.

## Endpoints

### PropCloud (NMA id, string)

| Method | Path                                | Returns                                  |
|--------|-------------------------------------|------------------------------------------|
| GET    | `/property/search?q={text}`      | Address-text matches                     |
| GET    | `/property/addresses/{id}`       | Property summary                         |
| GET    | `/property/buildings/{id}`       | Buildings on the cadastre                |
| GET    | `/property/units/{id}`           | Dwelling units (boenheter)               |
| GET    | `/property/owners/{id}`          | Current owners                           |
| GET    | `/property/transactions/{id}`    | Transaction/price history                |
| GET    | `/property/cadastres/{id}`       | Full property summary (area, geometry)   |

### v2-core-db (integer cadastreId)

| Method | Path                                                | Returns                              |
|--------|-----------------------------------------------------|--------------------------------------|
| GET    | `/property/panel/{cadastreId}/addresses`         | Address displays + coordinates       |
| GET    | `/property/panel/{cadastreId}/utilization`       | BYA% and BRA utilization             |
| GET    | `/properties/{nma}/tenants?page=&pageSize=&showEmpty=` | Current tenants — companies registered at the cadastre's addresses. Items: `{type:"tenant", matchConfidence, organization:{organisasjonsnummer,name,locationName,legalEntityTypeCode,foundedAt}, employeeCount, hasEmployees, movedInAt, contact:{phoneNumber,website,email}, industry:{sectionCode,naceCode,naceName}}`. Takes the NMA directly; `meta` carries `cadastreId`, structured `matrikkelId` **and `coverageStatus`**. `showEmpty=false` hides tenants without registered employees. Note the plural `/properties/` prefix (API design guide path). **Interpretation rules:** `matchConfidence` says how the tenant was tied to the property — `exact` (direct Enhetsregisteret address link), `normalized`/`fuzzy` (address-normalized or proximity match), `external-source` (business OBSERVED at the location via Mattilsynet inspections, Overture Maps places or the chains' own store locators, but NOT registered at the address in Enhetsregisteret — which is why register matching alone missed it). For `external-source` items, `organization.name` is the observed store/POI name and `organisasjonsnummer` **may be null**; it is set when the operating company could be resolved (franchises are often run by a differently-named AS — e.g. the store "REMA 1000 ÅNDALSNES" operated by Espen Vangen Handel AS, orgnr 918808590). `employeeCount`/`movedInAt` are null unless register-backed. `meta.coverageStatus` disambiguates an empty list: `register-matched` = the register answered; `external-source-only` = only fuzzy/external evidence (no register match); `no-match-low-confidence` = nothing matched, treat as *unknown occupancy*, NOT as "vacant"; `verified` = human-verified tenant list. **`estimatedEmployees`** `{estimate, lower, upper, method, modelVersion}` appears ONLY when `employeeCount` is null: a model-based p10–p90 interval for headcount at the location, from stratified empirical register quantiles conditioned on chain/franchise, NACE, allocated commercial floor area and building type (fallback in that order — `method` names the stratum that answered, e.g. `chain-area:rema-1000:400-1000`). Holdout interval coverage ≈89%. `method` `unmanned-chain:*` = known unmanned concept (fuel automat stations), fixed 0–2. **This is an estimate for risk-exposure purposes, never register data** — quote it as an interval ("likely 5–30 employees"), not as a fact, and never sum point estimates across tenants without saying so. Each item also has **`role`**: `occupant` (default) or `real-estate` (NACE 68.2x utleie-SPV/landlord) / `holding` (64.2x) / `property-entity` (sameie/borettslag) / `association` (FLI) — register-correct address matches that are NOT occupying businesses, kept for completeness; filter `role='occupant'` for the physical tenant list. When a parent company and its own sub-units are registered at the same property, `employeeCount` is the sub-units' site-level sum, not the whole-company figure. **`organization.locationName`** = the name of the company's own sub-unit at the location — often the store/branch name with the chain in it (`organization.name` "APE APOTEK AS", `locationName` "APOTEK 1 TEMPE"); it is `null` when no such sub-unit is registered there and may equal `name` when parent and sub-unit share a name, so use `locationName ?? name` as the display name and keep `name` for the legal entity. |

| GET    | `/buildings/{bygningsnr}/employee-capacity` | Model-based headcount-capacity interval per building (by Kartverket bygningsnummer). Items: `{type:"buildingEmployeeCapacity", buildingId, buildingNumber, buildingTypeCode (NS 3457), buildingStatusCode, commercialAreaM2, capacity:{min,estimate,max}, method, modelVersion, computedAt}`. `capacity` = the p10/p50/p90 headcount observed in fully register-covered buildings of the same type and size, scaled by this building's commercial floor space — **an empirical range for risk exposure, NOT a fire-code/legal capacity** (holdout: 81% of observed totals inside the interval). Quote it as a range. Building parts sharing the bygningsnummer (hovedbygg + tilbygg) are aggregated — one item per building, `commercialAreaM2` = summed commercial area. 404 = unknown number OR no registered commercial floor space (residential-only buildings are not scored). |
| GET    | `/properties/{nma}/energy-label` | **Latest Enova energy label (energimerke) from our own data** — no Newsec licence needed. Single object: `{type:"energyLabel", attestNumber, source, level, issuedAt, registrationType, energyRating (A–G), heatingRating (Green/Lightgreen/Yellow/Orange/Red), deliveredEnergyKWhM2Year, correctedDeliveredEnergyKWhM2Year, cadastre:{id,cadastreNumber,matrikkelId}, building:{id,buildingNumber,builtYear,areaM2,enovaCategory}, housingUnit:{id,unitNumber}, address:{…}, organisasjonsnummer, document:{storage,url,downloadPath}}`. Newest attest by `issuedAt`. 404 = no attest on the cadastre or its sections. |
| GET    | `/properties/{nma}/energy-labels` | Every energy label ever registered on the property, newest first. Same item shape. 200 + empty list when there are none. |
| GET    | `/energy-labels/{attestNumber}` | One label by Enova attest number (UUID for post-2024, `A2014-415899` for legacy). The only way to reach the ~4% of attests whose cadastre key never resolved to a matrikkelenhet. |

### Newsec (integer cadastreId)

| Method | Path                                                  | Returns                                  |
|--------|-------------------------------------------------------|------------------------------------------|
| GET    | `/property/panel/{cadastreId}/newsec-info`         | Base property data — buildings, areas, summary |
| GET    | `/property/panel/{cadastreId}/newsec-tenants?cursor=&showEmpty=` | Commercial tenants with employee counts, NACE codes, move-in dates |
| GET    | `/property/panel/{cadastreId}/newsec-transactions?cursor=` | Sale transactions (prices, dates, types) |
| GET    | `/property/panel/{cadastreId}/ownership-tree`      | Corporate ownership hierarchy            |
| GET    | `/property/panel/{cadastreId}/energy`              | Enova energy certificates (A–G)          |
| GET    | `/property/panel/{cadastreId}/breeam`              | BREEAM-NOR + BREEAM In-Use sustainability certs |

Pagination on `newsec-tenants` and `newsec-transactions`: pass `cursor=N` to skip the first N rows; response `meta.hasMore` indicates more pages.

### Stubs (not implemented yet)

`/property/cadastres/{id}/plots`, `/property/cadastres/{id}/addresses` — return `{_stub: true}`. Use the panel routes above for the same data.

## Reading an energy label (`/properties/{nma}/energy-label(s)`)

Backed by Placepoint's own Enova replica in v2-core-db (`property.EnergyLabels`,
~2.2M attests, ~96% resolved to a cadastre), **not** by Newsec. It is the energy-label
source for every customer, including those without a Newsec licence — the previous
substitute, `/analysis/energy-label/{nma}`, was withdrawn on 2026-08-31.

- **`level` is the field that decides how you may phrase the answer.** `housingUnit` = one
  apartment (bruksenhetsnummer in `housingUnit.unitNumber`), `building` = one building on
  the cadastre, `cadastre` = the whole matrikkelenhet. A property usually has several
  attests at different levels. Never say "this property is rated D" from a `housingUnit`
  attest — say which apartment or building it is.
- **Two independent ratings.** `energyRating` A–G is the energikarakter (energy demand);
  `heatingRating` is the oppvarmingskarakter traffic light (Green = mostly renewable
  heating, Red = mostly electricity/fossil). A building can be A/Red or G/Green.
- **`deliveredEnergyKWhM2Year`** is beregnet levert energi per m² per year;
  `correctedDeliveredEnergyKWhM2Year` is the TEK17 climate-corrected figure the letter is
  set from. Quote the corrected one when comparing buildings in different climate zones.
- **Sections are rolled up.** Attests on an eierseksjonssameie sit on the individual
  seksjoner, so the base NMA returns the base's own attests plus all its sections'. Read
  each item's `cadastre.cadastreNumber` — it frequently differs from the NMA you asked
  for. A seksjon's own NMA returns only that seksjon.
- **Pass today's NMA.** The ETL rewrites pre-reform keys through the 2020/2024
  kommune-reform map, so a 2014 attest printed with `3020-48-14-0-0` is stored (and found)
  as `3207-248-14-0-0`. A 3-digit municipality prefix is zero-padded for you.
- **`document`** is `null` for the ~36% of attests we hold no PDF for. `storage:"enova"`
  gives a direct, non-expiring `url`. `storage:"placepoint-lake"` means it is a legacy scan
  in our lake and `url` is `null` — fetch it via `downloadPath`
  (`GET /enova/pdf/{attestNumber}`, scope `enova:read`), which mints a 15-minute signed
  link.
- **`source`** `api` = Enova's live register, `export` = frozen CSV snapshot,
  `pdf_scrape` = OCR of an old attest PDF. `pdf_scrape` rows have the most null fields.
- **`organisasjonsnummer` is who registered the attest**, not who owns the property.
- **404 is not "unrated".** An NMA that is not in the matrikkel, and an attest whose
  cadastre key never resolved, both look identical to a genuinely unrated property.

## Data model

```
Cadastre (matrikkelenhet)            ← root entity, NMA id or integer id
├── Buildings (bygninger)
│     └── Dwelling units (boenheter, BRA-i)
├── Owners (eiere)                   ← share as fraction + percentage
├── Transactions (omsetninger)
└── Addresses (adresser)             ← one cadastre may have many
```

## Common patterns

**Find owner of an address:**
1. `GET /property/search?q=Karl Johans gate 1 Oslo`
2. Take `property_id_nma` from first result
3. `GET /property/owners/{property_id_nma}`

**Full property profile (parallel calls with the NMA id):**
`/addresses/{id}`, `/buildings/{id}`, `/units/{id}`, `/owners/{id}`, `/transactions/{id}`

**Commercial tenant analysis (NMA in, tenants out):**
`GET /properties/{nma}/tenants` — e.g. `/properties/3105-1-45-0-0/tenants`

**Energy label for a property (NMA in, energimerke out):**
`GET /properties/{nma}/energy-label` — e.g. `/properties/1804-30-426-0-0/energy-label`.
For a sectioned building ask the base NMA and read `cadastre.cadastreNumber` per item;
for the full picture use `/properties/{nma}/energy-labels`.

## Gotchas

- **NMA ids contain hyphens, not slashes** — Kartverket writes them `0301/1/2/0/0`, this API uses `0301-1-2-0-0`. Don't substitute.
- **`/search` only does address text** — for cadastre-number lookup, build the NMA id directly.
- **`/cadastres/{id}` returns an array internally** — the gateway unwraps the first element. If a property has multiple summary records, others are dropped.
- **Owner share format varies** — sometimes a percentage, sometimes numerator/denominator. Check both shapes.
- **Utilization needs the integer cadastreId**, not NMA. There is no auto-lookup yet — you must already know it. (Tenants takes the NMA directly.)
- **`/company/{id}/tenants` is a stub** — use `/properties/{nma}/tenants` instead.
- **A tenant here = a company registered at the address in Enhetsregisteret.** Franchise stores registered on a shared HQ elsewhere (e.g. chain stores) won't appear — that's a source-data property, not an API filter.
- **Transaction history coverage is partial** for older sales (pre-2007).
- **Two energy-label routes, two different backends.** `/properties/{nma}/energy-label(s)` reads our own Enova replica and works for every customer; `/property/panel/{nma}/energy` is a Newsec proxy and 502s without a Newsec licence. Prefer ours. They do not always agree — Newsec's copy carries its own attest ids.
