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://data.moulddetect.ai
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 STARTEDAI agents & MCP

Building with an AI assistant or your own agents? Mycelium Network puts this API behind a remote MCP server at https://mcp.moulddetect.ai/mcp, so Claude, ChatGPT, Cursor, Codex and any MCP client can call the mould growth index, forecasts, weather history, river flood and air-quality endpoints as tools, plus a mould check for photos. Keys are self-service and free during the beta, and every result carries the Copernicus credits this reference describes.

# Claude Code: one command, then ask in plain language
claude mcp add --scope user --transport http mould-detect https://mcp.moulddetect.ai/mcp \
  --header "Authorization: Bearer $MYCELIUM_KEY"
Which should I use?

Call this HTTP API directly from your own backend when you want raw JSON on a schedule or in a pipeline. Use Mycelium when an AI model should decide which data to fetch, or when you want a terminal agent (Mycelium CLI). Docs, keys and the tools reference: mycelium.moulddetect.ai · llms.txt.

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://data.moulddetect.ai/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
modelstringoptionalgfsgfs | ecmwf | best — ADR-0004 item 4, see below

Response body

FieldTypeDescription
latitude, longitudefloatEchoed request point (snapped to grid).
timezonestringResolved IANA zone.
utc_offset_secondsintOffset used for local-day bucketing.
modelstringThe requested model — gfs (default), ecmwf, or best.
runstringLatest model run id, e.g. 2026-07-20T06Z. For model=best this is the GFS baseline run only — see daily.model_used for which run actually produced each day.
daily.timestring[]Local calendar dates, ascending.
daily.<variable>number[]One value per date, aligned to time. null on a day/model where the underlying variable is unavailable (see ECMWF below) rather than an error.
daily_unitsobjectUnit label per returned variable.
samplesobjectOnly when granularity=3h: raw 3-hourly values. For model=best, always sourced from GFS regardless of that day's model_used (documented limitation).
attributionstringPresent only when the response includes ECMWF data (model=ecmwf, or model=best on any day using ECMWF) — the required ECMWF credit string, in the form ECMWF's Terms of Use ask for (copyright line with the year, source, CC BY 4.0 statement, liability disclaimer and a note that the data was modified). Omitted (not null) for GFS-only responses.
daily.model_usedstring[]model=best only — "gfs" or "ecmwf" per day, whichever's data actually populated that day's values.
daily.model_agreementobject[]|nullmodel=best only — see Model agreement below; null for a day where only one model has data.
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.

Second model: ECMWF, and model=best (ADR-0004 item 4)

ECMWF IFS 0.25° is a second forecast model, ingested from ECMWF Open Data (00Z/12Z runs, 3-hourly steps). Its stored horizon is 6 days (144h) — shorter than GFS's 7 — because the open-data feed only publishes 3-hourly steps out to 144h; model=ecmwf with forecast_days beyond 6 returns a clean 400 naming the horizon. A handful of ECMWF variables (gust, downward shortwave radiation, precipitation type) are not always present in the open-data feed; when one is missing for a run it is recorded in that run's manifest as missing_variables and the corresponding daily.<variable> values are null, not an error.

model=best picks, per local day, whichever model's run is fresher and covers that day — ECMWF is preferred when both cover the day and its run is not more than 6h older than GFS's; any day beyond ECMWF's 6-day horizon always falls back to GFS. Each day also gets a model_agreement confidence signal:

FieldDescription
rh_mean_spreadAbsolute difference (percentage points) between the two models' relative_humidity_2m_mean for the day.
temp_max_spreadAbsolute difference (°C, converted back from °F first if units=imperial) between the two models' temperature_2m_max.
condensation_risk_agreeWhether both models' condensation_risk boolean agree.
levelhigh (spread ≤ 5% RH and ≤ 1.5°C), low (spread ≥ 15% RH or ≥ 4°C), else medium.
ECMWF is off by default

ADR-0004 item 4 ships dark: with FEATURE_ECMWF=off (the default), model=ecmwf and model=best both return 400 {"error":true,"reason":"feature_disabled"} — every other model= value and the default (gfs) response are completely unaffected. Once enabled, if no ECMWF run has been ingested yet, model=best degrades gracefully to GFS-only (model_used: "gfs", model_agreement: null for every day) rather than erroring.

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.

Attribution

