# Drinking water records — Mississippi State Department of Health > Public drinking water system records from SDWIS/STATE, served as JSON. > Codes are decoded, values the agency never recorded are reported as null and explained, > and records that look wrong are flagged rather than silently corrected. Machine-readable description: https://api.alpha.dww-next.mswater.us/openapi.json — the whole API, ~81,340 tokens. You probably do not need all of it. https://api.alpha.dww-next.mswater.us/openapi/index.json lists every endpoint in one line and links a standalone spec for each; fetch the ones you need, several at once with a comma. https://api.alpha.dww-next.mswater.us/openapi/core.json is the two endpoints below and nothing else. Human-readable site: https://alpha.dww-next.mswater.us ## How to answer a question about a water system 1. The public identifier is the PWSID, e.g. MS0010002. Users will not know it. Find one with GET /v1/water-systems?q= 2. Then GET /v1/water-systems/{pwsid}/summary — identity, population, sources and every violation count in one request. Prefer it over assembling THOSE from several endpoints. 3. **To answer "has anything ever been detected", read the top-level `findings` block** on /v1/water-systems/{pwsid}/coliform-samples and /v1/water-systems/{pwsid}/samples. It describes everything that collection publishes for the system — its `findings.scope` says what that selects — not the page you asked for, and does not move when you change `limit` — so you do not have to page anything to use it. It carries how many analytes were ever detected, the detected/not-detected split across every result, and the detected analytes themselves. Page one of the rows is NOT a sample of this: newest-first ordering systematically hides analytes a system stopped detecting years ago. 4. Coded values arrive as {code, label, labelSource}. Resolve anything unfamiliar at GET /v1/code-tables/{domain}. ## Defaults that change the answer **Some collections return the last 2 years unless you give dates.** These, at the time of this request: /v1/analytes, /v1/coliform-samples, /v1/coliform-summaries, /v1/lead-and-copper, /v1/mdbp-summaries, /v1/samples. For each, the rule is: **No dates means the last two years, not the whole record.** Called without either of its two date parameters, this returns only rows whose window date — collection date, or monitoring-period start date on the summaries — falls in the last two years, the agency's own default on its matching page. So a count from an undated call is a two-year count, not the full history. window.source in the response says which window you got. At most five years may separate the two dates; a wider window is refused with the reason, and that limit is this API's, not the agency's. **THAT LIST IS EXACT, AND IT IS NOT A LIST OF SUBJECTS — IT IS A LIST OF PATHS.** Every path on it is an installation-wide collection. The per-system collections under /v1/water-systems/{pwsid}/ are NOT on it, and a path of the same name there applies no date window by default — /v1/samples is windowed, /v1/water-systems/{pwsid}/samples is not. No window is not the same as everything: see the rule on fixed selections below. A path not on the list may still narrow by a rule of its own; sample schedules do — each of those states its own rule in `window.source`, naming whose default it is. **A SCHEDULE OR VIOLATION FILED UNDER AN ANALYTE GROUP DOES NOT LIST ITS MEMBERS, AND NO FILTER REPRODUCES THEM.** The group stands for them and the members are a record of the agency's, not a property of the analytes: `analyteGroup.members` on those rows links to https://api.alpha.dww-next.mswater.us/v1/analyte-groups/{code}, which is the only thing that expands one. Do NOT reach for `analyteType` instead: a type is a property of an analyte and membership is a record the agency keeps, so the two sets differ — a system can hold analytes of the group's type that are not in the group. That substitution returns a confident wrong answer rather than an empty one, which is the worse of the two failures. So do not reason from the name. **`window` and `notShown` are two different facts about the same narrowing and you want both:** `window` is the date bounds IN FORCE, and `notShown` COUNTS the records a narrowing excluded and, where you can drop that narrowing, carries the URL that does — its key varies by collection (`everything`, or on violations `allValidated`), so read the keys `notShown` actually has rather than expecting one name. **ONE RULE, ON EVERY COLLECTION: `window` is present whenever date bounds apply — whether this API defaulted them or you passed them — and `window.source` is present only when this API chose them.** So a `window` with no `source` carries the bounds YOU asked for; a `source` names the default you got, or the end we supplied when you bounded only one — pass only `collectedFrom` to an all-systems collection and it reports "the collectedFrom you asked for, through today". **No `window` at all means no date bounds were applied.** **`notShown` is NOT universal, so never read its absence as "nothing was excluded".** Where it appears it counts what a narrowing removed; where it does not, you have learnt nothing either way. It is the per-system collections that carry it, because there the count is one cheap extra statement — but not all of them do, and the all-systems collections carry none at all. On those, read `window` for what was applied and `page.total` for what matched it. **NO `window` AND NO `notShown` MEANS NO DATE OR COUNT NARROWING — IT DOES NOT MEAN EVERYTHING THE AGENCY HOLDS.** Many collections also apply a FIXED SELECTION that no parameter lifts — the agency's own filter, reproduced, such as compliance-purpose samples only, or enforcement actions with status Taken — and each states it in its own `reading` or `scope`. So the whole of what a collection PUBLISHES is: no `window`, no `notShown`, and whatever its reading says it selects. Read that sentence before calling any list complete; it is different on every route and no rule here can stand in for it. **Most collections page; check `page.paginated` before you pass limit or offset.** Where it is false the response is the whole of what that collection publishes, in one call, within any window its `window` states — and limit or offset there is refused. Where it is true, `page.nextOffset` is the offset of the next page (null on the last): send the same request with that offset. Where it is true: Paged: 50 rows per request unless you ask for more with limit, at most 500. A larger limit is refused, not truncated. page.total counts every row that matched, not only the rows in this response; page.hasMore says whether to ask again with offset. ## Ways to be confidently wrong These are properties of the whole dataset. Each is shown with the case that is easiest to check, but do not read the example as the scope of the rule. **A count depends on how you count, so say which one you quoted.** Almost nothing here has a single defensible total, and each response says which one it is giving you. Sources: `sources.active` and `sources.total` differ because retired wells stay on the register, so the larger number is not "more complete", it is a different question. Population: `basis` names where each figure comes from, and a search row and a system record carry different ones, from different columns, which can disagree. Violations: /summary returns `violations.counts.ladder`, readings side by side, each named, from the narrowest to the widest — whether grouped records count as one and whether a record with no analyte counts at all — though the two middle rungs are unordered against each other, so the numbers can decrease along the list. It does not pick for you, because picking is the caller's job and it depends on the question. /violations takes no date filter; the newest determination among VALIDATED violations only is one field, `violations.lastDeterminedOn`, on the same /summary and on every /v1/water-systems search row — so `/v1/water-systems?county=…` answers "which systems have had a validated violation since a date" in one request. **A code describes the RECORD, not the water.** Statuses here are administrative states in a register. A validated violation is one the agency accepted; a rejected one is a record it did not accept, so it is not a violation at all. For some systems the rejected records are the MOST RECENT ones, running years past the last validated one, so a list read without checking status can look current when it is not. The same trap runs through the inventory: an inactive source is a facility on the register, not a dry well, and an activity status is about the paperwork, not the pipe. None of these codes says whether anything was fixed. What the record does carry is what the agency DID: each row of a system's violations list carries every enforcement action with status Taken, name and date, in the agency's order. An action's name is the agency's record of that action, not a status computed from it — read `takenEnforcementActionsScope` before treating one as a status. **A result's UNIT is a property of the record, not of the analyte** — so two results for one substance at one system may not be in the same unit, and a maximum, average, trend or comparison-to-a-limit taken across them is wrong by a factor of 1000 and looks entirely plausible. MG/L and UG/L both occur, and neither the analyte code nor its name tells you which you have; only `reported.unit` on each result does. The easiest case to check is lead or HAA5 at almost any system with a long record, but it holds across a wide range of analytes: it is a property of how the agency records units rather than of a handful of chemicals. `findings.unitsVary` names the affected analytes for whatever your request matched, and every entry in `findings.detectedAnalytes` carries `unitsReported`. ***Read one of those two before you aggregate anything.*** Values are published exactly as recorded and nothing here is converted for you. **An aggregate is not a statistic about the system in front of you.** The category mix across every system in the register is not the mix for the one you fetched; a system whose violations are mostly contaminant-level is entirely ordinary. Any aggregate here behaves this way. Read the rows you actually fetched before characterising them. **A measurement record here is compared to no limit, and supplying the limit yourself is not reading it.** On the sample results and the lead-and-copper summaries, no action level, MCL value, drinking-water standard or health guideline is published, and nothing beside a value says whether it was acceptable. **The exception is the MDBP summaries** (`/mdbp-summaries`): there the agency records both the level it assessed the period against (`assessedAgainst`) and its own finding (`levelCompliance`), and a facility's `analyteLevels` on `/facilities/{facilityId}` are those levels. Quote them as the agency's; they are not a limit for any other record. Some records DO carry a numeric threshold of a different kind — `reportingLevel` on a chemical result is the level the LABORATORY reported against, which says what the instrument could see and nothing about what is permitted — so the presence of a threshold in a payload is not the presence of a limit. The case easiest to check is `/v1/water-systems/{pwsid}/lead-and-copper`: `ninetiethPercentile` is precisely the statistic the federal Lead and Copper Rule compares against an action level, so the statistic is published, the action level is not, and the comparison is not made. An agent that recalled the action level from its own training and reported the result against it was reporting a comparison this service had not made and the agency had not published here. **A limit you bring is also an undated one as far as this service is concerned:** it cannot tell you which limit you used, when it was set, or whether it is still the current one, and it holds no record that the comparison was made at all. So an answer that uses one has to name the limit as the reader's own. Violation CATEGORIES do name the kind of limit a determination concerned — `MCL` decodes to "Maximum Contaminant Level" — but that is the name of a category, never a value to compare against. What the agency DETERMINED is in `/violations`, which is a record of determinations rather than of measurements; where this API can say nothing about a limit, it says nothing. ## Missing data SDWIS records "not recorded" as a value rather than as null — a date of 1901-01-01, a meter size of 0. This API reports those as null and explains them in `dataQuality`. A null means the agency has no value on record. It does not mean zero. ## Labels and provenance `labelSource` says how far to trust a label: `sdwis-code-table` is the agency's own wording, `dww-help` is quoted from the help topics that ship inside Drinking Water Watch and are linked from every records page — `epa-standard` is federally defined, `curated-unverified` means no authoritative decode exists and this API inferred it, and `no-decode-available` means the label is null because nothing decodes that code — report the bare code. A `dww-help` label is published text and `GET /v1/code-tables/{domain}` names the file it is in, so the quotation can be checked. It is the application's wording rather than the agency's, and it describes what a field is CALLED rather than what it means. Do not attribute a `curated-unverified` label to anyone: this API inferred it. ## What is recorded about requests This installation records each request to `/v1`: the route, the query, the response status, how long it took, and the client name the request sent. It records a scrambled identifier derived from the network address, which changes every day, and never the address itself. Records are kept for 90 days and then deleted. ## Request limit This installation allows 600 requests a minute from one address to `/v1`, an average of 10 a second. Over that, the answer is HTTP 429 with a `Retry-After` header giving the seconds to wait, and nothing else changes: a script that waits that long and continues loses nothing. `/health` reports the current setting as `requestLog.rateLimit`. ## Scope This API is capped at what the agency's existing public application publishes — records held in SDWIS but not surfaced there are out of scope and will never appear. It does not yet serve all of it: see coverage.notYetServed for areas the agency's own page publishes that this API does not. Distinguish the two absences. An empty collection means nothing matched that collection's scope — its reading or scope says what that is, and it is often narrower than every record the agency holds — not that the agency has no such record. A missing endpoint means this API does not carry that record type, which is not evidence the records do not exist. Never report either as an absence of fact.