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.
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.
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
| Topic | Convention |
|---|---|
| Transport | HTTPS only. HTTP is not served. |
| Method | All read endpoints are GET. Parameters are query-string. |
| Versioning | The version is in the path (/v1/…). Breaking changes ship under a new prefix; /v1 stays stable. |
| Coordinates | latitude ∈ [−90, 90], longitude ∈ [−180, 360] (both 0–360 and −180–180 longitudes accepted). Decimal degrees. |
| Time zones | timezone=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. |
| Units | units=metric (default) or units=imperial. Each response echoes a *_units / units block so you never guess. |
| Dates | ISO-8601 YYYY-MM-DD. Timestamps are ISO-8601 with offset. |
| Numbers | Rounded 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.
x-api-key
403Unknown / deactivated key
404No data for location (e.g. no river cell)
429Daily quota exceeded
503Data not yet available
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"
{
"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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −180 … 360 |
forecast_days | int | optional | 7 | 1 … 7 (includes today) |
past_days | int | optional | 0 | 0 … 7 |
daily | csv | optional | all base vars | Comma-separated variable list (see Daily variables). Omit for the full base set. |
timezone | string | optional | auto | auto or an IANA name |
units | string | optional | metric | metric | imperial |
granularity | string | optional | daily | 3h adds a raw 3-hourly samples block opt-in |
Response body
| Field | Type | Description |
|---|---|---|
latitude, longitude | float | Echoed request point (snapped to grid). |
timezone | string | Resolved IANA zone. |
utc_offset_seconds | int | Offset used for local-day bucketing. |
model | string | Source model (gfs). |
run | string | Latest model run id, e.g. 2026-07-20T06Z. |
daily.time | string[] | Local calendar dates, ascending. |
daily.<variable> | number[] | One value per date, aligned to time. |
daily_units | object | Unit label per returned variable. |
samples | object | Only when granularity=3h: raw 3-hourly values. |
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
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
| Name | Type | Default | |
|---|---|---|---|
latitude | float | required | — |
longitude | float | required | — |
timezone | string | optional | auto |
units | string | optional | metric |
{
"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
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
| Name | Type | Notes | |
|---|---|---|---|
latitude | float | required | Must fall inside an archived region. |
longitude | float | required | Outside → 400 listing available regions. |
start_date | date | required | ISO YYYY-MM-DD, inclusive. |
end_date | date | required | Inclusive; ≤ 366 days from start; ≤ archive's latest_complete_date (ERA5-Land lags real time ~5 days). |
daily | csv | optional | History variable list (temps, humidity, precip, soil moisture). |
timezone, units | string | optional | As elsewhere. |
Each day also carries a data_completeness fraction (0–1); fully-missing
days are omitted from the arrays entirely.
ENDPOINTFlood forecast
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | |
longitude | float | required | — | |
days | int | optional | 30 | 1 … 30 lead days |
Response body
| Field | Description |
|---|---|
snapped | The chosen river cell: latitude, longitude, distance_km, upstream_area_km2. |
run_date | GloFAS run date (daily, 00 UTC). |
daily.time | Forecast dates. |
daily.discharge | River discharge, m³/s, per day. |
daily.exceeds | Largest return period met that day: null | 2 | 5 | 10 | 20 | 50 | 100 (years). |
thresholds | The return-period discharge thresholds (rp2…rp100, m³/s) at the snapped cell. |
max_return_period_exceeded | Peak severity across the window; null if none exceeded. |
attribution | "Copernicus Emergency Management Service" — must be surfaced in client apps. |
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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −180 … 360 |
days | int | optional | 5 | 1 … 5 forecast days |
Response body
| Field | Description |
|---|---|
location | Echoed request point: lat, lon. |
model | Source model, "cams-global". |
run | CAMS forecast run id, e.g. "2026-08-04T00". |
units | Unit per species — always "ug/m3". |
guideline | Banding scheme id, "who-2021-multiples" — see Bands. |
voc_proxy | "hcho" — formaldehyde, standing in for a total-VOC read CAMS doesn't forecast. |
attribution | Required 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 / _mean | Per calendar day, aggregated across that day's 3-hourly samples. |
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.
| Species | WHO-2021 reference |
|---|---|
pm2_5 | 15 µg/m³ (annual mean) |
pm10 | 45 µg/m³ (annual mean) |
no2 | 25 µg/m³ (annual mean) |
so2 | 40 µg/m³ (24h mean) |
o3 | 100 µg/m³ (peak-season 8h mean) |
co | 4,000 µg/m³ (24h mean) |
hcho proxy | 2 / 6 / 12 µg/m³ good/fair/poor edges — a heuristic, not a WHO guideline |
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.
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
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.
| Variable | Metric unit | Meaning |
|---|---|---|
temperature_2m_max | °C | Daily maximum 2 m air temperature. |
temperature_2m_min | °C | Daily minimum 2 m air temperature. |
precipitation_sum | mm | Total daily precipitation. |
relative_humidity_2m_mean | % | Mean 2 m relative humidity. |
relative_humidity_2m_max | % | Maximum 2 m relative humidity. |
wind_speed_10m_max | km/h | Maximum 10 m wind speed. |
wind_gusts_10m_max | km/h | Maximum 10 m wind gust. |
wind_direction_10m_dominant | ° | Dominant wind direction. |
cloud_cover_mean | % | Mean cloud cover. |
shortwave_radiation_sum | MJ/m² | Daily shortwave radiation sum. |
weather_code | WMO | WMO weather interpretation code (see below). |
relative_humidity_hours_above_80 opt-in | h | Hours per day with RH ≥ 80% — mould-germination duration signal. |
dew_point_spread_min opt-in | °C | Minimum daily (T − dew point). Small = condensation-prone. |
condensation_risk opt-in | bool | Heuristic 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:
| Code | Meaning | Code | Meaning |
|---|---|---|---|
| 0 | Clear sky | 61 / 63 / 65 | Rain: light / moderate / heavy |
| 1 / 2 / 3 | Mainly clear / partly cloudy / overcast | 71 / 73 / 75 | Snowfall: light / moderate / heavy |
| 45 / 48 | Fog / depositing rime fog | 80 / 81 / 82 | Rain showers: slight / moderate / violent |
| 51 / 53 / 55 | Drizzle: light / moderate / dense | 95 / 96 / 99 | Thunderstorm / 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.
| Endpoint | Cache-Control | Conditional |
|---|---|---|
/v1/now | private, max-age=600 (10 min) | If-None-Match → 304 |
/v1/daily | private, max-age=3600 (1 h) | If-None-Match → 304 |
/v1/history | private, max-age=86400 (24 h) | If-None-Match → 304 |
/v1/flood | private, max-age=3600 (1 h) | If-None-Match → 304 |
/v1/air | private, 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:
| Endpoint | Upstream cadence | Suggested client TTL |
|---|---|---|
/v1/daily | New GFS model run every 6 h | 6 h, aligned to the run cycle; 24 h absolute max — forecasts are superseded 4×/day. |
/v1/now | Continuous | 10 min — match the server header; poll with ETag. |
/v1/flood | One GloFAS run per day | Up to 24 h. |
/v1/air | One CAMS run per day (00Z; 12Z optional) | Up to 24 h; align with the run cycle. |
/v1/history | Immutable once published; archive tail advances daily, ~5–6 days behind real time | A fixed date range can be cached 7–30 days or longer. Negotiate the tail via GET /health → history_latest_complete_date and use that as end_date. |
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.
- In Postman: Import → Link, paste
https://api.molddetect.app/openapi.json, and import. Postman generates a collection from the live schema. - Open the collection's Variables and set
baseUrlto the base URL above. - 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.
| Variable | Type | Value |
|---|---|---|
baseUrl | default | https://api.molddetect.app |
apiKey | secret | wk_live_… (your key) |
Set the key once, collection-wide
On the collection's Authorization tab, choose type API Key, with:
| Field | Value |
|---|---|
| Key | x-api-key |
| Value | {{apiKey}} |
| Add to | Header |
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" } }
]
}
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_unitsfrom the response rather than hard-coding °C/km/h — a client that flipsunits=imperialthen stays correct automatically. - Poll
/v1/nowconditionally. Cache the ETag and sendIf-None-Match; treat304as "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), history0.1°(≈ 10 km), flood0.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();