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 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"
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
| 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://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"
{
"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 |
model | string | optional | gfs | gfs | ecmwf | best — ADR-0004 item 4, see below |
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 | The requested model — gfs (default), ecmwf, or best. |
run | string | Latest 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.time | string[] | 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_units | object | Unit label per returned variable. |
samples | object | Only when granularity=3h: raw 3-hourly values. For model=best, always sourced from GFS regardless of that day's model_used (documented limitation). |
attribution | string | Present 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_used | string[] | model=best only — "gfs" or "ecmwf" per day, whichever's data actually populated that day's values. |
daily.model_agreement | object[]|null | model=best only — see Model agreement below; null for a day where only one model has data. |
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:
| Field | Description |
|---|---|
rh_mean_spread | Absolute difference (percentage points) between the two models' relative_humidity_2m_mean for the day. |
temp_max_spread | Absolute difference (°C, converted back from °F first if units=imperial) between the two models' temperature_2m_max. |
condensation_risk_agree | Whether both models' condensation_risk boolean agree. |
level | high (spread ≤ 5% RH and ≤ 1.5°C), low (spread ≥ 15% RH or ≥ 4°C), else medium. |
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
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.
Attribution
| Field | Type | Notes |
|---|---|---|
attribution | string | Always 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
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 | The 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. |
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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −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
| Field | Description |
|---|---|
grid_cell | The master-grid cell actually read: lat, lon, spacing_deg (0.002). |
layers.elevation_m | Copernicus DEM GLO-90, metres. |
layers.slope_deg, aspect_deg | Terrain derivatives (Horn's method) computed at master-grid resolution. aspect_deg is -1 on perfectly flat cells. |
layers.aspect_compass | 8-point compass (N/NE/E/SE/S/SW/W/NW) for aspect_deg; null when aspect_deg is null or -1 (flat). |
layers.twi | Topographic wetness index (D8 flow accumulation) — higher means water tends to collect there. |
layers.soil.clay_pct, sand_pct, silt_pct | ISRIC SoilGrids 0-5 cm mean, percent. |
layers.soil.texture_class | USDA soil-texture-triangle class (e.g. "clay_loam") from the three percentages above. |
layers.land_cover.tree_cover_pct, built_pct, water_pct | ESA WorldCover 2021 class fractions (classes 10/50/80) aggregated onto the master grid. |
layers.dist_coast_km, dist_water_km | Distance 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. |
attribution | Array of three required credit strings (Copernicus DEM, SoilGrids, ESA WorldCover) — surface all three in client UI. |
layers_version | The static build's version string (also on GET /health as site_layers_version). |
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.
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).
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.
| Field | Description |
|---|---|
water.gsw_occurrence_pct | JRC Global Surface Water 1984–2024 occurrence, 0–100 %, mean over the cell. |
water.gsw_seasonality_months | Months 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_km | Distance to the nearest cell of seasonal water (occurrence above 1 %, seasonality under 12 months), km. |
water.dist_lake_km | Distance to the nearest HydroLAKES lake polygon, km. |
coast.sea_inundation_band_m | Lowest 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_m | GEBCO_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_pct | GHS-BUILT-S 2020 built-up share of the cell, 0–100 %. |
built.ghsl_pop_per_km2 | GHS-POP 2020 people per km². |
built.ghsl_settlement_epoch | Earliest 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_m | Microsoft 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. |
attribution | Gains the atlas groups' credit strings (below), appended to the three above. |
"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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −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".
| Field | Description |
|---|---|
fire.as_of | Data date (ISO, UTC) of the newest satellite fire pass included. The windows below end on this date, not today. |
fire.grid_cell | lat, lon, spacing_deg (0.01) of the cell read. Same shape on cyclone (0.1). |
fire.radius_km | Search radius for the fire fields: 5. |
fire.last_fire_date, days_since_last_fire | Date 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_365d | Count 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_burned | Latest 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_m2 | Wildfire 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_mm | ERA5-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_reason | Plain-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_of | Build date (ISO, UTC) the windows end on. |
cyclone.radius_km | Distance from the cell centre within which a track counts: 100. |
cyclone.tracks_100km_10y, tracks_100km_30y | Number 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_30y | Highest 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_cyclone | Most 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. |
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.
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
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)
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −180 … 360 |
material_class | string | optional | sensitive | One of very_sensitive, sensitive, medium_resistant, resistant — see the scale table below. |
history_days | int | optional | 30 | 7 … 30 days of ERA5-Land history, ending at the archive's latest_complete_date. |
forecast_days | int | optional | 7 | 1 … 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
| Field | Type | Description |
|---|---|---|
model | string | Always "vtt-improved-2010". |
scale | object[] | The plain-language 0–6 ladder — see below. |
series[].date | string | Local calendar date, YYYY-MM-DD — the joined history+forecast window, contiguous with no duplicate dates. |
series[].index | float | The mould index (0–6, 1dp) at the end of that local day. |
series[].label | string | Plain-language band for that day's index — see the scale table. |
series[].source | string | "history" or "forecast" — which archive that day's samples came from. |
series[].rh_mean, temp_mean | float | That day's mean relative humidity (%) / temperature (°C). |
series[].rh_crit | float | The critical humidity (%) at that day's mean temperature, for the requested material_class. |
series[].growth | bool | true if any 3-hourly step that day had a positive growth rate. |
summary.index_today, index_day7 | float | The continuous run's index today / 7 days ahead. |
summary.peak_index_30d | float | Highest index reached in the last 30 days (history + today). |
summary.days_to_visible | int | null | Days from today until the first forecast day reaching index 3 ("Visible spots"); null if none of the forecast window does. |
summary.growth_days_30d | int | Days in the last 30 (history + today) with a positive growth rate. |
summary.trend | string | One of rising, steady, falling — the index's direction over the last 7 days. |
summary.clean_surface_index_day7 | float | null | A 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_date | string | null | The ERA5-Land archive's latest complete date this response drew on; null when history_available is false. |
forecast_run | string | The GFS run id this response drew on, e.g. "2026-09-04T00Z". |
attribution | string[] | ["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. |
caveat | string | The "what this is — and is not" text above, verbatim, for surfacing in client UI. |
history_available | bool | false when the point falls outside every archived ERA5-Land region — see below. |
Plain-language scale
| Index | Label |
|---|---|
| 0 | Dry, no growth |
| 1 | Starting (invisible) |
| 2 | Growing (invisible) |
| 3 | Visible spots |
| 4 | Spreading |
| 5 | Widespread |
| 6 | Severe |
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 class | Example materials | RHmin | Cmat |
|---|---|---|---|
very_sensitive | Untreated timber, paper-faced plasterboard, cardboard | 80% | 1.0 |
sensitive | Planed timber, paper, some insulation | 80% | 0.5 |
medium_resistant | Concrete, cement render, mineral wool | 85% | 0.25 |
resistant | Glass, metal, treated timber, tiles | 85% | 0.1 |
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.
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.
/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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
latitude | float | required | — | −90 … 90 |
longitude | float | required | — | −180 … 360 |
days | int | optional | 7 | 1 … 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
| Field | Type | Description |
|---|---|---|
timezone | string | Resolved IANA zone. |
days[].date | string | Local calendar date, YYYY-MM-DD. |
days[].sunrise, sunset | string | null | ISO-8601 local time. null on a day the sun never rises/sets (polar). |
days[].civil_dawn, civil_dusk | string | null | ISO-8601 local time, 6° below the horizon. |
days[].solar_noon | string | ISO-8601 local time — always present, even on a polar day/night. |
days[].daylight_hours | float | Hours between sunrise and sunset; 0.0 (polar night) or 24.0 (polar day) when there is no sunrise/sunset that day. |
days[].moon_phase | float | 0.0–1.0 fraction of the synodic cycle, 0 = new moon. |
days[].moon_phase_name | string | One of new, waxing_crescent, first_quarter, waxing_gibbous, full, waning_gibbous, last_quarter, waning_crescent. |
days[].moon_illumination | float | 0–100%, illuminated fraction of the moon's disc. |
days[].moonrise, moonset | string | null | ISO-8601 local time; null on a day with no moonrise/moonset. |
source | string | Always "computed (Astral)". |
attribution | null | Always null — computed, not fetched; no data licence involved. |
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
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
| 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 — "ug/m3" for all seven pollutants, "index" for uv_index when present. |
guideline | Pollutant banding scheme id, "who-2021-multiples" — see Bands. |
uv_banding | present 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. |
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 pollutant species plus uv_index when present. |
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. uv_index_max/_mean included when present. |
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.
| 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 |
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:
| Band | UV Index (rounded) |
|---|---|
low | 0 – 2 |
moderate | 3 – 5 |
high | 6 – 7 |
very_high | 8 – 10 |
extreme | 11 and above |
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.
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 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
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
| Name | Type | Default | Notes | |
|---|---|---|---|---|
region | string | optional | — | SA4 slug or ABS name, e.g. sydney-inner-west. |
postcode | string | optional | — | 4-digit AU postcode. |
suburb | string | optional | — | Free-text locality name. |
state | string | optional | — | Disambiguates suburb when the name repeats nationally. |
lat, lon | float | optional | — | Device geolocation; must be supplied together. |
category | string | optional | all | One of 12 categories, e.g. Mould Testing, Plumbing, Building Inspection. |
status | string | optional | all | unvetted | pending_review | approved | rejected | suspended | approved_only (shorthand for status=approved). |
coverage | string | optional | both | based_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_km | float | optional | — | Only meaningful with lat/lon; filters before sorting. |
limit | int | optional | 50 | max 200 |
offset | int | optional | 0 | For 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
| Field | Description |
|---|---|
query | Echoed request parameters. |
resolved | region_slug, region_name, state, match_type (region | postcode | suburb | coordinates), centroid. |
count, limit, offset | Pagination. |
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[].coverage | based_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_km | How 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.
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"], … } ]
}
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
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
| Field | Description |
|---|---|
regions[].slug | Region identifier used in region=, e.g. sydney-inner-west. |
regions[].name | ABS SA4 name, e.g. "Sydney - Inner West". |
regions[].state | Primary state (majority vote across the region's localities). |
regions[].states | Every state the region touches — most regions list one; a few border-straddling SA4s (e.g. Gold Coast) list two. |
regions[].count | Specialist count in that region. |
regions[].centroid | Mean lat/lon of the region's member localities. |
regions[].categories | Distinct 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
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.
| 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 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 | °C | Minimum daily (T − dew point). Small = condensation-prone. |
condensation_risk opt-in | bool | Heuristic condensation flag (spread ≤ 1.5 °C and RH ≥ 90%). |
sunrise, sunset opt-in | local ISO-8601 | From the astronomy engine; null on a polar day/night. |
daylight_hours opt-in | h | Hours between sunrise and sunset (0 or 24 on a polar day/night). |
moon_phase opt-in | fraction | 0.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-in | h | WMO 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-in | index (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-in | index 0-6 | Forecast-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. |
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.
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:
| 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
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.
| 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/site | private, max-age=86400 (24 h) | If-None-Match → 304 |
/v1/astro | 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/site | Static — one-off build, refreshed only when re-run | Days to weeks; poll GET /health → site_layers_version for a fresh build. |
/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://data.moulddetect.ai/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://data.moulddetect.ai |
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://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" } }
]
}
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();