FieldTypeNotes
attributionstringAlways present. The Copernicus notice for adapted data, e.g. "Contains modified Copernicus Climate Change Service information 2026 (ERA5-Land, CC BY 4.0, doi:10.24381/cds.e2161bac)"; the year is that of the latest day returned. ERA5-Land is licensed CC BY 4.0, so surface this string wherever the values (or anything derived from them) reach an end user. Added 2026-11-02; existing fields are unchanged.

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 (rp2…rp100, m³/s) at the snapped cell.
max_return_period_exceededPeak severity across the window; null if none exceeded.
attributionThe CEMS-FLOODS notice for adapted data, "Contains modified Copernicus Emergency Management Service information <year> (GloFAS, CEMS-FLOODS datasets licence)", where the year is that of the forecast run. 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.

ENDPOINTSite layers & dampness prior

GET/v1/siteAPI key

Static site layers — elevation, slope, aspect, topographic wetness index (TWI), soil texture, land cover, and distance to coast/water — from a one-off build onto a 0.002° (~200 m) master grid (ADR-0004 item 3, "Site Passport"), plus a documented site-dampness-prior heuristic gauge. Australia region (v1)

Query parameters

NameTypeDefaultNotes
latitudefloatrequired—−90 … 90
longitudefloatrequired—−180 … 360

No days/units/timezone parameter — a static-layer lookup is a single point-in-time read (nearest master-grid cell, no interpolation), not a time series.

Response body

