API Reference

Naseem EIAQI API Documentation

Send sensor data, retrieve EIAQI scores, and query compliance reports programmatically.

Overview

The Naseem EIAQI API lets Building Management Systems (BMS), IoT sensors, and third-party applications push indoor air quality readings directly, automatically calculate EIAQI scores, and store data in a tamper-proof audit chain that satisfies Dubai Municipality's DM-HSD-GU141-IAQI2 requirements.

Base URLhttps://naseem-eiaqi.com/api/v1
AuthAuthorization: Bearer nsm-...
Rate limit100 requests / minute per API key
TimestampsAll timestamps in ISO 8601 UTC
FormatJSON (Content-Type: application/json)

Available endpoints

POST
/api/v1/readings

Send one or a batch of sensor readings

GET
/api/v1/readings

Query stored sensor readings

POST
/api/v1/upload

Bulk upload CSV or Excel file

GET
/api/v1/export

Full data export as ZIP (org_admin only)

Authentication

Every endpoint uses API key authentication via a Bearer token. Create keys in the dashboard at Settings → API Keys.

PropertyDetail
Header nameAuthorization
Value formatBearer nsm-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Key prefixnsm-
Scopesreadings:write · readings:read
Building scopeOptional - key can be scoped to a single building
ExpiryNever, or after 30 / 90 / 365 days

Security: Keys are shown only once at creation. Store them in environment variables - never commit them to source code.

Request example

HTTP Header
Authorization: Bearer nsm-your-api-key-here

Error responses

StatusReason
401Key missing, invalid, or revoked
403Key exists but lacks required scope

Send Readings

POST/api/v1/readingsRequires: readings:write

Send a single reading or a batch (up to 100) from one or more sensors. EIAQI is calculated immediately for every reading and stored in the database.

Single reading - all parameters

JSON body
{
  "device_id": "SENSOR-001",
  "recorded_at": "2026-06-01T10:00:00Z",
  "readings": {
    "pm25": 12.4,
    "pm10": 28.0,
    "co2": 650,
    "co": 0.5,
    "vocs": 0.18,
    "hcho": 30,
    "no2": 25,
    "o3": 18,
    "temperature": 23.5,
    "humidity": 52
  }
}

Partial reading - not all parameters required

Send only the parameters your sensor measures. EIAQI is calculated from the available parameters.

JSON body (partial)
{
  "device_id": "SENSOR-001",
  "recorded_at": "2026-06-01T10:00:00Z",
  "readings": {
    "pm25": 12.4,
    "co2": 650,
    "temperature": 23.5,
    "humidity": 52
  }
}

Batch of readings

Send a JSON array instead of a single object. Maximum 100 readings per request.

JSON body (batch)
[
  {
    "device_id": "SENSOR-001",
    "recorded_at": "2026-06-01T10:00:00Z",
    "readings": { "pm25": 12.4, "co2": 650, "temperature": 23.5 }
  },
  {
    "device_id": "SENSOR-002",
    "recorded_at": "2026-06-01T10:00:00Z",
    "readings": { "pm25": 18.2, "co2": 720, "temperature": 24.1 }
  }
]

Response - single reading

200 OK
{
  "success": true,
  "eiaqi": {
    "score": 72,
    "category": "good",
    "dominant_pollutant": "pm25"
  }
}

Response - batch

200 OK
{
  "success": true,
  "results": [
    {
      "device_id": "SENSOR-001",
      "success": true,
      "eiaqi": { "score": 72, "category": "good", "dominant_pollutant": "pm25" }
    },
    {
      "device_id": "SENSOR-002",
      "success": true,
      "eiaqi": { "score": 68, "category": "good", "dominant_pollutant": "co2" }
    }
  ]
}

Parameter reference - inside readings{}

ParameterKeyUnitRangeDescription
PM₂.₅pm25μg/m³0-1,000Fine particulate matter (≤ 2.5 μm aerodynamic diameter)
PM₁₀pm10μg/m³0-1,000Coarse particulate matter (≤ 10 μm)
CO₂co2ppm400-5,000Carbon dioxide
COcoppm0-50Carbon monoxide
VOCsvocsmg/m³0-25Total volatile organic compounds (TVOC)
Formaldehydehchoμg/m³0-1,000Formaldehyde (HCHO / CH₂O)
NO₂no2μg/m³0-2,000Nitrogen dioxide
O₃o3μg/m³0-800Ozone
Temperaturetemperature°C−10 to 60Indoor air temperature
Humidityhumidity%0-100Relative humidity

* All readings fields are optional. The object must contain at least one field.

Upload CSV / Excel

POST/api/v1/uploadRequires: readings:write

Upload a CSV, TSV, or Excel file containing historical data or large batches. The system auto-detects column names and converts units.

PropertyDetail
Content-Typemultipart/form-data
Field namefile
Accepted formats.csv · .tsv · .xlsx
Max rows100,000
Max file size50 MB

