# buildings.list (version 1) (https://learn.relynk.io/docs/builder/api/buildings/list-v1) `POST https://ai.relynk.io/builder/v1/buildings.list/v1` Buildings the user can see in this tenant, ordered by name, or by how well they match search. Page with offset while hasMore is true. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "search": { "description": "Only buildings whose name resembles this text (fuzzy: a word, part of a name or a near miss), best matches first", "type": "string", "minLength": 1, "maxLength": 200 }, "limit": { "default": 100, "description": "Buildings per page", "type": "integer", "minimum": 1, "maximum": 500 }, "offset": { "default": 0, "description": "Buildings to skip, for paging", "type": "integer", "minimum": 0, "maximum": 9007199254740991 } }, "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "buildings": { "maxItems": 500, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Building id; pass it to other functions that take a building" }, "name": { "type": "string" } }, "required": [ "id", "name" ], "additionalProperties": false } }, "hasMore": { "type": "boolean", "description": "True when more buildings follow this page" } }, "required": [ "buildings", "hasMore" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # consumption.buildings (version 1) (https://learn.relynk.io/docs/builder/api/consumption/buildings-v1) `POST https://ai.relynk.io/builder/v1/consumption.buildings/v1` Energy, water and waste consumption of buildings from EOS, exactly as the EOS dashboard shows it: per building and carrier (electricity, heating, cooling, gas, water, waste), the sum of the building's building meters (physical and virtual; sub-meters never), per Oslo-time bucket over [from, to), with cost, estimated share, coverage and confidence per bucket and in total. Use this for any building or portfolio consumption, cost or comparison; never add up raw meter points for that. Values are already consumption per hour (not meter readings), in kWh (energy), L (water) or kg (waste); never add different carriers together. null means no data for the bucket, not zero. Omit buildingIds for every building with EOS data. day, week and month buckets need from and to at Oslo midnight; hour buckets up to 62 days, others up to 1100 days, and buildings × carriers × buckets up to 50000. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingIds": { "description": "Building ids (from buildings.list), at most 200; omit for all", "minItems": 1, "maxItems": 200, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "carriers": { "description": "Only these carriers; omit for all", "minItems": 1, "maxItems": 7, "type": "array", "items": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ], "description": "Energy carrier: electricity, gas, thermal_heat (heating, district heating), thermal_cool (cooling, district cooling), thermal (thermal energy not split into heating or cooling), water (hot and chilled water included) or waste" } }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "bucket": { "type": "string", "enum": [ "hour", "day", "week", "month" ], "description": "Bucket size; weeks start on Monday" }, "includeSubCategories": { "default": false, "description": "Also split each carrier by what it was consumed for (the waste fraction for waste)", "type": "boolean" } }, "required": [ "from", "to", "bucket" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "bucket": { "type": "string", "enum": [ "hour", "day", "week", "month" ] }, "from": { "type": "string" }, "to": { "type": "string" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Bucket starts, Oslo time with offset; index i matches every array" }, "buildings": { "type": "array", "items": { "type": "object", "properties": { "buildingId": { "type": "string" }, "buildingName": { "type": "string" }, "carriers": { "type": "array", "items": { "type": "object", "properties": { "carrier": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ] }, "unit": { "type": "string", "enum": [ "kWh", "L", "kg" ] }, "meterCount": { "type": "number", "description": "Building meters with data in the range" }, "consumption": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Consumption per bucket" }, "estimated": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Part of it EOS estimated to fill gaps" }, "cost": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Cost per bucket; null where no hour is priced" }, "coverage": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Share of the expected hours with measured data, 0 to 1" }, "confidence": { "type": "array", "items": { "anyOf": [ { "type": "string", "enum": [ "HIGH", "MEDIUM", "LOW", "MISSING" ] }, { "type": "null" } ] }, "description": "HIGH, MEDIUM, LOW or MISSING per bucket (coverage minus a penalty for estimated values); null when nothing could be scored yet" }, "sortedShare": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Waste only: share of the weight counted as sorted (sorting grade); empty for other carriers" }, "subCategories": { "type": "array", "items": { "type": "object", "properties": { "label": { "description": "What it was consumed for, or the waste fraction; null when not set", "type": [ "string", "null" ] }, "consumption": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Consumption per bucket" }, "cost": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Cost per bucket" }, "total": { "type": [ "number", "null" ] }, "totalCost": { "type": [ "number", "null" ] } }, "required": [ "label", "consumption", "cost", "total", "totalCost" ], "additionalProperties": false }, "description": "Only with includeSubCategories" }, "total": { "type": "object", "properties": { "consumption": { "type": [ "number", "null" ] }, "estimated": { "type": [ "number", "null" ] }, "estimatedShare": { "type": [ "number", "null" ] }, "cost": { "type": [ "number", "null" ] }, "costCoverage": { "description": "Share of the meter-hours with a price. Below 1 the cost is incomplete: say so", "type": [ "number", "null" ] }, "coverage": { "type": [ "number", "null" ] }, "confidence": { "anyOf": [ { "type": "string", "enum": [ "HIGH", "MEDIUM", "LOW", "MISSING" ] }, { "type": "null" } ], "description": "The lowest bucket confidence, as on the dashboard" }, "sortedShare": { "type": [ "number", "null" ] }, "bucketsWithoutData": { "type": "number" } }, "required": [ "consumption", "estimated", "estimatedShare", "cost", "costCoverage", "coverage", "confidence", "sortedShare", "bucketsWithoutData" ], "additionalProperties": false }, "overlappingBuildingMeters": { "type": "array", "items": { "type": "object", "properties": { "virtualMeterId": { "type": "string" }, "sourceMeterIds": { "type": "array", "items": { "type": "string" } } }, "required": [ "virtualMeterId", "sourceMeterIds" ], "additionalProperties": false }, "description": "Virtual building meters calculated from another building meter counted here, or from its sub-meters: that consumption is in the total twice. Mention it whenever not empty" } }, "required": [ "carrier", "unit", "meterCount", "consumption", "estimated", "cost", "coverage", "confidence", "sortedShare", "subCategories", "total", "overlappingBuildingMeters" ], "additionalProperties": false }, "description": "Carriers with data; empty when the building has no EOS data in the range" } }, "required": [ "buildingId", "buildingName", "carriers" ], "additionalProperties": false } } }, "required": [ "timeZone", "bucket", "from", "to", "t", "buildings" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # consumption.meters (version 1) (https://learn.relynk.io/docs/builder/api/consumption/meters-v1) `POST https://ai.relynk.io/builder/v1/consumption.meters/v1` Consumption of up to 10000 EOS meters (energy, water, waste; ids from meters.list) per Oslo-time bucket over [from, to), from the hourly values EOS processed (resets, gaps and virtual meter formulas handled): consumption, the estimated part, cost, missing hours and confidence per bucket, and per meter a total with the peak hour, coverage, cost coverage and hours per confidence tier. Values are consumption per hour, not meter readings: sum them, never take differences. Unit per meter: kWh, L or kg. null means no data, not zero (waste meters: no pickup is 0). For a building's total use consumption.buildings instead of adding meters: sub-meters are part of their parent. from and to must be whole hours; hour buckets up to 62 days, others up to 400, meters × days up to 20000. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "meterIds": { "minItems": 1, "maxItems": 10000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Meter ids (from meters.list), at most 10000" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "bucket": { "type": "string", "enum": [ "hour", "day", "week", "month" ], "description": "Bucket size; weeks start on Monday" } }, "required": [ "meterIds", "from", "to", "bucket" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "bucket": { "type": "string", "enum": [ "hour", "day", "week", "month" ] }, "from": { "type": "string" }, "to": { "type": "string" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Bucket starts, Oslo time with offset; index i matches every array" }, "meters": { "type": "array", "items": { "type": "object", "properties": { "meterId": { "type": "string" }, "name": { "type": "string" }, "carrier": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ] }, "unit": { "type": "string", "enum": [ "kWh", "L", "kg" ] }, "isBuildingMeter": { "type": "boolean" }, "isVirtual": { "type": "boolean" }, "buildingId": { "type": [ "string", "null" ] }, "consumption": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Consumption per bucket; null when the meter has no values in it" }, "estimated": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Part of it EOS estimated to fill gaps" }, "cost": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Cost per bucket; null where no hour is priced" }, "missingHours": { "type": "array", "items": { "type": "number" }, "description": "Expected hours without a measured or estimated value" }, "confidence": { "type": "array", "items": { "anyOf": [ { "type": "string", "enum": [ "HIGH", "MEDIUM", "LOW", "MISSING" ] }, { "type": "null" } ] }, "description": "HIGH, MEDIUM, LOW or MISSING per bucket; null when no hour of it could be scored yet (future, or still arriving)" }, "total": { "type": "object", "properties": { "consumption": { "type": [ "number", "null" ] }, "measured": { "type": [ "number", "null" ] }, "estimated": { "type": [ "number", "null" ] }, "estimatedShare": { "type": [ "number", "null" ] }, "cost": { "type": [ "number", "null" ] }, "costCoverage": { "description": "Share of the hours with a price. Below 1 the cost is incomplete: say so", "type": [ "number", "null" ] }, "peak": { "anyOf": [ { "type": "object", "properties": { "value": { "type": "number" }, "at": { "type": "string" } }, "required": [ "value", "at" ], "additionalProperties": false }, { "type": "null" } ], "description": "The highest hourly consumption and its hour (Oslo time)" }, "expectedHours": { "type": "number" }, "missingHours": { "type": "number" }, "pendingHours": { "type": "number", "description": "Recent hours that have often not arrived yet; not scored" }, "highHours": { "type": "number" }, "mediumHours": { "type": "number" }, "lowHours": { "type": "number" }, "coverage": { "description": "Measured share of the expected hours, 0 to 1", "type": [ "number", "null" ] }, "confidence": { "anyOf": [ { "type": "string", "enum": [ "HIGH", "MEDIUM", "LOW", "MISSING" ] }, { "type": "null" } ], "description": "The lowest bucket confidence" } }, "required": [ "consumption", "measured", "estimated", "estimatedShare", "cost", "costCoverage", "peak", "expectedHours", "missingHours", "pendingHours", "highHours", "mediumHours", "lowHours", "coverage", "confidence" ], "additionalProperties": false } }, "required": [ "meterId", "name", "carrier", "unit", "isBuildingMeter", "isVirtual", "buildingId", "consumption", "estimated", "cost", "missingHours", "confidence", "total" ], "additionalProperties": false } } }, "required": [ "timeZone", "bucket", "from", "to", "t", "meters" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # Who the token acts as and the tenants it can read (https://learn.relynk.io/docs/builder/api/me) `GET https://ai.relynk.io/builder/v1/me` Who the token acts as and the tenants it can read ## Response 200 The caller ```json { "type": "object", "required": [ "userId", "tenants" ], "properties": { "userId": { "type": "string" }, "tenants": { "type": "array", "items": { "type": "object", "required": [ "id", "name" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" } } } } } } ``` ## Response 401 No valid bearer token. # meters.list (version 1) (https://learn.relynk.io/docs/builder/api/meters/list-v1) `POST https://ai.relynk.io/builder/v1/meters.list/v1` EOS meters (energy, water and waste metering: electricity, district heating and cooling, gas, water, waste) with their carrier, unit, whether they count in building totals (building meters) or are sub-meters, virtual (calculated from other meters) or tenant (lease) meters, what they are used for, and until when EOS has processed them. Filter by building, carrier, id, name, buildingMetersOnly or virtual. Feed the ids to consumption.meters; for a building total use consumption.buildings, which sums exactly the building meters. Pages by id: pass nextCursor back until it is null. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list). Matches the building itself and everything that is part of it" }, "carriers": { "description": "Only these carriers; omit for all", "minItems": 1, "maxItems": 7, "type": "array", "items": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ], "description": "Energy carrier: electricity, gas, thermal_heat (heating, district heating), thermal_cool (cooling, district cooling), thermal (thermal energy not split into heating or cooling), water (hot and chilled water included) or waste" } }, "ids": { "description": "Only these meters", "minItems": 1, "maxItems": 1000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "buildingMetersOnly": { "default": false, "description": "Only the meters EOS sums into building totals", "type": "boolean" }, "virtual": { "description": "true: only virtual meters, false: only physical ones; omit for both", "type": "boolean" }, "search": { "description": "Only names containing this text, ignoring case", "type": "string", "minLength": 1, "maxLength": 200 }, "limit": { "default": 500, "type": "integer", "minimum": 1, "maximum": 1000 }, "cursor": { "description": "nextCursor from the previous page; omit for the first page", "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "meters": { "maxItems": 1000, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string", "description": "displayName when set, else the resource name" }, "typeKey": { "type": "string" }, "typeName": { "type": "string" }, "carrier": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ] }, "unit": { "type": "string", "enum": [ "kWh", "L", "kg" ], "description": "The unit of all its consumption: kWh for energy, L for water, kg for waste" }, "isBuildingMeter": { "type": "boolean", "description": "Counted in its building’s EOS total. Sub-meters are not: adding them to a building meter counts the same use twice" }, "isVirtual": { "type": "boolean", "description": "Calculated by EOS from other meters (meters.topology shows how)" }, "buildingId": { "type": [ "string", "null" ] }, "buildingName": { "type": [ "string", "null" ] }, "consumedForUse": { "description": "What its consumption is used for, e.g. \"Ventilation\"; null when not set", "type": [ "string", "null" ] }, "wasteFraction": { "description": "Waste meters: the fraction it weighs", "type": [ "string", "null" ] }, "countAsSortedWaste": { "description": "Waste meters: counted as sorted in the sorting grade; null when not set", "type": [ "boolean", "null" ] }, "lease": { "anyOf": [ { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "start": { "type": [ "string", "null" ] }, "end": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "start", "end" ], "additionalProperties": false }, { "type": "null" } ], "description": "A tenant meter: the lease it meters, whose period clips its values" }, "sourcePointId": { "description": "The point EOS reads it from; its raw readings are not consumption. Null for virtual meters", "type": [ "string", "null" ] }, "processedUntil": { "description": "Readings up to this time (Oslo, with offset) are processed; later consumption is not available yet. Null before the first run", "type": [ "string", "null" ] } }, "required": [ "id", "name", "typeKey", "typeName", "carrier", "unit", "isBuildingMeter", "isVirtual", "buildingId", "buildingName", "consumedForUse", "wasteFraction", "countAsSortedWaste", "lease", "sourcePointId", "processedUntil" ], "additionalProperties": false } }, "nextCursor": { "description": "Pass as cursor for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "meters", "nextCursor" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # meters.topology (version 1) (https://learn.relynk.io/docs/builder/api/meters/topology-v1) `POST https://ai.relynk.io/builder/v1/meters.topology/v1` How a building's EOS meters (energy, water, waste) are connected: every meter with its carrier and whether it counts in the building total, the sub-meter tree (parent → child; a parent's consumption includes its children's, and the part no child measures is not sub-metered), what each virtual meter is calculated from with its factor, and overlappingBuildingMeters: virtual building meters fed by another building meter of the same building and carrier, or by its sub-meters, which EOS (like its dashboard) counts both, so the building total includes that consumption twice. Use it to explain a building's metering, to break a total down by sub-meter, and before trusting a building total. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list)" }, "carriers": { "description": "Only these carriers; omit for all", "minItems": 1, "maxItems": 7, "type": "array", "items": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ], "description": "Energy carrier: electricity, gas, thermal_heat (heating, district heating), thermal_cool (cooling, district cooling), thermal (thermal energy not split into heating or cooling), water (hot and chilled water included) or waste" } } }, "required": [ "buildingId" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "buildingId": { "type": "string" }, "meters": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "typeKey": { "type": "string" }, "carrier": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ] }, "unit": { "type": "string", "enum": [ "kWh", "L", "kg" ] }, "isBuildingMeter": { "type": "boolean", "description": "Counted in its building’s EOS total" }, "isVirtual": { "type": "boolean" }, "buildingId": { "description": "Another building than the one asked for when a virtual meter is fed from there", "type": [ "string", "null" ] }, "consumedForUse": { "type": [ "string", "null" ] }, "wasteFraction": { "type": [ "string", "null" ] } }, "required": [ "id", "name", "typeKey", "carrier", "unit", "isBuildingMeter", "isVirtual", "buildingId", "consumedForUse", "wasteFraction" ], "additionalProperties": false }, "description": "The building's meters, and the meters elsewhere its virtual meters are fed from" }, "subMeters": { "type": "array", "items": { "type": "object", "properties": { "parentId": { "type": "string" }, "childId": { "type": "string" } }, "required": [ "parentId", "childId" ], "additionalProperties": false } }, "virtualSources": { "type": "array", "items": { "type": "object", "properties": { "virtualMeterId": { "type": "string" }, "sourceMeterId": { "type": "string" }, "factor": { "type": "object", "properties": { "kind": { "type": "string", "enum": [ "implicit", "static", "dynamic", "invalid" ], "description": "implicit: the whole source (×1); static: × value; dynamic: × the share of the building area derivedFrom has (areaKey), worked out by EOS; invalid: EOS cannot use the edge" }, "value": { "description": "static: the multiplier, from -1 to 1", "type": [ "number", "null" ] }, "areaKey": { "type": [ "string", "null" ] }, "derivedFromId": { "type": [ "string", "null" ] }, "derivedFromName": { "type": [ "string", "null" ] } }, "required": [ "kind", "value", "areaKey", "derivedFromId", "derivedFromName" ], "additionalProperties": false } }, "required": [ "virtualMeterId", "sourceMeterId", "factor" ], "additionalProperties": false } }, "overlappingBuildingMeters": { "type": "array", "items": { "type": "object", "properties": { "virtualMeterId": { "type": "string" }, "carrier": { "type": "string", "enum": [ "electricity", "gas", "thermal", "thermal_heat", "thermal_cool", "water", "waste" ] }, "sourceMeterIds": { "type": "array", "items": { "type": "string" }, "description": "Building meters of the same building and carrier it is calculated from, directly or through their sub-meters" } }, "required": [ "virtualMeterId", "carrier", "sourceMeterIds" ], "additionalProperties": false } } }, "required": [ "buildingId", "meters", "subMeters", "virtualSources", "overlappingBuildingMeters" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.list (version 1) (https://learn.relynk.io/docs/builder/api/points/list-v1) `POST https://ai.relynk.io/builder/v1/points.list/v1` Points (sensors, meters, setpoints, commands) with their unit and latest reading, for a building, for given equipment or rooms (resourceIds: the points they have), or by id. Give at least one of buildingId, resourceIds or ids. To find a kind of point on a kind of resource (room temperatures, the supply air temperature of air handling units), first list those resources with resources.list (typeKeys from resourceTypes.list), then pass their ids as resourceIds together with the point typeKeys: a point type with only a buildingId also matches points of every other resource in the building, and typeKeys include subtypes (brick_Temperature_Sensor also matches supply, return and outside air temperature sensors). Each point names its exact typeKey and its resourceIds. Pages by id: pass nextCursor back until it is null. Feed the ids to points.timeseries, points.summary or points.raw. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list). Matches the building itself and everything that is part of it" }, "resourceIds": { "description": "Only points of these resources (equipment, rooms, meters)", "minItems": 1, "maxItems": 1000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "ids": { "description": "Only these points", "minItems": 1, "maxItems": 1000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "typeKeys": { "description": "Only these types and their subtypes, e.g. [\"rec_Room\"] (keys from resourceTypes.list)", "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_]+$" } }, "unitKeys": { "description": "Only these units, e.g. [\"unit_Kilo_W_hr\"]", "minItems": 1, "maxItems": 50, "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_]+$" } }, "search": { "description": "Only names containing this text, ignoring case", "type": "string", "minLength": 1, "maxLength": 200 }, "limit": { "default": 500, "type": "integer", "minimum": 1, "maximum": 2000 }, "cursor": { "description": "nextCursor from the previous page; omit for the first page", "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "points": { "maxItems": 2000, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "typeKey": { "type": "string" }, "typeName": { "type": "string" }, "typePath": { "type": "string", "description": "e.g. \"Point.Sensor.Temperature_Sensor\"" }, "unitKey": { "description": "e.g. \"unit_Deg_C\", \"unit_Kilo_W_hr\"", "type": [ "string", "null" ] }, "unit": { "description": "Unit symbol, e.g. \"°C\", \"kW·h\"", "type": [ "string", "null" ] }, "resourceIds": { "type": "array", "items": { "type": "string" }, "description": "The resources (equipment, rooms, meters) that have this point" }, "buildingId": { "type": [ "string", "null" ] }, "latest": { "anyOf": [ { "type": "object", "properties": { "time": { "type": "string", "description": "Oslo time with offset" }, "value": { "type": [ "number", "null" ] }, "text": { "type": [ "string", "null" ] } }, "required": [ "time", "value", "text" ], "additionalProperties": false }, { "type": "null" } ], "description": "The most recent reading; null when there is none" } }, "required": [ "id", "name", "typeKey", "typeName", "typePath", "unitKey", "unit", "resourceIds", "buildingId", "latest" ], "additionalProperties": false } }, "nextCursor": { "description": "Pass as cursor for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "points", "nextCursor" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.raw (version 1) (https://learn.relynk.io/docs/builder/api/points/raw-v1) `POST https://ai.relynk.io/builder/v1/points.raw/v1` **Deprecated**: a newer version of this function exists. This version keeps working. Individual readings of up to 20 points over [from, to) (up to 31 days), ordered by point then time. Prefer points.timeseries or points.summary for analysis; use this to inspect exact readings. Pages: pass nextCursor back until it is null. Each page has one series per point that has readings in it, so merge pages by pointId. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "pointIds": { "minItems": 1, "maxItems": 20, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Point ids (from points.list), at most 20" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "limit": { "default": 5000, "description": "Readings per page, across all points", "type": "integer", "minimum": 1, "maximum": 20000 }, "cursor": { "description": "nextCursor from the previous page; omit for the first page", "type": "object", "properties": { "pointId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "time": { "type": "string", "minLength": 1, "maxLength": 64 } }, "required": [ "pointId", "time" ], "additionalProperties": false } }, "required": [ "pointIds", "from", "to" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "series": { "type": "array", "items": { "type": "object", "properties": { "pointId": { "type": "string" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Reading times, Oslo time with offset" }, "value": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Numeric readings; null for text-only readings" }, "text": { "type": "array", "items": { "type": [ "string", "null" ] }, "description": "Text readings; usually null" } }, "required": [ "pointId", "t", "value", "text" ], "additionalProperties": false } }, "nextCursor": { "anyOf": [ { "type": "object", "properties": { "pointId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "time": { "type": "string", "minLength": 1, "maxLength": 64 } }, "required": [ "pointId", "time" ], "additionalProperties": false }, { "type": "null" } ], "description": "Pass as cursor for the next page; null on the last" } }, "required": [ "timeZone", "series", "nextCursor" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.raw (version 2) (https://learn.relynk.io/docs/builder/api/points/raw-v2) `POST https://ai.relynk.io/builder/v1/points.raw/v2` Individual readings of up to 200 points over [from, to) (up to 31 days), ordered by point then time. Prefer points.timeseries or points.summary for analysis; use this to inspect exact readings. Pages: pass nextCursor back until it is null. Each page has one series per point that has readings in it, so merge pages by pointId. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "pointIds": { "minItems": 1, "maxItems": 200, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Point ids (from points.list), at most 200" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "limit": { "default": 5000, "description": "Readings per page, across all points", "type": "integer", "minimum": 1, "maximum": 100000 }, "cursor": { "description": "nextCursor from the previous page; omit for the first page", "type": "object", "properties": { "pointId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "time": { "type": "string", "minLength": 1, "maxLength": 64 } }, "required": [ "pointId", "time" ], "additionalProperties": false } }, "required": [ "pointIds", "from", "to" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "series": { "type": "array", "items": { "type": "object", "properties": { "pointId": { "type": "string" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Reading times, Oslo time with offset" }, "value": { "type": "array", "items": { "type": [ "number", "null" ] }, "description": "Numeric readings; null for text-only readings" }, "text": { "type": "array", "items": { "type": [ "string", "null" ] }, "description": "Text readings; usually null" } }, "required": [ "pointId", "t", "value", "text" ], "additionalProperties": false } }, "nextCursor": { "anyOf": [ { "type": "object", "properties": { "pointId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "time": { "type": "string", "minLength": 1, "maxLength": 64 } }, "required": [ "pointId", "time" ], "additionalProperties": false }, { "type": "null" } ], "description": "Pass as cursor for the next page; null on the last" } }, "required": [ "timeZone", "series", "nextCursor" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.summary (version 1) (https://learn.relynk.io/docs/builder/api/points/summary-v1) `POST https://ai.relynk.io/builder/v1/points.summary/v1` One summary per point over [from, to) (up to 380 days): count, min and max with their times, average, sum, and first and last reading with their times. Cheaper than points.timeseries when one number per point is enough, e.g. the warmest room last month. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "pointIds": { "minItems": 1, "maxItems": 500, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Point ids (from points.list), at most 500" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" } }, "required": [ "pointIds", "from", "to" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "from": { "type": "string", "description": "Resolved start, Oslo time with offset" }, "to": { "type": "string", "description": "Resolved end (exclusive), Oslo time with offset" }, "points": { "type": "array", "items": { "type": "object", "properties": { "pointId": { "type": "string" }, "unitKey": { "type": [ "string", "null" ] }, "count": { "type": "number", "description": "Readings in the range; 0 means no data and every other field is null" }, "min": { "type": [ "number", "null" ] }, "minAt": { "description": "When the lowest reading was taken", "type": [ "string", "null" ] }, "max": { "type": [ "number", "null" ] }, "maxAt": { "description": "When the highest reading was taken", "type": [ "string", "null" ] }, "avg": { "type": [ "number", "null" ] }, "sum": { "type": [ "number", "null" ] }, "first": { "type": [ "number", "null" ] }, "firstAt": { "type": [ "string", "null" ] }, "last": { "type": [ "number", "null" ] }, "lastAt": { "type": [ "string", "null" ] } }, "required": [ "pointId", "unitKey", "count", "min", "minAt", "max", "maxAt", "avg", "sum", "first", "firstAt", "last", "lastAt" ], "additionalProperties": false } } }, "required": [ "timeZone", "from", "to", "points" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.timeseries (version 1) (https://learn.relynk.io/docs/builder/api/points/timeseries-v1) `POST https://ai.relynk.io/builder/v1/points.timeseries/v1` **Deprecated**: a newer version of this function exists. This version keeps working. Readings of up to 100 points aggregated into Oslo-time buckets over [from, to): one shared time axis t and one array per requested metric per point, null where a bucket has no readings. Days, weeks (Monday) and months start at Oslo midnight, so DST days have 23 or 25 hours. Limits: 15m buckets up to 62 days, others up to 400 days, and points × buckets up to 50 000; split longer ranges into several calls. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "pointIds": { "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Point ids (from points.list), at most 100" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "bucket": { "type": "string", "enum": [ "15m", "hour", "day", "week", "month" ], "description": "Bucket size" }, "metrics": { "default": [ "avg" ], "description": "Per bucket: avg, min, max, sum, count, and first and last reading", "minItems": 1, "maxItems": 7, "type": "array", "items": { "type": "string", "enum": [ "avg", "min", "max", "sum", "count", "first", "last" ] } } }, "required": [ "pointIds", "from", "to", "bucket" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "bucket": { "type": "string", "enum": [ "15m", "hour", "day", "week", "month" ] }, "from": { "type": "string", "description": "Resolved start, Oslo time with offset" }, "to": { "type": "string", "description": "Resolved end (exclusive), Oslo time with offset" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Bucket starts, Oslo time with offset; index i matches every series array" }, "series": { "type": "array", "items": { "type": "object", "properties": { "pointId": { "type": "string" }, "unitKey": { "type": [ "string", "null" ] }, "avg": { "description": "Mean reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "min": { "description": "Lowest reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "max": { "description": "Highest reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "sum": { "description": "Sum of readings", "type": "array", "items": { "type": [ "number", "null" ] } }, "count": { "description": "Readings in the bucket", "type": "array", "items": { "type": "number" } }, "first": { "description": "First reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "last": { "description": "Last reading", "type": "array", "items": { "type": [ "number", "null" ] } } }, "required": [ "pointId", "unitKey" ], "additionalProperties": false } } }, "required": [ "timeZone", "bucket", "from", "to", "t", "series" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # points.timeseries (version 2) (https://learn.relynk.io/docs/builder/api/points/timeseries-v2) `POST https://ai.relynk.io/builder/v1/points.timeseries/v2` Readings of up to 1000 points aggregated into Oslo-time buckets over [from, to): one shared time axis t and one array per requested metric per point, null where a bucket has no readings. Days, weeks (Monday) and months start at Oslo midnight, so DST days have 23 or 25 hours. Limits: 15m buckets up to 62 days, others up to 400 days, and points × buckets up to 50 000; split longer ranges into several calls. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "pointIds": { "minItems": 1, "maxItems": 1000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Point ids (from points.list), at most 1000" }, "from": { "type": "string", "description": "Start, inclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "to": { "type": "string", "description": "End, exclusive: \"YYYY-MM\", \"YYYY-MM-DD\" (Oslo midnight) or ISO 8601 with offset, e.g. \"2026-09-01T06:00:00+02:00\"" }, "bucket": { "type": "string", "enum": [ "15m", "hour", "day", "week", "month" ], "description": "Bucket size" }, "metrics": { "default": [ "avg" ], "description": "Per bucket: avg, min, max, sum, count, and first and last reading", "minItems": 1, "maxItems": 7, "type": "array", "items": { "type": "string", "enum": [ "avg", "min", "max", "sum", "count", "first", "last" ] } } }, "required": [ "pointIds", "from", "to", "bucket" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "timeZone": { "type": "string", "const": "Europe/Oslo" }, "bucket": { "type": "string", "enum": [ "15m", "hour", "day", "week", "month" ] }, "from": { "type": "string", "description": "Resolved start, Oslo time with offset" }, "to": { "type": "string", "description": "Resolved end (exclusive), Oslo time with offset" }, "t": { "type": "array", "items": { "type": "string" }, "description": "Bucket starts, Oslo time with offset; index i matches every series array" }, "series": { "type": "array", "items": { "type": "object", "properties": { "pointId": { "type": "string" }, "unitKey": { "type": [ "string", "null" ] }, "avg": { "description": "Mean reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "min": { "description": "Lowest reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "max": { "description": "Highest reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "sum": { "description": "Sum of readings", "type": "array", "items": { "type": [ "number", "null" ] } }, "count": { "description": "Readings in the bucket", "type": "array", "items": { "type": "number" } }, "first": { "description": "First reading", "type": "array", "items": { "type": [ "number", "null" ] } }, "last": { "description": "Last reading", "type": "array", "items": { "type": [ "number", "null" ] } } }, "required": [ "pointId", "unitKey" ], "additionalProperties": false } } }, "required": [ "timeZone", "bucket", "from", "to", "t", "series" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # resourceTypes.list (version 1) (https://learn.relynk.io/docs/builder/api/resource-types/list-v1) `POST https://ai.relynk.io/builder/v1/resourceTypes.list/v1` The REC/Brick types the tenant actually uses, with how many resources of each type, e.g. to find the type keys of rooms, air handling units or temperature sensors before filtering other functions by them. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list). Matches the building itself and everything that is part of it" }, "mainTypes": { "description": "Only these main types", "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "Asset", "Space", "Collection", "Point", "Unit" ] } } }, "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "types": { "type": "array", "items": { "type": "object", "properties": { "typeKey": { "type": "string", "description": "Key to filter by, e.g. \"brick_Temperature_Sensor\"; filters include subtypes" }, "name": { "type": "string" }, "mainType": { "type": "string", "description": "Asset, Space, Collection, Point or Unit" }, "typePath": { "type": "string", "description": "Position in the type hierarchy, e.g. \"Point.Sensor.Temperature_Sensor\"" }, "parentTypeKey": { "type": [ "string", "null" ] }, "count": { "type": "number", "description": "Resources of exactly this type" } }, "required": [ "typeKey", "name", "mainType", "typePath", "parentTypeKey", "count" ], "additionalProperties": false } } }, "required": [ "types" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # resourceTypes.relationships (version 1) (https://learn.relynk.io/docs/builder/api/resource-types/relationships-v1) `POST https://ai.relynk.io/builder/v1/resourceTypes.relationships/v1` How types are connected in one building: which relationships exist between which types, and how often. Use it to plan a walk with resources.related, e.g. brick_Has_Part from the building to levels to rooms, or brick_Feeds from an air handling unit to the rooms it serves. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list). Matches the building itself and everything that is part of it" } }, "required": [ "buildingId" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "relationships": { "type": "array", "items": { "type": "object", "properties": { "fromTypeKey": { "type": "string" }, "relationship": { "type": "string", "description": "Relationship key for resources.related, e.g. \"brick_Has_Part\"" }, "name": { "type": "string", "description": "Read from → to, e.g. \"Has Part\"" }, "inverseName": { "type": "string", "description": "Read to → from, e.g. \"Is Part Of\" (resources.related direction \"in\")" }, "toTypeKey": { "type": "string" }, "count": { "type": "number" } }, "required": [ "fromTypeKey", "relationship", "name", "inverseName", "toTypeKey", "count" ], "additionalProperties": false } } }, "required": [ "relationships" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # resources.list (version 1) (https://learn.relynk.io/docs/builder/api/resources/list-v1) `POST https://ai.relynk.io/builder/v1/resources.list/v1` Resources (buildings, levels, rooms, equipment, points, …) filtered by building, type, name or id. Pages by id: pass nextCursor back until it is null. For the points of equipment or rooms use points.list; for connected resources use resources.related. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "buildingId": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$", "description": "A building id (from buildings.list). Matches the building itself and everything that is part of it" }, "typeKeys": { "description": "Only these types and their subtypes, e.g. [\"rec_Room\"] (keys from resourceTypes.list)", "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_]+$" } }, "mainTypes": { "description": "Only these main types", "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "Asset", "Space", "Collection", "Point", "Unit" ] } }, "ids": { "description": "Only these resources", "minItems": 1, "maxItems": 1000, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "search": { "description": "Only names containing this text, ignoring case", "type": "string", "minLength": 1, "maxLength": 200 }, "limit": { "default": 200, "type": "integer", "minimum": 1, "maximum": 1000 }, "cursor": { "description": "nextCursor from the previous page; omit for the first page", "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" } }, "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "resources": { "maxItems": 1000, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "typeKey": { "type": "string" }, "typeName": { "type": "string" }, "mainType": { "type": "string", "description": "Asset, Space, Collection, Point or Unit" }, "buildingId": { "description": "The building it is part of; null for buildings", "type": [ "string", "null" ] } }, "required": [ "id", "name", "typeKey", "typeName", "mainType", "buildingId" ], "additionalProperties": false } }, "nextCursor": { "description": "Pass as cursor for the next page; null on the last", "type": [ "string", "null" ] } }, "required": [ "resources", "nextCursor" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # resources.related (version 1) (https://learn.relynk.io/docs/builder/api/resources/related-v1) `POST https://ai.relynk.io/builder/v1/resources.related/v1` Resources connected to the given ones by one relationship, following it up to maxDepth steps. One row per (fromId, related resource) at its shortest depth. Common relationships: brick_Has_Part (building → levels → rooms; "in" = Is Part Of), brick_Feeds (air handling unit → terminal units → rooms; "in" = Is Fed By), brick_Has_Sub_Meter, brick_Is_Metered_By, rec_Located_In, relynk_Virtual_Meter_Calculated_From. resourceTypes.relationships shows which ones a building uses. For points use points.list with resourceIds. ## Parameters - `tenantId` (query, optional): The tenant to read. May be left out when the caller belongs to exactly one tenant; GET /me lists them. ## Request body (application/json) ```json { "type": "object", "properties": { "fromIds": { "minItems": 1, "maxItems": 200, "type": "array", "items": { "type": "string", "format": "uuid", "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$" }, "description": "Resources to start from" }, "relationship": { "type": "string", "pattern": "^[A-Za-z0-9_]+$", "description": "Relationship key, e.g. \"brick_Has_Part\"" }, "direction": { "default": "out", "description": "\"out\" follows the relationship as named (Has Part); \"in\" follows its inverse (Is Part Of)", "type": "string", "enum": [ "out", "in" ] }, "maxDepth": { "default": 1, "description": "Steps to follow; e.g. 2 for building → levels → rooms via brick_Has_Part", "type": "integer", "minimum": 1, "maximum": 10 }, "targetTypeKeys": { "description": "Only these types and their subtypes, e.g. [\"rec_Room\"] (keys from resourceTypes.list)", "minItems": 1, "maxItems": 100, "type": "array", "items": { "type": "string", "pattern": "^[A-Za-z0-9_]+$" } }, "targetMainTypes": { "description": "Only these main types", "minItems": 1, "type": "array", "items": { "type": "string", "enum": [ "Asset", "Space", "Collection", "Point", "Unit" ] } }, "limit": { "default": 1000, "type": "integer", "minimum": 1, "maximum": 5000 } }, "required": [ "fromIds", "relationship" ], "additionalProperties": false } ``` ## Response 200 The function output ```json { "type": "object", "properties": { "related": { "maxItems": 5000, "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "typeKey": { "type": "string" }, "typeName": { "type": "string" }, "mainType": { "type": "string", "description": "Asset, Space, Collection, Point or Unit" }, "buildingId": { "description": "The building it is part of; null for buildings", "type": [ "string", "null" ] }, "fromId": { "type": "string", "description": "Which of fromIds this was reached from" }, "depth": { "type": "number", "description": "Steps from fromId" } }, "required": [ "id", "name", "typeKey", "typeName", "mainType", "buildingId", "fromId", "depth" ], "additionalProperties": false } }, "truncated": { "type": "boolean", "description": "True when limit cut the result; narrow the filters or raise limit" } }, "required": [ "related", "truncated" ], "additionalProperties": false } ``` ## Response 400 The input cannot be served as given. error.code says why (invalid_input, invalid_range, range_too_large, too_many_buckets, unknown_points, …) and error.message how to change the request. ## Response 401 No valid bearer token. ## Response 403 The caller has no access to the tenant. ## Response 429 Too many requests, or too many at once. Retry after the Retry-After header. # Authentication (https://learn.relynk.io/docs/builder/authentication) A person signs in with their Relynk account, and the app calls the API with that person's access token. Every call except the docs and the OpenAPI document needs that token. There are no API keys. ## The sign-in [#the-sign-in] Relynk uses **OAuth 2.0 authorization code with PKCE**, the standard "Sign in with…" flow. The app sends the person to Relynk's sign-in page, gets a code back, and exchanges the code for an access token. PKCE (Proof Key for Code Exchange) protects that exchange, so apps that run in a browser or on a phone need no secret. | Setting | Value | | ---------------------- | ------------------------------ | | Authorization endpoint | `https://auth.relynk.io/authorize` | | Token endpoint | `https://auth.relynk.io/oauth/token` | | Audience | `https://ai.relynk.io/builder` | | Scopes | `openid offline_access` | | Client ID | ask your Relynk contact for it | - **Audience.** Send `audience=https://ai.relynk.io/builder` on the authorization request. It names the API the token is for. Without it, sign-in succeeds but the token is not accepted and every call answers 401. - **Scopes.** `openid` identifies the person; `offline_access` gives a refresh token, so the app can stay signed in without asking again. - **Client ID and redirect URL.** Relynk issues the client ID and registers the app's redirect URL (where Relynk sends the person back after sign-in). Ask your Relynk contact for both. - **No passwords.** An app never asks for a Relynk password. The person types it only on Relynk's own sign-in page. The authorization request to `https://auth.relynk.io/authorize` carries these query parameters: | Parameter | Value | | ---------------- | ----------------------------------------------------- | | `response_type` | `code` | | `client_id` | The client ID from Relynk | | `redirect_uri` | The redirect URL Relynk registered for the app | | `scope` | `openid offline_access` | | `audience` | `https://ai.relynk.io/builder` | | `code_challenge` | The PKCE challenge, with `code_challenge_method=S256` | | `state` | A random value the app checks when the person returns | Platforms with an OAuth connector do all of this for you. In Lovable, follow [Build an app with Lovable](/docs/builder/lovable). ## Calling the API [#calling-the-api] Send the access token as a bearer token: ```http GET https://ai.relynk.io/builder/v1/me authorization: Bearer ``` - Refresh the token with the refresh token before it expires. When a call answers 401, sign the person in again. - Keep tokens out of URLs, logs and source code. - A browser can call the API directly only from web addresses Relynk has allowed. Otherwise call it from the app's own backend, with the person's token. ## What a caller can see [#what-a-caller-can-see] A call sees **exactly what the signed-in person may see in Relynk**: the same tenants, the same buildings, nothing more. A person with access to two buildings sees those two buildings and their rooms, sensors and meters only. The API cannot read anything that person cannot. If the app is shared, as with a shared Lovable connection, everyone using it sees what the person who connected can see. Connect with an account that has only the access the app should show. ## Choosing a tenant [#choosing-a-tenant] A **tenant** is one organisation in Relynk, such as a property company. Most people belong to one; some belong to several. `GET /me` lists the tenants the person can read: ```json title="GET /me" { "userId": "6f1c…", "tenants": [ { "id": "8a1f…", "name": "Example Property AS" }, { "id": "41b0…", "name": "Another Property AS" } ] } ``` - **One tenant:** leave `tenantId` out; calls read that tenant. - **Several tenants:** let the person choose, and add `?tenantId=` to every call. Without it the call fails with 400: "The caller belongs to 2 tenants: pass tenantId (GET /me lists them)". - **A tenant the person cannot read:** 403, "No access to tenant …". - **No tenant at all:** 403, "The caller has no access to any tenant". # The data model (https://learn.relynk.io/docs/builder/concepts/data-model) Relynk describes each building as a set of things and how they are connected. - A **tenant** is one organisation in Relynk, such as a property company. Every call reads one tenant (see [Choose a tenant](/docs/builder/guides/choose-a-tenant)). - A **resource** is any thing in a building: the building itself, a level, a room, a piece of equipment, a meter or a sensor. See [Resources and points](/docs/builder/concepts/resources-and-points). - A **type key** says what kind of resource it is, such as `rec_Room` or `brick_Air_Handling_Unit`. The names come from two open standards for buildings, **REC** (RealEstateCore) and **Brick**. - A **point** is a resource that carries readings: a temperature sensor, a setpoint, a meter reading. Points have a time series of values. - A **relationship** connects two resources: a room is part of a level, an air handling unit feeds a room. See [Relationships](/docs/builder/concepts/relationships). - **EOS** is Relynk's processed energy, water and waste consumption, kept apart from raw readings. See [Energy data](/docs/builder/concepts/energy-data). ```txt Building (rec_Building) └─ Level (rec_Level) └─ Room (rec_Room) └─ Temperature sensor (brick_Temperature_Sensor) → readings over time ``` Times are always Oslo time (see [Time](/docs/builder/concepts/time)), and long lists come in pages (see [Paging and limits](/docs/builder/concepts/paging-and-limits)). # Energy data and data quality (https://learn.relynk.io/docs/builder/concepts/energy-data) Energy, water and waste numbers come from EOS, Relynk's processed hourly consumption, never from raw meter readings. ## The functions [#the-functions] - `meters.list`: EOS meters with carrier, unit, and whether each counts in the building total (building meters) or is a sub-meter, virtual or tenant meter. - `meters.topology`: the sub-meter tree, what virtual meters are calculated from, and `overlappingBuildingMeters`. - `consumption.buildings`: building totals per carrier, exactly as the Relynk portal's EOS dashboard shows them. - `consumption.meters`: individual meters. ## Carriers and units [#carriers-and-units] A **carrier** is what is consumed: `electricity`, `gas`, `thermal`, `thermal_heat`, `thermal_cool`, `water` and `waste`. Energy is in kWh, water in L and waste in kg. ## Rules that keep the numbers right [#rules-that-keep-the-numbers-right] - Take building consumption and cost from `consumption.buildings`. Never compute it 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. - Values are whole hours: ranges must start and end on an hour, and day, week and month buckets of `consumption.buildings` on Oslo midnight. Otherwise the call fails with `invalid_range`. - For waste meters, a bucket without a pickup is 0. Everywhere else, `null` means no data. ## Data quality [#data-quality] Every number comes with how much to trust it: | Field | Meaning | | --------------------------- | ------------------------------------------------------------------------------------ | | `confidence` | `HIGH`, `MEDIUM`, `LOW` or `MISSING`. Show it next to the number. | | `estimatedShare` | How much of the number is estimated rather than measured. | | `costCoverage` | How much of the consumption has a price. Below 1, the cost is incomplete. | | `overlappingBuildingMeters` | When not empty, the total counts that consumption twice, as the portal does. Say so. | See the guide [A building's energy use](/docs/builder/guides/building-consumption). # Paging and limits (https://learn.relynk.io/docs/builder/concepts/paging-and-limits) Long lists come in pages, and each call has limits; a call over a limit tells you how to split it. ## Paging [#paging] - Pass `nextCursor` back until it is `null`: `resources.list`, `points.list`, `meters.list`, `points.raw`. - Pass `offset` while `hasMore` is true: `buildings.list`. - `truncated: true` (`resources.related`) means the result was cut: narrow the filters. ## Limits per call [#limits-per-call] | Function | Limits | | ------------------------ | -------------------------------------------------------------------------------------------------------------------- | | `points.timeseries` (v2) | up to 1000 points; 15m buckets up to 62 days, others up to 400 days; points × buckets up to 50 000 | | `points.summary` | up to 500 points, up to 380 days | | `points.raw` (v2) | up to 200 points, up to 31 days, paged | | `consumption.buildings` | up to 200 buildings; hour buckets up to 62 days, others up to 1100 days; buildings × carriers × buckets up to 50 000 | | `consumption.meters` | up to 10 000 meters; hour buckets up to 62 days, others up to 400; meters × days up to 20 000 | Split long ranges into several calls instead of working around a limit. Limits per caller are in [Errors and limits](/docs/builder/errors-and-limits). # Relationships (https://learn.relynk.io/docs/builder/concepts/relationships) Relationships connect resources, and `resources.related` walks them. | Relationship | Example | | --------------------------------------------- | ------------------------------------------ | | `brick_Has_Part` | building → levels → rooms (`maxDepth: 2`) | | `brick_Feeds` | air handling unit → terminal units → rooms | | `rec_Located_In` | equipment located in a space | | `brick_Has_Sub_Meter` / `brick_Is_Metered_By` | meter hierarchies, and what a meter meters | - `direction: "out"` follows the relationship as named; `"in"` follows it backwards. - `maxDepth` is how many steps to walk; `targetTypeKeys` keeps only the resources of those types. - `resourceTypes.relationships` shows which relationships a building uses and between which types, so you can plan a walk before making it. - `truncated: true` in the response means the result was cut: narrow the filters. - An unknown relationship name is an error (`unknown_relationship`), not an empty result. # Resources and points (https://learn.relynk.io/docs/builder/concepts/resources-and-points) Everything in a building is a resource with a type key, and points are the resources that carry readings. ## Type keys [#type-keys] | Kind of resource | Example type keys | | ------------------- | -------------------------------------------------------------------- | | Buildings | `rec_Building` | | Levels | `rec_Level` | | Rooms, other spaces | `rec_Room`, `rec_Space` | | Equipment | `brick_Air_Handling_Unit`, … | | Points | `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 matches its subtypes.** `brick_Temperature_Sensor` includes supply, return, zone and outside air temperature sensors. Each point names its exact `typeKey`, so you can tell them apart. ## A kind of point on a kind of resource [#a-kind-of-point-on-a-kind-of-resource] To get, say, room temperatures or the supply air temperature of air handling units: 1. List the resources first, with `resources.related` or `resources.list` and their type keys. 2. 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 (`null` when it has none yet). The guide [Rooms and their temperatures](/docs/builder/guides/rooms-and-temperatures) shows both calls. # Time and time zones (https://learn.relynk.io/docs/builder/concepts/time) All times are Oslo time, and every range includes its start and excludes its end. - **Time zone:** Europe/Oslo. Output times carry Oslo offsets, such as `2026-10-05T00:00:00+02:00`: show them as they are. - **Half-open ranges:** `[from, to)`. `from: "2026-10-05", to: "2026-10-12"` is seven full days. - **Input formats:** `"YYYY-MM-DD"` (Oslo midnight), `"YYYY-MM"` (the first of the month) or ISO 8601 with an offset, such as `"2026-09-01T06:00:00+02:00"`. - **Buckets:** day, week (starting Monday) and month buckets are cut at Oslo midnight, so days when daylight saving time starts or ends have 23 or 25 hours. - **Incomplete periods:** the current day, week or month is not over yet. Compare complete periods of the same length and bucket. ## Time series are columnar [#time-series-are-columnar] `points.timeseries` returns one shared time axis `t` and, per point, one array per metric, where index `i` matches `t[i]`. That is the shape chart libraries take directly (see [A temperature chart](/docs/builder/guides/temperature-chart)). `null` means no data, never zero. Draw gaps as gaps and never fill them with 0. # Errors and limits (https://learn.relynk.io/docs/builder/errors-and-limits) Every error has the same shape, and its message says what to change. ```json title="Error response" { "error": { "code": "range_too_large", "message": "…" } } ``` ## Status codes [#status-codes] | Status | Meaning | What to do | | ------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | 400 | The input cannot be served as given. | Read `code` and `message` and change the request (a shorter range, a larger bucket, fewer points). Never retry it unchanged. | | 401 | Not signed in, the sign-in expired, or the token is for another API. | Refresh the token or sign in again. Check the `audience` (see [Authentication](/docs/builder/authentication)). | | 403 | No access to the tenant, or to any tenant. | Use a tenant from `GET /me`, or ask Relynk for access. | | 404 | No such function or version. | Check the path against the [API reference](/docs/builder/api/me). | | 429 | Too many requests, or too many at once. | Wait the `Retry-After` seconds when the header is set, and make fewer calls at the same time. | | 500 | Something went wrong on Relynk's side. | Try again later; tell Relynk if it persists. | ## Input error codes [#input-error-codes] A 400 carries one of these codes: | Code | Meaning | | ---------------------- | --------------------------------------------------------------------------------------------------------- | | `BAD_REQUEST` | The request itself is wrong, such as a missing `tenantId` when the caller has several tenants. | | `invalid_input` | The body does not match the function's input. | | `invalid_range` | The time range is not valid (for example `to` before `from`, or not on a whole hour where one is needed). | | `range_too_large` | The range is longer than the function allows: split it. | | `too_many_buckets` | Too many points × buckets: use fewer points or a larger bucket. | | `unknown_points` | A point id does not exist or is not visible to the caller. | | `unknown_meters` | A meter id does not exist or is not visible to the caller. | | `unknown_buildings` | A building id does not exist or is not visible to the caller. | | `unknown_relationship` | The relationship name is not known. | | `unknown_type_key` | The type key is not known; look it up with `resourceTypes.list`. | Unknown ids, type keys and relationships are errors, never empty results. ## Limits per caller [#limits-per-caller] | Limit | Value | | ---------------------- | ----------------------------------------------------------------------- | | Requests | 600 per minute per caller (`RATE_LIMITED` beyond) | | Calls at the same time | 8 per caller (`TOO_MANY_CALLS` beyond); `BUSY` when the service is busy | | Request body | 256 KiB | Make calls one after another, or a few at a time. ## Limits per call [#limits-per-call] Each function limits how much one call may ask for. A call over a limit fails with a 400 that says how to split it. See [Paging and limits](/docs/builder/concepts/paging-and-limits). # For AI agents (https://learn.relynk.io/docs/builder/for-ai-agents) Every page here, and the API's own docs, are available as plain text for AI tools. ## On this site [#on-this-site] | What | Where | | --------------------- | ---------------------------------------------------------------------- | | Index of all pages | [`/llms.txt`](/llms.txt) | | All pages in one file | [`/llms-full.txt`](/llms-full.txt) | | One page as Markdown | Add `.md` or `.mdx` to the page's URL, or send `Accept: text/markdown` | Every page also has buttons to copy it as Markdown or open it in ChatGPT, Claude and other tools. ## On the API host [#on-the-api-host] The API host serves docs generated from the same function definitions the API runs, so they are always current. They are public and need no sign-in. | What | Where | | --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | OpenAPI 3.1 document | `https://ai.relynk.io/builder/v1/openapi.json` | | Index of the docs below | `https://ai.relynk.io/builder/v1/docs/llms.txt` | | Guide and full function reference | `https://ai.relynk.io/builder/v1/docs/llms-full.txt` | | `AGENTS.md` for an app repository | `https://ai.relynk.io/builder/v1/docs/AGENTS.md` | | Agent Skill | `https://ai.relynk.io/builder/v1/docs/skills/relynk/SKILL.md` | | Starter prompt for an AI builder | `https://ai.relynk.io/builder/v1/docs/starter-prompt.md` | | Project knowledge (at most 10,000 characters) | `https://ai.relynk.io/builder/v1/docs/project-knowledge.md` | | Lovable connector knowledge files | `https://ai.relynk.io/builder/v1/docs/lovable/…` (see [Lovable](/docs/builder/lovable#add-the-knowledge-files)) | - **Coding agents** (Claude Code, Cursor, Codex): put `AGENTS.md` in the app's repository, or install the Agent Skill as `skills/relynk/SKILL.md`. - **AI app builders** (Replit, Bolt): paste the starter prompt into your first prompt, and add the project knowledge where the builder takes it. - **Lovable**: use the [custom connector](/docs/builder/lovable) and its knowledge files instead. All of them describe sign-in with OAuth 2.0 user tokens, as in [Authentication](/docs/builder/authentication). # A building's energy use (https://learn.relynk.io/docs/builder/guides/building-consumption) One call gives a building's electricity, heating, water and other use per month, with the data quality of each number. See [Energy data](/docs/builder/concepts/energy-data) for what EOS is and the rules behind these numbers. ## Request [#request] ```js const { buildings } = await relynk('/consumption.buildings/v1', { buildingIds: [buildingId], from: '2026-01-01', to: '2027-01-01', bucket: 'month' }); ``` ## What you get back [#what-you-get-back] Each `buildings[0].carriers[i]` holds `carrier`, `unit`, and per month `consumption`, `cost` and `confidence`, plus a `total` with `consumption`, `confidence`, `cost` and `costCoverage`. - These are the building totals as the Relynk portal's EOS dashboard shows them. - Show each carrier on its own: never add electricity, heating and water together. - Show `confidence` next to the numbers, and cost only with `costCoverage` (below 1, some hours have no price). - If `overlappingBuildingMeters` is not empty, the total counts some consumption twice: say so. - The current month is not complete yet: compare complete months only. # Choose a tenant (https://learn.relynk.io/docs/builder/guides/choose-a-tenant) When a person can read more than one tenant, the app asks them to choose one and names it on every call. A **tenant** is one organisation in Relynk, such as a property company. ## Request [#request] ```http GET https://ai.relynk.io/builder/v1/me authorization: Bearer ``` ## What you get back [#what-you-get-back] ```json { "userId": "6f1c…", "tenants": [ { "id": "8a1f…", "name": "Example Property AS" }, { "id": "41b0…", "name": "Another Property AS" } ] } ``` ## Use the choice [#use-the-choice] Show the tenants' names, and add the chosen `id` as `?tenantId=` to every call: ```http POST https://ai.relynk.io/builder/v1/buildings.list/v1?tenantId=8a1f… authorization: Bearer content-type: application/json {} ``` - With exactly one tenant, `tenantId` can be left out. - With several and no `tenantId`, calls fail with 400: "The caller belongs to 2 tenants: pass tenantId (GET /me lists them)". - A tenant the person cannot read fails with 403, "No access to tenant …". # Your first call (https://learn.relynk.io/docs/builder/guides/first-call) Check that your sign-in works, then list the buildings you can see. You need an access token from a signed-in Relynk user (see [Authentication](/docs/builder/authentication)). Below, `$TOKEN` stands for it. ## Check the connection [#check-the-connection] ```bash curl -s https://ai.relynk.io/builder/v1/me \ -H "authorization: Bearer $TOKEN" ``` ```js const res = await fetch('https://ai.relynk.io/builder/v1/me', { headers: { authorization: `Bearer ${token}` } }); const me = await res.json(); ``` You get back who you are and the tenants you can read: ```json title="Response" { "userId": "6f1c…", "tenants": [{ "id": "8a1f…", "name": "Example Property AS" }] } ``` With one tenant, later calls need no `tenantId`. With several, see [Choose a tenant](/docs/builder/guides/choose-a-tenant). ## List your buildings [#list-your-buildings] ```bash curl -s -X POST https://ai.relynk.io/builder/v1/buildings.list/v1 \ -H "authorization: Bearer $TOKEN" \ -H 'content-type: application/json' \ -d '{}' ``` ```js async function relynk(path, input = {}) { const res = await fetch(`https://ai.relynk.io/builder/v1${path}`, { method: 'POST', headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json' }, body: JSON.stringify(input) }); const body = await res.json(); if (!res.ok) throw new Error(`Relynk ${path}: ${body.error.code}: ${body.error.message}`); return body; } const { buildings } = await relynk('/buildings.list/v1'); ``` ```json title="Response" { "buildings": [{ "id": "3c0d…", "name": "Storgata 1" }], "hasMore": false } ``` The other guides use the same `relynk(path, input)` helper. Each building's `id` is what other functions take as `buildingId` or `buildingIds`. # Rooms and their temperatures (https://learn.relynk.io/docs/builder/guides/rooms-and-temperatures) Two calls give you every room in a building with its current temperature. `buildingId` comes from [`buildings.list`](/docs/builder/guides/first-call). Check the type keys with `resourceTypes.list` first: they differ per tenant (see [Resources and points](/docs/builder/concepts/resources-and-points)). ## 1. The rooms of the building [#1-the-rooms-of-the-building] ```js const { related: rooms } = await relynk('/resources.related/v1', { fromIds: [buildingId], relationship: 'brick_Has_Part', maxDepth: 2, targetTypeKeys: ['rec_Room'] }); ``` `brick_Has_Part` with `maxDepth: 2` walks building → levels → rooms, and `targetTypeKeys` keeps only the rooms. See [Relationships](/docs/builder/concepts/relationships). ## 2. Their temperature sensors [#2-their-temperature-sensors] ```js const { points } = await relynk('/points.list/v1', { resourceIds: rooms.map((room) => room.id), typeKeys: ['brick_Temperature_Sensor'] }); ``` ## What you get back [#what-you-get-back] - `points[i].resourceIds` names the room the sensor belongs to. - `points[i].latest.value` is the newest reading. `latest` is `null` when the sensor has no reading yet: show it as missing, never as 0. Passing the rooms' ids as `resourceIds` matters: a type key with only a `buildingId` would also match temperature sensors on air handling units and outdoors. # A temperature chart (https://learn.relynk.io/docs/builder/guides/temperature-chart) One call gives you a week of hourly averages, ready for a chart. `points` comes from [Rooms and their temperatures](/docs/builder/guides/rooms-and-temperatures). ## Request [#request] ```js const { t, series } = await relynk('/points.timeseries/v2', { pointIds: points.map((p) => p.id), from: '2026-10-05', to: '2026-10-12', bucket: 'hour', metrics: ['avg'] }); ``` The range is `[2026-10-05 00:00, 2026-10-12 00:00)` in Oslo time: seven full days (see [Time](/docs/builder/concepts/time)). ## What you get back [#what-you-get-back] ```json { "timeZone": "Europe/Oslo", "bucket": "hour", "t": ["2026-10-05T00:00:00+02:00", "2026-10-05T01:00:00+02:00"], "series": [{ "pointId": "…", "unitKey": "unit_Deg_C", "avg": [21.4, null] }] } ``` - `t` is the shared time axis. `series[i].avg[j]` is the average of point `series[i].pointId` in the hour starting at `t[j]`. - Pass `t` as the x values and one dataset per point. - `null` is an hour without readings: draw a gap. # The warmest room last month (https://learn.relynk.io/docs/builder/guides/warmest-room) One summary per sensor is enough to rank rooms, and far cheaper than a time series. `points` comes from [Rooms and their temperatures](/docs/builder/guides/rooms-and-temperatures). ## Request [#request] ```js const { points: summaries } = await relynk('/points.summary/v1', { pointIds: points.map((p) => p.id), from: '2026-09', to: '2026-10' }); ``` `"2026-09"` to `"2026-10"` is all of September. ## What you get back [#what-you-get-back] - One summary per point: `avg`, `min` and `max` with their times (`minAt`, `maxAt`), `first` and `last`. - Sort by `avg` (or `max`), and map each `pointId` back to its room through `points.list`'s `resourceIds`. # Get started (https://learn.relynk.io/docs/builder) The Relynk Builder API lets an app read your building data in Relynk, with the access of the person who signs in. ## What you can build [#what-you-can-build] - A dashboard of room temperatures, CO2 or humidity in a building. - Charts of a sensor over a day, a week or a year. - An overview of energy, water and waste use per building and month, with cost. - A list of the rooms, equipment and sensors in a building. The API only **reads** data. It never changes anything in Relynk. ## What you need [#what-you-need] - **A Relynk account** with access to the buildings the app should show. The app sees exactly what this account may see, nothing more. - **The connection details from Relynk**: for an app builder such as Lovable, an OAuth client ID and client secret. Ask your Relynk contact for them. ## The steps [#the-steps] ### Choose how you build [#choose-how-you-build] With **Lovable** you describe the app in plain language and Lovable writes it: follow [Build an app with Lovable](/docs/builder/lovable). With **your own code**, any language that can make HTTPS requests works: start with [your first call](/docs/builder/guides/first-call). ### Connect with your Relynk account [#connect-with-your-relynk-account] The app signs you in with your Relynk account (OAuth 2.0, the standard "Sign in with…" flow) and then calls the API on your behalf. See [Authentication](/docs/builder/authentication). ### Check the connection [#check-the-connection] `GET /me` answers with who you are and the Relynk tenants (organisations) you can read. If it works, the connection works. ### Ask for the data [#ask-for-the-data] Every piece of data comes from a **function**, such as `buildings.list` or `points.timeseries`. The [guides](/docs/builder/guides/rooms-and-temperatures) show the common tasks, and the [concepts](/docs/builder/concepts/data-model) explain the data. ## The API in one paragraph [#the-api-in-one-paragraph] The base URL is `https://ai.relynk.io/builder/v1`. Every function is a `POST` to `//v` with a JSON body, for example `POST /buildings.list/v1` with `{}`, and answers with JSON. | Function | What it gives you | | ----------------------------- | --------------------------------------------------------------- | | `buildings.list` | The buildings you can see, with search | | `resourceTypes.list` | The kinds of things (rooms, equipment, sensors) in the tenant | | `resourceTypes.relationships` | How those kinds are connected in a building | | `resources.list` | Rooms, equipment and other resources, filtered | | `resources.related` | Resources connected to others, such as the rooms of a building | | `points.list` | Sensors and setpoints, with their latest reading | | `points.timeseries` | Readings over time in hourly, daily or other buckets | | `points.summary` | Average, minimum and maximum per sensor over a period | | `points.raw` | Individual readings | | `meters.list` | Energy, water and waste meters | | `meters.topology` | How a building's meters are connected | | `consumption.buildings` | Building consumption per carrier, as the EOS dashboard shows it | | `consumption.meters` | Consumption of individual meters | The [API reference](/docs/builder/api/me) documents the input and output of every function. # Build an app with Lovable (https://learn.relynk.io/docs/builder/lovable) Set up a Relynk connector in Lovable once, connect your Relynk account to it, and then build apps on your building data by describing them in Lovable's chat. A **connector** is how Lovable talks to another service. Lovable signs you in to Relynk, keeps the sign-in safe on its own servers and adds it to every request, so it never appears in your chat or in the app's code. A Relynk connection shows the data of the Relynk account that connected it. **Everyone who uses the app, and everyone the connection is shared with, sees what that Relynk account can see.** Connect with a Relynk account that has access to only what the app should show. ## Before you start [#before-you-start] - **Lovable:** to create the connector you must be an **admin or owner** of the Lovable workspace. Other members only connect to it (part 2). - **Relynk:** an account with access to the buildings the app should show. - **From your Relynk contact:** the OAuth client ID and client secret for Lovable. - **Client ID:** ask your Relynk contact for it. - **Client secret:** Relynk sends it to you privately. Never paste it into a Lovable chat, a prompt or the app's code. ## Part 1: Create the Relynk connector [#part-1-create-the-relynk-connector] A workspace admin or owner does this once for the whole workspace. The form has four sections, **Details**, **Authentication**, **Data** and **Agent knowledge**, and you fill them in from top to bottom. Every value below has a copy button: copy it and paste it into the field with the same name. ### Open the form [#open-the-form] In Lovable, open **Connectors**, select **+** in the top right, then select **Custom connector** ("Connect an API to your workspace"). The **Create custom connector** form opens. ### Details [#details] Open **Details** and fill in: ```txt title="Display name" Relynk ``` ```txt title="Short description" Building data from Relynk: buildings, rooms, sensors and energy use. ``` ```txt title="Description" Read-only access to Relynk building data: buildings, rooms and equipment, sensor readings over time, and energy, water and waste consumption. ``` **Logo (optional)**: download the Relynk icon below ([relynk-icon-512.png](/brand/relynk-icon-512.png)), then select **Upload logo** and choose the file. **Category**: select **Other**. ```txt title="Documentation URL (optional)" https://learn.relynk.io/docs/builder/lovable ``` ### Authentication [#authentication] Open **Authentication**. Under **Method**, select **OAuth 2.0**, then fill in: ```txt title="Authorization URL" https://auth.relynk.io/authorize?audience=https%3A%2F%2Fai.relynk.io%2Fbuilder ``` ```txt title="Token URL" https://auth.relynk.io/oauth/token ``` ```txt title="Scopes (optional)" openid offline_access ``` - **Scope separator (optional)**: leave it empty. - **Use PKCE**: turn it on. The Authorization URL ends with `?audience=…` on purpose: it tells Relynk's sign-in which API the sign-in is for (`https://ai.relynk.io/builder`). Without it, signing in works but every request fails with 401. Below **Use PKCE** the form shows **Add this redirect URL to the service's OAuth app settings**. It should read: ```txt title="Redirect URL (shown by Lovable, nothing to enter)" https://api.lovable.dev/workspaces/connectors/standard/oauth/callback ``` Relynk has already allowed this address, so there is nothing to do. If your form shows a different address, send it to your Relynk contact before you connect (part 2). Further down in **Authentication**, fill in: ```txt title="API base URL" https://ai.relynk.io/builder/v1 ``` Under **Test request**, leave **Method** on **GET** and enter: ```txt title="Path" /me ``` A connector cannot switch between OAuth 2.0 and another method later. If you chose the wrong method, delete the connector and create it again. ### Data [#data] Open **Data** and select **Personal data** ("In chat, only the person who connected can use it"). Relynk shows each person what their own Relynk account may see, so in Lovable's chat everyone should use their own connection. Apps still use the connection linked to them; see [What others can see](#what-others-can-see). ### Agent knowledge [#agent-knowledge] Knowledge files teach Lovable how to use Relynk. Add all seven, in this order. For each file, copy the **Name**, the **Description** and the **Content** into the fields with the same name: Use the empty knowledge file the form starts with. ```txt title="Name" Relynk guide ``` ```txt title="Description" How the Relynk API works: the data model, choosing a tenant, errors and limits, and the rules that keep the numbers right. Read it before any Relynk call. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-guide.md](/lovable/relynk-guide.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk buildings ``` ```txt title="Description" Listing the tenant's buildings and their ids, the starting point for most other Relynk calls. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-buildings.md](/lovable/relynk-functions-buildings.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk resource types ``` ```txt title="Description" Looking up type keys (rooms, equipment, kinds of sensor) and the relationships a building uses, before filtering resources or points by type. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-resourceTypes.md](/lovable/relynk-functions-resourceTypes.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk resources ``` ```txt title="Description" Finding levels, rooms and equipment in a building and walking the relationships between them. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-resources.md](/lovable/relynk-functions-resources.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk points and time series ``` ```txt title="Description" Finding sensors, setpoints and meter points and reading their latest values, time series, summaries and raw readings for charts and tables. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-points.md](/lovable/relynk-functions-points.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk meters ``` ```txt title="Description" Listing EOS energy, water and waste meters, and which meters are sub-meters of which. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-meters.md](/lovable/relynk-functions-meters.md)) Select **Add knowledge file** first. ```txt title="Name" Relynk consumption ``` ```txt title="Description" Energy, water and waste consumption and cost per building or per meter, with data quality, for energy dashboards and reports. ``` **Content**: select **Copy content** and paste it into **Content**. ([relynk-functions-consumption.md](/lovable/relynk-functions-consumption.md)) The files hold no credentials and no addresses, and together they are well under Lovable's limits (50 files per connector, 50,000 characters each, 256 KB in total). ### Create [#create] Select **Create**. **Relynk** now appears in the workspace's connector catalog. ## Part 2: Connect your Relynk account [#part-2-connect-your-relynk-account] Each person who builds with Relynk data does this once. ### Add a connection [#add-a-connection] Open **Connectors**, find **Relynk**, and select **Add connection**. ### Enter the client ID and secret [#enter-the-client-id-and-secret] ```txt title="Client ID" ask your Relynk contact for it ``` **Client secret**: your Relynk contact sends it to you privately. Enter it only in this form, never in a chat, a prompt or the app's code. ### Sign in to Relynk [#sign-in-to-relynk] A window opens with Relynk's sign-in. Sign in with the Relynk account whose data you want to build with. ### Choose who can use the connection [#choose-who-can-use-the-connection] Under **Sharing**, choose who may use it: only you (the default, shown as **Private**), specific people, or the whole workspace. Everyone you share with sees what your Relynk account can see. ### Check the connection [#check-the-connection] Lovable sends the test request (`GET /me`). When it succeeds, the connection is ready. If it fails, see [Troubleshooting](#troubleshooting). ### Link it to your project [#link-it-to-your-project] Link the connection to the Lovable projects that should use Relynk data. Lovable encrypts the client secret and your sign-in and adds them to requests on its own servers. They never appear in the chat or in the project's code. ## Part 3: Build your app [#part-3-build-your-app] Start a project, link the Relynk connection, and paste this as your first prompt. Replace the last line with what you want to build. ```txt title="Starter prompt" Build an app on my Relynk building data, using the Relynk connector for every Relynk request. Before writing code, read the Relynk knowledge files, starting with "Relynk guide". Start with GET /me. If it lists more than one tenant, let me choose one and add ?tenantId= to every request. Look up type keys with resourceTypes.list instead of guessing them. Show null as missing data, never as 0, and show times in Europe/Oslo. What I want: ``` ### Example prompts [#example-prompts] - "Show a dashboard of room temperatures in my building." - "Chart the hourly temperature of every room on level 2 for the last week." - "List the five warmest and the five coldest rooms last month." - "Show electricity, heating and water use per month this year, with the data quality next to each number." - "Make a page where I pick a building and see its rooms, equipment and sensors." Ask for one thing at a time, check it, then ask for the next. ## What others can see [#what-others-can-see] - **Everyone using the app** sees the data of the Relynk account that connected, whether they have a Relynk account themselves or not. Put the app behind Lovable's own login if the data should not be public. - **Everyone the connection is shared with** can build apps with that same data. - Use a Relynk account with access to only the buildings the app should show. Your Relynk contact can set one up. ## Troubleshooting [#troubleshooting] The connector form shows a redirect URL that Relynk has not allowed. Copy it from **Authentication** in the connector form and send it to your Relynk contact. It must match character for character. The sign-in is not for the Relynk API. Check that the Authorization URL is exactly `https://auth.relynk.io/authorize?audience=https%3A%2F%2Fai.relynk.io%2Fbuilder`, including everything after the `?`. If it is and the test still fails, tell your Relynk contact. Your Relynk account has no access to any tenant. Ask your Relynk contact to give it access to the buildings the app should show. The app asked for a tenant your Relynk account cannot read. Ask Lovable to call `GET /me` and use one of the tenants it lists. Your account can read more than one tenant. Ask Lovable to let you choose one from `GET /me` and add `?tenantId=` to every request. The app made too many requests at once or in a short time. Ask Lovable to make Relynk requests one after another, to wait as long as the response says before trying again, and to keep results it already has instead of asking again. Ask Lovable to follow the "Relynk guide" knowledge file: missing data is `null` and must show as missing, never as 0, and energy totals come from `consumption.buildings`, never from adding meters together. # Versioning (https://learn.relynk.io/docs/builder/versioning) A function's path is a contract: what it takes and returns never changes. - Every function has a version in its path: `/points.timeseries/v2`. - A path takes and returns the same shapes for as long as it exists. Descriptions may get better; fields are never removed or changed. - A breaking change is a **new version at a new path**, such as `/points.timeseries/v2` next to `/points.timeseries/v1`. The old version keeps working. - Older versions are marked deprecated in the OpenAPI document and the [API reference](/docs/builder/api/me). Build new code against the newest version. An app you built keeps working without changes, even when newer versions appear. # Relynk Learn (https://learn.relynk.io/docs) Relynk Learn shows you how to build apps, dashboards and reports on the building data you already have in Relynk. Relynk is a building automation and IoT platform for commercial real estate. It knows your buildings, the rooms and equipment in them, their sensors and their energy use. The **Builder API** makes that data available to apps, and AI app builders such as Lovable can build an app from a description in plain language. Connect Lovable to Relynk once, then describe the app you want. No code needed. What the API offers, what you need to get started, and how to make your first call. ## Where to go next [#where-to-go-next] - **New to Relynk data?** Read [Get started](/docs/builder), then the [data model](/docs/builder/concepts/data-model). - **Building with Lovable?** Follow [Build an app with Lovable](/docs/builder/lovable) step by step. - **Writing code?** Start with [your first call](/docs/builder/guides/first-call) and the [API reference](/docs/builder/api/me). - **Using an AI coding agent?** Point it at [For AI agents](/docs/builder/for-ai-agents).