Mould Detect · Developer Platform

Mould Detect Data API

The REST API over Mould Detect's environmental data lake: global daily forecasts, current conditions, historical reanalysis, and river-flood severity — for any point on Earth, with Australia as the priority region. Available to integration partners with an API key.

Base URLhttps://api.molddetect.app
AuthAPI key · x-api-key header
FormatJSON · UTF-8
Versionv1 (path-prefixed)

The base URL above is a CloudFront passthrough. The origin Lambda Function URL, https://m6a25u3askxmnbhz4s7dhlrzwe0vnubs.lambda-url.ap-southeast-2.on.aws, remains fully functional and can be called directly if CloudFront is ever bypassed.

▶ See it live — the demo gallery  three working apps on this API: a consumer weather card, a WebGL flood globe, and a mould-risk dashboard.

GETTING STARTEDAuthentication

Every endpoint except GET /health requires an API key, sent in the x-api-key request header. Keys look like wk_live_<32 characters>. There is no OAuth, no bearer token, and no query-string key — only the header.

# every authenticated request carries this header
x-api-key: wk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

API keys are issued directly to integration partners — request one through your Mould Detect contact. Each key carries a daily request quota; exceeding it returns 429 until the UTC day rolls over. Only a SHA-256 hash of your key is ever stored server-side, so keep the plaintext safe — it cannot be recovered, only re-issued.

Keep keys server-side

In a browser or mobile app, proxy requests through your own backend rather than shipping a wk_live_ key in client code. The bundled Skycard demo embeds a deliberately low-quota key for illustration only.

GETTING STARTEDConventions

TopicConvention
TransportHTTPS only. HTTP is not served.
MethodAll read endpoints are GET. Parameters are query-string.
VersioningThe version is in the path (/v1/…). Breaking changes ship under a new prefix; /v1 stays stable.
Coordinateslatitude ∈ [−90, 90], longitude ∈ [−180, 360] (both 0–360 and −180–180 longitudes accepted). Decimal degrees.
Time zonestimezone=auto (default) resolves the local zone from the coordinates; or pass an IANA name (e.g. Australia/Sydney). Daily values are bucketed to local calendar days.
Unitsunits=metric (default) or units=imperial. Each response echoes a *_units / units block so you never guess.
DatesISO-8601 YYYY-MM-DD. Timestamps are ISO-8601 with offset.
NumbersRounded floats; null for a genuinely missing value (never a sentinel like -999).

GETTING STARTEDErrors & status codes

Errors use one consistent envelope on every endpoint — a JSON body with error: true and a human-readable reason:

{ "error": true, "reason": "invalid parameter 'latitude': input should be less than or equal to 90" }

Error reasons are safe to surface in logs — they never contain AWS identifiers, internal ARNs, or account IDs.

200OK 304Not Modified (ETag) 400Bad request / invalid parameter 401Missing x-api-key 403Unknown / deactivated key 404No data for location (e.g. no river cell) 429Daily quota exceeded 503Data not yet available
503 is expected during warm-up

A 503 "…data is not yet available" means the underlying dataset (e.g. the flood archive or history archive) has not been populated yet — it is a clean, temporary state, not a bug. Retry later; live datasets return 200.

GETTING STARTEDQuickstart

A two-day Sydney forecast, temperatures + precipitation + weather code:

curl -s -H "x-api-key: wk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  "https://api.molddetect.app/v1/daily?latitude=-33.8688&longitude=151.2093&forecast_days=2&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weather_code&timezone=auto"
200 · application/json
{
  "latitude": -33.8688, "longitude": 151.209,
  "timezone": "Australia/Sydney", "utc_offset_seconds": 36000,
  "model": "gfs", "run": "2026-07-20T06Z",
  "daily": {
    "time":                ["2026-07-20", "2026-07-21"],
    "temperature_2m_max":  [16.1, 18.2],
    "temperature_2m_min":  [12.3, 9.1],
    "precipitation_sum":   [0.0, 0.0],
    "weather_code":        [0, 3]
  },
  "daily_units": { "temperature_2m_max": "°C", "precipitation_sum": "mm", … }
}

