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.
https://naseem-eiaqi.com/api/v1Authorization: Bearer nsm-...Available endpoints
/api/v1/readingsSend one or a batch of sensor readings
/api/v1/readingsQuery stored sensor readings
/api/v1/uploadBulk upload CSV or Excel file
/api/v1/exportFull 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.
| Property | Detail |
|---|---|
| Header name | Authorization |
| Value format | Bearer nsm-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx |
| Key prefix | nsm- |
| Scopes | readings:write · readings:read |
| Building scope | Optional - key can be scoped to a single building |
| Expiry | Never, 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
Authorization: Bearer nsm-your-api-key-hereError responses
| Status | Reason |
|---|---|
| 401 | Key missing, invalid, or revoked |
| 403 | Key exists but lacks required scope |
Send Readings
/api/v1/readingsRequires: readings:writeSend 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
{
"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.
{
"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.
[
{
"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
{
"success": true,
"eiaqi": {
"score": 72,
"category": "good",
"dominant_pollutant": "pm25"
}
}Response - batch
{
"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{}
| Parameter | Key | Unit | Range | Description |
|---|---|---|---|---|
| PM₂.₅ | pm25 | μg/m³ | 0-1,000 | Fine particulate matter (≤ 2.5 μm aerodynamic diameter) |
| PM₁₀ | pm10 | μg/m³ | 0-1,000 | Coarse particulate matter (≤ 10 μm) |
| CO₂ | co2 | ppm | 400-5,000 | Carbon dioxide |
| CO | co | ppm | 0-50 | Carbon monoxide |
| VOCs | vocs | mg/m³ | 0-25 | Total volatile organic compounds (TVOC) |
| Formaldehyde | hcho | μg/m³ | 0-1,000 | Formaldehyde (HCHO / CH₂O) |
| NO₂ | no2 | μg/m³ | 0-2,000 | Nitrogen dioxide |
| O₃ | o3 | μg/m³ | 0-800 | Ozone |
| Temperature | temperature | °C | −10 to 60 | Indoor air temperature |
| Humidity | humidity | % | 0-100 | Relative humidity |
* All readings fields are optional. The object must contain at least one field.
Upload CSV / Excel
/api/v1/uploadRequires: readings:writeUpload a CSV, TSV, or Excel file containing historical data or large batches. The system auto-detects column names and converts units.
| Property | Detail |
|---|---|
| Content-Type | multipart/form-data |
| Field name | file |
| Accepted formats | .csv · .tsv · .xlsx |
| Max rows | 100,000 |
| Max file size | 50 MB |
curl example
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
{
"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" }
]
}Query Readings
/api/v1/readingsRequires: readings:readRetrieve stored sensor readings. At least one of building_id, zone_id, or sensor_id must be provided.
| Parameter | Type | Required | Description |
|---|---|---|---|
| building_id | uuid | cond. | Building to query (required unless zone_id or sensor_id supplied) |
| zone_id | uuid | optional | Narrow results to a specific zone |
| sensor_id | uuid | optional | Narrow results to a specific sensor |
| from | ISO 8601 | optional | Start of time range (inclusive) |
| to | ISO 8601 | optional | End of time range (inclusive) |
| limit | integer | optional | Max results - default 100, max 500 |
curl example
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
{
"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
/api/v1/scoresRequires: readings:readcoming soonRetrieve aggregated EIAQI scores by period. Scores are currently available via the dashboard and export reports.
| Parameter | Type | Required | Description |
|---|---|---|---|
| building_id | uuid | optional | Filter by building |
| zone_id | uuid | optional | Filter by zone |
| period_type | enum | optional | hourly | eight_hour | daily | weekly | monthly |
| from | ISO 8601 | optional | Start of time range |
| to | ISO 8601 | optional | End of time range |
Response example
{
"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.
| Parameter | Canonical key | Accepted column names |
|---|---|---|
| PM₂.₅ | pm25 | pm25pm2.5PM2.5pm2_5fine_particlesPM25pm2p5pm25_ugm3PM 2.5 |
| PM₁₀ | pm10 | pm10PM10pm1_0coarse_particlespm10_ugm3 |
| CO₂ | co2 | co2CO2carbon_dioxideco2_ppmco2ppmco2_levelcarbondioxide |
| CO | co | coCOcarbon_monoxideco_ppmcoppmco_mgm3 |
| VOCs | vocs | vocsvocVOCVOCstvocTVOCtotal_vocvolatile_organictvoc_ppbtvoc_mgm3 |
| Formaldehyde | hcho | hchoHCHOformaldehydeFormaldehydech2oCH2Ohcho_ugm3 |
| NO₂ | no2 | no2NO2nitrogen_dioxideno2_ugm3no2_ppbnitrogendioxide |
| O₃ | o3 | o3O3ozoneOzoneo3_ugm3o3_ppb |
| Temperature | temperature | temperaturetempTempTEMPtemp_cair_tempindoor_temptemperature_ctemperature_ftemp_ftempf |
| Humidity | humidity | humidityrhRHrelative_humidityhumhumidity_pctrh_pctrel_humidity |
Accepted timestamp column names (recorded_at)
Units & Conversion
Canonical units follow DM-HSD-GU141-IAQI2. When uploading files, the system detects the unit from the column name and converts automatically.
| Parameter | Key | Canonical unit | Auto-converted from |
|---|---|---|---|
| PM₂.₅ | pm25 | μg/m³ | mg/m³ (×1,000) |
| PM₁₀ | pm10 | μg/m³ | mg/m³ (×1,000) |
| CO₂ | co2 | ppm | ppb (÷1,000) |
| CO | co | ppm | mg/m³ (×0.873 at 25 °C, 1 atm) |
| VOCs | vocs | mg/m³ | ppb (toluene MW 92.14), ppm, μg/m³ |
| Formaldehyde | hcho | μ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 |
| Temperature | temperature | °C | °F → (F − 32) × 5/9; auto-detected from column name |
| Humidity | humidity | % | 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.
| Code | Name | Description |
|---|---|---|
| 400 | Bad Request | Validation failed. Response includes a `details` array with per-field messages. |
| 401 | Unauthorized | API key is missing, invalid, or revoked. |
| 403 | Forbidden | Key exists but lacks the required scope (e.g. `readings:write`). |
| 404 | Not Found | The requested resource does not exist. |
| 422 | Unprocessable Entity | Schema validation passed but business rules failed (e.g. sensor not found, value out of range). |
| 429 | Too Many Requests | Rate limit exceeded (100 req/min per key). Check `Retry-After` header. |
| 500 | Internal Server Error | Transient server error. Retry with exponential backoff. |
400 - example
{
"success": false,
"error": "Validation failed",
"details": [
{
"field": "readings.pm25",
"message": "Value 1500 exceeds maximum 1000 μg/m³"
}
]
}401 - example
{
"success": false,
"error": "Invalid or expired API key"
}429 - example
{
"success": false,
"error": "Rate limit exceeded: 100 requests per minute"
}Code Examples
curl
Send a single sensor reading
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.jsonPython
Using the requests library - pip install requests
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+
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.
"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.