{
  "openapi": "3.1.0",
  "info": {
    "title": "Segura Public API",
    "version": "1.2.0",
    "summary": "Read water-quality test results as JSON, CSV or GeoJSON.",
    "description": "A read-only API over the Segura water-quality database. Authenticate with a key and pull results.\n\nThe API is deliberately small: list results, fetch one, summarise. Everything else — filtering, paging, incremental sync, export format, which regulatory standard to apply — is query parameters.\n\n**Try it now, no key required.** Use the published demo key `segura_demo_public_2026`. It serves a synthetic dataset from a separate database with no real measurements in it.\n\n**Reading a measurement correctly.** Always check `measurement.qualifier` before using `measurement.value`. A value of `null` with a qualifier of `not_detected` means the scan ran and found no peak; it does **not** mean zero, and it must not be recorded as zero. `value` is non-null only when `qualifier` is `ok`.\n\n**Reading compliance correctly.** `compliance.status` is judged against a named standard, given in the same object. `below_detection` is not a pass — see the field description. An EPA action level is not a per-sample health limit; `compliance.basis` tells you which kind you are looking at.\n\n**Synthetic data.** The Segura app has a demo mode that produces fabricated measurements. These are excluded from every listing by default. Do not pass `includeDemo=true` when populating a system of record.",
    "contact": {
      "name": "Segura",
      "url": "https://segura-water.com"
    }
  },
  "servers": [
    {
      "url": "https://app.segura-water.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "Results", "description": "Water-quality test results" },
    { "name": "Service", "description": "Availability" }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": ["Service"],
        "summary": "Check the API is up",
        "description": "No key required. Suitable for uptime monitoring.",
        "security": [],
        "responses": {
          "200": {
            "description": "The service is running.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["status"],
                  "properties": {
                    "status": { "type": "string", "const": "ok" },
                    "service": { "type": "string" },
                    "version": { "type": "string" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/analytes": {
      "get": {
        "tags": ["Service"],
        "summary": "The analyte vocabulary",
        "description": "The controlled list of metals and the measurement methods that target them. No key required. Fetch this to map Segura's codes onto your own test codes, and to notice when a metal or method is added.",
        "security": [],
        "responses": {
          "200": {
            "description": "The vocabulary.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["analytes", "methods"],
                  "properties": {
                    "analytes": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": { "type": "string", "examples": ["Pb"] },
                          "name": { "type": "string", "examples": ["Lead"] },
                          "cas": { "type": "string", "description": "CAS registry number, for mapping onto a LIMS test code.", "examples": ["7439-92-1"] },
                          "methods": {
                            "type": "array",
                            "description": "Methods that target this metal.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "id": { "type": "string" },
                                "range": { "type": ["string", "null"], "enum": ["low", "high", null] }
                              }
                            }
                          }
                        }
                      }
                    },
                    "methods": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": { "type": "string", "examples": ["lead-low-asv-v1"] },
                          "targets": { "type": "array", "items": { "type": "string" }, "description": "Analyte codes this method sets out to measure. Empty for an untargeted survey scan." },
                          "range": { "type": ["string", "null"], "enum": ["low", "high", null] },
                          "kind": { "type": "string", "enum": ["targeted", "survey"] }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/summary": {
      "get": {
        "tags": ["Results"],
        "summary": "Aggregate counts and exceedances",
        "description": "Groups the whole matching set — not a page — by batch, analyte or method, with a compliance count per group. This is what a supervisor wants instead of a list, and what a dashboard should call first.\n\nAccepts the same filters as `/results` (`since`, `until`, `cluster`, `supercluster`, `analyte`, `method`, `standard`, `includeDemo`).\n\nNote `notAssessed`: it counts results that were NOT judged against a limit — below detection, no limit published for that metal, unknown analyte. Those are deliberately kept out of `withinLimit`, because none of them is evidence of compliance.",
        "parameters": [
          {
            "name": "groupBy",
            "in": "query",
            "description": "What to group by.",
            "schema": { "type": "string", "enum": ["cluster", "supercluster", "analyte", "method"], "default": "cluster" }
          },
          { "name": "since", "in": "query", "schema": { "type": "string" } },
          { "name": "until", "in": "query", "schema": { "type": "string" } },
          { "name": "cluster", "in": "query", "schema": { "type": "string" } },
          { "name": "supercluster", "in": "query", "schema": { "type": "string" } },
          { "name": "analyte", "in": "query", "schema": { "type": "string" } },
          { "name": "method", "in": "query", "schema": { "type": "string" } },
          { "name": "standard", "in": "query", "schema": { "type": "string" } },
          { "name": "includeDemo", "in": "query", "schema": { "type": "boolean", "default": false } }
        ],
        "responses": {
          "200": {
            "description": "Aggregates.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["groupBy", "standard", "totalResults", "groups"],
                  "properties": {
                    "groupBy": { "type": "string" },
                    "standard": { "type": "string", "description": "The standard the counts were judged against." },
                    "totalResults": { "type": "integer" },
                    "truncated": { "type": "boolean", "description": "True if the matching set hit the 10000-record ceiling and the aggregate is therefore partial. Narrow the filters." },
                    "mode": { "type": "string", "enum": ["demo"], "description": "Present only on the public demo key." },
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "group": { "type": "string", "description": "The cluster / supercluster / analyte code / method id. `(unspecified)` where the record has none." },
                          "results": { "type": "integer" },
                          "qualifiers": { "type": "object", "description": "Count per qualifier, e.g. {\"ok\":4,\"not_detected\":17}." },
                          "exceeding": { "type": "integer", "description": "Results over the limit." },
                          "withinLimit": { "type": "integer", "description": "Results measured AND under the limit." },
                          "notAssessed": { "type": "integer", "description": "Results not judged: below detection, no published limit, or unknown analyte. NOT evidence of compliance." },
                          "maxValue": { "type": ["number", "null"] },
                          "unit": { "type": "string", "const": "ppb" },
                          "firstMeasuredAt": { "type": ["string", "null"] },
                          "lastMeasuredAt": { "type": ["string", "null"] }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/results/{sessionId}/photo": {
      "get": {
        "tags": ["Results"],
        "summary": "The field photograph for a result",
        "description": "Returns the image attached in the field, if there is one. Addressed by `sessionId` rather than filename, so there is no directory to enumerate. 404 when the result has no photo, does not exist, or is synthetic and you did not ask for demo data.\n\nOn the public demo key this returns a generated SVG placeholder, labelled on its face as synthetic — a demo will not hand you an image that could later be mistaken for a real sample.",
        "parameters": [
          { "name": "sessionId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "includeDemo", "in": "query", "schema": { "type": "boolean", "default": false } }
        ],
        "responses": {
          "200": {
            "description": "The image.",
            "content": {
              "image/jpeg": { "schema": { "type": "string", "format": "binary" } },
              "image/png": { "schema": { "type": "string", "format": "binary" } },
              "image/svg+xml": { "schema": { "type": "string", "description": "Demo placeholder only." } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/results": {
      "get": {
        "tags": ["Results"],
        "summary": "List results",
        "description": "This endpoint has two modes.\n\n**Browse** (the default) returns results newest first by measurement time, paged with `limit`/`offset`. Good for looking around, reports, and one-off extracts.\n\n**Sync** is entered by passing `cursor`, and is the only correct way to replicate results into another system. It returns results oldest-change-first and hands back a `nextCursor` to resume from. Start with `cursor=0` and keep calling with the `nextCursor` you were last given, including after you catch up — the token stays valid and new work appends to the feed.\n\n**Do not build a sync on `since`.** It filters on `measuredAt`, which is set by the device, not by us. A phone that was offline for a fortnight uploads a record already dated two weeks ago — below any high-water mark you have advanced past — and you would never receive it, with nothing to indicate a gap. `cursor` orders by `updatedAt`, which this server stamps on every write, so late uploads and later corrections both land at the end of the feed where you will see them. Keep `since` for what it is good at: asking a domain question like \"everything measured in August\".\n\nThe feed is at-least-once. A record can be re-delivered (a re-sync, a re-file on the dashboard); de-duplicate on `sessionId` and treat a repeat as an update.",
        "parameters": [
          {
            "name": "cursor",
            "in": "query",
            "description": "Switches to sync mode and resumes from this position. Use `0` to start from the beginning, then pass back the `nextCursor` from each response. Opaque — do not construct or parse these yourself.",
            "schema": { "type": "string", "examples": ["0"] }
          },
          {
            "name": "since",
            "in": "query",
            "description": "Only results MEASURED at or after this instant. A domain filter, not a sync watermark — see the description. ISO-8601 date or datetime.",
            "schema": { "type": "string", "examples": ["2026-08-01", "2026-08-01T09:00:00Z"] }
          },
          {
            "name": "cluster",
            "in": "query",
            "description": "Exact match on the batch/folder name the results were filed under.",
            "schema": { "type": "string" }
          },
          {
            "name": "supercluster",
            "in": "query",
            "description": "Every result whose cluster is filed under this campaign — one call for a whole town's survey instead of one per day. Case-insensitive.",
            "schema": { "type": "string", "examples": ["Kisumu"] }
          },
          {
            "name": "analyte",
            "in": "query",
            "description": "Every result whose method targets this metal, across all of its range and combination variants. Accepts a code or a name, case-insensitively: `Pb` and `Lead` are the same query, and both match the plain, low-range, high-range and Lead/Copper methods. An unrecognised value is a 400 listing what is valid — never an empty 200. See `GET /analytes`.",
            "schema": { "type": "string", "examples": ["Pb", "Lead", "Mn"] }
          },
          {
            "name": "method",
            "in": "query",
            "description": "Exact match on one measurement method, e.g. `lead-low-asv-v1`. Use this when the range matters and `analyte` is too broad. See `GET /analytes`.",
            "schema": { "type": "string", "examples": ["lead-low-asv-v1"] }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Results per page.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 1000, "default": 100 }
          },
          {
            "name": "offset",
            "in": "query",
            "description": "Results to skip, for paging. Browse mode only — ignored, and omitted from the response, when `cursor` is supplied.",
            "schema": { "type": "integer", "minimum": 0, "default": 0 }
          },
          {
            "name": "until",
            "in": "query",
            "description": "Only results MEASURED at or before this instant. Pairs with `since`. An inverted range is a 400, not an empty result.",
            "schema": { "type": "string", "examples": ["2026-08-31"] }
          },
          {
            "name": "standard",
            "in": "query",
            "description": "Which regulatory standard to judge results against — `WHO-2022`, `US-EPA`, `EU-2020`. Defaults to WHO. See `GET /analytes` for the published limits. An unrecognised name is a 400; it never falls back to another jurisdiction's numbers.",
            "schema": { "type": "string", "examples": ["WHO-2022"] }
          },
          {
            "name": "exceeds",
            "in": "query",
            "description": "`true` returns only results over the limit.\n\nFiltering happens BEFORE paging: rows are scanned forward and the page is cut from the matching breaches, so `limit=1` returns a breach rather than an empty page that happens to begin with a passing result. In browse mode `total` is therefore an exact count of breaches and `offset` pages the filtered set. Exports get the whole filtered set, not a filtered page.\n\nIn cursor mode `total` is null — a feed has no end to count — and the cursor advances over every row scanned, not merely those returned, so a filtered feed still makes progress. If the scan hits its 10000-record ceiling the response carries `truncated: true` and a note; narrow the range with `since`/`until`.\n\nMulti-metal results are included: their roll-up status is used, so a Lead/Copper scan breaching on either metal appears here.",
            "schema": { "type": "boolean", "default": false }
          },
          {
            "name": "format",
            "in": "query",
            "description": "`json` (default), `csv`, or `geojson`.\n\n`csv` is RFC 4180 with a UTF-8 BOM, and puts `qualifier` beside `value` so an empty value cell can never be read as zero. `geojson` is a FeatureCollection of Points; results without coordinates are omitted, and the number omitted is reported in a `segura` member on the collection rather than left for you to notice.\n\nExports may set `limit` up to 10000; JSON stays at 1000.",
            "schema": { "type": "string", "enum": ["json", "csv", "geojson"], "default": "json" }
          },
          {
            "name": "includeDemo",
            "in": "query",
            "description": "Include synthetic demo-mode measurements. Off by default. Leave it off for any system of record. No effect on the public demo key, where every record is synthetic by construction.",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of results. Content type follows `format`: `application/json`, `text/csv`, or `application/geo+json`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["total", "count", "limit", "results"],
                  "properties": {
                    "total": { "type": "integer", "description": "Browse mode: total results matching the filter. Sync mode: how many remain after this cursor, so you can see the size of the backlog." },
                    "count": { "type": "integer", "description": "Results in this page." },
                    "limit": { "type": "integer" },
                    "offset": { "type": "integer", "description": "Browse mode only. Absent in sync mode." },
                    "nextCursor": { "type": "string", "description": "Sync mode only. Pass this back to get the next batch. Always present in sync mode, including on an empty page — an exhausted feed is not a finished one, so keep the token and poll again rather than restarting from 0." },
                    "hasMore": { "type": "boolean", "description": "Sync mode only. True when this page filled completely, which almost always means more is waiting. False means you have caught up." },
                    "results": { "type": "array", "items": { "$ref": "#/components/schemas/Result" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/results/{sessionId}": {
      "get": {
        "tags": ["Results"],
        "summary": "Fetch one result",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "description": "The result's identifier, as returned by /results.",
            "schema": { "type": "string" }
          },
          {
            "name": "include",
            "in": "query",
            "description": "Pass `raw` to append the unprocessed measurement traces. These are large — thousands of points per run — so they are opt-in.",
            "schema": { "type": "string", "enum": ["raw"] }
          },
          {
            "name": "includeDemo",
            "in": "query",
            "description": "Required to fetch a synthetic demo record by id. Without it a demo record returns 404, identically to one that does not exist — the same rule the listing applies, enforced here so a stored session id cannot be used to pull fabricated data.",
            "schema": { "type": "boolean", "default": false }
          }
        ],
        "responses": {
          "200": {
            "description": "The result.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/Result" } }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A key issued by a Segura administrator, sent as `Authorization: Bearer segura_...`. Keys are long-lived and are revoked rather than expiring.\n\n**Try it without one.** `segura_demo_public_2026` is a published demo key. It serves a synthetic dataset held in a separate database that contains no real measurements — every record comes back with `isDemo: true` and every response carries `mode: \"demo\"`. Use it to build and test an integration before asking anyone for access."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "A query parameter was malformed. The message says which.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unauthorized": {
        "description": "The key was missing, unrecognised, or revoked.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "No result with that identifier.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": ["error", "message"],
        "properties": {
          "error": {
            "type": "string",
            "description": "A stable machine-readable code. Branch on this, not on the message.",
            "enum": ["missing_key", "invalid_key", "revoked_key", "invalid_query", "not_found"]
          },
          "message": { "type": "string", "description": "Human-readable explanation. May change; do not parse." }
        }
      },
      "Result": {
        "type": "object",
        "description": "One water-quality test.",
        "required": ["sessionId", "measuredAt", "receivedAt", "updatedAt", "sample", "location", "analyte", "measurement", "compliance", "photo", "signal", "instrument", "provenance", "isDemo"],
        "properties": {
          "sessionId": { "type": "string", "description": "Stable identifier for this test. De-duplicate on it." },
          "measuredAt": { "type": "string", "format": "date-time", "description": "ISO-8601 with a UTC offset, from the device. When the sample was tested. Do not use as a sync watermark — see the /results description." },
          "receivedAt": { "type": "string", "format": "date-time", "description": "ISO-8601 UTC (`...Z`). When this server first received the record. Later than measuredAt when the device was offline in the field." },
          "updatedAt": { "type": "string", "format": "date-time", "description": "ISO-8601 UTC (`...Z`). When this record last changed here — first upload, a re-sync carrying a correction, or a re-file. Server-stamped, monotonic, and what `cursor` orders by. The only field safe to treat as a sync watermark." },
          "sample": {
            "type": "object",
            "required": ["name", "cluster", "supercluster", "operator"],
            "properties": {
              "name": { "type": ["string", "null"], "description": "Free text as typed by the operator. Not a controlled identifier — do not use it as a foreign key." },
              "cluster": { "type": ["string", "null"], "description": "Batch or folder the test was filed under." },
              "supercluster": { "type": ["string", "null"], "description": "The campaign the cluster belongs to — a town surveyed over several days files each day's cluster under it. Resolved from the cluster name, so it is null for a test with no cluster and for a cluster nobody has filed. Matched case-insensitively by the `supercluster` filter." },
              "operator": { "type": ["string", "null"] }
            }
          },
          "location": {
            "type": "object",
            "required": ["latitude", "longitude", "description"],
            "properties": {
              "latitude": { "type": ["number", "null"] },
              "longitude": { "type": ["number", "null"] },
              "description": { "type": ["string", "null"] }
            }
          },
          "analyte": {
            "type": "object",
            "description": "What the method set out to measure, resolved to a controlled vocabulary — NOT the operator's display label, which is `provenance.testType`.",
            "required": ["kind", "code", "name", "cas", "targets", "range"],
            "properties": {
              "kind": {
                "type": "string",
                "enum": ["targeted", "survey", "unknown"],
                "description": "`targeted` — the method aimed at specific metals, listed in `targets`.\n`survey` — an untargeted sweep looking for whatever is present; `targets` is empty by nature, and findings are in `signal.detectedPeaks`.\n`unknown` — a record predating method tracking. Not a guess; genuinely unrecoverable."
              },
              "code": { "type": ["string", "null"], "description": "The metal that `measurement.value` refers to, e.g. `Pb`. **Null when the method targets more than one metal** — the single top-level value would be ambiguous, so use `signal.detectedPeaks` instead. Also null for survey and unknown.", "examples": ["Pb"] },
              "name": { "type": ["string", "null"], "examples": ["Lead"] },
              "cas": { "type": ["string", "null"], "description": "CAS registry number, for mapping onto a LIMS test code.", "examples": ["7439-92-1"] },
              "targets": { "type": "array", "items": { "type": "string" }, "description": "Every metal the method aims at, e.g. `[\"Pb\",\"Cu\"]`. What it looked for, not what it found.", "examples": [["Pb"]] },
              "range": { "type": ["string", "null"], "enum": ["low", "high", null], "description": "Calibration range of the method, where it has one." }
            }
          },
          "measurement": {
            "type": "object",
            "description": "The reportable result. Read `qualifier` first.",
            "required": ["value", "unit", "qualifier"],
            "properties": {
              "value": {
                "type": ["number", "null"],
                "description": "Concentration. Non-null ONLY when qualifier is `ok`. Null otherwise — never substitute zero."
              },
              "unit": { "type": "string", "const": "ppb", "description": "Micrograms per litre. Always stated explicitly." },
              "qualifier": {
                "type": "string",
                "enum": ["ok", "not_detected", "not_quantified", "error", "no_result"],
                "description": "`ok` — quantified, use `value`.\n`not_detected` — the scan ran, the analysis completed, and it found no peak. Below what this method can see. **NOT zero, and not a failure** — this is a reportable result. Where the search looked is recorded in `note`.\n`not_quantified` — a peak was found but could not be converted to a concentration, typically because no calibration exists for this analyte.\n`error` — the run or the analysis could not complete; see `error`. A sample worth re-running.\n`no_result` — uploaded with no analysis attached.\n\nThe distinction between `not_detected` and `error` is the one most worth getting right on your side: the first is an answer, the second is the absence of one."
              },
              "quantifiedBy": {
                "type": ["string", "null"],
                "enum": ["area", "height", null],
                "description": "Which peak measure produced the value. Values derived by area and by height are not directly comparable."
              },
              "dilutionFactor": { "type": ["number", "null"], "description": "Pre-dilution applied to the sample before measurement." },
              "note": { "type": ["string", "null"], "description": "Qualifying remark recorded with the result. For a `not_detected` result this also carries the analysis's own account of where it searched, e.g. \"No peak found at -600 ± 90 mV\". Multiple remarks are joined with \" — \"." },
              "error": { "type": ["string", "null"], "description": "Why the run or analysis could not complete. Populated ONLY when `qualifier` is `error` — a below-detection result leaves this null, so branching on `error != null` is safe." }
            }
          },
          "compliance": {
            "type": "object",
            "description": "Whether this result is over a named regulatory limit. **Always read `standard` with `status`** — \"over the limit\" is meaningless until you say whose limit, and the three published standards give three different numbers for the same metal.",
            "required": ["standard", "limit", "unit", "status"],
            "properties": {
              "standard": { "type": "string", "examples": ["WHO-2022"] },
              "standardLabel": { "type": "string", "description": "Full title and edition of the standard." },
              "reference": { "type": "string", "description": "Where to read it." },
              "limit": { "type": ["number", "null"], "description": "The limit applied, in ppb. Null when this standard publishes none for this metal." },
              "unit": { "type": "string", "const": "ppb" },
              "basis": {
                "type": ["string", "null"],
                "enum": ["health", "action-level", "aesthetic", "indicator", null],
                "description": "What KIND of limit this is, which changes what a breach means. `health` is a toxicological guideline. `action-level` (US EPA lead/copper) is a 90th-percentile trigger for utility treatment obligations, **not** a per-sample health limit — applying it to one sample is a category error. `aesthetic`/`indicator` concern taste, odour and staining, not toxicity."
              },
              "status": {
                "type": "string",
                "enum": ["exceeds", "within_limit", "below_detection", "not_assessed", "no_limit", "not_applicable"],
                "description": "`exceeds` — measured above the limit.\n`within_limit` — measured, and below it.\n`below_detection` — nothing was found. **Deliberately not a pass**: this method's detection floor is not known to sit under every limit, so \"we saw nothing\" does not establish \"under 10 ppb\".\n`not_assessed` — no usable number (error, no result, peak not quantified).\n`no_limit` — this standard publishes none for this metal. Silence from a standard is not permission.\n`not_applicable` — the analyte is unknown, so there is nothing to compare."
              },
              "ratio": { "type": ["number", "null"], "description": "value ÷ limit, to three places. Null unless a comparison was actually made." },
              "perAnalyte": {
                "type": "array",
                "description": "Present ONLY for multi-metal methods (`analyte.targets` has more than one entry), where a single top-level limit would be meaningless — there `limit`, `basis` and `ratio` are null and `status` is a roll-up of this array.\n\nEvery metal the method targets appears, including ones that produced no peak: a Lead/Copper scan that found only lead has not shown the water is free of copper. The roll-up is `exceeds` if any metal exceeds, `within_limit` only if EVERY metal was measured and passed, and `not_assessed` otherwise — one metal passing while another was never seen is a partial answer, not a pass.",
                "items": {
                  "type": "object",
                  "properties": {
                    "code": { "type": "string", "examples": ["Pb"] },
                    "name": { "type": "string" },
                    "cas": { "type": "string" },
                    "value": { "type": ["number", "null"], "description": "This metal's concentration, from its own peak." },
                    "unit": { "type": "string", "const": "ppb" },
                    "qualifier": { "type": "string", "enum": ["ok", "not_detected", "not_quantified"] },
                    "limit": { "type": ["number", "null"], "description": "This metal's own limit under the selected standard." },
                    "basis": { "type": ["string", "null"] },
                    "status": { "type": "string", "enum": ["exceeds", "within_limit", "below_detection", "not_assessed", "no_limit"] },
                    "ratio": { "type": ["number", "null"] }
                  }
                }
              }
            }
          },
          "photo": {
            "type": ["object", "null"],
            "description": "Present only when a photograph was taken in the field.",
            "properties": {
              "url": { "type": "string", "description": "Path on this API, taking the same key. Not the dashboard's /photos path, which needs a user session you do not have." }
            }
          },
          "signal": {
            "type": "object",
            "description": "The underlying electrochemistry. Useful for reviewing a borderline result; ignorable otherwise.",
            "properties": {
              "peakFound": { "type": ["boolean", "null"] },
              "peakPotential_mV": { "type": ["number", "null"] },
              "peakCurrent_nA": { "type": ["number", "null"] },
              "peakArea_nAmV": { "type": ["number", "null"] },
              "peakWidth_mV": { "type": ["number", "null"] },
              "signalToNoise": { "type": ["number", "null"] },
              "detectedPeaks": {
                "type": ["array", "null"],
                "description": "Per-peak identification, produced by multi-metal and survey methods. **When `analyte.code` is null because the method targets several metals, this is the result** — the single top-level `measurement.value` is not. Null when the analysis did not produce one.",
                "items": {
                  "type": "object",
                  "properties": {
                    "metal": { "type": ["string", "null"], "description": "Identified metal, or `Unidentified` — an honest value meaning a peak was seen at a potential the method could not attribute." },
                    "potential_mV": { "type": ["number", "null"] },
                    "current_nA": { "type": ["number", "null"] },
                    "value": { "type": ["number", "null"], "description": "Concentration for this peak, or null where no calibration exists." },
                    "unit": { "type": "string", "const": "ppb" }
                  }
                }
              }
            }
          },
          "instrument": {
            "type": "object",
            "properties": {
              "readerDeviceId": { "type": ["string", "null"] },
              "phoneModel": { "type": ["string", "null"] }
            }
          },
          "provenance": {
            "type": "object",
            "description": "How the run was taken. Retained for audit.",
            "properties": {
              "testType": { "type": ["string", "null"], "description": "The free-text label the operator saw, e.g. `Lead — Low Range (0–100 ppb)`. Traceability only — do not filter or group on it; use `analyte` or `templateId`." },
              "templateId": { "type": ["string", "null"], "description": "Stable identifier for the measurement method, e.g. `lead-low-asv-v1`. Filterable via the `method` parameter." },
              "templateName": { "type": ["string", "null"] },
              "appVersion": { "type": ["string", "null"] },
              "appBuild": { "type": ["string", "null"] },
              "uploadedBy": { "type": ["string", "null"], "description": "Account that uploaded the record." }
            }
          },
          "isDemo": {
            "type": "boolean",
            "description": "True for synthetic demo-mode data. Never present unless includeDemo=true was passed."
          },
          "raw": {
            "type": "object",
            "description": "Unprocessed traces. Present only when include=raw.",
            "properties": {
              "ocp": { "type": ["object", "null"] },
              "deposition": { "type": ["object", "null"] },
              "swv": { "type": ["object", "null"] }
            }
          }
        }
      }
    }
  }
}