ENDPOINTDaily forecast

GET/v1/dailyAPI key

The core feed: daily aggregates for any point — up to 7 past days, today, and up to 7 forecast days — bucketed to local calendar days. Past days are served from each day's own model run; forecast days (including today) come from the latest run. The top-level run field always reports the latest run id.

Query parameters

NameTypeDefaultNotes
latitudefloatrequired−90 … 90
longitudefloatrequired−180 … 360
forecast_daysintoptional71 … 7 (includes today)
past_daysintoptional00 … 7
dailycsvoptionalall base varsComma-separated variable list (see Daily variables). Omit for the full base set.
timezonestringoptionalautoauto or an IANA name
unitsstringoptionalmetricmetric | imperial
granularitystringoptionaldaily3h adds a raw 3-hourly samples block opt-in

Response body

FieldTypeDescription
latitude, longitudefloatEchoed request point (snapped to grid).
timezonestringResolved IANA zone.
utc_offset_secondsintOffset used for local-day bucketing.
modelstringSource model (gfs).
runstringLatest model run id, e.g. 2026-07-20T06Z.
daily.timestring[]Local calendar dates, ascending.
daily.<variable>number[]One value per date, aligned to time.
daily_unitsobjectUnit label per returned variable.
samplesobjectOnly when granularity=3h: raw 3-hourly values.
Mould-risk derived variables

Naming any of relative_humidity_hours_above_80, dew_point_spread_min, or condensation_risk in daily= adds those computed fields. They're opt-in and never appear in the default response.

ENDPOINTCurrent conditions

GET/v1/nowAPI key

A single lightweight reading for the current moment — the model sample at-or-immediately-before now, plus today's high/low. Built for polling clients like Theme Clock: it is cacheable and supports conditional requests (see Caching).

Query parameters

NameTypeDefault
latitudefloatrequired
longitudefloatrequired
timezonestringoptionalauto
unitsstringoptionalmetric
200 · application/json
{
  "timezone": "Australia/Sydney", "model": "gfs",
  "run_id": "2026-07-20T06Z", "sample_time_utc": "2026-07-20T09:00:00+00:00",
  "temperature_2m": 13.7, "relative_humidity_2m": 74.8,
  "wind_speed_10m": 13.2, "wind_gusts_10m": 25.2, "weather_code": 0,
  "temperature_2m_max": 16.1, "temperature_2m_min": 12.3,
  "units": { "temperature_2m": "°C", "wind_speed_10m": "km/h", … }
}

ENDPOINTHistorical daily

GET/v1/historyAPI key

Historical daily weather from the ERA5-Land reanalysis archive — including soil-moisture layers the forecast feed doesn't carry. Aggregated to local-time days. Australia region (v1)

Query parameters

NameTypeNotes
latitudefloatrequiredMust fall inside an archived region.
longitudefloatrequiredOutside → 400 listing available regions.
start_datedaterequiredISO YYYY-MM-DD, inclusive.
end_datedaterequiredInclusive; ≤ 366 days from start; ≤ archive's latest_complete_date (ERA5-Land lags real time ~5 days).
dailycsvoptionalHistory variable list (temps, humidity, precip, soil moisture).
timezone, unitsstringoptionalAs elsewhere.

Each day also carries a data_completeness fraction (0–1); fully-missing days are omitted from the arrays entirely.

ENDPOINTFlood forecast

GET/v1/floodAPI key

River-discharge forecast and flood severity from the Global Flood Awareness System (GloFAS). The requested point is snapped to the nearest real river cell — the highest-upstream-area cell in a 5×5 (±0.1°) window — because a naïve nearest-cell lookup misses rivers. Australia region (v1)

Query parameters

NameTypeDefaultNotes
latitudefloatrequired
longitudefloatrequired
daysintoptional301 … 30 lead days

Response body

