Partner Integration

Device Data API

Push air-quality readings from your devices to MyAQI over HTTP. We ingest them into our Apache Kafka pipeline and return enriched, hyperlocal data. We can also build a custom adapter to your own format, provided it carries the mandatory fields.

⬇ Download the API spec (Markdown)
We support HTTP GET (URL parameters) for simple or legacy devices — but we prefer HTTPS POST with a JSON body. It is encrypted (TLS), supports batching to save device battery, and keeps data and tokens out of URLs and server logs. New integrations should use HTTPS + POST + JSON where possible.

Authentication

Every request must carry a per-device API token issued by MyAQI during onboarding. Send it as a Bearer token in the Authorization header:

Authorization: Bearer <apitoken>
  • The token is issued per device (or vendor fleet) and maps to the device’s imei.
  • Preferred: token in the Authorization header over HTTPS — never exposed in URLs or logs.
  • Legacy fallback: for devices that cannot set headers, pass ?apitoken=<apitoken> on a GET. Discouraged (tokens in URLs can leak into logs/proxies); HTTPS only.
  • Missing/invalid/revoked token → 401 Unauthorized. Token not matching the payload imei → 403 Forbidden.

Endpoints

MethodURLBodyUse
POST preferredhttps://devices.myaqi.org/readingsJSONNew integrations; single or batch
GET supportedhttp(s)://devices.myaqi.org/readings?…query paramsSimple / legacy devices; one reading

Success: 200 OK / 202 Accepted.  Errors: 401 bad/missing token, 403 token/imei mismatch, 400 malformed or missing mandatory field.

Field reference

FieldTypeUnitReq.Notes
imeistring—YesDevice id (<=30 chars); is a unique id for each device.
latnumberdegreesYesLatitude, e.g. 18.55348.
lonnumberdegreesYesLongitude, e.g. 73.807503.
timestampstringISO 8601YesYYYY-MM-DDThh:mm:ss.sssZ (UTC, ms).
pm1numberµg/m³NoPM1.0.
pm25numberµg/m³NoPM2.5.
pm10numberµg/m³NoPM10.
so2numberµg/m³NoSulphur dioxide.
no2numberµg/m³NoNitrogen dioxide (NOx accepted in lieu).
conumberppmNoCarbon monoxide.
co2numberppmNoCarbon dioxide.
hchonumberµg/m³NoFormaldehyde.
tempnumber°CNoTemperature (Celsius).
humidnumberg/m³NoHumidity.
altnumbermNoAltitude (AGL).
rssi_4gnumberdBmNo4G signal; negative (e.g. -75).
rssi_5gnumberdBmNo5G signal; negative.
batnumber%NoBattery level, 0–100.

Measurement values are JSON numbers (e.g. 33.0, not "33.0"); only imei and timestamp are strings. Optional fields may be omitted (preferred) or sent as null — never as empty strings. New fields may be added over time; unknown fields are ignored.

Preferred — HTTPS POST + JSON recommended

Single reading

curl --location 'https://devices.myaqi.org/readings' \ --header 'Authorization: Bearer <apitoken>' \ --header 'Content-Type: application/json' \ --data '{ "imei": "E868E7C7CA23", "lat": 18.55348, "lon": 73.807503, "timestamp": "2026-05-06T14:01:00.000Z", "pm1": 21.0, "pm25": 33.0, "pm10": 43.0, "temp": 33.9, "humid": 28.1, "rssi_4g": -75, "bat": 85 }'

Batch upload (recommended for low-power devices)

To conserve battery, cache readings and upload several at once (e.g. every 5–15 min). Send identity once and an array of time-stamped readings.

curl --location 'https://devices.myaqi.org/readings' \ --header 'Authorization: Bearer <apitoken>' \ --header 'Content-Type: application/json' \ --data '{ "imei": "E868E7C7CA23", "lat": 18.55348, "lon": 73.807503, "readings": [ { "timestamp": "2026-05-06T14:01:00.000Z", "pm25": 33.0, "pm10": 43.0, "temp": 33.9, "humid": 28.1, "bat": 85 }, { "timestamp": "2026-05-06T14:16:00.000Z", "pm25": 35.0, "pm10": 46.0, "temp": 34.1, "humid": 27.4, "bat": 84 } ] }'

