# API - Data Retrieval

# How to retrieve data from the Whysor API

# Whysor API - Device & Sensor Data

## Overview

This document describes how to retrieve devices, sensors, and sensor measurements using the Whysor API, reachable at **https://api.whysor.com**.

This API guide explains how users can:

- Log in and obtain an access token
- Retrieve devices they have access to (single device, list, or filtered list)
- Retrieve sensors belonging to devices (single sensor, list, or filtered list)
- Read the latest/historical measurement data for a single sensor
- Read measurement data for multiple sensors in one request

All API requests below (except login) require authentication.

---

## Authentication


### Log in

**Request**

```http
POST /users/login
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "your-password"
}
```

If you're logging in through a white-labeled deployment, pass that deployment's own domain instead, e.g.:

```json
{
  "email": "user@example.com",
  "password": "your-password",
  "domain": "my.example.org"
}
```

`domain` selects which login realm to authenticate against. Whysor is white-labeled — the same email can exist under different organizations, each tied to its own domain, and the `domain` parameter tells the server which one to log you into. whysor.com is the default realm, used automatically if `domain` is omitted or doesn't match any known theme's domain.

**Response** `200 OK`

```json
{
  "id": "<access-token>",
  "ttl": 1209600,
  "created": "2026-09-15T09:12:03.000Z",
  "userId": "usr-0001-mock"
}
```

The `id` field is your access token. It is valid for `ttl` seconds (14 days by default). The examples below use `<token>` as a placeholder for this value.

### Authenticate subsequent requests

Pass the token in the `Authorization` header on every request (no `Bearer` prefix):

```http
Authorization: <token>
Content-Type: application/json
```

If the header is missing or invalid, every endpoint returns:

```json
{ "error": { "statusCode": 401, "name": "Error", "message": "Authorization Required", "code": "AUTHORIZATION_REQUIRED" } }
```

### Log out

**Request**

```http
POST /users/logout
Authorization: <token>
```

**Response** `204 No Content` (empty body — the token is invalidated).

---

## Devices

### Get all devices you have access to

**Request**

```http
GET /devices
Authorization: <token>
```

**Response** `200 OK`

```json
[
  {
    "id": "dev-0001-mock",
    "active": true,
    "externalDomain": "visual-crossing",
    "externalId": "ext-0001-mock",
    "additionalId": "000001",
    "name": "Mock Weather Station 1",
    "description": "",
    "additionalInfo": { "location": { "latitude": 51.0, "longitude": 6.0 } },
    "deleted": false,
    "created": "2024-06-10T12:47:18.669Z",
    "modified": "2025-05-23T09:35:17.438Z",
    "organizationId": "org-0001-mock"
  }
]
```

(Additional device objects in the array are truncated here for brevity — each has the same shape.)

### Get devices with a filter

The `filter` query parameter takes a JSON-encoded LoopBack-style filter object supporting `where`, `fields`, `include`, `order`, `limit`, and `offset`.

**Request** — devices with a name matching "Mock" (case-insensitive)

```http
GET /devices?filter={"where":{"name":{"like":"Mock","options":"i"}}}
Authorization: <token>
```

**Response** `200 OK`

```json
[
  {
    "id": "dev-0001-mock",
    "active": true,
    "externalDomain": "visual-crossing",
    "externalId": "ext-0001-mock",
    "additionalId": "000001",
    "name": "Mock Weather Station 1",
    "description": "",
    "additionalInfo": { "location": { "latitude": 51.0, "longitude": 6.0 } },
    "deleted": false,
    "created": "2024-06-10T12:47:18.669Z",
    "modified": "2025-05-23T09:35:17.438Z",
    "organizationId": "org-0001-mock"
  }
]
```

### Get a single device by ID

**Request**

```http
GET /devices/dev-0001-mock
Authorization: <token>
```

**Response** `200 OK`

```json
{
  "id": "dev-0001-mock",
  "active": true,
  "externalDomain": "visual-crossing",
  "externalId": "ext-0001-mock",
  "additionalId": "000001",
  "name": "Mock Weather Station 1",
  "description": "",
  "additionalInfo": { "location": { "latitude": 51.0, "longitude": 6.0 } },
  "deleted": false,
  "created": "2024-06-10T12:47:18.669Z",
  "modified": "2025-05-23T09:35:17.438Z",
  "organizationId": "org-0001-mock"
}
```

### Find the first device matching a filter

**Request**

```http
GET /devices/findOne?filter={"where":{"externalId":"ext-0001-mock"}}
Authorization: <token>
```

**Response** `200 OK` — a single object, same shape as above (not an array).

If no device matches, the API returns `404`:

```json
{ "error": { "statusCode": 404, "name": "Error", "message": "Unknown \"Device\" id \"undefined\".", "code": "MODEL_NOT_FOUND" } }
```

### Count devices matching a filter

**Request**

```http
GET /devices/count?where={"active":true}
Authorization: <token>
```

