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:

All API requests below (except login) require authentication.


Authentication

Log in

Request

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.:

{
  "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

{
  "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):

Authorization: <token>
Content-Type: application/json

If the header is missing or invalid, every endpoint returns:

{ "error": { "statusCode": 401, "name": "Error", "message": "Authorization Required", "code": "AUTHORIZATION_REQUIRED" } }

Log out

Request

POST /users/logout
Authorization: <token>

Response 204 No Content (empty body — the token is invalidated).


Devices

Get all devices you have access to

Request

GET /devices
Authorization: <token>

Response 200 OK

[
  {
    "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)

GET /devices?filter={"where":{"name":{"like":"Mock","options":"i"}}}
Authorization: <token>

Response 200 OK

[
  {
    "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

GET /devices/dev-0001-mock
Authorization: <token>

Response 200 OK

{
  "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

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:

{ "error": { "statusCode": 404, "name": "Error", "message": "Unknown \"Device\" id \"undefined\".", "code": "MODEL_NOT_FOUND" } }

Count devices matching a filter

Request

GET /devices/count?where={"active":true}
Authorization: <token>

Response 200 OK

{ "count": 3 }

Sensors

Get all sensors you have access to

Request

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

GET /sensors?filter={"where":{"deviceId":"dev-0001-mock"}}
Authorization: <token>

Response 200 OK

[
  {
    "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

GET /sensors?filter={"where":{"tag":"temperature"},"limit":3}
Authorization: <token>

Response 200 OK

[
  {
    "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

GET /sensors?filter={"where":{"deviceId":"dev-0001-mock","tag":"temperature"}}
Authorization: <token>

Response 200 OK

[
  {
    "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

GET /sensors/sen-0002-mock
Authorization: <token>

Response 200 OK

{
  "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

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

GET /sensors/count?where={"deviceId":"dev-0001-mock"}
Authorization: <token>

Response 200 OK

{ "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

GET /sensors/sen-0002-mock/read?filter={"limit":3,"order":"datetimeMeasure DESC"}
Authorization: <token>

Response 200 OK

[
  { "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

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):

[
  { "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

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):

[
  { "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)

POST /sensors/read
Authorization: <token>
Content-Type: application/json

{
  "sensorIds": [
    "sen-0002-mock",
    "sen-0001-mock"
  ]
}

Response 200 OK

{
  "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:

{ "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

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

{
  "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

POST /sensors/read
Authorization: <token>
Content-Type: application/json

{ "sensorIds": [] }

Response 422 Unprocessable Entity

{ "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

# 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):

{
  "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 }
  ]
}

Created 2026-07-17 10:57:15 UTC by developer
Updated 2026-09-15 09:08:14 UTC by Daniel | Whysor