imei required at top level. lat/lon once at top level (stationary device) or per reading. Each readings element needs a timestamp.

Supported — HTTP GET + URL legacy

For simple or legacy devices. Prefer HTTPS; prefer the Authorization header over the apitoken query parameter.

Token in header (preferred, over HTTPS)

curl --location --get 'https://devices.myaqi.org/readings' \ --header 'Authorization: Bearer <apitoken>' \ --data-urlencode 'imei=E868E7C7CA23' \ --data-urlencode 'lat=18.55348' \ --data-urlencode 'lon=73.807503' \ --data-urlencode 'timestamp=2026-05-06T14:01:00.000Z' \ --data-urlencode 'pm25=33.0' \ --data-urlencode 'pm10=43.0' \ --data-urlencode 'temp=33.9' \ --data-urlencode 'humid=28.1' \ --data-urlencode 'rssi_4g=-75' \ --data-urlencode 'bat=85'

Token in URL (discouraged — only if headers are impossible, HTTPS only)

https://devices.myaqi.org/readings?apitoken=<apitoken> &imei=E868E7C7CA23&lat=18.55348&lon=73.807503 &timestamp=2026-05-06T14%3A01%3A00.000Z &pm25=33.0&pm10=43.0&temp=33.9&humid=28.1&rssi_4g=-75&bat=85

Omit optional parameters entirely (don’t send empty so2=). Colons in the timestamp are percent-encoded (%3A) in a raw URL.

Rules & conventions

⬇ Download this spec (Markdown)   Request an API token →

Responses & error handling

POST /readings returns one of these HTTP status codes. Always read the status code and the JSON status field; for a batch, also read the per-item results array.

HTTPstatusMeaning
200successFull success. A single reading was saved, or every reading in a batch was saved.
202partialPartial batch. Some readings were saved and some failed — inspect results to see which.
400errorRequest rejected: missing/invalid fields, bad timestamp, empty readings array, more than 50 readings, non-JSON body, or every reading in a batch failed validation. Nothing is saved.
401errorThe Authorization: Bearer token is missing or not valid.
500errorThe server failed while saving. For a batch, returned only when nothing was saved and the failure was a server error.

Single reading

Success — 200

{ "status": "success", "message": "Telemetry saved", "success": 1, "failed": 0 }

Failure — 400 (a required field is missing)

{ "status": "error", "message": "lat is required" }

Batch — full success 200

Every item carries its index, a per-item status, and the saved record under data (stored field names such as pm25conc). Two readings shown; data abbreviated.

{ "status": "success", "message": "Telemetry saved", "success": 2, "failed": 0, "results": [ { "index": 0, "status": "success", "data": { "id": 101, "timestamp": "2026-09-15T10:00:00.000Z", "imei": "E868E7CFFFFF", "authorized": true, "pm25conc": "33.0", "pm10conc": "43.0", "bat": "85" } }, { "index": 1, "status": "success", "data": { "id": 102, "timestamp": "2026-09-15T10:15:00.000Z", "imei": "E868E7CFFFFF", "authorized": true, "pm25conc": "35.0", "pm10conc": "46.0", "bat": "84" } } ] }

Batch — partial 202

Good readings are saved; each failed reading returns its own message and the original data it was sent, so you can fix and resend only that one. Here reading index 1 was missing its timestamp.

{ "status": "partial", "message": "1 of 2 readings saved", "success": 1, "failed": 1, "results": [ { "index": 0, "status": "success", "data": { "id": 101, "timestamp": "2026-09-15T10:00:00.000Z", "pm25conc": "33.0" } }, { "index": 1, "status": "error", "message": "timestamp is required", "data": { "imei": "E868E7CFFFFF", "pm25": 35.0, "pm10": 46.0, "bat": 84 } } ] }

Batch too large — 400

A batch may contain at most 50 readings. Over the limit the whole request is rejected and nothing is saved.

{ "status": "error", "message": "readings cannot exceed 50" }
Handling tip. On 202 partial, re-send only the items whose per-item status is error (their original payload is echoed back under data). On 400 or 500, nothing was saved — retry the whole request after fixing the issue. On 401, refresh or re-issue the token; do not retry blindly.