**Response** `200 OK`

```json
{ "count": 3 }
```

---

## Sensors

### Get all sensors you have access to

**Request**

```http
GET /sensors
Authorization: <token>
```

**Response** `200 OK` — an array of sensor objects (see shape below).

### Get sensors for a specific device

Use `where.deviceId` in the filter.

**Request**

```http
GET /sensors?filter={"where":{"deviceId":"dev-0001-mock"}}
Authorization: <token>
```

**Response** `200 OK`

```json
[
  {
    "id": "sen-0001-mock",
    "name": "",
    "description": "",
    "tag": "humidity",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "deviceId": "dev-0001-mock",
    "created": "2024-06-10T12:47:18.814Z",
    "modified": "2024-06-10T12:47:18.814Z"
  },
  {
    "id": "sen-0002-mock",
    "name": "",
    "description": "",
    "tag": "temperature",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "deviceId": "dev-0001-mock",
    "created": "2024-06-10T12:47:18.813Z",
    "modified": "2024-06-10T12:47:18.814Z"
  }
]
```

### Get sensors with other filters

Any Sensor field can be used in `where` (e.g. `tag`, `name`, `sensorTemplateId`). This is **not** scoped to one device — it matches across all devices you have access to.

**Request**

```http
GET /sensors?filter={"where":{"tag":"temperature"},"limit":3}
Authorization: <token>
```

**Response** `200 OK`

```json
[
  {
    "id": "sen-0004-mock",
    "tag": "temperature",
    "deviceId": "dev-0002-mock",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "name": "",
    "description": "",
    "created": "2024-06-10T13:12:59.478Z",
    "modified": "2024-06-10T13:12:59.478Z"
  },
  {
    "id": "sen-0002-mock",
    "tag": "temperature",
    "deviceId": "dev-0001-mock",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "name": "",
    "description": "",
    "created": "2024-06-10T12:47:18.813Z",
    "modified": "2024-06-10T12:47:18.814Z"
  },
  {
    "id": "sen-0005-mock",
    "tag": "temperature",
    "deviceId": "dev-0003-mock",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "name": "",
    "description": "",
    "created": "2024-06-07T12:54:47.520Z",
    "modified": "2024-06-07T12:54:47.520Z"
  }
]
```

Combine device and other filters:

**Request**

```http
GET /sensors?filter={"where":{"deviceId":"dev-0001-mock","tag":"temperature"}}
Authorization: <token>
```

**Response** `200 OK`

```json
[
  {
    "id": "sen-0002-mock",
    "tag": "temperature",
    "deviceId": "dev-0001-mock",
    "hasForecast": true,
    "virtual": false,
    "additionalInfo": {},
    "calibrations": [],
    "name": "",
    "description": "",
    "created": "2024-06-10T12:47:18.813Z",
    "modified": "2024-06-10T12:47:18.814Z"
  }
]
```

### Get a single sensor by ID

**Request**

```http
GET /sensors/sen-0002-mock
Authorization: <token>
```

**Response** `200 OK`

```json
{
  "id": "sen-0002-mock",
  "name": "",
  "description": "",
  "tag": "temperature",
  "hasForecast": true,
  "virtual": false,
  "additionalInfo": {},
  "calibrations": [],
  "deviceId": "dev-0001-mock",
  "created": "2024-06-10T12:47:18.813Z",
  "modified": "2024-06-10T12:47:18.814Z"
}
```

### Find the first sensor matching a filter

**Request**

```http
GET /sensors/findOne?filter={"where":{"deviceId":"dev-0001-mock"}}
Authorization: <token>
```

**Response** `200 OK` — a single object, same shape as above (not an array).

### Count sensors matching a filter

**Request**

```http
GET /sensors/count?where={"deviceId":"dev-0001-mock"}
Authorization: <token>
```

**Response** `200 OK`

```json
{ "count": 21 }
```

---

## Sensor Data

There are two ways to read measurement data: one sensor at a time, or many sensors in a single request.

### Read data for a single sensor

**Request**

```http
GET /sensors/sen-0002-mock/read?filter={"limit":3,"order":"datetimeMeasure DESC"}
Authorization: <token>
```

**Response** `200 OK`

```json
[
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 17.7, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T05:00:00.000Z", "value": 15.2, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T04:00:00.000Z", "value": 15.7, "groupId": null, "metadata": null }
]
```

The `filter` supports the usual `where`, `order`, `limit`, `offset` options, applied against the measurement records (e.g. filter by `datetimeMeasure` using `gt`, `lt`, or `between`).

**Request** — a time range

```http
GET /sensors/sen-0002-mock/read?filter={"where":{"datetimeMeasure":{"between":["2026-09-14T00:00:00.000Z","2026-09-15T00:00:00.000Z"]}},"order":"datetimeMeasure ASC"}
Authorization: <token>
```

**Response** `200 OK` — returned 19 measurements for that 24-hour window (array truncated here):

