> For the complete documentation index, see [llms.txt](https://docs.raalabs.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.raalabs.io/2025-10-31/getting-started/changelog/versions.md).

# API Versions

{% updates format="full" %}
{% update date="2026-09-10" tags="new-release,stable" %}

## 2026-09-10

#### Updates

Use this version by specifying the header:

```json
"x-api-version": "2026-09-10"
```

This version was previously available as the beta version `2026-01-01-beta`. The two are identical, and `2026-01-01-beta` remains callable, so if you are already using the beta you only need to change the version header. Omitting the `x-api-version` header targets the default version, which is unchanged by this release.

This version includes several updates for all endpoints. Two of them address the same underlying fact: an `id` is not guaranteed to be unique within a domain, because some domains name groups of time series rather than every time series individually, so a single query can match several time series that share one `id`.

* **Metadata**: changed response format for `/metadata`, the response is now a JSON array with metadata objects, where the `id` is part of the metadata object. In earlier versions this is a JSON object with the `id` as the key, so time series sharing an `id` arrive as repeated keys in the same object. While the JSON standard does allow duplicate object keys, most parsers resolve them by keeping only the last occurrence, which drops the other time series without any error. With the array, every matched time series is its own element, so nothing is lost regardless of what parser or software library is used, and the number of elements tells you how many time series the query matched. Additionally, NDJSON has been introduced as a new response format for `/metadata`.
* **Measurements**: the response format for `/measurements` is unchanged from `2025-10-31`, each data point carries an `id`, a `timestamp` and a `value`.
* **Statistics**: changed response format for `/statistics`, which now follows more closely the response format of the `/measurements` endpoint. The response contains `timestamp` and `id` (like in `/measurements`), and an `aggregations` object containing all requested aggregations. The aggregation type is not part of the `id` anymore, and the default aggregation has been changed from `avg` to `mean` (`avg` is not defined when there is only 1 datapoint within a window, while the `mean` is equal to that datapoint). In earlier versions each result key is `{domainId}/{AggregationName}`, so time series sharing a `domainId` produce several objects with the same key and the same `timestamp`. Nothing in the response says which time series a value came from, and code that indexes the results by key and timestamp overwrites one value with another. This version gives two well defined outcomes instead:
  * By default, time series that resolve to the same `id` are merged into a single result per time window and `id`, covering all of their data points.
  * Query with `output_domain=id` to keep every time series separate. The `id` domain guarantees uniqueness, so each result carries its own unique `id` and every value can be traced back to one time series.

#### Migration guide

This guide walks you through the breaking changes in `2026-09-10` compared to `2025-10-31`. Requests and query parameters remain identical to the previous version, apart from one changed default aggregation, so this guide focuses on how to update parsing logic to the changed response formats (with Python code examples).

**Measurements endpoint**

The `/measurements` response format is identical in API versions `2025-10-31` and `2026-09-10`, in every response format including `parquet`, so your code does not need updating.

**Metadata endpoint** The `/metadata` response has undergone substantial changes. See below for a sample of how the new format looks like, and how to update parsing logic.

Version `2025-10-31` response:

```json
{
  "IMO1000003/jsmea_nav/PositioningSystem/GPS///Longitude/": {
    "name": "Longitude",
    "description": "SHIP POSITION (LONGITUDE)",
    "source": "FurunoVDR",
    "scale": 1.0,
    "uom": "deg",
    "mapsTo": {
      "jsmea": "IMO1000003/jsmea_nav/PositioningSystem/GPS///Longitude/",
      "raalabs": "IMO1000003/GPS_LONG"
    },
    "rangeHigh": 180.0,
    "rangeLow": -180.0,
    "timeSeriesId": "11000001-0000-0000-0000-000000000000",
    "vesselImo": "1000003",
    "vesselName": "Flying Dutchman"
  }
}
```

Version `2026-09-10` response:

```json
[
  {
    "id": "IMO1234567/jsmea_mac/MainEngine/Cylinder9/ScavAir//Temp/",
    "timeSeriesId": "a8009573-5013-4d18-8e63-1ae2f91d6b80",
    "source": "Aconis",
    "dataProvider": "Raa Labs",
    "unitOfMeasure": "°C",
    "scale": 1.0,
    "vessel": {
      "name": "Flying Dutchman",
      "imo": "1234567"
    },
    "domains": {
      "jsmea": {
        "id": "IMO1234567/jsmea_mac/MainEngine/Cylinder9/ScavAir//Temp/",
        "namingRule": "jsmea_mac",
        "category": "MainEngine",
        "subcategory": "Cylinder9",
        "content": "ScavAir",
        "item": "Temp"
      },
      "raalabs": {
        "id": "IMO1234567/ME CylinderScavAirTemp_9",
        "shortName": "ME CylinderScavAirTemp_9"
      },
      "maker": {
        "id": "IMO1234567/Aconis/0517",
        "name": "0517",
        "description": "M/E CYL.#9 SCAV. AIR BOX FIRE",
        "rangeHigh": 200.0,
        "rangeLow": 0.0
      }
    }
  }
]
```

How to update your code:

```python
import requests

TOKEN = "YOUR_TOKEN"
ENVIRONMENT = "YOUR_ENVIRONMENT"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "x-api-version": "2026-09-10"  # was "2025-10-31"
}

url = f"https://portal.raalabs.io/{ENVIRONMENT}/metadata/raalabs/IMO1234567/*"
response = requests.get(url, headers=headers)

# Version 2025-10-31 — response is a dict with domainIds as keys
# note that standard json parsers drop duplicate keys (this is one of major issues fixed with the new api version)
metadata = response.json()  # dict
for tag_id, meta in metadata.items():
    name = meta["name"]
    description = meta["description"]
    unit = meta["uom"]
    scale = meta["scale"]
    source = meta["source"]
    time_series_id = meta["timeSeriesId"]
    vessel_imo = meta["vesselImo"]
    vessel_name = meta["vesselName"]
    range_high = meta["rangeHigh"]
    range_low = meta["rangeLow"]
    raalabs_id = meta["mapsTo"].get("raalabs")

# Version 2026-09-10 — response is a list of metadata objects

metadata = response.json()  # list
for meta in metadata:
    tag_id = meta["id"]                            # was the dict key
    unit = meta["unitOfMeasure"]                   # was "uom"
    scale = meta["scale"]                          # unchanged
    source = meta["source"]                        # unchanged
    time_series_id = meta["timeSeriesId"]          # unchanged
    data_provider = meta["dataProvider"]           # new in this version
    vessel_imo = meta["vessel"]["imo"]             # was "vesselImo"
    vessel_name = meta["vessel"]["name"]           # was "vesselName"
    raalabs_id = meta["domains"]["raalabs"]["id"]  # was mapsTo["raalabs"]
    maker = meta["domains"].get("maker", {})       # a domain is only present if the time series has a name in it
    name = maker.get("name")                       # was "name"
    description = maker.get("description")         # was "description"
    range_high = maker.get("rangeHigh")            # was "rangeHigh"
    range_low = maker.get("rangeLow")              # was "rangeLow"
```

A time series that has no name in a given domain has no entry for it in `domains`, so guard the reads that go through one.

If your code needs to look up metadata by identifier, build the dictionary yourself and key it by `timeSeriesId`. Keying it by `id` reintroduces the dropped duplicates that the array response fixes.

**Statistics endpoint**

Two changes affect your queries and their results before you get to the response shape:

* The default aggregation is `mean` instead of `avg`. A request that does not set the `aggregations` parameter now returns a plain mean of the values in the window, where it previously returned a time-weighted average. Request `aggregations=avg` explicitly to keep the old calculation.
* The aggregation names in the response are the lowercase names you requested, for example `mean` and `count`. In `2025-10-31` the names and the delimiter were determined by the queried domain, and appeared capitalized in the result keys (`.../Mean`, `.../Count`). Code that matches on `/Avg` or `/Mean`, or that splits a result key to recover the aggregation type, needs updating. See below:

Version `2025-10-31` response:

```json
[
  {
    "IMO1000001/ME ShaftPower/Avg": 13.12,
    "timestamp": "2025-09-10T09:00:00.000000Z"
  },
  {
    "IMO1000001/ME ShaftPower/Max": 15.47,
    "timestamp": "2025-09-10T09:00:00.000000Z"
  },
  {
    "IMO1000001/ME ShaftPower/Min": 10.85,
    "timestamp": "2025-09-10T09:00:00.000000Z"
  },
  {
    "IMO1000001/ME ShaftPower/Count": 3600,
    "timestamp": "2025-09-10T09:00:00.000000Z"
  }
]
```

Version `2026-09-10` response:

```json
[
  {
    "timestamp": "2025-09-10T09:00:00.000000Z",
    "id": "IMO1000001/ME ShaftPower",
    "aggregations": {
      "avg": 13.12,
      "max": 15.47,
      "min": 10.85,
      "count": 3600
    }
  }
]
```

How to update your code:

```python
import requests

TOKEN = "YOUR_TOKEN"
ENVIRONMENT = "YOUR_ENVIRONMENT"

headers = {
    "Authorization": f"Bearer {TOKEN}",
    "x-api-version": "2026-09-10"  # was "2025-10-31"
}

url = f"https://portal.raalabs.io/{ENVIRONMENT}/statistics/raalabs/IMO1234567/ME%20ShaftPower"
params = {"aggregations": "avg,max,min,count", "window": "1h", "last": "1day"}
response = requests.get(url, headers=headers, params=params)
data = response.json()

# Version 2025-10-31 — each object has a dynamic key like "{domainId}/{AggType}"
for entry in data:
    timestamp = entry["timestamp"]
    for key, value in entry.items():
        if key != "timestamp":
            # key is e.g. "IMO1234567/ME ShaftPower/Avg"
            # Split to extract the domainId and aggregation type
            *id_parts, agg_type = key.split("/")
            domain_id = "/".join(id_parts)
            print(f"{domain_id} {agg_type}={value} at {timestamp}")

# Version 2026-09-10 — structured fields with aggregations object
# there is no need for string splitting to get aggregation types
for entry in data:
    timestamp = entry["timestamp"]
    domain_id = entry["id"]
    for agg_type, value in entry["aggregations"].items():
        print(f"{domain_id} {agg_type}={value} at {timestamp}")
```

{% endupdate %}

{% update date="2025-11-03" tags="stable" %}

## 2025-10-31 (default)

Initial dated version. Target this version by using the following version header in your requests.

```json
"x-api-version": "2025-10-31"
```

This version is supported until 2027-03-10, six months after the release of `2026-09-10`, and is deactivated after that date. The migration guide in the `2026-09-10` entry above covers the changes you need to make in order to change to this version.

#### Migration guide

The functionality of this version is identical to the `default` version, which is targeted when no version header is specified. There are no changes to client code required when updating to version `2025-10-31` from the unversioned API.

### Unversioned API (default)

The unversioned API, this is currently the `default` version. This version is targeted when no `x-api-version` header is provided in the request.
{% endupdate %}
{% endupdates %}