FieldDescription
snappedThe chosen river cell: latitude, longitude, distance_km, upstream_area_km2.
run_dateGloFAS run date (daily, 00 UTC).
daily.timeForecast dates.
daily.dischargeRiver discharge, m³/s, per day.
daily.exceedsLargest return period met that day: null | 2 | 5 | 10 | 20 | 50 | 100 (years).
thresholdsThe return-period discharge thresholds (rp2rp100, m³/s) at the snapped cell.
max_return_period_exceededPeak severity across the window; null if none exceeded.
attribution"Copernicus Emergency Management Service" — must be surfaced in client apps.
No river nearby → 404, not an error

If no cell in the window clears the 10 km² upstream-area floor (e.g. an inland point far from any modelled river), the response is a clean data-coverage 404 "no river cell within 0.1° of this location" — distinguish it from a genuine failure.

ENDPOINTAir quality forecast

GET/v1/airAPI key

CAMS global atmospheric-composition forecast — seven pollutants (PM2.5, PM10, O3, NO2, SO2, CO, and an HCHO VOC proxy), three-hourly, up to 5 days ahead. The requested point is snapped to the nearest grid cell on the dataset's native 0.4° grid — no interpolation, same convention as /v1/flood. Every value is banded against the WHO 2021 Air Quality Guideline (see Bands below). Australia region (v1)

Query parameters

NameTypeDefaultNotes
latitudefloatrequired−90 … 90
longitudefloatrequired−180 … 360
daysintoptional51 … 5 forecast days

Response body

FieldDescription
locationEchoed request point: lat, lon.
modelSource model, "cams-global".
runCAMS forecast run id, e.g. "2026-08-04T00".
unitsUnit per species — always "ug/m3".
guidelineBanding scheme id, "who-2021-multiples" — see Bands.
voc_proxy"hcho" — formaldehyde, standing in for a total-VOC read CAMS doesn't forecast.
attributionRequired Copernicus credit string — surface it in any client UI (see below).
now.<species>{value, band} for the sample at-or-immediately-before request time, for each of the 7 species.
hourly.<species>3-hourly array, unbanded, aligned to hourly.time.
daily.<species>_max / _meanPer calendar day, aggregated across that day's 3-hourly samples.
200 · application/json
curl -s -H "x-api-key: wk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  "https://api.molddetect.app/v1/air?latitude=-33.8688&longitude=151.2093&days=2"
{
  "location": { "lat": -33.8688, "lon": 151.2093 },
  "model": "cams-global", "run": "2026-08-04T00",
  "units": { "pm2_5": "ug/m3",  },
  "guideline": "who-2021-multiples", "voc_proxy": "hcho",
  "attribution":
    "Generated using Copernicus Atmosphere Monitoring Service information 2026",
  "now": {
    "time": "2026-08-04T09:00Z",
    "pm2_5": { "value": 8.4, "band": "good" },
    "pm10": { "value": 14.2, "band": "good" },
    
  },
  "hourly": {
    "time": ["2026-08-04T06:00Z", "2026-08-04T09:00Z", ],
    "pm2_5": [7.9, 8.4, ], … 6 more species
  },
  "daily": {
    "date": ["2026-08-04", "2026-08-05"],
    "pm2_5_max": [9.6, 11.1], "pm2_5_mean": [8.1, 9.4], 
  }
}

Bands — WHO-2021-multiples

Each species is banded against its WHO 2021 Air Quality Guideline reference level: good ≤ 1× reference, fair ≤ 2×, poor ≤ 4×, very_poor above 4×. HCHO has no WHO ambient guideline, so it uses a separate, clearly-labelled heuristic instead of a multiple.

SpeciesWHO-2021 reference
pm2_515 µg/m³ (annual mean)
pm1045 µg/m³ (annual mean)
no225 µg/m³ (annual mean)
so240 µg/m³ (24h mean)
o3100 µg/m³ (peak-season 8h mean)
co4,000 µg/m³ (24h mean)
hcho proxy2 / 6 / 12 µg/m³ good/fair/poor edges — a heuristic, not a WHO guideline
Not yet ingested → 404, not 503

Unlike this platform's other data feeds, which answer 503 ("temporarily unavailable") when their store is empty, /v1/air answers a clean 404 "air quality data is not yet available" instead — for a brand-new feed, "never ingested yet" is the overwhelmingly likely reason, not a transient outage. Check GET /health's nullable air_latest_run field to see whether a run has ever landed.