FieldDescription
grid_cellThe master-grid cell actually read: lat, lon, spacing_deg (0.002).
layers.elevation_mCopernicus DEM GLO-90, metres.
layers.slope_deg, aspect_degTerrain derivatives (Horn's method) computed at master-grid resolution. aspect_deg is -1 on perfectly flat cells.
layers.aspect_compass8-point compass (N/NE/E/SE/S/SW/W/NW) for aspect_deg; null when aspect_deg is null or -1 (flat).
layers.twiTopographic wetness index (D8 flow accumulation) — higher means water tends to collect there.
layers.soil.clay_pct, sand_pct, silt_pctISRIC SoilGrids 0-5 cm mean, percent.
layers.soil.texture_classUSDA soil-texture-triangle class (e.g. "clay_loam") from the three percentages above.
layers.land_cover.tree_cover_pct, built_pct, water_pctESA WorldCover 2021 class fractions (classes 10/50/80) aggregated onto the master grid.
layers.dist_coast_km, dist_water_kmDistance to the nearest coastline / any water body, from the region's own water_pct layer.
site_dampness_prior{score: 0-10, band: "low"|"moderate"|"elevated"|"high", reasons: [...]} — see the callout below. A prior, not a prediction.
attributionArray of three required credit strings (Copernicus DEM, SoilGrids, ESA WorldCover) — surface all three in client UI.
layers_versionThe static build's version string (also on GET /health as site_layers_version).
Site dampness prior — the formula

score = 10 × Σ(weight × component), six components each clipped to [0, 1] and each monotone in its own direction, weights summing to 1.0 so the score is bounded to [0, 10] by construction: TWI (0.30, wetter → higher), slope (0.15, flatter → higher — poor drainage), aspect (0.15, shaded aspect for the hemisphere → higher — south-facing in the southern hemisphere, north-facing in the northern), soil clay% (0.15, higher clay → higher — retains moisture), tree cover% (0.15, higher → higher — shade + reduced airflow), and distance to coast/water (0.10, closer → higher, tapering to zero by 5 km). Bands: <3.0 low, <5.5 moderate, <7.5 elevated, else high. reasons lists the plain-language drivers whose component exceeded 0.6. Full formula: api/site_layers.py's compute_site_dampness_prior() docstring.

Errors

Outside every archived region → 400 in the same "regions-listed" style /v1/flood uses. Static store not yet built (or FEATURE_SITE=off) → 404 {"error":true,"reason":"site_layers_unavailable"} (store missing) or 404 {"error":true,"reason":"feature_disabled"} (flag off).

Attribution — required, and one restriction

Emit verbatim: "Copernicus DEM © DLR e.V. 2010-2014 and © Airbus Defence and Space GmbH 2014-2018, provided under COPERNICUS by the European Union and ESA; all rights reserved", "SoilGrids © ISRIC — World Soil Information, CC BY 4.0", and "© ESA WorldCover project 2021 / Contains modified Copernicus Sentinel data (2021) processed by ESA WorldCover consortium, CC BY 4.0". ESA WorldCover imagery/derivatives must not be used in advertising or promotional material without ESA's approval.

Mould Atlas groups: water, coast, built (ADR-0009)

When FEATURE_ATLAS_PLACE is on and the Mould Atlas place store has been built, the body also carries three more groups from a second static store on the same 0.002° grid, plus atlas_layers_version (also on GET /health as atlas_place_version). Otherwise these keys are absent (not null) and the body and ETag are exactly the ones above. Within a group, a key is null when the value is unknown for the cell.

FieldDescription
water.gsw_occurrence_pctJRC Global Surface Water 1984–2024 occurrence, 0–100 %, mean over the cell.
water.gsw_seasonality_monthsMonths per year with water, mean over the cell's water pixels; 0 = no water seen. A half-permanent, half-seasonal cell reads about 9.
water.dist_seasonal_water_kmDistance to the nearest cell of seasonal water (occurrence above 1 %, seasonality under 12 months), km.
water.dist_lake_kmDistance to the nearest HydroLAKES lake polygon, km.
coast.sea_inundation_band_mLowest sea-level rise of 0.5, 1, 2 or 5 m that floods the cell and is connected to the sea; 0 = at or below sea level; null when not flooded at +5 m or unknown. Indicative only (surface DEM with metres of vertical error; the 0.5 m and 1 m bands especially). Not a flood forecast.
coast.sea_inundation_status"at_or_below_sea_level" | "flooded_at_rise" | "above_5m" | null (no elevation).
coast.nearshore_depth_mGEBCO_2026 depth, metres below sea level, about 2 km off the nearest coast; null inland or when the GEBCO layer was not built.
built.ghsl_built_pctGHS-BUILT-S 2020 built-up share of the cell, 0–100 %.
built.ghsl_pop_per_km2GHS-POP 2020 people per km².
built.ghsl_settlement_epochEarliest GHSL epoch (1975…2020, 5-year steps) at which the cell was at least 2 % built; 1975 = by 1975 or earlier; null = not built up by 2020.
built.buildings.count, mean_footprint_m2, mean_height_mMicrosoft footprints whose centroid is in the cell: count, mean footprint (m²), mean height (m; null if no footprint in the cell carries a height; Australian coverage is partial). Only aggregates are served, never geometry.
attributionGains the atlas groups' credit strings (below), appended to the three above.
Attribution — Atlas groups, emit verbatim

"Source: EC JRC/Google. Pekel, J.-F., Cottam, A., Gorelick, N., Belward, A.S. (2016) High-resolution mapping of global surface water and its long-term changes. Nature 540, 418-422. Global Surface Water Explorer, Joint Research Centre, European Commission"; "HydroLAKES v1.0 (CC BY 4.0): Messager, M.L., Lehner, B., Grill, G., Nedeva, I., Schmitt, O. (2016) Estimating the volume and age of water stored in global lakes using a geo-statistical approach. Nature Communications 7:13603"; "GEBCO Bathymetric Compilation Group 2026 (2026). The GEBCO_2026 Grid - a continuous terrain model for oceans and land at 15 arc-second intervals. NERC EDS British Oceanographic Data Centre NOC. doi:10.5285/4f68d5c7-45eb-f999-e063-7086abc036fa. Not for navigation; no endorsement by GEBCO, the IHO or the IOC is implied"; "GHSL Data Package 2023, European Commission, Joint Research Centre (JRC), CC BY 4.0 (GHS-BUILT-S R2023A: Pesaresi & Politis 2023, doi:10.2905/9F06F36F-4B11-47EC-ABB0-4F8B7B1D72EA; GHS-POP R2023A: Schiavina, Freire & MacManus 2023, doi:10.2905/2FF68A52-5B5B-4A22-8F40-C41DA8332CFE)"; "Building footprints: Microsoft Global ML Building Footprints (https://github.com/microsoft/GlobalMLBuildingFootprints), CDLA-Permissive-2.0; only per-cell aggregates are published, no geometry". The exact strings are returned in attribution; surface what the response gives you. The coast credit must keep its not-for-navigation wording.

ENDPOINTHazard layers: fire and cyclone

GET/v1/hazardAPI key

Fire and tropical-cyclone hazard history near a point, read from precomputed rasters (Mould Atlas, ADR-0009). fire: satellite fire detections within 5 km, last burned date, optional smoke load, and the rain that has fallen since the last fire. cyclone: tropical cyclones within 100 km over 10 and 30 years. Each block carries its own as_of and attribution. fire: Australia region (v1) cyclone: global

Query parameters

NameTypeDefaultNotes
latitudefloatrequired—−90 … 90
longitudefloatrequired—−180 … 360

No days/units/timezone parameter. The value is for the nearest grid-cell centre (no interpolation); every date is an ISO UTC date and the one rainfall figure is millimetres.

Response body

Top level: latitude, longitude, and fire and/or cyclone. A block is omitted when its feature flag is off. A block that cannot be produced is {"available": false, "reason": ...}: "outside_region" (with regions, the regions the store covers, for example a point outside Australia on fire) or "hazard_layers_unavailable".

FieldDescription
fire.as_ofData date (ISO, UTC) of the newest satellite fire pass included. The windows below end on this date, not today.
fire.grid_celllat, lon, spacing_deg (0.01) of the cell read. Same shape on cyclone (0.1).
fire.radius_kmSearch radius for the fire fields: 5.
fire.last_fire_date, days_since_last_fireDate of the most recent satellite fire detection within 5 km (any satellite), and whole days from it to as_of. null if none since 2000.
fire.fires_5km_30d, fires_5km_90d, fires_5km_365dCount of VIIRS S-NPP fire detections within 5 km in the 30/90/365 days to as_of. A detection is one satellite pixel at one pass, not one fire.
fire.last_burned_date, days_since_last_burnedLatest burn mapped by MODIS MCD64A1 (500 m) in this 0.01° cell, and days from it to as_of. Lags 1–2 months. null if none mapped since late 2000 or the layer is not built.
fire.smoke_pm25_30d_g_m2Wildfire PM2.5 emitted over the 30 days to as_of, grams per square metre (CAMS GFAS, 0.1°). null when the smoke layer is not enabled.
fire.rain_since_last_fire_mmERA5-Land rainfall, mm, summed over the UTC days after last_fire_date up to the rainfall history's latest complete date (about a week behind). null when that span is not fully inside the archive.
fire.rain_since_last_fire_reasonPlain-language reason when rain_since_last_fire_mm is null (no fire on record, fire newer than the history, older than the archive, longer than 366 days, location not covered, history unavailable or incomplete).
cyclone.as_ofBuild date (ISO, UTC) the windows end on.
cyclone.radius_kmDistance from the cell centre within which a track counts: 100.
cyclone.tracks_100km_10y, tracks_100km_30yNumber of tropical cyclones whose track came within 100 km, storms of the last 10 / 30 years. Only points at tropical-storm strength (34 kt or more) count.
cyclone.max_wind_kt_30yHighest best-track wind, knots, at any track point within 100 km, storms of the last 30 years. Mostly 1-minute sustained (agencies differ in averaging period). null if none.
cyclone.last_cyclone_date, days_since_last_cycloneMost recent tropical-storm-strength track point within 100 km since 1980, and whole days from it to as_of. null if none.
attribution, licences (per block)Credit strings that must accompany the block's values, and the source licence or terms of use.
Flags and errors

Two flags, both default off in code: FEATURE_HAZARD_FIRE and FEATURE_HAZARD_CYCLONE. Both off → 404 {"error":true,"reason":"feature_disabled"} (checked before parameter validation). One off → that block is simply absent. Every enabled store missing → 404 {"error":true,"reason":"hazard_layers_unavailable"}. Invalid parameters → 400. Cache-Control: private, max-age=3600 and a deterministic ETag (a new daily or weekly build changes it); send If-None-Match to get a 304. In production since 2 Nov 2026, fire is on and refreshes daily at 04:00 UTC. The cyclone block stays off in production until NOAA NCEI confirms commercial reuse in writing. Freshness is also on GET /health: hazard_fire_as_of, hazard_cyclone_as_of.

Attribution — emit what the block returns

Fire: "Fire detections: NASA FIRMS / LANCE (VIIRS S-NPP, NOAA-20, NOAA-21, MODIS), NASA open data.", "Burned area: MODIS MCD64A1 v6.1, NASA LP DAAC (Giglio et al.), NASA open data." and, when smoke is enabled, "Smoke: Contains modified Copernicus Atmosphere Monitoring Service information 2026 (CAMS GFAS)." Cyclone: "Cyclone tracks: NOAA NCEI IBTrACS v04r01 (Knapp, K.R., M.C. Kruk, D.H. Levinson, H.J. Diamond, C.J. Neumann, 2010: The International Best Track Archive for Climate Stewardship (IBTrACS), Bull. Amer. Meteor. Soc., 91, 363-376), compiled from the agencies' best tracks (JTWC, NHC/CPHC, BoM, RSMC La Reunion, RSMC Nadi, JMA and others)." These strings are written by the build job into the data store, so the response is the source of truth. Detection counts are not unique fires, and near-real-time detections can include static heat sources until standard-processing data replaces them.

ENDPOINTMould Growth Index

GET/v1/mould-indexAPI key

A daily 0–6 mould growth index, computed from 3-hourly temperature and relative humidity: up to 30 days of ERA5-Land history joined to up to 7 days of GFS forecast, for a chosen material sensitivity class. This is the published VTT mould growth model (Hukka & Viitanen 1999; improved/parameterized by Ojanen et al. 2010) — the model used in building-physics practice and WUFI/Delphin tooling, transparent and material-aware enough for hygienists to cite and labs to compare against a microscope read. Australia region (v1, ERA5-Land coverage)

What this is — and is not

Driven by outdoor conditions: it describes exterior surfaces and unconditioned spaces (roof voids, subfloors, garages, sheds, unheated rooms), and is a proxy for poorly-ventilated rooms. It is not a reading from inside a wall and does not replace a moisture meter.

Query parameters

NameTypeDefaultNotes
latitudefloatrequired—−90 … 90
longitudefloatrequired—−180 … 360
material_classstringoptionalsensitiveOne of very_sensitive, sensitive, medium_resistant, resistant — see the scale table below.
history_daysintoptional307 … 30 days of ERA5-Land history, ending at the archive's latest_complete_date.
forecast_daysintoptional71 … 7 days of GFS forecast.

No timezone/units parameter: timezone is always resolved automatically (the same lookup /v1/astro uses), and the index itself has no metric/imperial variant.

Response body

FieldTypeDescription
modelstringAlways "vtt-improved-2010".
scaleobject[]The plain-language 0–6 ladder — see below.
series[].datestringLocal calendar date, YYYY-MM-DD — the joined history+forecast window, contiguous with no duplicate dates.
series[].indexfloatThe mould index (0–6, 1dp) at the end of that local day.
series[].labelstringPlain-language band for that day's index — see the scale table.
series[].sourcestring"history" or "forecast" — which archive that day's samples came from.
series[].rh_mean, temp_meanfloatThat day's mean relative humidity (%) / temperature (°C).
series[].rh_critfloatThe critical humidity (%) at that day's mean temperature, for the requested material_class.
series[].growthbooltrue if any 3-hourly step that day had a positive growth rate.
summary.index_today, index_day7floatThe continuous run's index today / 7 days ahead.
summary.peak_index_30dfloatHighest index reached in the last 30 days (history + today).
summary.days_to_visibleint | nullDays from today until the first forecast day reaching index 3 ("Visible spots"); null if none of the forecast window does.
summary.growth_days_30dintDays in the last 30 (history + today) with a positive growth rate.
summary.trendstringOne of rising, steady, falling — the index's direction over the last 7 days.
summary.clean_surface_index_day7float | nullA second, independent run's day-7 index, started at M=0 on the first forecast step ("if this surface were clean today") — contrast with index_day7, which continues from the history-derived index.
history_latest_complete_datestring | nullThe ERA5-Land archive's latest complete date this response drew on; null when history_available is false.
forecast_runstringThe GFS run id this response drew on, e.g. "2026-09-04T00Z".
attributionstring[]["Contains modified Copernicus Climate Change Service information <year> (ERA5-Land, CC BY 4.0, doi:10.24381/cds.e2161bac)", "Contains modified NOAA/NCEP Global Forecast System (GFS) data; not endorsed by NOAA"], or just the NOAA entry when history_available is false.
caveatstringThe "what this is — and is not" text above, verbatim, for surfacing in client UI.
history_availableboolfalse when the point falls outside every archived ERA5-Land region — see below.

Plain-language scale

IndexLabel
0Dry, no growth
1Starting (invisible)
2Growing (invisible)
3Visible spots
4Spreading
5Widespread
6Severe

How this is worked out — the model

Time step 3 h. Growth requires temperature 0–50 °C and relative humidity at or above a critical threshold RHcrit(T) — a cubic in temperature down to 80% (very sensitive/sensitive materials) or 85% (medium resistant/resistant) at 20 °C and above. Above that threshold, the index grows at a material- and history-dependent rate that slows as it approaches a material- and humidity-dependent ceiling (Mmax). Below the threshold (or outside 0–50 °C), it declines on a fixed 6 h / 24 h schedule, scaled by a material "stickiness" factor (Cmat). Every constant is from Ojanen et al. 2010's parameterization; see api/mould_index.py's module docstring for the exact constants table and both references. The index is clamped to [0, 6] throughout.

Material classExample materialsRHminCmat
very_sensitiveUntreated timber, paper-faced plasterboard, cardboard80%1.0
sensitivePlaned timber, paper, some insulation80%0.5
medium_resistantConcrete, cement render, mineral wool85%0.25
resistantGlass, metal, treated timber, tiles85%0.1
Outside ERA5-Land coverage → forecast-only, not a failure

A point outside every archived ERA5-Land region degrades cleanly to a forecast-only response: history_available: false, history_latest_complete_date: null, every series[].source is "forecast", and the continuous run starts at M=0 on the first forecast step (indistinguishable from clean_surface_index_day7's own run in that case). This never produces an error — only a 503 from the underlying store being genuinely unreadable does.

Feature flag

When the stack is deployed with FEATURE_MOULD_INDEX=off, this endpoint returns 404 {"error":true,"reason":"feature_disabled"}, and the mould_index daily= variable below is rejected as unknown on /v1/daily.

Also on /v1/daily

daily=mould_index adds a forecast-only run (M=0 at the first forecast step, fixed at sensitive) aligned to /v1/daily's own date range — see Daily variables. Any date that opt-in run doesn't cover (e.g. a past_days day) is null.

References

Hukka, A. & Viitanen, H. (1999). "A mathematical model of mould growth on wooden material." Wood Science and Technology 33, 475–485. — Ojanen, T., Viitanen, H., Peuhkuri, R., Lähdesmäki, K., Vinha, J. & Salminen, K. (2010). "Mold growth modeling of building structures using sensitivity classes of materials." Buildings XI, ASHRAE.

ENDPOINTAstronomy

GET/v1/astroAPI key

Sun and moon astronomy for any point, computed locally with Astral — no ingest, no ephemeris file (ADR-0004 item 1). Sunrise, sunset, civil dawn/dusk, solar noon, daylight hours, moon phase/illumination/rise/set, for up to 31 local calendar days starting today — enough to cover a whole lunar cycle (the mean synodic month is 29.53 days) in one request.

Query parameters

NameTypeDefaultNotes
latitudefloatrequired—−90 … 90
longitudefloatrequired—−180 … 360
daysintoptional71 … 31, starting today (local). 31 covers a full lunar cycle.

Timezone is always resolved automatically (the same tzfpy lookup /v1/daily's timezone=auto uses) — there is no timezone or units parameter; none of the response fields have a metric/imperial variant.

Response body

FieldTypeDescription
timezonestringResolved IANA zone.
days[].datestringLocal calendar date, YYYY-MM-DD.
days[].sunrise, sunsetstring | nullISO-8601 local time. null on a day the sun never rises/sets (polar).
days[].civil_dawn, civil_duskstring | nullISO-8601 local time, 6° below the horizon.
days[].solar_noonstringISO-8601 local time — always present, even on a polar day/night.
days[].daylight_hoursfloatHours between sunrise and sunset; 0.0 (polar night) or 24.0 (polar day) when there is no sunrise/sunset that day.
days[].moon_phasefloat0.0–1.0 fraction of the synodic cycle, 0 = new moon.
days[].moon_phase_namestringOne of new, waxing_crescent, first_quarter, waxing_gibbous, full, waning_gibbous, last_quarter, waning_crescent.
days[].moon_illuminationfloat0–100%, illuminated fraction of the moon's disc.
days[].moonrise, moonsetstring | nullISO-8601 local time; null on a day with no moonrise/moonset.
sourcestringAlways "computed (Astral)".
attributionnullAlways null — computed, not fetched; no data licence involved.
Feature flag

When the stack is deployed with FEATURE_ASTRO=off (ADR-0004 §6), this endpoint returns 404 {"error":true,"reason":"feature_disabled"}, and the astronomy daily= variables below are rejected as unknown on /v1/daily.

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)

Also carries uv_index (ADR-0004 §2) — the CAMS UV biologically effective dose × 40 — banded against the WHO UV-Index scale (see Bands below). Present in now/hourly/daily and the top-level uv_banding field only when the run has it and FEATURE_UV is on; older runs and FEATURE_UV=off both omit it cleanly (no key, not a null).

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 — "ug/m3" for all seven pollutants, "index" for uv_index when present.
guidelinePollutant banding scheme id, "who-2021-multiples" — see Bands.
uv_bandingpresent only when the run has uv_index UV banding scheme id, "who-uv-index" — 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 pollutant species plus uv_index when present.
hourly.<species>3-hourly array, unbanded, aligned to hourly.time.
daily.<species>_max / _meanPer calendar day, aggregated across that day's 3-hourly samples. uv_index_max/_mean included when present.
200 · application/json
curl -s -H "x-api-key: wk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  "https://data.moulddetect.ai/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", …, "uv_index": "index" },
  "guideline": "who-2021-multiples", "uv_banding": "who-uv-index",
  "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" },
    "uv_index": { "value": 6.4, "band": "high" },
    …
  },
  "hourly": {
    "time": ["2026-08-04T06:00Z", "2026-08-04T09:00Z", …],
    "pm2_5": [7.9, 8.4, …], … 6 more species, "uv_index": [5.1, 6.4, …]
  },
  "daily": {
    "date": ["2026-08-04", "2026-08-05"],
    "pm2_5_max": [9.6, 11.1], "pm2_5_mean": [8.1, 9.4], …,
    "uv_index_max": [9.0, 8.5], "uv_index_mean": [4.2, 3.9]
  }
}

