# Relynk Builder API

Relynk is a building automation and IoT platform for commercial real estate. This API reads one tenant's buildings, the REC/Brick model of what is in them (levels, rooms, equipment, meters, points), sensor time series and EOS energy, water and waste consumption. It is read-only.

## Calling it
- Paths are relative to the connector's base URL.
- Every function is `POST /<name>/v<version>` with its input as the JSON body and its output as the JSON response, e.g. `POST /buildings.list/v1` with `{}`.
- `GET /me` returns who the caller is and the tenants they can read. Use it to test the connection.
- Tenant: when `GET /me` lists more than one tenant, let the user choose one and add `?tenantId=<id>` to every call; with one tenant it can be left out.
- Errors are `{ "error": { "code": "...", "message": "..." } }`. A 400 message says how to change the request (a shorter range, a larger bucket, fewer points): change it, never retry it unchanged. 401: not signed in, or the sign-in expired. 403: no access to the tenant. 404: no such function or version. 429: wait the `Retry-After` seconds.
- Make calls one after another or a few at a time: a 429 `TOO_MANY_CALLS` says how many may be in flight per caller.
- A path's contract never changes. A breaking change is a new version at a new path; old versions keep working.

## Data model
- Everything is a **resource** with a REC/Brick type key: buildings (`rec_Building`), levels (`rec_Level`), rooms and other spaces (`rec_Room`, `rec_Space`), equipment (`brick_Air_Handling_Unit`, …) and **points**, the sensors, setpoints and meters that carry readings (`brick_Temperature_Sensor`, `brick_Supply_Air_Temperature_Sensor`, …).
- Type keys differ per tenant: look them up with `resourceTypes.list`, never guess them. A type key also matches its subtypes: `brick_Temperature_Sensor` includes supply, return, zone and outside air temperature sensors.
- Resources are connected by **relationships**, walked with `resources.related`: `brick_Has_Part` (building → levels → rooms, `maxDepth: 2`), `brick_Feeds` (air handling unit → terminal units → rooms), `rec_Located_In`, `brick_Has_Sub_Meter` / `brick_Is_Metered_By`. `resourceTypes.relationships` shows which ones a building uses.
- **A kind of point on a kind of resource** (room temperatures, the supply air temperature of air handling units): list the resources first, then call `points.list` with their ids as `resourceIds` and the point `typeKeys`. A point type with only a `buildingId` matches the points of every resource in the building. Each point names its `resourceIds`, so you can map readings back to rooms. `latest` is its most recent reading.
- **Energy, water and waste** come from EOS (processed hourly consumption): `meters.list`, `meters.topology`, `consumption.buildings` (building totals, as the Relynk portal's EOS dashboard shows them) and `consumption.meters` (individual meters).

## Rules that keep the numbers right
1. Time is Europe/Oslo. Ranges are half-open `[from, to)`. Give `"YYYY-MM-DD"` (Oslo midnight), `"YYYY-MM"` or ISO 8601 with an offset. Day, week (Monday) and month buckets are cut at Oslo midnight, so DST days have 23 or 25 hours. Output times carry Oslo offsets: show them as they are.
2. Time series are columnar: one shared time axis `t` and per point an array per metric, index i matching `t[i]`. That is exactly what a chart library needs.
3. `null` means no data, never zero. Draw gaps as gaps; never fill them.
4. Limits are per call (`points.timeseries`: 15m buckets up to 62 days, others up to 400 days, points × buckets up to 50 000). Split longer ranges into several calls instead of working around them.
5. Lists page: pass `nextCursor` back until it is null (or `offset` while `hasMore`). `truncated: true` means narrow the filters.
6. Building consumption and cost come from `consumption.buildings`. Never compute consumption from raw meter points, never add sub-meters to a building meter (a sub-meter is already part of its parent) and never add different carriers together (electricity, heating, cooling, gas in kWh; water in L; waste in kg).
7. Report EOS numbers with their quality: `confidence` (HIGH, MEDIUM, LOW, MISSING), `estimatedShare`, and cost only with `costCoverage` (below 1 the cost is incomplete). When `overlappingBuildingMeters` is not empty, the total counts that consumption twice, as the portal does: say so.
8. The current day, week or month is not complete yet. Compare complete periods of the same length and bucket.

## Request examples

### Rooms of a building
```http
POST /resources.related/v1

{ "fromIds": ["<building id>"], "relationship": "brick_Has_Part", "maxDepth": 2, "targetTypeKeys": ["rec_Room"] }
```

### The temperature sensors of those rooms, with their latest reading
```http
POST /points.list/v1

{ "resourceIds": ["<room id>", "<room id>"], "typeKeys": ["brick_Temperature_Sensor"] }
```
Response: points[i].resourceIds names the room, points[i].latest.value is the reading (null: none yet).

### Hourly average temperature of those points for a week, for a chart
```http
POST /points.timeseries/v2

{ "pointIds": ["<point id>"], "from": "2026-10-05", "to": "2026-10-12", "bucket": "hour", "metrics": ["avg"] }
```

### Warmest room last month: one summary per point (avg, min, max with their times)
```http
POST /points.summary/v1

{ "pointIds": ["<point id>"], "from": "2026-09", "to": "2026-10" }
```

### A building's consumption per carrier and month, with data quality
```http
POST /consumption.buildings/v1

{ "buildingIds": ["<building id>"], "from": "2026-01-01", "to": "2027-01-01", "bucket": "month" }
```
Response: buildings[0].carriers[i]: { carrier, unit, consumption[], cost[], confidence[], total: { consumption, confidence, cost, costCoverage } }.