Attribution is required

CAMS data is published under Copernicus's CC-BY-style "Licence to Use Copernicus Products" — commercial use and redistribution are explicitly permitted, on the condition that the exact credit string in the response's attribution field is surfaced wherever this data (or something derived from it) reaches an end user.

ENDPOINTHealth

GET/healthno auth

Liveness plus data freshness — no API key required. Reports the latest run and its age, so you can monitor whether ingest is current. Also serves GET /openapi.json (the machine-readable OpenAPI schema) for codegen and Postman import.

{ "status": "ok", "model": "gfs", "latest_run": "2026-07-20T06Z", "age_seconds": 10800 }

REFERENCEDaily variables

Pass any subset in daily= (comma-separated). Omitting daily returns the full base set. The three opt-in derived variables are only returned when named explicitly.

VariableMetric unitMeaning
temperature_2m_max°CDaily maximum 2 m air temperature.
temperature_2m_min°CDaily minimum 2 m air temperature.
precipitation_summmTotal daily precipitation.
relative_humidity_2m_mean%Mean 2 m relative humidity.
relative_humidity_2m_max%Maximum 2 m relative humidity.
wind_speed_10m_maxkm/hMaximum 10 m wind speed.
wind_gusts_10m_maxkm/hMaximum 10 m wind gust.
wind_direction_10m_dominant°Dominant wind direction.
cloud_cover_mean%Mean cloud cover.
shortwave_radiation_sumMJ/m²Daily shortwave radiation sum.
weather_codeWMOWMO weather interpretation code (see below).
relative_humidity_hours_above_80 opt-inhHours per day with RH ≥ 80% — mould-germination duration signal.
dew_point_spread_min opt-in°CMinimum daily (T − dew point). Small = condensation-prone.
condensation_risk opt-inboolHeuristic condensation flag (spread ≤ 1.5 °C and RH ≥ 90%).

REFERENCEWeather codes

The weather_code field follows the WMO 4677 convention (the same family Open-Meteo and many clients use). Common values:

CodeMeaningCodeMeaning
0Clear sky61 / 63 / 65Rain: light / moderate / heavy
1 / 2 / 3Mainly clear / partly cloudy / overcast71 / 73 / 75Snowfall: light / moderate / heavy
45 / 48Fog / depositing rime fog80 / 81 / 82Rain showers: slight / moderate / violent
51 / 53 / 55Drizzle: light / moderate / dense95 / 96 / 99Thunderstorm / with slight / heavy hail

REFERENCECaching & conditional requests

All four data endpoints return a Cache-Control header and a strong ETag. To poll cheaply, store the ETag and send it back as If-None-Match: an unchanged resource returns 304 Not Modified with no body and no quota-relevant payload.

EndpointCache-ControlConditional
/v1/nowprivate, max-age=600 (10 min)If-None-Match → 304
/v1/dailyprivate, max-age=3600 (1 h)If-None-Match → 304
/v1/historyprivate, max-age=86400 (24 h)If-None-Match → 304
/v1/floodprivate, max-age=3600 (1 h)If-None-Match → 304
/v1/airprivate, max-age=3600 (1 h)If-None-Match → 304
# 1) first call returns an ETag header:  ETag: "a1b2c3d4e5f6..."
# 2) poll conditionally — 304 if nothing changed:
curl -s -H "x-api-key: wk_live_…" -H 'If-None-Match: "a1b2c3d4e5f6..."' \
  "…/v1/now?latitude=-33.8688&longitude=151.2093" -o /dev/null -w "%{http_code}\n"

How long should a client cache?

The header above is a floor, not a recommendation — a client can hold data longer based on how often the upstream source actually changes:

EndpointUpstream cadenceSuggested client TTL
/v1/dailyNew GFS model run every 6 h6 h, aligned to the run cycle; 24 h absolute max — forecasts are superseded 4×/day.
/v1/nowContinuous10 min — match the server header; poll with ETag.
/v1/floodOne GloFAS run per dayUp to 24 h.
/v1/airOne CAMS run per day (00Z; 12Z optional)Up to 24 h; align with the run cycle.
/v1/historyImmutable once published; archive tail advances daily, ~5–6 days behind real timeA fixed date range can be cached 7–30 days or longer. Negotiate the tail via GET /healthhistory_latest_complete_date and use that as end_date.
A 304 still costs a request

A conditional request that returns 304 still authenticates and still counts toward the key's daily quota — the saving is bandwidth and latency, not request count.

TOOLINGTesting with Postman

Postman is the fastest way to explore the API interactively. There are two ways in.

Option A — import the OpenAPI schema (recommended)

The API self-describes at /openapi.json. Postman turns that into a ready collection with every endpoint and parameter pre-filled.

  1. In Postman: Import → Link, paste https://api.molddetect.app/openapi.json, and import. Postman generates a collection from the live schema.
  2. Open the collection's Variables and set baseUrl to the base URL above.
  3. Add the API key once, at the collection level (next section), so every request inherits it.

Option B — a minimal collection by hand

Prefer to hand-roll it? Create a collection, add these two variables, then add one request per endpoint using {{baseUrl}} in the URL.

VariableTypeValue
baseUrldefaulthttps://api.molddetect.app
apiKeysecretwk_live_… (your key)

Set the key once, collection-wide

On the collection's Authorization tab, choose type API Key, with:

FieldValue
Keyx-api-key
Value{{apiKey}}
Add toHeader

Leave each request's auth as Inherit auth from parent and the header is added automatically. Storing the key as a secret variable keeps it out of exported collection files.

A starter collection (paste into a new .json, then Import → File)

{
  "info": { "name": "Weather-API v1",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" },
  "auth": { "type": "apikey", "apikey": [
    { "key": "key",   "value": "x-api-key" },
    { "key": "value", "value": "{{apiKey}}" },
    { "key": "in",    "value": "header" } ] },
  "variable": [
    { "key": "baseUrl", "value": "https://api.molddetect.app" } ],
  "item": [
    { "name": "Daily forecast", "request": { "method": "GET", "url":
      "{{baseUrl}}/v1/daily?latitude=-33.8688&longitude=151.2093&forecast_days=3" } },
    { "name": "Current conditions", "request": { "method": "GET", "url":
      "{{baseUrl}}/v1/now?latitude=-33.8688&longitude=151.2093" } },
    { "name": "Flood forecast", "request": { "method": "GET", "url":
      "{{baseUrl}}/v1/flood?latitude=-27.47&longitude=153.03" } },
    { "name": "Health", "request": { "method": "GET", "url": "{{baseUrl}}/health" } }
  ]
}
Environments, not hard-coded keys

Use a Postman Environment per stage (e.g. one holding your apiKey). Never commit an exported collection containing a plaintext key — secret variables and environments keep the key out of the file.

TOOLINGClient patterns

There is no SDK to install — it's plain HTTPS/JSON, so any HTTP client works. Three idioms worth adopting:

  • Respect the units block. Read units / daily_units from the response rather than hard-coding °C/km/h — a client that flips units=imperial then stays correct automatically.
  • Poll /v1/now conditionally. Cache the ETag and send If-None-Match; treat 304 as "no change", which keeps a clock or widget cheap and within quota.
  • Cache per grid cell, not per place-name. Every request snaps to a grid cell — forecast 0.25° (≈ 25 km), history 0.1° (≈ 10 km), flood 0.05° (≈ 5 km, snapped to the river network). Round the request coordinates to the endpoint's grid resolution and use that, plus the query params that affect the payload (units, timezone, granularity, variable list), as the cache key. Nearby suburbs and localities then collapse into a single upstream request per TTL window instead of one each.
// JavaScript (fetch) — conditional /v1/now poll
const r = await fetch(`${base}/v1/now?latitude=-33.87&longitude=151.21`, {
  headers: { 'x-api-key': key, ...(etag && { 'If-None-Match': etag }) }
});
if (r.status === 304) return cached;        // unchanged
etag = r.headers.get('ETag');
const data = await r.json();