curl example

bash
curl #86efac">"color:#fbbf24">-X POST https://naseem-eiaqi.com/api/v1/upload \
  #86efac">"color:#fbbf24">-H "Authorization: Bearer nsm-your-api-key" \
  #86efac">"color:#fbbf24">-F "file=@readings.csv"

Response

200 OK
{
  "batch_id": "7f8a91c2-d3b4-4e5f-a6c7-8d9e0f1a2b3c",
  "total_rows": 480,
  "accepted": 478,
  "rejected": 2,
  "rejected_rows": [
    { "row": 47, "reason": "pm25 value 1800 exceeds maximum" },
    { "row": 312, "reason": "recorded_at is in the future" }
  ]
}
Tip: Use POST /api/v1/upload/analyze to preview column mapping before committing the upload.

Query Readings

GET/api/v1/readingsRequires: readings:read

Retrieve stored sensor readings. At least one of building_id, zone_id, or sensor_id must be provided.

ParameterTypeRequiredDescription
building_iduuidcond.Building to query (required unless zone_id or sensor_id supplied)
zone_iduuidoptionalNarrow results to a specific zone
sensor_iduuidoptionalNarrow results to a specific sensor
fromISO 8601optionalStart of time range (inclusive)
toISO 8601optionalEnd of time range (inclusive)
limitintegeroptionalMax results - default 100, max 500

curl example

bash
curl #86efac">"https://naseem-eiaqi.com/api/v1/readings?building_id=BUILD-UUID&from=2026-06-01T00:00:00Z&limit=100" \
  #86efac">"color:#fbbf24">-H "Authorization: Bearer nsm-your-api-key"

Response

200 OK
{
  "success": true,
  "count": 2,
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "sensor_id": "uuid",
      "zone_id": "uuid",
      "building_id": "uuid",
      "recorded_at": "2026-06-01T10:00:00Z",
      "pm25": 12.4,
      "co2": 650,
      "temperature": 23.5,
      "humidity": 52
    }
  ]
}

Query Scores

GET/api/v1/scoresRequires: readings:readcoming soon

Retrieve aggregated EIAQI scores by period. Scores are currently available via the dashboard and export reports.

ParameterTypeRequiredDescription
building_iduuidoptionalFilter by building
zone_iduuidoptionalFilter by zone
period_typeenumoptionalhourly | eight_hour | daily | weekly | monthly
fromISO 8601optionalStart of time range
toISO 8601optionalEnd of time range

Response example

200 OK
{
  "data": [
    {
      "id": "abc12345",
      "building_id": "uuid",
      "zone_id": "uuid",
      "period_type": "hourly",
      "period_start": "2026-06-01T10:00:00Z",
      "period_end": "2026-06-01T11:00:00Z",
      "eiaqi_score": 72,
      "eiaqi_category": "good",
      "dominant_pollutant": "pm25",
      "sub_indices": {
        "pm25": 68,
        "co2": 45,
        "vocs": 30
      }
    }
  ]
}

Supported Field Names

When uploading CSV or Excel files, the system automatically recognises dozens of common column names per parameter. The table below is the complete reference for BMS vendors.

ParameterCanonical keyAccepted column names
PM₂.₅pm25
pm25pm2.5PM2.5pm2_5fine_particlesPM25pm2p5pm25_ugm3PM 2.5
PM₁₀pm10
pm10PM10pm1_0coarse_particlespm10_ugm3
CO₂co2
co2CO2carbon_dioxideco2_ppmco2ppmco2_levelcarbondioxide
COco
coCOcarbon_monoxideco_ppmcoppmco_mgm3
VOCsvocs
vocsvocVOCVOCstvocTVOCtotal_vocvolatile_organictvoc_ppbtvoc_mgm3
Formaldehydehcho
hchoHCHOformaldehydeFormaldehydech2oCH2Ohcho_ugm3
NO₂no2
no2NO2nitrogen_dioxideno2_ugm3no2_ppbnitrogendioxide
O₃o3
o3O3ozoneOzoneo3_ugm3o3_ppb
Temperaturetemperature
temperaturetempTempTEMPtemp_cair_tempindoor_temptemperature_ctemperature_ftemp_ftempf
Humidityhumidity
humidityrhRHrelative_humidityhumhumidity_pctrh_pctrel_humidity
Column names are fully normalised before matching: lowercased, spaces, hyphens, and dots removed. Example: "PM 2.5 (µg/m³)" matches pm25.

Accepted timestamp column names (recorded_at)

recorded_attimestampTimestamptimedatetimeDateTimetsmeasured_atdateDateDate/Timerecording_timemeasurement_time

Units & Conversion

Canonical units follow DM-HSD-GU141-IAQI2. When uploading files, the system detects the unit from the column name and converts automatically.