Bands — WHO-2021-multiples, and WHO UV-Index

Each pollutant 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

uv_index (source: CAMS UV biologically effective dose × 40, ADR-0004 §2) uses a separate scheme, "who-uv-index" — the WHO UV-Index exposure categories, banded on the value rounded to the nearest whole UV-Index unit:

BandUV Index (rounded)
low0 – 2
moderate3 – 5
high6 – 7
very_high8 – 10
extreme11 and above
FEATURE_UV

uv_index/uv_banding/units.uv_index only appear when the served run has a uv_index array (older runs, ingested before ADR-0004 §2 shipped, don't) and the deploy's FEATURE_UV flag is on (default). Either condition failing omits them cleanly — no key, never a fabricated null. The nightly CAMS ingest keeps storing uv_index regardless of the flag.

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 the CC BY 4.0 licence (Copernicus moved it from the "Licence to Use Copernicus Products" on 2 July 2025) — 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.

ENDPOINTSpecialists directory

GET/v1/specialistsAPI key

An Australia-wide directory of 4,525 mould and building specialists — plumbers, building inspectors, remediators, occupational hygienists, mycology labs and cleaners. Resolves a typed suburb, typed postcode, typed region name, or device geolocation to one of 84 ABS SA4 regions and returns its specialists, filtered and sorted. Full architecture, the offline geocoding pipeline, and the partitioning rationale: docs/specialists-api.html and docs/adr/0003-specialists-directory-partitioning.md.

Query parameters

NameTypeDefaultNotes
regionstringoptional—SA4 slug or ABS name, e.g. sydney-inner-west.
postcodestringoptional—4-digit AU postcode.
suburbstringoptional—Free-text locality name.
statestringoptional—Disambiguates suburb when the name repeats nationally.
lat, lonfloatoptional—Device geolocation; must be supplied together.
categorystringoptionalallOne of 12 categories, e.g. Mould Testing, Plumbing, Building Inspection.
statusstringoptionalallunvetted | pending_review | approved | rejected | suspended | approved_only (shorthand for status=approved).
coveragestringoptionalbothbased_here (companies based in the resolved region) | service_area (vetted companies that service the region but are based elsewhere). Any other value returns 400.
radius_kmfloatoptional—Only meaningful with lat/lon; filters before sorting.
limitintoptional50max 200
offsetintoptional0For pagination.

Exactly one location path is used per request, in a fixed resolution order: region > postcode > suburb (+ state) > lat/lon. Supplying more than one uses the highest-precedence one; supplying none returns 400.

Response body

FieldDescription
queryEchoed request parameters.
resolvedregion_slug, region_name, state, match_type (region | postcode | suburb | coordinates), centroid.
count, limit, offsetPagination.
specialists[]The specialist object (name, coverage, categories, location, contact, ratings, accreditations, mould_detect vetting state, provenance). Gains a distance_km field per item when lat/lon was supplied.
specialists[].coveragebased_here — the region this company's own address geocodes to; service_area — a region it services but is not based in. Vetted companies (approved/pending_review) are duplicated into every region they service, so the same id can appear in several regions' results — never twice in one response, since a request reads exactly one region. Dedupe by id only if you merge multiple regions client-side.
specialists[].mould_detect.service_radius_kmHow far the company travels from its base, in km — the radius the fan-out uses. null when unknown, always null for unvetted companies (never fanned out).

Sort order, in order of precedence: approved first, then pending_review, then the rest; then based_here before service_area; then distance_km ascending when lat/lon was given, else google_rating descending; then name. So an approved specialist leads even in a region it only services, and among equally-vetted companies a local one outranks a visiting one.

200 · application/json
curl -s -H "x-api-key: wk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  "https://data.moulddetect.ai/v1/specialists?suburb=Parramatta&state=NSW&category=Mould%20Testing&limit=5"
{
  "query": { "suburb": "Parramatta", "state": "NSW", "category": "Mould Testing", "limit": 5 },
  "resolved": {
    "region_slug": "sydney-parramatta", "region_name": "Sydney - Parramatta",
    "state": "NSW", "match_type": "suburb",
    "centroid": { "latitude": -33.8034, "longitude": 151.0016 }
  },
  "count": 3, "limit": 5, "offset": 0,
  "specialists": [ { "id": "mds-02931", "name": "…", "coverage": "based_here", "categories": ["Mould Testing"], … } ]
}
Unresolvable location → 404 with suggestions

A typed suburb/postcode/region that doesn't match returns 404 { "error": true, "reason": "…", "did_you_mean": [...] } with close matches from the locality index — e.g. suburb=Paramata suggests ["Parramatta", "Parramatta North"]. A request with no location parameter at all returns 400.

ENDPOINTSpecialists regions

GET/v1/specialists/regionsAPI key

The full region catalogue — every ABS SA4 that has at least one specialist, with its slug, name, state(s) and a category summary. Built for app pickers and typeahead widgets rather than for resolving a single search.

Response body

FieldDescription
regions[].slugRegion identifier used in region=, e.g. sydney-inner-west.
regions[].nameABS SA4 name, e.g. "Sydney - Inner West".
regions[].statePrimary state (majority vote across the region's localities).
regions[].statesEvery state the region touches — most regions list one; a few border-straddling SA4s (e.g. Gold Coast) list two.
regions[].countSpecialist count in that region.
regions[].centroidMean lat/lon of the region's member localities.
regions[].categoriesDistinct categories present in that region.
{ "regions": [
  { "slug": "sydney-inner-west", "name": "Sydney - Inner West", "state": "NSW",
    "states": ["NSW"], "count": 7,
    "centroid": { "latitude": -33.88071, "longitude": 151.11907 },
    "categories": ["Cleaning", "Plumbing"] }, …
] }

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,
  "ecmwf_latest_run": null }

ecmwf_latest_run (ADR-0004 item 4) is the latest ingested ECMWF run id, or null when FEATURE_ECMWF is off or no ECMWF run has been ingested yet — same graceful-absence pattern as history_latest_complete_date and air_latest_run.

ADR-0009 adds three more fields, each null while its feature flag is off or its store has not been built: atlas_place_version (the Mould Atlas place-layers build version behind GET /v1/site's water/coast/built groups), hazard_fire_as_of and hazard_cyclone_as_of (the data date of the newest GET /v1/hazard build for each block). Ask /health before relying on a hazard block being fresh.

REFERENCEDaily variables

Pass any subset in daily= (comma-separated). Omitting daily returns the full base set. Every opt-in variable below is only returned when named explicitly, and never changes the default response.

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 the outdoor air held RH ≥ 80% — a proxy for how long cold indoor surfaces stay damp, not an indoor-air reading.
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%).
sunrise, sunset opt-inlocal ISO-8601From the astronomy engine; null on a polar day/night.
daylight_hours opt-inhHours between sunrise and sunset (0 or 24 on a polar day/night).
moon_phase opt-infraction0.0–1.0 fraction of the synodic cycle, 0 = new moon.
moon_illumination opt-in%Illuminated fraction of the moon's disc.
sunshine_hours opt-inhWMO 120 W/m² shortwave-radiation proxy: each 3-hourly daylight sample at or above that threshold counts as 3 sunny hours, capped at the day's daylight_hours. A proxy, not a sunshine-recorder measurement.
drying_index opt-inindex (0–10)Mean over the day's daylight samples of (1 − RH/100) × min(1, wind_speed_10m_kmh / 20), times sunshine_hours / daylight_hours, scaled to 0–10. Bounded and monotonically non-decreasing in dryness, wind (up to 20 km/h) and sunshine.
mould_index opt-inindex 0-6Forecast-only VTT mould growth index (M=0 at the first forecast step, fixed at material class sensitive) — see Mould Growth Index for the full model and the standalone endpoint's history+forecast joined run. 1dp; null for a date this forecast-only run doesn't cover.
ADR-0004 items 1 & 5

The five astronomy variables (sunrise … moon_illumination) come from GET /v1/astro's engine and are gated by FEATURE_ASTRO; sunshine_hours/drying_index are computed from the same 3-hourly samples the base variables already aggregate (no ingest change) and are gated by FEATURE_DERIVED_SUN. Naming a gated-off variable in daily= gets the same 400 unknown daily variable(s) as a typo.

docs/design-brief-adr0004-demos.md §3

mould_index is gated by FEATURE_MOULD_INDEX (default on) — see Mould Growth Index.

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

Every data endpoint returns 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/siteprivate, max-age=86400 (24 h)If-None-Match → 304
/v1/astroprivate, 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/siteStatic — one-off build, refreshed only when re-runDays to weeks; poll GET /health → site_layers_version for a fresh build.
/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 /health → history_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://data.moulddetect.ai/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://data.moulddetect.ai
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://data.moulddetect.ai" } ],
  "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();