Kaiterra API
Version: 2022-05-24
Overview ¶
The Kaiterra API allows your programs to access the air quality data reported by your Sensedge, Sensedge Mini, or other device.
How to get started:
-
Get your API key at app.kaiterra.com. Once you’ve created an account, go to your Account Settings and click API Keys. Finally, click Generate API Key.
-
If you’re a Postman user, you can try out our API using our public Demo workspace. Note that you’ll need to create a Postman environment with a variable called
api-key
(set to your Kaiterra API key) to try the examples in the collection. -
Find the Unique Device IDs (UDIDs) for your Kaiterra devices in the device’s settings (Sensedge) or in the Kaiterra mobile app.
-
Don’t have any Kaiterra devices yet? Feel free to use our test devices from this documentation – the ones with IDs beginning with “00000000-” – to get familiar with the API:
-
00000000-0031-0101-0000-00007e57c0de
is a test Sensedge -
00000000-0001-0101-0000-00007e57c0de
is a test Laser Egg
-
Auth
All requests to the API must be authenticated. Get your API key by signing up at app.kaiterra.com.
We support the following kinds of authentication:
- URL-based key: When issuing an HTTP request, the client includes the key in a URL parameter called
key
(this is how e.g. Google Maps works). All requests must use HTTPS, so the key is safe from eavesdropping. This method is simple, but it’s only suitable for clients that are running on trusted devices, such as a server you control, or a researcher’s workstation. These keys must NOT be embedded directly into web pages, or iOS or Android apps.
If authentication information is required but not present, or if it is not accepted, HTTP 401 is returned (401 is called “Unauthorized”, but it should really be called “Unauthenticated”).
Common Parameters
Unique Device IDs (UDIDs) for Kaiterra devices are 128-bit random UUIDs. They’re case-insensitive and may be submitted with or without intermediate dashes (46048117-86a7-488e-9c58-708f3470ee11
and 4604811786a7488e9c58708f3470ee11
are both OK).
The API enforces access control on device data as follows:
-
For devices managed under an organization, access is restricted to users within that organization, and the API keys they create. For example, consider a device owned by Organization A: if a user who belongs to another organization (or who does not belong to any organization) creates an API key and attempts to access data for that device, the API will return
HTTP 403 Forbidden
. -
Data for devices not added to an organization can be accessed by any API key. While UDIDs consist of 128 random bits and are therefore unguessable, API users should treat them as secrets that should not be published online (such as in a public Git repository).
All dates/times follow RFC3339 (a refinement of ISO8601), which looks like 2016-12-07T05:32:16Z
. Milliseconds are accepted but are ignored (truncated). All times must be in UTC; any that aren’t will be rejected with 400 bad request
.
All sampling intervals, such as for hourly-averaged data, are labelled by the ending time of the sample sample, not the starting time. This is consistent with the way all apps report hourly average data from official sensors, probably because it avoids the problem of users thinking that the data they’re seeing is an hour older than it is. Note that this implies daily data for e.g. the 15th of March is labelled as 2016-03-16T00:00Z
, so clients may want to subtract a day when labelling such data.
There is no hour 24; a bucket extending from 11pm on Dec 31 to midnight is labelled as 01-01 00:00, length 1 hour.
Latitude and longitude are specified in requests as a comma-separated pair, like 39.96,115.98
(latitude, longitude), and in responses as an array of length 2 (again, latitude, then longitude).
The aqi
query string parameter, if present, causes APIs returning pollutant data to precalculate corresponding air quality index values according to the air quality index of the given country or region. Codes are the 2-character codes from ISO3166. Supported values are cn
, in
, and us
.
Request Headers
The following headers are technically optional but are a good idea:
-
Accept-Encoding
:gzip
is supported; clients are encouraged to use it. -
Content-Encoding
: if there is a request body, this header denotes its encoding. Always use UTF-8.
Sensor Reading Data Format
Data on various pollutants or other parameters (like temperature and humidity) are returned first as an array of parameters, with a series of data points under each parameter:
{
"data": [
{
"param": "rpm25c",
"units": "µg/m³",
"source": "km100",
"span": 60,
"points": [
{
"ts": "2020-06-17T03:40:00Z",
"value": 120
}
]
},
{
"param": "rtemp",
"units": "%",
"span": 60,
"points": [
{
"ts": "2020-06-17T03:40:00Z",
"value": 62
}
]
}
]
}
Each series under data
has the following properties:
-
param
: The parameter code. These are listed below. -
units
: The units under which the parameter is expressed. These are listed below. -
source
: (Present for sensor modules) The module that captured the parameter reading. For instance,km102
identifies the KM-102 sensor module. -
span
: The sampling interval, in seconds, over which this measurement was taken. For example, 60 refers to a sample taken over the span of one minute, and 3600 designates hourly averages. -
points
: The actual array of data points for this series. Even if the series has only one data point, this is still an array.
PARAMETER CODES
Code | Meaning | Default Units |
---|---|---|
rco2 |
Carbon dioxide | ppm |
ro3 |
Ozone | ppb |
rpm25c |
PM2.5 | µg/m³ |
rpm10c |
PM10 | µg/m³ |
rhumid |
Relative humidity | % |
rtemp |
Temperature | C |
rtvoc |
Total Volatile Organic Compounds (TVOC) | ppb |
UNIT CODES
Code | Meaning |
---|---|
ppm |
Parts per million (volumetric concentration) |
ppb |
Parts per billion |
µg/m³ |
Micrograms per cubic meter (mass concentration) |
mg/m³ |
Milligrams per cubic meter |
C |
Degrees Celsius |
F |
Degrees Fahrenheit |
x |
Count of something, such as readings in a sampling interval |
% |
Percentage, as with relative humidity |
Multi-Region ¶
The Kaiterra cloud is split into two regions. The exact same Kaiterra API is published in each region, under these domains:
-
api.kaiterra.com (Worldwide, based in AWS eu-central-1)
-
api.kaiterra.cn (Mainland China)
If your code only deals with devices in a single region, just use the domain from that region. Edge cases that arise when dealing with both regions are detailed below.
Edge Cases
Data for a particular device is stored in exactly one region. The region where a device’s data is stored is called the device’s home region.
To query data for a device, you must make the request against the Kaiterra API in the device’s home region. If you make the request against the API in any other region, you will receive an HTTP 301 (Moved Permanently) response, with the response’s Location
header indicating where the query should be retried from now on.
For HTTP verbs other than GET, you’ll receive HTTP 308 (Permanent Redirect) instead. This is because, as RFC 7538 states, “This status code is similar to 301 (Moved Permanently) … except that it does not allow changing the request method from POST to GET.”
For example:
$ curl -v "https://api.kaiterra.cn/v1/devices/00000000-0001-0101-0000-00007e57c0de/top?key=$KAITERRA_APIV1_URL_KEY"
> GET /v1/devices/00000000-0001-0101-0000-00007e57c0de/top?key=0123456789abcdef-YOUR-API-KEY HTTP/2
> Host: api.kaiterra.cn
> user-agent: curl/7.81.0
> accept: */*
>
< HTTP/2 301
< location: https://api.eur.kaiterra.com/v1/devices/00000000-0001-0101-0000-00007e57c0de/top?key=0123456789abcdef-YOUR-API-KEY
< date: Wed, 13 May 2020 07:21:32 GMT
< content-length: 0
< vary: Origin
<
* Connection #0 to host api.kaiterra.cn left intact
Since HTTP 301/308 are part of the HTTP standard, most HTTP libraries will follow the redirects automatically.
Redirect vs. Proxy
Why does the Kaiterra API redirect the client, instead of proxying the call (making the call to the correct region on behalf of the client)? There are at least two reasons for this:
-
In a strict sense, the resource has indeed permanently moved – it is no longer available at the domain under which it was requested. This is precisely what response code 301 is for.
-
Internet connections crossing in and out of Mainland China are not nearly as reliable as connections that do not need to cross this boundary. It’s far better that the client make the call to the device’s home region directly, especially if both the client and device are outside (or inside) Mainland China.
The one exception to this is the /batch
endpoint, which is described below.
Redirects and the /batch Endpoint
The /batch
API endpoint allows the client to gather data from multiple devices in a single HTTP request. This includes the possibility of sub-requests returning HTTP 301 or 308 if the device’s home region is elsewhere. This behavior is problematic for clients not written to deal with it.
As a special case, only for requests for the most recent data for a device (/devices/{id}/top
), the Kaiterra API does proxy those requests to the device’s home region on behalf of the client.
However, there is a nonzero chance that these calls will fail. So, ideally, the client would detect these redirects and itself make the /batch
request directly against the other region.
Redirects and Root Certificates
The SSL certificates for api.kaiterra.com and api.kaiterra.cn use the ISRG Root X1 certificate as their root. While this certificate is already trusted by all modern environments, some older Android devices do not trust it.
The deprecated api.origins-china.cn domain uses an IdentTrust DST root. If your code runs in an environment without the ISRG Root X1 certificate, connects to api.origins-china.cn, and receives a redirect to api.kaiterra.com or .cn, then the SSL handshake will fail. In this case, you will need to either add the ISRG Root X1 certificate to your device’s trusted root certificate store, or supply it as a trusted root certificate at connection time. Most SSL libraries support this.
Devices ¶
Data and metadata for all types of Kaiterra devices are available under the /devices
endpoint.
All Devices ¶
Device metadataGET/devices/{id}
Retrieves metadata, such as name and firmware version, for the given device.
Example URI
- id
string
(required) Example: 00000000-0031-0101-0000-00007e57c0deDevice unique ID
200
Here is some text.
-
name
(string) - Friendly name of the device assigned by the owner in the mobile app or dashboard. -
model
(string) - Model number of the device. -
firmware_version
(string) - The last reported firmware version of the device. -
home_region
(string) - Home region of the current device. -
handshake
(object) - Extended metadata reported by the device in its last hourly handshake. Properties are as follows:-
_device_ts
(string) - The device’s internal clock -
dmac_eth
ordmac_ethernet
(string) - The device’s Ethernet MAC address, if applicable -
dmac_wifi
ordmac
(string) - The device’s Wifi MAC address -
dsn
(string) - The device’s serial number -
sbay102.slifetime
(number) - The fraction of useful sensor module lifetime remaining (left sensor bay) -
sbay102.stype
(string) - The model number of the sensor module installed (left sensor bay) -
sbay103.slifetime
(number) - The fraction of useful sensor module lifetime remaining (right sensor bay) -
sbay103.stype
(string) - The model number of the sensor module installed (right sensor bay) -
ts
(string) - The timestamp on the server when the handshake was received
-
Body
{
"id": "00000000-0031-0101-0000-00007e57c0de",
"name": "Office",
"model": "SE-100",
"firmware_version": "1.15.0.2",
"home_region": "row.europe",
"handshake": {
"_device_ts": "2022-07-25T02:48:00Z",
"dmac_eth": "00:00:00:00:0F:FF",
"dmac_wifi": "54:86:FF:28:04:87",
"dsn": "VC27911111",
"sbay102": {
"slifetime": 0.2996,
"stype": "KM103"
},
"sbay103": {
"slifetime": 0.2994,
"stype": "KM100"
},
"ts": "2022-07-25T02:47:59Z"
}
}
404
Indicates that a device with the given UDID has not yet been registered.
Latest sensor readingGET/devices/{id}/top
Retrieves the last sensor reading uploaded by the device.
Example URI
- id
string
(required) Example: 00000000-0031-0101-0000-00007e57c0deUnique ID of the device whose details are being requested.
200
The device’s most recent sensor reading is returned.
Body
{
"data": [
{
"param": "rco2",
"units": "ppm",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 1673
}
]
},
{
"param": "rhumid",
"source": "km102",
"units": "%",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 55.81
}
]
},
{
"param": "rpm10c",
"source": "km100",
"units": "µg/m³",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 125
}
]
},
{
"param": "rpm25c",
"source": "km100",
"units": "µg/m³",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 169
}
]
},
{
"param": "rtemp",
"source": "km102",
"units": "C",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 26.25
}
]
},
{
"param": "rtvoc",
"source": "km102",
"units": "ppb",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:40:00Z",
"value": 397
}
]
}
]
}
Historical sensor readingsGET/devices/{id}/history{?limit,group_by}
Retrieves sensor air quality historical data using the specified parameters. Returned data points are marked with the end of the sampling interval; for instance, the one hour average from 1pm to 2pm is marked with a timestamp of 14:00, not 13:00. All available intervals that overlap the requested range are returned. If data is unavailable, it is omitted or null
is the value for that data point.
Notes on display of hourly and daily data:
- Time stamps are returned in UTC, so they must be converted back to local time for display. Recall that the timestamp of an interval is the end of that interval, so data for the 10th of January UTC is stamped 2017-01-11T00:00:00Z. You will probably want to subtract a day before displaying the data’s date.
Notes on data availability:
- Hourly and daily averages are computed only after the hour- or day-long average window has already closed. Furthermore, late-arriving data may continue to contribute to the averages even after the window has closed. In particular, Sensedges that lose internet connectivity will buffer data indefinitely, then upload it when connectivity is restored.
Example URI
- id
string
(required) Example: 00000000-0001-0101-0000-00007e57c0deLaser egg unique ID
- begin
string
(optional)Beginning timestamp of the data being requested. Default is 1 week (168 hours) before
end
.- end
string
(optional) Example: 2017-01-17T10:24:12ZEnding timestamp of the data being requested. If unspecified, the current UTC time is used.
- limit
number
(optional) Example: 10Retrieves only the latest N data points. This value has no upper limit; if the number of results is too large to fit into a single response, pagination is used to allow the client to continue the query.
- group_by
string
(optional) Example: 15mPerforms time averaging on the raw data. Valid intervals are:
1m
,5m
,15m
, or any number of minutes that divides evenly into one hour;1h
,2h
, or any whole number of hours that divides evenly into one day; or1d
. Use of larger averaging intervals requires use of thetime_zone
parameter to specify where hour or day boundaries lie.- time_zone
string
(optional) Example: Europe/BrusselsDefines the hourly and daily averaging window boundaries for larger values of
group_by
. Must be an entry from the TZ Database.
200
The the data
array contains available data in the query range, in ascending order by timestamp, subject to the defaults for begin
and end
described above. For queries that return large amounts of data (for example, more than one week of raw data), see the below section on pagination.
Body
{
"data": [
{
"param": "rhumid",
"units": "%",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:25:00Z",
"value": 33
},
{
"ts": "2020-06-17T06:30:00Z",
"value": 81
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 43
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 55
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 44
}
]
},
{
"param": "rpm10c",
"units": "µg/m³",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:25:00Z",
"value": 120
},
{
"ts": "2020-06-17T06:30:00Z",
"value": 120
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 120
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 120
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 120
}
]
},
{
"param": "rpm25c",
"units": "µg/m³",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:25:00Z",
"value": 234
},
{
"ts": "2020-06-17T06:30:00Z",
"value": 135
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 250
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 205
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 192
}
]
},
{
"param": "rtemp",
"units": "C",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:25:00Z",
"value": 12
},
{
"ts": "2020-06-17T06:30:00Z",
"value": -3
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 39
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 3
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 13
}
]
},
{
"param": "rtvoc",
"units": "ppb",
"span": 60,
"points": [
{
"ts": "2020-06-17T06:25:00Z",
"value": 369
},
{
"ts": "2020-06-17T06:30:00Z",
"value": 313
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 303
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 397
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 234
}
]
}
]
}
Hourly Average Data
200
When group_by
is 1h
, hourly data is reported. The example below is for time_zone=Asia/Kathmandu
, which has a UTC offset of +5:45; the hourly divisions happen on the hour in local time, which in UTC time is not 45 but 15 minutes after the hour.
Body
{
"data": [
{
"param": "rpm25c",
"units": "µg/m³",
"span": 3600,
"points": [
{
"ts": "2020-06-17T02:15:00Z",
"value": 195.3
},
{
"ts": "2020-06-17T03:15:00Z",
"value": 192.5
},
{
"ts": "2020-06-17T04:15:00Z",
"value": 198.5
},
{
"ts": "2020-06-17T05:15:00Z",
"value": 208
},
{
"ts": "2020-06-17T06:15:00Z",
"value": 211.1
}
]
},
{
"...": "..."
}
]
}
Daily Average Data
200
Pagination for Large Amounts of Data
200
Since most devices record a sensor reading once per minute, requesting a week or more of data results in very large response bodies – for example, the compressed size of a reponse body for 10,000 items can approach 1 megabyte. So, a single response body is capped around 10,000 items, with items with the latest timestamps being returned first, along with a link to the next “page” of data.
Warning: This item count cap is subject to change without notice. Clients must not hard-code this value.
The link to the next page in the results is returned under _links
, next
. This is an absolute URL; just add your auth credentials and make the call.
Body
{
"_links": {
"next": "https://api.kaiterra.com/v1/devices/00000000-0001-0101-0000-00007e57c0de/history?begin=2020-06-10T06%3A29%3A00Z&end=2020-06-17T06%3A29%3A00Z"
},
"data": [
{
"param": "rpm25c",
"units": "µg/m³",
"span": 60,
"points": [
{
"...": "..."
},
{
"ts": "2020-06-17T06:30:00Z",
"value": 135
},
{
"ts": "2020-06-17T06:35:00Z",
"value": 250
},
{
"ts": "2020-06-17T06:40:00Z",
"value": 205
},
{
"ts": "2020-06-17T06:45:00Z",
"value": 192
},
{
"ts": "2020-06-17T06:50:00Z",
"value": 206
}
]
},
{
"...": "..."
}
]
}
400
The request is improperly formatted; check the response body for additional context. Also, if the entire time range (after defaults for unspecified parameters are applied) lies more than one hour in the future, this error code is returned.
403
404
Other ¶
Batch Requests ¶
Submit a Batch RequestPOST/batch
This endpoint makes it faster to make many API requests against the Kaiterra API.
For example: the API request to get a sensor’s latest reading, GET /devices/{id}/top
,
typically takes less than 1ms of processing time on the server. This means the time for your application to
retrieve the data will be dominated by the round-trip time to Kaiterra’s servers and back. If you need to
get data for many devices, then that round-trip time becomes non-negligible.
The POST /batch
endpoint allows you to make up to 100 API requests at once, with only one round-trip.
For endpoints like GET /devices/{id}/top
, you’ll get the information nearly 100 times faster than if
you had made the requests individually.
The format is nearly identical to Facebook’s batch request API, except that there is no support for dependent requests and other advanced features.
Authentication parameters, such as the key
URL parameter, must be included in URL or headers of the POST batch request. Each “sub-request” inherits the auth context of the POST batch request; any auth parameters on the sub-requests will be ignored. Therefore, it’s possible that some sub-requests will succeed, and others will fail with 403 Forbidden
.
While the JSON objects in the batch response will be in the same order as the objects in the batch request, the relative order in which the server fulfills each request is undefined. For example, in a batch request that contains a PATCH
and a GET
request on the same resource, the result of the GET
request may or may not reflect the changes made by the PATCH
request.
Example URI
- include_headers
boolean
(required) Example: falseDefault: false. Whether to include the response headers for individual responses.
The batch request body is a JSON array of request objects. Each object must have a method
and relative_url
property; other properties are optional.
-
method
(string) - The HTTP method to use. -
relative_url
(string) - The URL to request, relative to the Kaiterra API’s base URL. -
headers
(json, optional) - A JSON array of header description objects, each of which has aname
andvalue
object. -
body
(string, optional) - The request body for this request. JSON objects must be JSON-encoded before being placed in this string.
Headers
Content-Type: application/json
Body
[
{
"method": "GET",
"relative_url": "/devices/00000000-0001-0101-0000-00007e57c0de/top"
},
{
"method": "GET",
"relative_url": "/devices/00000000-0031-0001-0000-00007e57c0de/top"
}
]
200
Returns the result of each of the requested operations, with the body returned as a JSON string. Note that HTTP 200 will be returned as long as the batch request itself did not fail processing, even if none of the sub-requests succeeded.
Body
[
{
"body": "{\"data\":[{\"param\":\"rhumid\",\"units\":\"%\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":54}]},{\"param\":\"rpm10c\",\"units\":\"µg/m³\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":120}]},{\"param\":\"rpm25c\",\"units\":\"µg/m³\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":191}]},{\"param\":\"rtemp\",\"units\":\"C\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":16}]},{\"param\":\"rtvoc\",\"units\":\"ppb\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":342}]}]}",
"code": 200
},
{
"body": "{\"data\":[{\"param\":\"rco2\",\"units\":\"ppm\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":1673}]},{\"param\":\"rhumid\",\"source\":\"km102\",\"units\":\"%\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":54.79}]},{\"param\":\"rpm10c\",\"source\":\"km100\",\"units\":\"µg/m³\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":125}]},{\"param\":\"rpm25c\",\"source\":\"km100\",\"units\":\"µg/m³\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":275}]},{\"param\":\"rtemp\",\"source\":\"km102\",\"units\":\"C\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":20.57}]},{\"param\":\"rtvoc\",\"source\":\"km102\",\"units\":\"ppb\",\"span\":60,\"points\":[{\"ts\":\"2020-06-17T07:05:00Z\",\"value\":435.6}]}]}",
"code": 200
}
]