ParameterKeyCanonical unitAuto-converted from
PM₂.₅pm25μg/m³mg/m³ (×1,000)
PM₁₀pm10μg/m³mg/m³ (×1,000)
CO₂co2ppmppb (÷1,000)
COcoppmmg/m³ (×0.873 at 25 °C, 1 atm)
VOCsvocsmg/m³ppb (toluene MW 92.14), ppm, μg/m³
Formaldehydehchoμg/m³mg/m³ (×1,000), ppb
NO₂no2μg/m³mg/m³ (×1,000), ppb
O₃o3μg/m³mg/m³ (×1,000), ppb
Temperaturetemperature°C°F → (F − 32) × 5/9; auto-detected from column name
Humidityhumidity%0-1 fraction auto-scaled to 0-100

How auto-detection works

  • Column name containing "mg" (without "ug") → treated as mg/m³
  • Column name containing "ppb" → converted to ppm or μg/m³ per parameter
  • Temperature column name containing "fahrenheit", "_f", or "°f" → Fahrenheit→Celsius conversion
  • Humidity value ≤ 1.0 → automatically multiplied by 100

Error Codes

All errors return JSON with an error field and an optional details array for field-level messages.

CodeNameDescription
400Bad RequestValidation failed. Response includes a `details` array with per-field messages.
401UnauthorizedAPI key is missing, invalid, or revoked.
403ForbiddenKey exists but lacks the required scope (e.g. `readings:write`).
404Not FoundThe requested resource does not exist.
422Unprocessable EntitySchema validation passed but business rules failed (e.g. sensor not found, value out of range).
429Too Many RequestsRate limit exceeded (100 req/min per key). Check `Retry-After` header.
500Internal Server ErrorTransient server error. Retry with exponential backoff.

400 - example

400 Bad Request
{
  "success": false,
  "error": "Validation failed",
  "details": [
    {
      "field": "readings.pm25",
      "message": "Value 1500 exceeds maximum 1000 μg/m³"
    }
  ]
}

401 - example

401 Unauthorized
{
  "success": false,
  "error": "Invalid or expired API key"
}

429 - example

429 Too Many Requests
{
  "success": false,
  "error": "Rate limit exceeded: 100 requests per minute"
}

Code Examples

curl

Send a single sensor reading

bash
curl #86efac">"color:#fbbf24">-X POST https://naseem-eiaqi.com/api/v1/readings \
  #86efac">"color:#fbbf24">-H "Authorization: Bearer nsm-your-api-key" \
  #86efac">"color:#fbbf24">-H "Content-Type: application/json" \
  #86efac">"color:#fbbf24">-d @reading.json

Python

Using the requests library - pip install requests

python
import requests

API_KEY = "nsm-your-api-key"
BASE_URL = "https://naseem-eiaqi.com/api/v1"

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

reading = {
    "device_id": "SENSOR-001",
    "recorded_at": "2026-06-01T10:00:00Z",
    "readings": {
        "pm25": 12.4,
        "pm10": 28.0,
        "co2": 650,
        "temperature": 23.5,
        "humidity": 52,
    },
}

response = requests.post(
    f"{BASE_URL}/readings",
    json=reading,
    headers=headers
)
print(response.json())

Node.js

Native fetch in Node.js 18+

javascript
const API_KEY = 'nsm-your-api-key'
const BASE_URL = 'https:"color:#6b7280">//naseem-eiaqi.com/api/v1'

async function sendReading(data) {
  const res = await fetch(BASE_URL + '/readings', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + API_KEY,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(data),
  })
  if (!res.ok) throw new Error('HTTP ' + res.status)
  return res.json()
}

"color:#6b7280">// Send a single reading
const result = await sendReading({
  device_id: 'SENSOR-001',
  recorded_at: new Date().toISOString(),
  readings: { pm25: 12.4, co2: 650, temperature: 23.5 },
})
console.log(result)

n8n

Use the "HTTP Request" node with the following settings. Map data from previous workflow nodes using n8n expressions.

n8n HTTP Request node
"color:#6b7280">// HTTP Request node - configure as follows:

Method:   POST
URL:      https:"color:#6b7280">//naseem-eiaqi.com/api/v1/readings

"color:#6b7280">// Headers tab
Authorization:  Bearer nsm-your-api-key
Content-Type:   application/json

"color:#6b7280">// Body tab - select "JSON (Raw)"
{
  "device_id":   "{{ $json.device_id }}",
  "recorded_at": "{{ $now.toISO() }}",
  "readings": {
    "pm25":        {{ $json.pm25 }},
    "co2":         {{ $json.co2 }},
    "temperature": {{ $json.temperature }},
    "humidity":    {{ $json.humidity }}
  }
}

Need help?

Create an API key in Settings and make your first request in under two minutes. For technical support, reach us at hello@naseem-eiaqi.com.