```json
[
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-14T01:00:00.000Z", "value": 16.5, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-14T03:00:00.000Z", "value": 16.3, "groupId": null, "metadata": null }
]
```

#### Downsampling large ranges (decimation)

For large time ranges, you can ask the API to downsample the results using the `lttb` (Largest-Triangle-Three-Buckets) method instead of returning every raw point.

**Request**

```http
GET /sensors/sen-0002-mock/read?filter={"where":{"datetimeMeasure":{"between":["2026-08-01T00:00:00.000Z","2026-09-15T00:00:00.000Z"]}},"decimate":{"method":"lttb","samples":10}}
Authorization: <token>
```

**Response** `200 OK` — 13 downsampled points covering the ~6-week range (`samples` is a target, not an exact count):

```json
[
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-08-01T00:00:00.000Z", "value": 18.1, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-08-03T15:00:00.000Z", "value": 34.8, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-08-07T04:00:00.000Z", "value": 8.8, "groupId": null, "metadata": null },
  { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-14T23:00:00.000Z", "value": 16.7, "groupId": null, "metadata": null }
]
```

`samples` is the target number of points to return (defaults to 50 if omitted).

### Read data for multiple sensors at once

**Request** — no filter (returns only the latest measurement per sensor)

```http
POST /sensors/read
Authorization: <token>
Content-Type: application/json

{
  "sensorIds": [
    "sen-0002-mock",
    "sen-0001-mock"
  ]
}
```

**Response** `200 OK`

```json
{
  "sen-0002-mock": [
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 17.7, "groupId": null, "metadata": null }
  ],
  "sen-0001-mock": [
    { "sensorId": "sen-0001-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 95, "groupId": null, "metadata": null }
  ]
}
```

Response is an object keyed by `sensorId` (not an array like the single-sensor endpoint), each value an array of measurements for that sensor.

#### Filtering the multi-sensor read

You can include a `filter` alongside `sensorIds` for a time range or decimation. **This must go in the JSON body, not the query string** — passing `?filter=...` on this endpoint conflicts with the body and the server responds `422`:

```json
{ "error": { "statusCode": 422, "name": "InvalidRequestError", "message": "At least one sensor ID is required.", "code": "INVALID_REQUEST" } }
```

The correct way — put `filter` as a sibling key of `sensorIds`:

**Request**

```http
POST /sensors/read
Authorization: <token>
Content-Type: application/json

{
  "sensorIds": ["sen-0002-mock"],
  "filter": {
    "where": { "datetimeMeasure": { "gt": "2026-09-15T00:00:00.000Z" } },
    "order": "datetimeMeasure ASC"
  }
}
```

**Response** `200 OK`

```json
{
  "sen-0002-mock": [
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T02:00:00.000Z", "value": 16.1, "groupId": null, "metadata": null },
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T03:00:00.000Z", "value": 16.0, "groupId": null, "metadata": null },
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T04:00:00.000Z", "value": 15.7, "groupId": null, "metadata": null },
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T05:00:00.000Z", "value": 15.2, "groupId": null, "metadata": null },
    { "sensorId": "sen-0002-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 17.7, "groupId": null, "metadata": null }
  ]
}
```

#### Errors

At least one sensor ID is required — an empty array is rejected:

**Request**

```http
POST /sensors/read
Authorization: <token>
Content-Type: application/json

{ "sensorIds": [] }
```

**Response** `422 Unprocessable Entity`

```json
{ "error": { "statusCode": 422, "name": "InvalidRequestError", "message": "At least one sensor ID is required.", "code": "INVALID_REQUEST" } }
```

Only sensors your token has access to, whose device is active, will be included in the response — sensor IDs you don't have permission for are silently omitted rather than causing an error.

---

## Putting it together: fetch a device's sensors and their latest data

```http
# 1. Log in
POST /users/login
{ "email": "user@example.com", "password": "your-password" }
→ token = response.id

# 2. Find the device
GET /devices/findOne?filter={"where":{"externalId":"ext-0001-mock"}}
Authorization: <token>
→ deviceId = response.id  // "dev-0001-mock"

# 3. Get its sensors
GET /sensors?filter={"where":{"deviceId":"dev-0001-mock"},"limit":2}
Authorization: <token>
→ sensorIds = response.map(s => s.id)  // ["sen-0001-mock", "sen-0003-mock"]

# 4. Read latest data for all sensors in one call
POST /sensors/read
Authorization: <token>
{ "sensorIds": ["sen-0001-mock", "sen-0003-mock"] }
```

Example response from step 4 (values/shape verified live, IDs mocked):

```json
{
  "sen-0001-mock": [
    { "sensorId": "sen-0001-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 95, "groupId": null, "metadata": null }
  ],
  "sen-0003-mock": [
    { "sensorId": "sen-0003-mock", "datetimeMeasure": "2026-09-15T07:00:00.000Z", "value": 0, "groupId": null, "metadata": null }
  ]
}
```