Postcode-level water quality data for any UK address. Single endpoint. Under 200ms. Free to start.
// Enter a UK postcode and hit Try it →
Built on official DWI annual compliance data and covering every water company in England, Wales and Scotland.
From property platforms to insurance underwriting — water quality data that fits your product.
Add water quality data to property listings and area guides. Hardness classification, PFAS status and supply zone for every UK address.
Include water quality in property search reports. PFAS notices, lead levels, and DWI enforcement status as structured data.
Price hard water risk and PFAS exposure into home insurance models. Zone-level compliance and contaminant data via a single endpoint.
Personalise filter recommendations and health guidance by location. Full parameter set with legal limits, trend data, and actionable scoring.
No contracts, no credit card required for the free tier. Paid tiers available immediately.
Pass your API key as an HTTP header. This is the only method we recommend for production.
X-API-Key: mtw_live_your_key_here
Referer headers.
GET /lookup?postcode=SW1A1AA&api_key=mtw_live_your_key_here
Always prefer -H 'X-API-Key: ...' over ?api_key=... in shell scripts — query parameters leak the key in shell history, process listings (ps aux), and any access logs in the request chain.
| Parameter | Type | Required | Description |
|---|---|---|---|
postcode | string | Yes | UK postcode, with or without space. Case insensitive. |
api_key | string | Cond. | Required only if not using the X-API-Key header (recommended). Avoid in browser / client-side calls due to log and Referer exposure. |
value: null and status: "not_reported", so the response shape is stable across all 21 water companies and 1,327 zones.
| Field | Type | Description |
|---|---|---|
postcode | string | Normalised postcode with space (e.g. "SW1A 1AA") |
district | string | Postcode district (e.g. "SW1A") |
zone.name | string | Water supply zone name (e.g. "Kentish Town") |
zone.code | string | Internal zone identifier (e.g. "0374") |
zone.water_company | string | Water company name (e.g. "Thames Water") |
zone.company_id | string | URL-safe slug for the water company (e.g. "thames-water") |
| Field | Type | Description |
|---|---|---|
quality.score | integer | Overall quality score 0–100. Weighted composite (see methodology below). |
quality.score_label | string | "Excellent" (≥80), "Good" (≥60), "Fair" (≥40), "Poor" (<40) |
quality.score_description | string | Plain-English verdict matched to the score band — full enumeration below |
quality.score_description — complete enumerationExactly four values are possible. Strings are fixed English (not localised) and identical across all postcodes, tiers and clients. Dashes are em-dashes (Unicode U+2014 "—"), not hyphens.
| Band | score_label | score_description (verbatim) |
|---|---|---|
score ≥ 80 | "Excellent" | "Your water quality is excellent — among the best in the UK" |
60 – 79 | "Good" | "Good quality water with a few things to be aware of" |
40 – 59 | "Fair" | "Water has some concerns worth addressing" |
score < 40 | "Poor" | "Water has significant issues — see recommendations" |
An active PFAS notice caps the underlying score at 59 (forcing "Fair" or "Poor"), but the description string itself is identical regardless of why the score sits in that band.
parameters.hardness| Field | Type | Unit | Description |
|---|---|---|---|
value | number | mg/l CaCO₃ | Total hardness as calcium carbonate |
min / max | number | mg/l CaCO₃ | Annual range where reported |
unit | string | — | Always "mg/l CaCO3" |
classification | string | — | Six-class label — see below |
label | string | — | Human-readable summary (e.g. "Hard water — significant limescale expected") |
value_clarke | number | °Clarke | Clarke degrees (where reported) |
value_german | number | °dH | German degrees (°dH) |
value_french | number | °f | French degrees (°f) |
value_mmol | number | mmol/l | mmol/l of CaCO₃ |
calcium | number | mg/l | Calcium content |
magnesium | number | mg/l | Magnesium content |
status | string | — | "measured" or "not_reported" |
parameters.hardness.label — complete enumeration
The label is built from the six-class classification, with an extra suffix added when hardness_caco3 > 200 mg/l. Seven non-null strings are possible (plus null when the supplier doesn't report hardness). Strings are fixed English (not localised) and identical across all postcodes, tiers and clients. Dashes are em-dashes (Unicode U+2014 "—"), not hyphens.
| Classification | CaCO₃ range (mg/l) | label (verbatim) |
|---|---|---|
"Soft" | < 60 | "Soft water" |
"Moderately Soft" | 60 – 119 | "Moderately Soft water" |
"Slightly Hard" | 120 – 179 | "Slightly Hard water" |
"Moderately Hard" | 180 – 200 | "Moderately Hard water" |
"Moderately Hard" | 201 – 239 | "Moderately Hard water — significant limescale expected" |
"Hard" | 240 – 299 | "Hard water — significant limescale expected" |
"Very Hard" | ≥ 300 | "Very Hard water — significant limescale expected" |
| (no data) | null | null |
Note on "Moderately Hard": this classification spans CaCO₃ 180–239 mg/l, which straddles the 200 mg/l limescale threshold. It therefore produces two different label variants — one below 200 (no suffix) and one above 200 (with the limescale suffix). All other classifications produce a single deterministic label string.
| Block | Unit | Legal limit | Description |
|---|---|---|---|
parameters.nitrate | mg/l | 50 | Nitrate — value/min/max/samples + status |
parameters.nitrite | mg/l | 0.5 | Nitrite (where reported) |
parameters.lead_mean | µg/l | 10 | Lead — annual mean, with min/max range |
parameters.lead_max | µg/l | 10 | Lead — peak reading (kept for backwards compatibility) |
| Block | Unit | Legal limit | Description |
|---|---|---|---|
parameters.arsenic | µg/l | 10 | Arsenic — most companies report max only |
parameters.copper | mg/l | 2 | Copper |
parameters.iron | µg/l | 200 | Iron |
parameters.nickel | µg/l | 20 | Nickel |
parameters.bromate | µg/l | 10 | Bromate (disinfection by-product) |
parameters.manganese | µg/l | 50 | Manganese (where reported) |
parameters.aluminium | µg/l | 200 | Aluminium (where reported) |
parameters.sodium | mg/l | 200 | Sodium (where reported) |
Each block returns { value, min, max, samples, unit, legal_limit, status }.
| Block | Unit | Legal limit | Description |
|---|---|---|---|
parameters.chlorine_mean | mg/l | — | Free / total chlorine residual (no UK PCV) |
parameters.thm_mean | µg/l | 100 | Total trihalomethanes (chlorination by-product) |
| Block | Unit | Legal limit | Description |
|---|---|---|---|
parameters.fluoride | mg/l | 1.5 | Fluoride. added: true if >0.3 mg/l (suggests artificial fluoridation) |
parameters.ph | pH | 6.5–9.5 | Returns legal_range: "6.5-9.5" instead of legal_limit |
parameters.turbidity | NTU | 4 | Turbidity (where reported) |
parameters.colour | mg/l Pt/Co | 20 | Apparent colour (where reported) |
parameters.conductivity | µS/cm | 2500 | Electrical conductivity (where reported) |
| Block | Unit | Legal limit | Description |
|---|---|---|---|
parameters.pesticides_total | µg/l | 0.5 | Sum of all pesticides detected — annual mean / min / max |
Roadmap: per-pesticide concentrations (atrazine, MCPA, mecoprop, glyphosate, simazine, etc.) are a planned future enhancement. About half of UK water companies publish individual pesticide breakdowns in their zone reports. Subscribe at hello@mytapwater.co.uk if this is on your roadmap.
coliform & ecoli| Field | Type | Description |
|---|---|---|
samples | integer | Total samples tested in the year |
failures | integer | Samples failing the <1 /100ml threshold |
compliance_rate | number | Percentage of compliant samples (0–100) |
unit | string | Always "/100ml" |
legal_limit | integer | Always 0 (must be absent) |
status | string | "pass", "fail", or "not_reported" |
compliance| Field | Type | Description |
|---|---|---|
pfas_notice | boolean | True if any active DWI improvement notice is in force at the water-company level. Note: this flag is set when any active notice exists — see meta.enforcement_flags.pfas for the PFAS-specific flag. |
pfas_completion_target | string|null | ISO date of the earliest active notice's completion target, if any |
active_notices | integer | Count of active DWI notices — company-wide, not zone-specific |
overall_compliance | string | "compliant" if no active notices, otherwise "improvement_required" |
| Field | Type | Description |
|---|---|---|
recommendations.filter_type | string | One of: "carbon_block", "reverse_osmosis", "water_softener" |
recommendations.reason | string | Plain-English justification for the suggested filter |
recommendations.hardness_treatment | string | One of: "softener_required", "softener_recommended", "not_required" |
meta| Field | Type | Description |
|---|---|---|
data_year | integer | Year of the underlying DWI compliance report |
last_updated | string | ISO date when MyTapWater last refreshed this zone |
source | string | Citation: "DWI Annual Compliance Reports 2025" |
api_version | string | API schema version (e.g. "1.1") |
zone_population | integer|null | Population served by the supply zone (where reported) |
reporting_period | string|null | Free-text period the data covers (e.g. "1 Jan 2025 to 31 Dec 2025") |
dwi_assessment | string|null | DWI's own free-text assessment for the zone, where available |
enforcement_flags.pfas | boolean | Active PFAS-specific notice in force for the supplying company |
enforcement_flags.lead | boolean | Active lead-specific notice in force |
enforcement_flags.microbiological | boolean | Active bacteriological/coliform notice in force |
enforcement_flags.other | boolean | Active notice for any other parameter |
quality.score is a weighted composite of four sub-scores:
Safety 50% (nitrate, lead, THMs, arsenic, bromate),
Purity 25% (chlorine residual, turbidity, aluminium),
Minerals 10% (hardness + pH),
Compliance 15% (tiered by enforcement severity).
Hard caps apply when enforcement is active:
a PFAS notice caps the score at 59 ("Fair"),
a lead or microbiological notice caps it at 72.
parameters.hardness.classification returns one of six strings:
"Soft" (<60 mg/l),
"Moderately Soft" (60–119),
"Slightly Hard" (120–179),
"Moderately Hard" (180–239),
"Hard" (240–299),
"Very Hard" (≥300).
Every measurement block returns a status string:
"pass" (within legal limit),
"fail" (exceeds legal limit),
"measured" (reported but no UK regulatory limit applies — e.g. chlorine, hardness),
"not_reported" (this water company doesn't publish this parameter).
For the compliance.overall_compliance field the values are
"compliant" or "improvement_required".
All measurements are zone-level annual means / ranges from official UK Drinking Water Inspectorate (DWI) compliance reports. They are NOT real-time readings and NOT property-specific. Lead values in particular reflect the water leaving the treatment works and reaching the zone — lead picked up from individual property plumbing is not represented.
legal_limit values
All legal_limit values are the UK Prescribed Concentration Values (PCVs) from the
Water Supply (Water Quality) Regulations 2018 / DWI guidance.
null indicates no UK regulatory limit applies to that parameter (e.g. chlorine residual).
compliance.active_notices scope
This integer is the count of company-wide DWI improvement notices, not the number specific to your zone.
Most active notices affect the entire supplier's operating area.
For the PFAS-specific signal use meta.enforcement_flags.pfas.
{
"postcode": "NW3 3SU",
"district": "NW3",
"zone": { "name": "Kentish Town", "code": "0374",
"water_company": "Thames Water", "company_id": "thames-water" },
"quality": { "score": 47, "score_label": "Fair",
"score_description": "Water has some concerns worth addressing" },
"parameters": {
"hardness": { "value": 263, "min": null, "max": null, "unit": "mg/l CaCO3",
"classification": "Hard", "label": "Hard water — significant limescale expected",
"value_clarke": 18.4, "value_german": 14.7, "value_french": 26.3,
"value_mmol": 2.63, "calcium": 105.1, "magnesium": null,
"status": "measured" },
"nitrate": { "value": 28.4, "min": 23.2, "max": 34.1, "samples": 36,
"unit": "mg/l", "legal_limit": 50, "status": "pass" },
"nitrite": { "value": null, "unit": "mg/l", "legal_limit": 0.5, "status": "not_reported" },
"lead_mean": { "value": 1.0, "min": 0.9, "max": 1.5, "unit": "µg/l",
"legal_limit": 10, "status": "pass" },
"lead_max": { "value": 1.5, "unit": "µg/l", "legal_limit": 10, "status": "pass" },
"arsenic": { "value": null, "min": null, "max": 1.2, "samples": null,
"unit": "µg/l", "legal_limit": 10, "status": "pass" },
"copper": { "value": null, "min": null, "max": 0.16, "samples": null,
"unit": "mg/l", "legal_limit": 2, "status": "pass" },
"iron": { "value": null, "min": null, "max": 21, "samples": null,
"unit": "µg/l", "legal_limit": 200, "status": "pass" },
"nickel": { "value": null, "min": null, "max": 1.5, "samples": null,
"unit": "µg/l", "legal_limit": 20, "status": "pass" },
"bromate": { "value": null, "min": null, "max": 5.5, "samples": null,
"unit": "µg/l", "legal_limit": 10, "status": "pass" },
"manganese": { "value": null, "min": null, "max": null, "samples": null,
"unit": "µg/l", "legal_limit": 50, "status": "not_reported" },
"aluminium": { "value": null, "min": null, "max": null, "samples": null,
"unit": "µg/l", "legal_limit": 200, "status": "not_reported" },
"sodium": { "value": null, "min": null, "max": null, "samples": null,
"unit": "mg/l", "legal_limit": 200, "status": "not_reported" },
"chlorine_mean": { "value": 0.71, "min": null, "max": 0.98, "unit": "mg/l",
"legal_limit": null, "status": "measured" },
"thm_mean": { "value": 14, "min": 12, "max": 16, "unit": "µg/l",
"legal_limit": 100, "status": "pass" },
"fluoride": { "value": 0.13, "min": 0.12, "max": 0.16, "unit": "mg/l",
"added": false, "legal_limit": 1.5, "status": "pass" },
"ph": { "value": 7.84, "min": 7.6, "max": 8.1, "unit": "pH",
"legal_range": "6.5-9.5", "status": "pass" },
"turbidity": { "value": null, ..., "legal_limit": 4, "status": "not_reported" },
"colour": { "value": null, ..., "legal_limit": 20, "status": "not_reported" },
"conductivity": { "value": null, ..., "legal_limit": 2500, "status": "not_reported" },
"pesticides_total": { "value": 0.006, "min": 0, "max": 0.03,
"unit": "µg/l", "legal_limit": 0.5, "status": "pass" },
"coliform": { "value": null, "samples": 96, "failures": 0,
"compliance_rate": 100, "unit": "/100ml",
"legal_limit": 0, "status": "pass" },
"ecoli": { "value": null, "samples": 96, "failures": 0,
"compliance_rate": 100, "unit": "/100ml",
"legal_limit": 0, "status": "pass" }
},
"compliance": { "pfas_notice": true, "pfas_completion_target": null,
"active_notices": 30, "overall_compliance": "improvement_required" },
"recommendations": { "filter_type": "reverse_osmosis",
"reason": "PFAS enforcement notice active — only RO removes PFAS",
"hardness_treatment": "softener_recommended" },
"meta": { "data_year": 2025, "last_updated": "2026-03-01",
"source": "DWI Annual Compliance Reports 2025", "api_version": "1.1",
"zone_population": 35200, "reporting_period": "1 Jan 2025 to 31 Dec 2025",
"dwi_assessment": "Excellent quality water with no infringements...",
"enforcement_flags": { "pfas": true, "lead": true,
"microbiological": true, "other": true } }
}
When a water company doesn't publish a parameter, the field is still present — with value: null and status: "not_reported". Example for a postcode in a region where the supplier doesn't test for manganese:
"manganese": {
"value": null,
"min": null,
"max": null,
"samples": null,
"unit": "µg/l",
"legal_limit": 50,
"status": "not_reported"
}
| Code | Meaning |
|---|---|
| 200 | Success |
| 401 | Invalid or missing API key |
| 404 | Postcode not found in any supply zone |
| 429 | Monthly rate limit exceeded |
| 500 | Server error |
const response = await fetch( 'https://api.mytapwater.co.uk/lookup?postcode=SW1A1AA', { headers: { 'X-API-Key': 'your_api_key_here' } } ); const data = await response.json(); console.log(data.parameters.hardness.classification); // "Very Hard" console.log(data.compliance.pfas_notice); // true/false console.log(data.quality.score); // 0–100
import requests response = requests.get( 'https://api.mytapwater.co.uk/lookup', params={'postcode': 'SW1A1AA'}, headers={'X-API-Key': 'your_api_key_here'} ) data = response.json() print(data['parameters']['hardness']['classification']) # Very Hard print(data['compliance']['pfas_notice']) # True/False
$response = file_get_contents( 'https://api.mytapwater.co.uk/lookup?postcode=SW1A1AA', false, stream_context_create(['http' => ['header' => 'X-API-Key: your_api_key_here']]) ); $data = json_decode($response, true); echo $data['parameters']['hardness']['classification']; // Very Hard
Your key will be emailed instantly. No credit card required.