{"components":{"schemas":{"AbuseOverview":{"description":"Self-serve abuse signals for the overview — the issuance-velocity\ncounters the signup path already writes, surfaced for the first time.","properties":{"distinct_ips_today":{"description":"Distinct source IPs that issued a key today (UTC) — a spike relative\nto signups is an IP-rotating-farm signal.","format":"int64","type":"integer"},"issuances_this_hour":{"description":"Key issuances recorded in the current UTC hour (across all IPs).","format":"int64","type":"integer"},"issuances_today":{"description":"Self-serve key issuances recorded today (UTC), across all IPs.","format":"int64","type":"integer"},"max_issuances_per_hour":{"description":"The configured global hourly ceiling (`SN_MAX_ISSUANCES_PER_HOUR`);\n`0` means the ceiling is disabled.","format":"int64","type":"integer"}},"required":["issuances_this_hour","max_issuances_per_hour","issuances_today","distinct_ips_today"],"type":"object"},"ActivationOverview":{"description":"Activation funnel for the overview — the time-to-first-call north star\n(`docs/DEVELOPER-FEEDBACK-LOOP.md`). Computed over the signup cohort of\nthe last [`ACTIVATION_WINDOW_DAYS`] days: what share of those signups\never reached a first successful metered call, and how long it took the\nones that did. Recent, still-pending signups are counted as signups but\nnot yet converted, so the rate is a conservative lower bound for the\nfreshest cohort. Percentiles are `null` until at least one identity in\nthe window has activated.","properties":{"conversion_rate":{"description":"`converted / signups`, in `0.0..=1.0`; `0.0` when there were no\nsignups in the window.","format":"double","type":"number"},"converted":{"description":"Of those, how many reached a first successful metered call.","format":"int64","type":"integer"},"median_ttfc_seconds":{"description":"Median (p50) time-to-first-call in whole seconds across the converted\nidentities, or `null` if none have converted.","format":"int64","type":["integer","null"]},"p90_ttfc_seconds":{"description":"p90 time-to-first-call in whole seconds across the converted\nidentities, or `null` if none have converted.","format":"int64","type":["integer","null"]},"signups":{"description":"Identities created within the window.","format":"int64","type":"integer"},"window_days":{"description":"Length of the signup-cohort window, in days.","format":"int64","type":"integer"}},"required":["window_days","signups","converted","conversion_rate"],"type":"object"},"AdoptStyleRequest":{"description":"Body for `POST /admin/styles/{id}/owner`.","properties":{"key_id":{"description":"Id of the API key that should own the style from now on.","type":"string"}},"required":["key_id"],"type":"object"},"AdrCheckRequest":{"description":"Doc-only mirror of the `/adr/check` request body.","properties":{"adr":{"$ref":"#/components/schemas/AdrVehicleProfile","description":"The vehicle profile and dangerous-goods declaration."},"tunnel_category":{"description":"The ADR category of the tunnel to check.","example":"D","type":"string"}},"required":["adr","tunnel_category"],"type":"object"},"AdrVehicleProfile":{"description":"Doc-only mirror of [`sn_adr::AdrVehicleProfile`].","properties":{"dimensions":{"$ref":"#/components/schemas/TruckDimensions","description":"Physical dimensions of the transport unit."},"hazmat":{"description":"Whether the vehicle carries dangerous goods at all.","type":"boolean"},"tunnel_code":{"description":"ADR 8.6.4 tunnel restriction code of the load, e.g. `\"B/D\"`,\n`\"C5000D\"` or `\"(—)\"`; omit when unknown.","example":"C/E","type":["string","null"]}},"required":["dimensions","hazmat"],"type":"object"},"Advisory":{"description":"A note attached to a stretch of route: vegetation age, a wire-limited\nreading, a seam between datasets.","properties":{"note":{"description":"The note, in a sentence.","type":"string"},"section":{"$ref":"#/components/schemas/RouteSection","description":"The stretch the note is about."}},"required":["section","note"],"type":"object"},"ApiKeyRecord":{"description":"A stored API key (no plaintext, no hash exposure in serialisation).","properties":{"created_at":{"description":"RFC 3339 creation timestamp (UTC).","type":"string"},"expires_at":{"description":"RFC 3339 expiry (provisional keys only).","type":["string","null"]},"first_success_at":{"description":"RFC 3339 timestamp of the key's first successful (2xx) metered API\ncall — the activation event — or `null` if it has never had one.","type":["string","null"]},"id":{"description":"Opaque key id (UUID v4).","type":"string"},"monthly_quota":{"description":"Requests allowed per calendar month.","format":"int64","type":"integer"},"name":{"description":"Human-readable key name.","type":"string"},"rate_per_min":{"description":"Requests allowed per minute.","format":"int64","type":"integer"},"revoked_at":{"description":"RFC 3339 revocation timestamp, if revoked.","type":["string","null"]},"state":{"description":"Lifecycle state: `admin`, `provisional` or `verified`.","type":"string"}},"required":["id","name","created_at","monthly_quota","rate_per_min","state"],"type":"object"},"AssetRevenue":{"description":"Settled x402 revenue for one (network, asset) pair.","properties":{"amount_atomic":{"description":"Sum of their `amount_atomic` in the asset's base units (decimal\nstring — u128-range, so summed in Rust, never in SQL).","type":"string"},"asset":{"description":"Asset (token contract / SPL mint); `null` when the settlement row\nrecorded none.","type":["string","null"]},"network":{"description":"Network the settlements landed on, e.g. `base`.","type":"string"},"settlements":{"description":"Successful settlements in this bucket.","format":"int64","type":"integer"}},"required":["network","settlements","amount_atomic"],"type":"object"},"CameraAlong":{"description":"One camera along the route.","properties":{"arc_m":{"description":"Distance along the route shape to the camera's projection, metres\n(ascending in the response). For a corridor this is its start —\nthe same value as `from_arc_m`.","format":"double","type":"number"},"bearing":{"description":"Camera facing in degrees clockwise from north, when known.","format":"int32","minimum":0,"type":["integer","null"]},"from_arc_m":{"description":"Distance along the route to the corridor's start, metres. Corridor\nrecords only.","format":"double","type":["number","null"]},"kind":{"description":"`fixed`, `average`, `redlight`, `mobile_site` or `unknown`.","type":"string"},"lat":{"description":"WGS84 latitude of the camera itself (a corridor's start).","format":"double","type":"number"},"licence":{"description":"SPDX licence identifier of this record's source.","type":["string","null"]},"limit_confidence":{"description":"How far `limit_kph` can be trusted: `official`, `osm`,\n`inferred_from_road` or `unknown` (also the value whenever\n`limit_kph` is `null`).","type":"string"},"limit_kph":{"description":"Enforced limit in km/h, when known. `null` is common and honest:\nIreland's published zones carry no limit at all.","format":"int32","minimum":0,"type":["integer","null"]},"lon":{"description":"WGS84 longitude.","format":"double","type":"number"},"source":{"description":"The `meta.sources[].id` this record came from, when the sidecar\nrecords provenance.","type":["string","null"]},"to_arc_m":{"description":"Distance along the route to the corridor's end, metres. Corridor\nrecords only. May be less than `from_arc_m` when the route runs\nagainst the corridor's published direction — a mobile zone is\nenforced both ways, so the direction carries no meaning; take the\nenforced span as the interval between the two.","format":"double","type":["number","null"]},"zone":{"description":"Corridor geometry, on corridor records only (average-speed\nsections and mobile-camera zones). Absent for point devices.","type":"object"}},"required":["arc_m","lat","lon","kind","limit_confidence"],"type":"object"},"CamerasAlongRequest":{"description":"Request body.","properties":{"buffer_m":{"description":"Corridor half-width in metres; default 40, clamped to at most 100.","format":"double","type":["number","null"]},"shape":{"description":"The route shape as a polyline6 string.","type":"string"}},"required":["shape"],"type":"object"},"CamerasAlongResponse":{"description":"Response body.","properties":{"attribution":{"description":"Attribution notices for the data actually returned, to display\nverbatim. Empty when no cameras were returned. Deduplicated and\nordered by territory, then by the source's own order.","items":{"type":"string"},"type":"array"},"cameras":{"description":"Cameras within the corridor, `arc_m` ascending. Always empty in\nan `omitted` jurisdiction.","items":{"$ref":"#/components/schemas/CameraAlong"},"type":"array"},"policy":{"$ref":"#/components/schemas/CamerasPolicy","description":"The policy that was applied server-side."}},"required":["cameras","policy"],"type":"object"},"CamerasPolicy":{"description":"The jurisdiction policy the gateway applied.","properties":{"country":{"description":"ISO 3166-1 alpha-2 country the policy was resolved for (`\"ZZ\"`\nwhen no camera data exists near the route).","type":"string"},"mode":{"description":"`exact` or `omitted` — see [`crate::cameras_policy`].","type":"string"}},"required":["country","mode"],"type":"object"},"ClaimMintResponse":{"description":"Response for `POST /v1/keys/claim`.","properties":{"claim_url":{"description":"Zero-authority claim URL for the human behind this agent.","type":"string"},"display_to_user":{"description":"Sentence for the agent to show its human verbatim (no secrets).","type":"string"},"expires_in_h":{"description":"RFC 3339 pointer expiry.","format":"int64","type":"integer"}},"required":["claim_url","expires_in_h","display_to_user"],"type":"object"},"ClaimResolveResponse":{"description":"Response for `GET /v1/console/claims/{token}`.","properties":{"masked_email":{"description":"Masked delivery email (`m•••@example.com`) — the only fact a\npointer reveals.","type":"string"}},"required":["masked_email"],"type":"object"},"ClearanceAlongRequest":{"description":"Request body for `POST /v1/clearance/along`.","properties":{"datasets":{"description":"Narrow the search to these datasets. The default is every dataset\nthis key may see whose survey covers the corridor. A name this key\nmay not see, or that does not exist, is dropped: the answer is the\nsame \"not surveyed\" either way.","items":{"type":"string"},"type":["array","null"]},"geometry_polyline6":{"description":"The corridor as a Google encoded polyline, six-digit precision —\ntypically `geometry_polyline6` from a prior `/route` response.\nCapped at 10,000 vertices.","type":"string"},"margin_m":{"description":"The caller's operating margin in metres, added to the height.\nDefaults to zero, and is echoed back: no compliance policy is baked\nin here, both figures are reported.","format":"double","type":"number"},"vehicle_height_m":{"description":"Vehicle height in metres. Required: there is no default vehicle.","format":"double","type":"number"},"vehicle_width_m":{"description":"Vehicle width in metres. The width axis is assessed only when this\nis given; without it the report answers for height and says so.","format":"double","type":["number","null"]}},"required":["geometry_polyline6","vehicle_height_m"],"type":"object"},"ClearanceArtefactRow":{"description":"One artefact's row in the [`ClearanceOverview`].","properties":{"artefact":{"description":"Artefact stem under `<dataset>/clearance/`.","type":"string"},"cell_size_m":{"description":"Grid cell size in metres.","format":"double","type":["number","null"]},"crs_epsg":{"description":"EPSG code of the grid's projected CRS.","format":"int32","minimum":0,"type":["integer","null"]},"dataset":{"description":"Dataset id (directory name).","type":"string"},"error":{"description":"Why it did not open. Present only on `unreadable` rows.","type":["string","null"]},"grid":{"description":"Grid dimensions, columns by rows.","items":{"format":"int32","minimum":0,"type":"integer"},"type":["array","null"]},"qa":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/QaOut","description":"The bake's own QA canary, verbatim."}]},"sources_confirmed":{"description":"Sources the bake recorded that the manifest still confirms.","minimum":0,"type":"integer"},"sources_declared":{"description":"Source files the manifest declares for this dataset.","minimum":0,"type":"integer"},"stale":{"description":"Whether the artefact lags the sources the manifest declares, or\ncannot be confirmed against them. Either way it decides no `pass`.","type":"boolean"},"status":{"description":"`ok` when the artefact opened, `unreadable` when it did not.","type":"string"},"survey_dates":{"description":"Capture range, `from/to` in ISO dates.","type":["string","null"]},"vertical_datum":{"description":"The vertical frame, spelled for a reader.","type":["string","null"]}},"required":["dataset","artefact","status","stale","sources_declared","sources_confirmed"],"type":"object"},"ClearanceEnforcement":{"description":"The honesty block carried on every response, including the clear one.\n\nCopied from the ADR tunnel-enforcement pattern for the same reason: the\nclear answer is the dangerous one, because it reads as a certificate.","properties":{"basis":{"description":"Always `surveyed_pointcloud_within_survey_difference`.","type":"string"},"caveat":{"description":"What this answer does and does not prove.","type":"string"},"route_certified":{"description":"Always false.","type":"boolean"},"vertical_datum":{"description":"The vertical frame the surveyed figures live in, spelled for a\nreader (Rule D5).","type":"string"}},"required":["basis","route_certified","vertical_datum","caveat"],"type":"object"},"ClearanceManifest":{"description":"The `clearance` block of a dataset's `dataset.json`.\n\nWritten by the operator alongside the bake. Its presence is what makes a\ndataset answerable at all; its `sources` are the authority the sidecar's\nown source hashes are checked against.","properties":{"artefact":{"description":"Artefact stem under `<dataset>/clearance/`, so `downtown` names\n`downtown.clr.bin` and `downtown.clr.json`. Same grammar as a\ndataset id (lowercase, digits, hyphen), so it can never escape the\ndataset directory.","type":"string"},"sources":{"additionalProperties":{"type":"string"},"description":"The source files the dataset currently holds, keyed by the name the\nbake recorded, valued by SHA-256. Empty means nothing can be\nconfirmed, and nothing baked from it will pass.","propertyNames":{"type":"string"},"type":"object"}},"required":["artefact"],"type":"object"},"ClearanceOverview":{"description":"Response for `GET /admin/clearance`.","properties":{"artefacts":{"description":"Every dataset carrying a clearance block, dataset order, whether or\nnot its artefact could be read.","items":{"$ref":"#/components/schemas/ClearanceArtefactRow"},"type":"array"},"enabled":{"description":"Whether any clearance artefact is configured at all. `false` is\nwhat makes `POST /v1/clearance/along` answer 501.","type":"boolean"}},"required":["enabled","artefacts"],"type":"object"},"CommercialFunnel":{"description":"Signup → key → activation → paid funnel, as counts per stage.","properties":{"activated_identities":{"description":"Identities with at least one key that ever had a successful metered\ncall (`api_keys.first_success_at` set).","format":"int64","type":"integer"},"activated_keys":{"description":"Keys with `first_success_at` set — the same activation event counted\nper key rather than per identity.","format":"int64","type":"integer"},"identities":{"description":"Identities ever created.","format":"int64","type":"integer"},"median_activation_seconds":{"description":"Median time from signup to first successful call, in whole seconds,\nacross every identity that ever activated (not just a recent\ncohort); `null` until at least one has. Computed by the pure,\ntested [`crate::activation::activation_stats`].","format":"int64","type":["integer","null"]},"paid_identities":{"description":"Identities on a plan other than [`FREE_PLAN`].","format":"int64","type":"integer"},"with_key":{"description":"Identities holding at least one key (revoked keys included).","format":"int64","type":"integer"}},"required":["identities","with_key","activated_identities","activated_keys","paid_identities"],"type":"object"},"CommercialResponse":{"description":"Response for `GET /admin/commercial`.","properties":{"demo_lane":{"$ref":"#/components/schemas/DemoLane","description":"What the keyless demo lane gave away over the same window."},"funnel":{"$ref":"#/components/schemas/CommercialFunnel","description":"Signup → key → activation → paid funnel."},"generated_at":{"description":"RFC 3339 timestamp this snapshot was computed at.","type":"string"},"prepaid":{"$ref":"#/components/schemas/PrepaidRevenue","description":"The prepaid (pay-as-you-go) rail: card and crypto top-ups this\ncalendar month, what is still unspent across every identity, and\nwhether the per-call settlement worker is failing. Until this\nblock, \"top-up revenue this month\" was not a computable number\nanywhere in the product — the first customer's money was a number\nin Stripe, not in the console."},"retention_proxy":{"description":"Month-on-month retention proxy, oldest month first.","items":{"$ref":"#/components/schemas/RetentionMonth"},"type":"array"},"revenue":{"$ref":"#/components/schemas/CommercialRevenue","description":"Settled x402 revenue, Stripe linkage and plan distribution."},"usage_mix":{"$ref":"#/components/schemas/CommercialUsageMix","description":"Standard/premium call mix and busiest endpoints."}},"required":["generated_at","revenue","funnel","usage_mix","retention_proxy","demo_lane","prepaid"],"type":"object"},"CommercialRevenue":{"properties":{"all_time":{"$ref":"#/components/schemas/SettledRevenue","description":"All settled x402 revenue ever journalled."},"last_30d":{"$ref":"#/components/schemas/SettledRevenue","description":"Settled x402 revenue in the trailing [`COMMERCIAL_WINDOW_DAYS`]."},"mrr_billed_accounts":{"description":"How many identities the billed MRR is summed over.","format":"int64","type":"integer"},"mrr_billed_pence":{"description":"Identities carrying a non-empty `stripe_customer_id`.\nREAL monthly recurring revenue in pence, summed from what Stripe\nactually charges (`subscription_state`, migration 0027): statuses\nthat still entitle (`active`, `trialing`, `past_due`), yearly\nintervals normalised /12. Zero with `mrr_billed_accounts == 0`\nmeans \"no data recorded yet\", and the console then falls back to\nthe labelled estimate rather than showing a false zero.","format":"int64","type":"integer"},"plans":{"description":"Identities per plan label, biggest bucket first.","items":{"$ref":"#/components/schemas/PlanCount"},"type":"array"},"stripe_customers":{"format":"int64","type":"integer"},"window_days":{"description":"Length of the trailing window used for `last_30d`, in days.","format":"int64","type":"integer"}},"required":["window_days","all_time","last_30d","mrr_billed_pence","mrr_billed_accounts","stripe_customers","plans"],"type":"object"},"CommercialUsageMix":{"description":"Standard/premium call mix over the trailing window, plus the busiest\nendpoints.\n\nThe class split reuses the overview's endpoint-label rule: `/adr/check`\nis the premium-by-path endpoint, everything else counts as standard.\nTruck/ADR calls on mixed endpoints are classified per request body at\nbill time and are indistinguishable in `usage_counters`, so `premium`\nis a floor, not an exact billing total.","properties":{"premium":{"description":"Premium-class calls in the window (a floor; see the type docs).","format":"int64","type":"integer"},"standard":{"description":"Standard-class calls in the window.","format":"int64","type":"integer"},"top_endpoints":{"description":"The busiest [`COMMERCIAL_TOP_ENDPOINTS`] endpoints in the window.","items":{"$ref":"#/components/schemas/EndpointUsage"},"type":"array"},"total":{"description":"`standard + premium`.","format":"int64","type":"integer"},"window_days":{"description":"Length of the trailing window, in days.","format":"int64","type":"integer"}},"required":["window_days","standard","premium","total","top_endpoints"],"type":"object"},"CompleteCoverage":{"description":"Coverage with no gaps: every metre of the route was assessed against\nsurvey data good enough to decide on.\n\n**This type has no field for a gap, and that absence is the safety\ninvariant.** [`RouteVerdictOut::Pass`] accepts nothing else, so a passing\nroute with unsurveyed sections is not a state this crate can build,\nserialise or be talked into.","properties":{"assessed_m":{"description":"Metres of route assessed. Equal to the route length.","format":"double","type":"number"},"kind":{"description":"Always `complete`.","type":"string"}},"required":["kind","assessed_m"],"type":"object"},"ConsoleKey":{"description":"One key's metadata in the console view — **no plaintext, no hash**.","properties":{"created_at":{"description":"RFC 3339 creation timestamp.","type":"string"},"expires_at":{"description":"RFC 3339 expiry (provisional keys only).","type":["string","null"]},"key_id":{"description":"Key id (UUID) — safe to display; it is not a credential.","type":"string"},"label":{"description":"Human-readable label (the `label` given at issuance, or the email).","type":"string"},"revoked_at":{"description":"RFC 3339 revocation timestamp, if revoked.","type":["string","null"]},"state":{"description":"Lifecycle state: `provisional` or `verified`.","type":"string"},"used_this_month":{"description":"Requests recorded for this key this calendar month (flushed rows).","format":"int64","type":"integer"}},"required":["key_id","label","state","created_at","used_this_month"],"type":"object"},"ConsoleSelfResponse":{"description":"Response for `GET /v1/console/self` — the identity account view.","properties":{"balance_millipence":{"description":"Prepaid balance in internal millipence.","format":"int64","type":"integer"},"balance_pence":{"description":"Prepaid balance in pence (floor of the millipence balance).","format":"int64","type":"integer"},"created_at":{"description":"RFC 3339 identity creation timestamp.","type":"string"},"email":{"description":"Delivery email of the identity.","type":"string"},"history":{"description":"Credit history, oldest first.","items":{"$ref":"#/components/schemas/LedgerEntry"},"type":"array"},"keys":{"description":"Keys of this identity, newest first (metadata only).","items":{"$ref":"#/components/schemas/ConsoleKey"},"type":"array"},"monthly_quota":{"description":"Monthly quota currently applied to verified keys of this identity.","format":"int64","type":"integer"},"used_this_month":{"description":"Requests used across the identity this calendar month.","format":"int64","type":"integer"}},"required":["email","created_at","balance_pence","balance_millipence","monthly_quota","used_this_month","keys","history"],"type":"object"},"CoverageOut":{"description":"Either kind of coverage, for the verdicts that accept both.","oneOf":[{"$ref":"#/components/schemas/CompleteCoverage","description":"No gaps."},{"$ref":"#/components/schemas/PartialCoverage","description":"Gaps, listed."}]},"CreateKeyRequest":{"description":"Body for `POST /admin/keys`.","properties":{"monthly_quota":{"description":"Requests allowed per calendar month (>= 1).","format":"int64","type":"integer"},"name":{"description":"Human-readable key name (1–120 characters).","type":"string"},"rate_per_min":{"description":"Requests allowed per minute (>= 1).","format":"int64","type":"integer"}},"required":["name","monthly_quota","rate_per_min"],"type":"object"},"CreateMapIssueRequest":{"description":"Body for `POST /map-issues`.","properties":{"category":{"description":"One of [`CATEGORIES`], e.g. `\"restriction-wrong\"`.","type":"string"},"contact":{"description":"Optional contact email for follow-up (trimmed, at most 200 bytes;\nlength-checked only).","type":["string","null"]},"lat":{"description":"Latitude of the problem, decimal degrees (WGS84), within [-90, 90].","format":"double","type":"number"},"lon":{"description":"Longitude of the problem, decimal degrees (WGS84), within\n[-180, 180].","format":"double","type":"number"},"note":{"description":"Optional free-form description (trimmed, at most 1000 bytes).","type":["string","null"]}},"required":["lat","lon","category"],"type":"object"},"CreateStyleRequest":{"description":"Body for `POST /styles`.","properties":{"name":{"description":"Human-readable style name (1–120 characters); its kebab-case slug\nseeds the style id.","type":"string"},"theme":{"description":"Optional sn-style theme document. Omitted, version 1 is the default\ntheme carrying `name`."}},"required":["name"],"type":"object"},"CreatedKeyResponse":{"description":"Response for `POST /admin/keys` — the only time the key is revealed.","properties":{"created_at":{"description":"RFC 3339 creation timestamp.","type":"string"},"id":{"description":"Key id, used for revocation and usage queries.","type":"string"},"key":{"description":"The full API key (`snk_…`). **Store it now; it is never shown\nagain** — only its BLAKE3 hash is persisted.","type":"string"},"monthly_quota":{"description":"Requests allowed per calendar month.","format":"int64","type":"integer"},"name":{"description":"Key name.","type":"string"},"rate_per_min":{"description":"Requests allowed per minute.","format":"int64","type":"integer"}},"required":["id","name","key","created_at","monthly_quota","rate_per_min"],"type":"object"},"CreatedMapIssueResponse":{"description":"Response for `POST /map-issues`.","properties":{"id":{"description":"Report id (UUID v4).","type":"string"}},"required":["id"],"type":"object"},"CreatedStyleResponse":{"description":"Response for `POST /styles`.","properties":{"id":{"description":"The generated style id.","type":"string"},"style_url":{"description":"Immutable URL of the compiled style.","type":"string"},"theme":{"description":"The theme document that was stored."},"version":{"description":"The published version (always 1 on create).","format":"int64","minimum":0,"type":"integer"}},"required":["id","version","style_url","theme"],"type":"object"},"CreditIdentityRequest":{"description":"Body for `POST /admin/identities/credit`.","properties":{"email":{"description":"Identity email (matched case-insensitively).","type":"string"},"pence":{"description":"Amount to credit, in pence (>= 1).","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe:cs_…`.","type":"string"}},"required":["email","pence","reason"],"type":"object"},"CreditKeyRequest":{"description":"Body for `POST /admin/keys/{id}/credit`.","properties":{"pence":{"description":"Amount to credit, in pence (>= 1).","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe:cs_…`. Repeating a reason is a\nno-op returning the balance the original credit produced.","type":"string"}},"required":["pence","reason"],"type":"object"},"CreditOverview":{"description":"Prepaid-credit figures for the overview.","properties":{"balance_pence":{"description":"Prepaid balance outstanding across all identities, in pence.","format":"int64","type":"integer"},"consumed_pence":{"description":"Credit consumed all-time (`credited_total - balance`), in pence —\nprepaid revenue actually recognised as metered usage.","format":"int64","type":"integer"},"credited_total_pence":{"description":"Lifetime credited total across all identities, in pence.","format":"int64","type":"integer"}},"required":["balance_pence","credited_total_pence","consumed_pence"],"type":"object"},"CreditResponse":{"description":"Response for both credit endpoints.","properties":{"balance_pence":{"description":"Balance after this credit, in pence.","format":"int64","type":"integer"},"duplicate":{"description":"`true` when the reason had already been applied (idempotent\nreplay; nothing changed).","type":"boolean"},"identity_id":{"description":"Identity whose ledger was credited.","type":"string"}},"required":["identity_id","balance_pence","duplicate"],"type":"object"},"DatasetManifest":{"description":"One dataset's manifest (`dataset.json`), written at upload time by\n`scripts/publish-pointcloud.mjs`. The authority for access control\n(who may fetch), metering (declared sizes) and hosting fees (total\ndeclared bytes).","properties":{"files":{"additionalProperties":{"format":"int64","minimum":0,"type":"integer"},"description":"Declared byte size per servable file, e.g.\n`{ \"corridor-a.bin\": 43123456, \"corridor-a.json\": 2048 }`.\nThe meter bills from these declared sizes; a file absent here is\nnot served.","propertyNames":{"type":"string"},"type":"object"},"identity_id":{"description":"Owning self-serve identity: every key of this identity may fetch,\nand this identity is the one hosting fees attribute to.","type":["string","null"]},"key_ids":{"description":"Explicitly allowed key ids — the operator-key analogue of\n`identity_id` (fleet keys have no identity).","items":{"type":"string"},"type":"array"},"public":{"description":"`true` serves the dataset to **any** authenticated key (demo\ndatasets). Defaults to `false`: a manifest that names nobody\nserves nobody.","type":"boolean"}},"type":"object"},"DatasetRef":{"description":"One dataset the report drew on.","properties":{"dataset":{"description":"The dataset id.","type":"string"},"stale":{"description":"Whether the artefact lags its sources, or has not been checked\nagainst them. Either way no route passes on it.","type":"boolean"},"survey_dates":{"description":"Capture range, `from/to` in ISO dates.","type":"string"}},"required":["dataset","survey_dates","stale"],"type":"object"},"DatasetSummary":{"description":"One dataset row in the [`PointcloudsOverview`].","properties":{"bytes":{"description":"Declared bytes across the dataset's files.","format":"int64","minimum":0,"type":"integer"},"dataset":{"description":"Dataset id (directory name).","type":"string"},"files":{"description":"Number of declared files.","minimum":0,"type":"integer"},"identity_id":{"description":"Owning identity, when self-served.","type":["string","null"]},"key_ids":{"description":"Explicitly allowed operator keys.","items":{"type":"string"},"type":"array"},"public":{"description":"Whether any authenticated key may fetch it.","type":"boolean"}},"required":["dataset","public","bytes","files"],"type":"object"},"Decision":{"description":"Doc-only mirror of [`sn_adr::Decision`].","properties":{"reason":{"description":"Present when blocked: a human-readable explanation citing ADR\n8.6.4.","type":["string","null"]},"status":{"description":"`allowed` or `blocked`.","example":"blocked","type":"string"}},"required":["status"],"type":"object"},"DemoFeatureUsage":{"description":"One premium-feature combination given away on the keyless demo lane.","properties":{"calls":{"description":"Keyless calls that engaged exactly this combination.","format":"int64","type":"integer"},"features":{"description":"Canonical feature label from [`crate::demo::DemoFeatures::label`] —\na comma-joined subset of `truck`, `dimensions`, `hazmat`,\n`tunnel_code`, `adr`, or `none`.","type":"string"},"units":{"description":"Billable units those calls would have drawn if keyed.","format":"int64","type":"integer"}},"required":["features","calls","units"],"type":"object"},"DemoLane":{"description":"What the keyless demo lane (`SN_DEMO_RPM`) gave away.\n\nCounting only. Keyless traffic is free by design and this endpoint\nchanges nothing about that: no quota, no block, no charge. It exists to\nanswer the question the demo lane could not answer before — how much of\nthe premium differentiator (truck dimensions, `hazmat`, ADR\n`tunnel_code`) is being served for nothing — so the enforcement\ndecision, if it comes, is taken on evidence.","properties":{"calls":{"description":"Keyless calls served in the window.","format":"int64","type":"integer"},"clients":{"$ref":"#/components/schemas/DemoLaneClients","description":"The bounded per-client top-talker table."},"enabled":{"description":"Whether the keyless lane is currently admitting traffic\n(`SN_DEMO_RPM > 0`). Historic counts are still reported when it is\noff.","type":"boolean"},"notional_millipence":{"description":"What those units would have listed at, in internal millipence\n(1,000 = 1p), priced at the entry band of each class — a NOTIONAL\nfigure for sizing the give-away. Nothing was charged.","format":"int64","type":"integer"},"premium":{"description":"Premium-class keyless calls in the window. Unlike\n[`CommercialUsageMix::premium`] this is exact, not a floor: the\nclass is recorded by the same classifier that bills a keyed call,\nper request, at admission time.","format":"int64","type":"integer"},"premium_features":{"description":"Premium-feature combinations, busiest first; the `none` bucket is\nexcluded (it is ordinary standard traffic).","items":{"$ref":"#/components/schemas/DemoFeatureUsage"},"type":"array"},"premium_units":{"description":"Billable units of the premium-class ones.","format":"int64","type":"integer"},"rpm_per_ip":{"description":"The configured per-client-IP rate, in requests/minute.","format":"int64","type":"integer"},"standard":{"description":"Standard-class keyless calls in the window.","format":"int64","type":"integer"},"top_endpoints":{"description":"Busiest keyless endpoints in the window.","items":{"$ref":"#/components/schemas/EndpointUsage"},"type":"array"},"units":{"description":"Billable units of every keyless call in the window.","format":"int64","type":"integer"},"window_days":{"description":"Length of the trailing window these counts cover, in days.","format":"int64","type":"integer"}},"required":["enabled","rpm_per_ip","window_days","calls","standard","premium","units","premium_units","notional_millipence","premium_features","top_endpoints","clients"],"type":"object"},"DemoLaneClient":{"description":"One keyless client (an IP, as the demo rate limiter buckets it) in the\nbounded in-memory top-talker table.","properties":{"calls":{"description":"Keyless calls served to it since the process started.","format":"int64","type":"integer"},"client":{"description":"The client address the demo bucket keys on.","type":"string"},"first_seen":{"description":"RFC 3339 first and last time it was seen.","type":"string"},"last_seen":{"description":"RFC 3339 time of its most recent keyless call.","type":"string"},"premium_calls":{"description":"How many of those were premium-class.","format":"int64","type":"integer"},"units":{"description":"Billable units they would have drawn if keyed.","format":"int64","type":"integer"}},"required":["client","calls","premium_calls","units","first_seen","last_seen"],"type":"object"},"DemoLaneClients":{"description":"The bounded per-client view of the keyless lane.\n\nIn memory only and never persisted: no client address reaches the\ndatabase (`demo_usage` has no IP column). The table is hard-capped and\nevicted least-recently-seen-first, so a churn of one-shot demo IPs\ncannot leak memory — the cost is that it is a snapshot of recent\ntalkers since process start, not a census.","properties":{"capacity":{"description":"The hard cap.","format":"int64","type":"integer"},"evicted":{"description":"Clients dropped by eviction; non-zero means the table is full and\nthe list below is a sample, not the whole population.","format":"int64","type":"integer"},"since":{"description":"RFC 3339 time the table started collecting (process start).","type":"string"},"top":{"description":"Busiest clients first, at most [`DEMO_TOP_CLIENTS`].","items":{"$ref":"#/components/schemas/DemoLaneClient"},"type":"array"},"tracked":{"description":"Clients currently tracked.","format":"int64","type":"integer"}},"required":["since","tracked","capacity","evicted","top"],"type":"object"},"DownloadAllowanceResponse":{"description":"Response for `POST /admin/identities/{email}/download-allowance`.","properties":{"download_allowance_mib":{"description":"The download allowance now recorded for this reason, in MiB.","format":"int64","type":"integer"},"duplicate":{"description":"`true` when the reason had already been applied (idempotent replay;\nnothing changed).","type":"boolean"},"email":{"description":"Canonical (lowercased) identity email.","type":"string"}},"required":["email","download_allowance_mib","duplicate"],"type":"object"},"DownloadsOverview":{"description":"Offline territory-download figures for the overview.","properties":{"allowance_mib":{"description":"Sum of every identity's monthly allowance, in MiB.","format":"int64","type":"integer"},"month":{"description":"The calendar month reported (`YYYY-MM`, UTC).","type":"string"},"used_mib":{"description":"Bytes served this month across all identities, in MiB (rounded up).","format":"int64","type":"integer"}},"required":["month","used_mib","allowance_mib"],"type":"object"},"EndpointUsage":{"description":"One collapsed endpoint's metered-call count for the current month.","properties":{"count":{"description":"Metered calls recorded to it this calendar month.","format":"int64","type":"integer"},"endpoint":{"description":"Collapsed endpoint label, e.g. `/route/v1`, `/geocode`, `/tiles`.","type":"string"}},"required":["endpoint","count"],"type":"object"},"GeodataCoordinate":{"description":"Doc-only mirror of [`sn_crs::XY`].","properties":{"x":{"description":"First ordinate, in the dataset's OWN units — not necessarily degrees.","example":529000.0,"format":"double","type":"number"},"y":{"description":"Second ordinate, in the dataset's OWN units — not necessarily degrees.","example":4555000.0,"format":"double","type":"number"}},"required":["x","y"],"type":"object"},"GeodataExtent":{"description":"Doc-only mirror of [`sn_crs::Extent`].","properties":{"max_x":{"format":"double","type":"number"},"max_y":{"format":"double","type":"number"},"min_x":{"format":"double","type":"number"},"min_y":{"format":"double","type":"number"}},"required":["min_x","max_x","min_y","max_y"],"type":"object"},"HealthResponse":{"description":"`GET /health` response body.","properties":{"backend":{"description":"Name of the routing engine serving requests (`valhalla`,\n`graphhopper` or `auto`).","type":"string"},"build":{"description":"Git commit the running image was built from (`SN_BUILD_SHA`, baked\nin at image build), or `unknown`. The deploy guard\n(`deploy/hetzner/rebuild-gateway.sh`) compares this against the\ncommit it built to prove a rebuild actually shipped — Docker\nbuild-cache poisoning has previously shipped stale code silently.","type":"string"},"status":{"description":"`ok` when the routing backend answers its health probe, `degraded`\notherwise.","type":"string"},"version":{"description":"Gateway crate version.","type":"string"}},"required":["status","backend","version","build"],"type":"object"},"HostingFeeLine":{"description":"One owner's hosting-fee line in the [`PointcloudsOverview`].","properties":{"bytes":{"description":"Total declared bytes across the owner's datasets.","format":"int64","minimum":0,"type":"integer"},"monthly_fee_pence":{"description":"Monthly hosting fee in pence: £1/GB-month (decimal GB, rounded up\nto the next penny), floored at the £10/month minimum.","format":"int64","type":"integer"},"owner":{"description":"The owner: an identity id, or `operator:<key_id>` / `public` for\ndatasets without one.","type":"string"}},"required":["owner","bytes","monthly_fee_pence"],"type":"object"},"IdentitiesOverview":{"description":"Signup counts for the overview.","properties":{"by_source":{"description":"Signups by channel tag, largest first — identities whose issuance\ncarried a `source` (\"chatgpt\", \"premium-wall\", \"agent-recipe\" …).\nUntagged identities are counted in `untagged`, not listed.","items":{"$ref":"#/components/schemas/SourceCount"},"type":"array"},"new_30d":{"description":"Identities created in the last 30 days.","format":"int64","type":"integer"},"new_7d":{"description":"Identities created in the last 7 days.","format":"int64","type":"integer"},"total":{"description":"Total identities ever created.","format":"int64","type":"integer"},"untagged":{"description":"Identities with no recorded signup source (predate tagging, or\narrived without one).","format":"int64","type":"integer"}},"required":["total","new_7d","new_30d","by_source","untagged"],"type":"object"},"IdentityDetailResponse":{"description":"Response for `GET /admin/identities/{email}`.","properties":{"balance_pence":{"description":"Prepaid balance, in pence.","format":"int64","type":"integer"},"credited_total_pence":{"description":"Lifetime credited total, in pence.","format":"int64","type":"integer"},"history":{"description":"Credit history, oldest first.","items":{"$ref":"#/components/schemas/LedgerEntry"},"type":"array"},"identity":{"$ref":"#/components/schemas/IdentityRecord","description":"The identity record."},"keys":{"description":"Keys belonging to the identity, newest first.","items":{"$ref":"#/components/schemas/ApiKeyRecord"},"type":"array"},"mau_included":{"description":"Included monthly active users per calendar month.","format":"int64","type":"integer"},"mau_overdrawn":{"description":"Whether the identity is MAU-overdrawn this month (fail-open flag:\nserving continued; sustained overdraw is a commercial conversation).","type":"boolean"},"mau_this_month":{"description":"Distinct monthly active users recorded this calendar month.","format":"int64","type":"integer"},"month_used":{"description":"Metered quota units recorded this calendar month across the\nidentity's keys. Paired with the live keys' `monthly_quota` it\ngives quota burn, which is what the account page leads with.","format":"int64","type":"integer"},"plan":{"description":"Coarse plan label (`free` until a subscription is activated).","type":"string"},"stripe_customer_id":{"description":"The Stripe customer this identity is linked to, or `null` when it has\nnever been through checkout.","type":["string","null"]}},"required":["identity","balance_pence","credited_total_pence","keys","history","mau_this_month","mau_included","mau_overdrawn","month_used","plan"],"type":"object"},"IdentityListResponse":{"description":"Response for `GET /admin/identities`.","properties":{"identities":{"description":"The page, in the requested order.","items":{"$ref":"#/components/schemas/IdentitySummary"},"type":"array"},"limit":{"description":"Page size applied.","format":"int64","type":"integer"},"offset":{"description":"Offset applied.","format":"int64","type":"integer"},"total":{"description":"Total identities (across all pages).","format":"int64","type":"integer"}},"required":["total","limit","offset","identities"],"type":"object"},"IdentityRecord":{"description":"One stored identity.","properties":{"created_at":{"description":"RFC 3339 creation timestamp.","type":"string"},"email":{"description":"Canonical (lowercased) email address.","type":"string"},"id":{"description":"Opaque identity id (UUID v4).","type":"string"},"tos_accepted_at":{"description":"RFC 3339 timestamp of the most recent ToS acceptance.","type":"string"}},"required":["id","email","created_at","tos_accepted_at"],"type":"object"},"IdentitySummary":{"description":"One identity row in the admin list.","properties":{"balance_pence":{"description":"Prepaid balance, in pence.","format":"int64","type":"integer"},"created_at":{"description":"RFC 3339 creation timestamp.","type":"string"},"download_mib":{"description":"Offline-download bytes served to this identity this month, in MiB\n(rounded up).","format":"int64","type":"integer"},"email":{"description":"Canonical (lowercased) email.","type":"string"},"first_success_at":{"description":"RFC 3339 timestamp of the identity's activation — the earliest\n`first_success_at` across its keys — or `null` if no key of the\nidentity has ever had a successful metered call.","type":["string","null"]},"id":{"description":"Identity id (UUID v4).","type":"string"},"keys":{"description":"Keys ever issued to the identity (including revoked).","format":"int64","type":"integer"},"last_active":{"description":"UTC day (`YYYY-MM-DD`) of the identity's most recent metered call, or\n`null` if it has not called since `last_seen_at` tracking began.","type":["string","null"]},"mau_overdrawn":{"description":"Whether the identity is MAU-overdrawn this month (fail-open flag).","type":"boolean"},"mau_this_month":{"description":"Distinct monthly active users recorded this month.","format":"int64","type":"integer"},"month_used":{"description":"Metered calls recorded this calendar month across all its keys\n(flushed rows only).","format":"int64","type":"integer"},"monthly_quota":{"description":"The ENFORCED monthly ceiling `month_used` is spent against.\n\nQuotas are identity-pooled (`keys::find_for_auth` sums usage across\nall of an identity's keys and compares against ONE key's\n`monthly_quota`), and every live verified key carries the same\nvalue by construction (`identity::set_identity_quota` writes one\nvalue to all of them; there is no per-key setter for identity\nkeys). So the ceiling is MAX over live verified keys — never SUM,\nwhich would overstate it by the key count; an earlier revision made\nexactly that mistake and reported a paying customer's 250,000 plan\nas a 1,000,000 ceiling. When the identity holds only provisional\nkeys, the ceiling is the provisional allowance instead (a separate\nall-time pool, not a monthly one).\n\nRead together with `month_used` this gives quota burn — the\nclearest buy signal the gateway produces: a free account pinned at\nits ceiling wants to pay, and a paid account using a fraction of\nwhat it bought is a renewal risk.","format":"int64","type":"integer"},"plan":{"description":"Coarse plan label (`free` by default).","type":"string"},"status":{"description":"Account lifecycle status (`active`, `suspended`, `closed`).","type":"string"}},"required":["id","email","created_at","keys","month_used","mau_this_month","balance_pence","download_mib","mau_overdrawn","monthly_quota","status","plan"],"type":"object"},"IncidentAlong":{"description":"One incident along the route.","properties":{"arc_m":{"description":"Distance along the route shape to the incident's nearest matched\npoint, metres (ascending in the response).","format":"double","type":"number"},"category":{"description":"What the feed says the incident is (`accident` | `roadworks` |\n`obstruction` | `congestion` | `closure` | `other`). Present for\nNTIS events rows only: the closures feed states no record type, and\n`severity` already says how much of the road it takes.","type":["string","null"]},"cause":{"description":"Cause, as the feed states it (`causeType` and, when it adds detail,\n`causeType: detail`) for the closures source; the record's own type\n(`accident`, `roadworks`, …) for the NTIS events source, which\npublishes no separate cause. `\"unknown\"` when neither is stated.","type":"string"},"closure_type":{"description":"The feed's own closure-type value (`laneClosures`,\n`carriagewayClosures`, `roadClosed`, …), when present.","type":["string","null"]},"comment":{"description":"The feed's free-text public comment, when it carries one.","type":["string","null"]},"delay_s":{"description":"Delay in seconds **as the source feed reports it**, when it reports\none. Never a MapMap estimate and never derived: the engine consumes\nno live speed data, so this figure is the operator's claim about\ntheir own network, passed through unchanged.","format":"int64","minimum":0,"type":["integer","null"]},"from_arc_m":{"description":"Distance along the route to the nearest of the incident's geometry\nvertices that fell inside the corridor — equal to `arc_m` for a\npoint incident. For a line, this bounds where the closure enters\nthis route's corridor, not necessarily where the closure itself\nbegins.","format":"double","type":"number"},"id":{"description":"The source feed's own record id.","type":"string"},"lanes_operational":{"description":"Lanes still operational, when the feed states a count.","format":"int64","minimum":0,"type":["integer","null"]},"lanes_restricted":{"description":"Lanes restricted, when the feed states a count.","format":"int64","minimum":0,"type":["integer","null"]},"location_name":{"description":"The matched geometry's own name in the source's network model\n(e.g. a junction-to-junction link name), when it carries one.","type":["string","null"]},"offset_m":{"description":"Perpendicular distance from the route to the incident's nearest\npoint, metres.","format":"double","type":"number"},"overall_end":{"description":"Planned/actual end time, RFC 3339, when the feed states one.","type":["string","null"]},"overall_start":{"description":"Planned/actual start time, RFC 3339, when the feed states one.","type":["string","null"]},"planned":{"description":"`true` when the closure's `overallStartTime` is still in the\nfuture — an imminent planned closure, not yet active.","type":"boolean"},"probable":{"description":"`true` when the feed's `probabilityOfOccurrence` is not `certain`.","type":"boolean"},"reported_severity":{"description":"The source feed's own severity word, verbatim (NTIS events:\n`lowest` | `low` | `medium` | `high` | `highest`). Absent when the\nfeed states none, or states `unknown` — never mapped onto\n[`IncidentSeverity`], which answers a different question.","type":["string","null"]},"road":{"description":"The first road name the feed's location reference carries. A\nclosure spanning a junction may affect roads not named here.","type":["string","null"]},"severity":{"description":"Derived severity: `closure` | `lane_restriction` | `other` — see\n[`IncidentSeverity`].","type":"string"},"side":{"description":"Which side of the route's local bearing the incident sits on\n(`left` | `right` | `unknown`); see the module docs on\ndirectionality.","type":"string"},"situation_id":{"description":"The feed's situation id, when it has one — several rows (records)\ncan belong to one situation, and this is the key that groups them.","type":["string","null"]},"source":{"description":"The `sources[].source_id` this record came from.","type":"string"},"source_kind":{"description":"Which feed family that source is: `closures` | `ntis-events`.","type":"string"},"status":{"description":"`active` or `unknown` — `suspended` records and records whose\n`overallEndTime` has passed are dropped before matching, never\nreturned as `status`.","type":"string"},"to_arc_m":{"description":"Distance along the route to the farthest matched vertex. May equal\n`from_arc_m` for a point, or for a line that only clips the\ncorridor briefly.","format":"double","type":"number"}},"required":["id","arc_m","from_arc_m","to_arc_m","offset_m","side","cause","severity","status","planned","probable","source","source_kind"],"type":"object"},"IncidentSourceMetaOut":{"description":"[`IncidentSourceMeta`] without the `attribution` field (surfaced\nseparately at response level, deduplicated, like `/v1/cameras/along`).","properties":{"coverage_note":{"type":"string"},"licence":{"type":"string"},"network":{"type":"string"},"source_id":{"type":"string"}},"required":["source_id","network","coverage_note","licence"],"type":"object"},"IncidentsAlongRequest":{"description":"Request body.","properties":{"buffer_m":{"description":"Corridor half-width in metres; default 60, clamped to at most 500.","format":"double","type":["number","null"]},"shape":{"description":"The route shape as a polyline6 string.","type":"string"}},"required":["shape"],"type":"object"},"IncidentsAlongResponse":{"description":"Response body.","properties":{"attribution":{"description":"Attribution notices for the sources actually configured, to\ndisplay verbatim.","items":{"type":"string"},"type":"array"},"incidents":{"description":"Incidents within the corridor, `arc_m` ascending.","items":{"$ref":"#/components/schemas/IncidentAlong"},"type":"array"},"sources":{"description":"Every configured source's coverage metadata — present even when\n`incidents` is empty.","items":{"$ref":"#/components/schemas/IncidentSourceMetaOut"},"type":"array"}},"required":["incidents","sources"],"type":"object"},"IndeterminateReason":{"description":"Why an axis could not be called either way.\n\nStructured rather than left to the prose in `resolution_hint`, because a\ncaller that has to parse English to find out whether an answer was\nwithheld for uncertainty or for staleness will eventually not bother.\nThe sentence stays, for a human; the list is for a program.","enum":["inside_uncertainty_band","vegetation_age_exceeded","artefact_stale","artefact_freshness_unchecked"],"type":"string"},"IntegrationRetroRequest":{"description":"Body for `POST /v1/feedback`.","properties":{"agent_name":{"description":"The submitting agent's name, when sent by an AI agent (at most\n100 bytes).","type":["string","null"]},"docs_gaps":{"description":"Documentation gaps hit (at most 20 × 500 bytes).","items":{"type":"string"},"type":"array"},"gotchas":{"description":"Surprises/traps worth documenting (at most 20 × 500 bytes).","items":{"type":"string"},"type":"array"},"problems":{"description":"Problems hit (at most 20).","items":{"$ref":"#/components/schemas/RetroProblem"},"type":"array"},"sdk_version":{"description":"MapMap SDK version integrated against, when known (at most 50\nbytes).","type":["string","null"]},"what_built":{"description":"What was built with MapMap (required, at most 500 bytes).","type":"string"},"wins":{"description":"What went well (at most 20 × 500 bytes).","items":{"type":"string"},"type":"array"}},"required":["what_built"],"type":"object"},"IntegrationRetroResponse":{"description":"Response for `POST /v1/feedback`.","properties":{"id":{"description":"Stored retro id (UUID v4).","type":"string"},"status":{"description":"Always `received`.","type":"string"}},"required":["id","status"],"type":"object"},"IssueKeyRequest":{"description":"Body for `POST /v1/keys`.","properties":{"accept_tos":{"description":"Must be literally `true` — anything else is a 400 echoing the\nterms-of-service URL."},"email":{"description":"Email address; becomes the (lowercased) identity of the key.","type":"string"},"label":{"description":"Optional human-readable label for this key (shown on the account\npage, e.g. `\"nightly-dispatch-agent\"`). Defaults to the email.","type":["string","null"]},"source":{"description":"Optional channel tag naming where this signup came from\n(`\"chatgpt\"`, `\"agent-recipe\"`, `\"cursor\"` …). Lowercased and\nrestricted to `[a-z0-9._-]`, at most 40 characters; anything else\nis dropped, never rejected — attribution must not break signups.","type":["string","null"]}},"required":["email"],"type":"object"},"IssuedKeyResponse":{"description":"Response for `POST /v1/keys` — the only time the key is revealed.","properties":{"calls":{"description":"Total call allowance until verified.","format":"int64","type":"integer"},"claim_url":{"description":"Zero-authority claim pointer for the HUMAN behind this agent: the\npage shows a masked email and offers to send a sign-in link. Safe\nto print in chat or logs; it grants nothing by itself. Present only\nwhen the deployment has a console (`SN_CONSOLE_URL`).","type":["string","null"]},"display_to_user":{"description":"Sentence for the agent to show its human verbatim (contains the\nclaim URL, never the key). Present only alongside `claim_url`.","type":["string","null"]},"expires_in_h":{"description":"Hours until the provisional key expires unverified.","format":"int64","type":"integer"},"key":{"description":"The full API key (`snk_…`). Shown once, never again.","type":"string"},"key_id":{"description":"Key id (UUID), for `/v1/keys/self` correlation and admin ops.","type":"string"},"state":{"description":"Always `provisional` at issuance.","type":"string"},"tos":{"description":"Terms-of-service URL that was accepted.","type":"string"},"verify":{"description":"What happens next.","type":"string"}},"required":["key","key_id","state","calls","expires_in_h","verify","tos"],"type":"object"},"KeyUsageResponse":{"description":"Response for `GET /admin/keys/{id}/usage`.","properties":{"entries":{"description":"Per-day, per-endpoint counters (flushed; the buffered writer lags by\nat most the flush interval).","items":{"$ref":"#/components/schemas/UsageRow"},"type":"array"},"key_id":{"description":"Key id.","type":"string"},"total":{"description":"Sum of all recorded requests for this key.","format":"int64","type":"integer"}},"required":["key_id","total","entries"],"type":"object"},"KeysOverview":{"description":"Key counts for the overview, split by lifecycle state.","properties":{"active_this_month":{"description":"Distinct keys that made at least one metered call this calendar\nmonth (issued-and-idle keys are excluded).","format":"int64","type":"integer"},"operator":{"description":"Operator-issued (`admin` state) keys.","format":"int64","type":"integer"},"provisional":{"description":"Self-served, unverified keys.","format":"int64","type":"integer"},"revoked":{"description":"Keys that have been revoked (any state).","format":"int64","type":"integer"},"total":{"description":"Total keys ever issued (including revoked).","format":"int64","type":"integer"},"verified":{"description":"Email-verified free-tier keys.","format":"int64","type":"integer"}},"required":["total","provisional","verified","operator","revoked","active_this_month"],"type":"object"},"LateralPointOut":{"description":"A point on the route where the corridor narrows, with its measurement.","properties":{"dataset":{"description":"The dataset the measurement came from.","type":"string"},"lateral":{"$ref":"#/components/schemas/MeasuredLateral","description":"The lateral measurement. Deliberately named `lateral`, not\n`clearance`, so the two axes cannot be confused at the call site."},"location":{"$ref":"#/components/schemas/LocationOut","description":"Where on the ground."},"route_distance_m":{"description":"Distance along the route, metres.","format":"double","type":"number"},"view_url":{"description":"Deep link to this exact view in the survey viewer.","type":"string"}},"required":["location","route_distance_m","lateral","dataset","view_url"],"type":"object"},"LedgerEntry":{"description":"One credit-ledger history entry (credits only; per-call debits mutate\nthe balance directly).\n\nAmounts are internal millipence (1p = 1000 millipence), the same unit\nthe ledger stores, so fractional per-call prices round-trip exactly.","properties":{"balance_after_millipence":{"description":"Balance after this credit was applied, in millipence.","format":"int64","type":"integer"},"created_at":{"description":"RFC 3339 timestamp.","type":"string"},"delta_millipence":{"description":"Credited amount in millipence.","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe:cs_…`.","type":"string"}},"required":["delta_millipence","balance_after_millipence","reason","created_at"],"type":"object"},"LimitingPointOut":{"description":"A point on the route with its measurement and a link to see it.","properties":{"clearance":{"$ref":"#/components/schemas/MeasuredClearance","description":"The measurement that made this point limiting."},"dataset":{"description":"The dataset the measurement came from.","type":"string"},"location":{"$ref":"#/components/schemas/LocationOut","description":"Where on the ground."},"route_distance_m":{"description":"Distance along the route, metres.","format":"double","type":"number"},"view_url":{"description":"Deep link to this exact view in the survey viewer.","type":"string"}},"required":["location","route_distance_m","clearance","dataset","view_url"],"type":"object"},"LocationOut":{"description":"A WGS84 position, in the order the wire uses.","properties":{"lat":{"description":"Latitude, degrees.","format":"double","type":"number"},"lng":{"description":"Longitude, degrees.","format":"double","type":"number"}},"required":["lng","lat"],"type":"object"},"MapIssueListResponse":{"description":"Response for `GET /admin/map-issues`.","properties":{"count":{"description":"Number of reports returned (capped at 200).","minimum":0,"type":"integer"},"issues":{"description":"Reports, newest first.","items":{"$ref":"#/components/schemas/MapIssueRecord"},"type":"array"}},"required":["count","issues"],"type":"object"},"MapIssueRecord":{"description":"One stored report, as the admin triage list returns it.","properties":{"admin_note":{"description":"Triage note, when set.","type":["string","null"]},"category":{"description":"Report category, one of [`CATEGORIES`].","type":"string"},"contact":{"description":"Reporter's contact email, when given.","type":["string","null"]},"created_at":{"description":"RFC 3339 submission timestamp.","type":"string"},"id":{"description":"Report id (UUID v4).","type":"string"},"key_id":{"description":"API key id that submitted the report (the website proxy's demo key\nfor anonymous web reports).","type":["string","null"]},"lat":{"description":"Latitude of the reported problem.","format":"double","type":"number"},"lon":{"description":"Longitude of the reported problem.","format":"double","type":"number"},"note":{"description":"Reporter's free-form note, when given.","type":["string","null"]},"status":{"description":"Triage status, one of [`STATUSES`].","type":"string"}},"required":["id","created_at","lat","lon","category","status"],"type":"object"},"MauIncludedResponse":{"description":"Response for `POST /admin/identities/{email}/mau-included`.","properties":{"duplicate":{"description":"`true` when the reason had already been applied (idempotent replay;\nnothing changed).","type":"boolean"},"email":{"description":"Canonical (lowercased) identity email.","type":"string"},"mau_included":{"description":"The included MAU now recorded for this reason.","format":"int64","type":"integer"}},"required":["email","mau_included","duplicate"],"type":"object"},"MauOverview":{"description":"Monthly-active-user figures for the overview.","properties":{"distinct_users":{"description":"Distinct (identity, end user) pairs recorded this month.","format":"int64","type":"integer"},"month":{"description":"The calendar month reported (`YYYY-MM`, UTC).","type":"string"},"new_this_month":{"description":"Distinct end users first seen this month (no prior-month row) —\ngenuinely new users.","format":"int64","type":"integer"},"overdrawn_identities":{"description":"Identities marked MAU-overdrawn this month (the fail-open flag;\nserving continued).","format":"int64","type":"integer"},"returning_this_month":{"description":"Distinct end users this month that were also seen in an earlier\nmonth — returning users (`distinct_users - new_this_month`).","format":"int64","type":"integer"}},"required":["month","distinct_users","overdrawn_identities","new_this_month","returning_this_month"],"type":"object"},"McpMapIssueListResponse":{"description":"Response for `GET /admin/mcp-map-issues`.","properties":{"issues":{"description":"One page of queued MCP map-issue observations, newest first, each\ncarrying a `source_file` field naming the NDJSON file it came from.","items":{},"type":"array"},"total":{"description":"Total parsed observations across the queue directory.","format":"int64","type":"integer"}},"required":["issues","total"],"type":"object"},"MeasuredClearance":{"description":"One measured clearance.\n\nUncertainty is not optional, and that is the point: a headroom figure\nwithout its bound is a number somebody will treat as exact. There is no\nfield here for a signed or legal height, by design.","properties":{"headroom_m":{"description":"`overhead_z - road_z`, a within-survey difference (Rule D1), metres.","format":"double","type":"number"},"overhead_class":{"$ref":"#/components/schemas/OverheadClass","description":"What the overhead surface is made of."},"safe_headroom_m":{"description":"`headroom_m - k*sigma_m - sampling_gap_m`, metres. The verdict input.","format":"double","type":"number"},"sampling_gap_m":{"description":"One-sided sampling bias bound, metres: how far below the lowest\nsample the true low point could hang, given the sample spacing.","format":"double","type":"number"},"sigma_m":{"description":"Combined one-sigma measurement uncertainty, metres.","format":"double","type":"number"},"support_m2":{"description":"Connected-component area of the overhead cluster, square metres.","format":"double","type":"number"},"surveyed_on":{"description":"Last capture date of the survey behind this measurement, ISO.","type":"string"}},"required":["headroom_m","sigma_m","sampling_gap_m","safe_headroom_m","overhead_class","support_m2","surveyed_on"],"type":"object"},"MeasuredLateral":{"description":"One measured lateral clearance: how wide the clear corridor is, not how\ntall.\n\nA separate type from [`MeasuredClearance`] on purpose. These are\ndifferent physical quantities, and a shared type is how the reader of a\nwidth answer ends up quoting a headroom. There is no headroom field here,\nso there is nothing to misread.\n\nThe width measured is the free width of the contiguous clear corridor\nthat contains the route shape, out to the edge of the assessed band. It\nis not a statement about where in the lane a vehicle sits: the route\nshape is a centreline approximation and lane position is not recoverable\nfrom it, so this answers \"a corridor this wide is clear here\" and nothing\nmore.","properties":{"clear_width_m":{"description":"Free width of the clear corridor, metres.","format":"double","type":"number"},"limited_by":{"$ref":"#/components/schemas/OverheadClass","description":"What closed the corridor."},"safe_clear_width_m":{"description":"`clear_width_m - k*sigma_m - sampling_gap_m`, metres. The verdict\ninput.","format":"double","type":"number"},"sampling_gap_m":{"description":"One-sided sampling bias bound on the width, metres: one unsampled\ncell at each edge, since an obstruction can begin anywhere inside the\ncell that closed the corridor.","format":"double","type":"number"},"sigma_m":{"description":"Combined one-sigma lateral uncertainty, metres: the survey's own\ndifferential georeferencing error applied to both edges.","format":"double","type":"number"},"support_m2":{"description":"Connected-component area of the cluster that closed it, square\nmetres.","format":"double","type":"number"},"surveyed_on":{"description":"Last capture date of the survey behind this measurement, ISO.","type":"string"}},"required":["clear_width_m","sigma_m","sampling_gap_m","safe_clear_width_m","limited_by","support_m2","surveyed_on"],"type":"object"},"MonthUsage":{"description":"Current-calendar-month call totals, split like [`UsageDay`].","properties":{"premium":{"description":"Premium-class calls this month.","format":"int64","type":"integer"},"standard":{"description":"Standard-class calls this month.","format":"int64","type":"integer"},"total":{"description":"All metered calls this month.","format":"int64","type":"integer"}},"required":["standard","premium","total"],"type":"object"},"NoVerdictReason":{"description":"Why no verdict could be given.","enum":["no_survey_coverage","insufficient_data"],"type":"string"},"NotAssessed":{"description":"The width axis declining to answer.","properties":{"reason":{"description":"Why, in one sentence a caller can act on.","type":"string"},"verdict":{"description":"Always `not_assessed`.","type":"string"}},"required":["verdict","reason"],"type":"object"},"OutcomeClassCount":{"description":"One status class's request count for the current month.","properties":{"count":{"description":"Requests recorded with that outcome this calendar month.","format":"int64","type":"integer"},"status_class":{"description":"Status class: `2xx`, `401`, `402`, `404`, `422`, `429`, `4xx` or\n`5xx` (see `crate::outcomes`).","type":"string"}},"required":["status_class","count"],"type":"object"},"OutcomesOverview":{"description":"Request-outcome figures for the overview — the error/denial signal the\ngateway historically never stored. Flushed rows only: the buffered\noutcome writer lags by at most the flush interval.","properties":{"by_class":{"description":"Current-month request counts by status class, in class order.\nClasses with no recorded outcome are omitted.","items":{"$ref":"#/components/schemas/OutcomeClassCount"},"type":"array"},"month":{"description":"The calendar month reported (`YYYY-MM`, UTC).","type":"string"},"rejections":{"description":"Current-month requests answered with anything other than `2xx` —\nthe aggregate friction signal (401 storms, 422 traps, 429 walls).","format":"int64","type":"integer"},"server_errors":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/ServerErrorDetail","description":"Endpoint and recency of this month's 5xx, when there are any."}]}},"required":["month","by_class","rejections"],"type":"object"},"OverheadClass":{"description":"What the lowest validated overhead surface above a cell is made of.\n\nThe class is a label on a measurement, never a reason to drop one. A\nbranch across a lane is a real obstruction, so vegetation and wire\nconstrain the verdict exactly as a structure does; what the class changes\nis how the answer is caveated (a vegetation-limited reading on an old\nsurvey degrades, per the verdict arithmetic).","enum":["structure","vegetation","wire","unknown"],"type":"string"},"OverviewResponse":{"description":"Response for `GET /admin/overview`.","properties":{"abuse":{"$ref":"#/components/schemas/AbuseOverview","description":"Self-serve abuse signals (issuance velocity)."},"activation":{"$ref":"#/components/schemas/ActivationOverview","description":"Activation funnel: TTFC north star and conversion rate over the\nrecent signup cohort. Additive; pre-existing fields are unchanged."},"credit":{"$ref":"#/components/schemas/CreditOverview","description":"Prepaid credit ledger totals."},"downloads":{"$ref":"#/components/schemas/DownloadsOverview","description":"Offline territory downloads this month."},"generated_at":{"description":"RFC 3339 timestamp this snapshot was computed at.","type":"string"},"identities":{"$ref":"#/components/schemas/IdentitiesOverview","description":"Signup counts."},"keys":{"$ref":"#/components/schemas/KeysOverview","description":"Key counts by lifecycle state."},"mau":{"$ref":"#/components/schemas/MauOverview","description":"Monthly active users this month."},"outcomes":{"$ref":"#/components/schemas/OutcomesOverview","description":"Request outcomes (successes and denials) by status class this\nmonth. Additive: the pre-existing overview fields are unchanged."},"territories":{"description":"Territories staged on this gateway (update-channel packages and\nhosted tiles), sorted by id. Empty when neither directory is\nconfigured.","items":{"$ref":"#/components/schemas/TerritoryPresence"},"type":"array"},"usage":{"$ref":"#/components/schemas/UsageOverview","description":"Metered calls, per day and for the current month."},"x402":{"$ref":"#/components/schemas/X402Overview","description":"x402 machine-payment status and settled revenue."}},"required":["generated_at","identities","activation","keys","usage","outcomes","mau","downloads","credit","abuse","x402","territories"],"type":"object"},"PartialCoverage":{"description":"Coverage with gaps, each gap named and located.","properties":{"assessed_m":{"description":"Metres of route assessed against usable survey data.","format":"double","type":"number"},"insufficient_data":{"description":"Runs the survey covered but not well enough to decide on.","items":{"$ref":"#/components/schemas/RouteSection"},"type":"array"},"kind":{"description":"Always `partial`.","type":"string"},"not_surveyed":{"description":"Runs with no survey coverage at all: the absence of data.","items":{"$ref":"#/components/schemas/RouteSection"},"type":"array"}},"required":["kind","assessed_m","not_surveyed","insufficient_data"],"type":"object"},"Place":{"description":"One customer place, as uploaded and as returned.","properties":{"alt_names":{"description":"Alternative names (abbreviations, old names); at most 8.","items":{"type":"string"},"type":"array"},"categories":{"description":"Categories used by the `category` search filter (e.g.\n`\"supermarket\"`); at most 16, matched case-insensitively.","items":{"type":"string"},"type":"array"},"country_code":{"description":"ISO 3166-1 alpha-2 country code, e.g. `\"GB\"`.","type":["string","null"]},"id":{"description":"Caller-chosen stable identifier, unique within the dataset\n(1–128 bytes), e.g. `\"store-042\"`.","type":"string"},"importance":{"description":"Rank boost in [0, 1]; larger means more prominent. Defaults to 0.5.","format":"float","type":["number","null"]},"lat":{"description":"Latitude in decimal degrees (WGS84), within [-90, 90].","format":"double","type":"number"},"locality":{"description":"Town or city, for display and search context.","type":["string","null"]},"lon":{"description":"Longitude in decimal degrees (WGS84), within [-180, 180].","format":"double","type":"number"},"name":{"description":"Primary display name (1–200 bytes), e.g. `\"MyBrand Croydon\"`.","type":"string"},"postcode":{"description":"Postcode, formatted as displayed.","type":["string","null"]},"properties":{"description":"Free-form JSON object carried verbatim (opening hours, phone,\ninternal ids, …); at most 2 KiB serialised. Stored and returned,\nnever indexed."},"region":{"description":"County / region, for display and search context.","type":["string","null"]},"street":{"description":"Street, for display and search context.","type":["string","null"]}},"required":["id","name","lat","lon"],"type":"object"},"PlaceNearestHit":{"description":"One `GET /places/nearest` hit.","properties":{"distance_m":{"description":"Haversine distance in metres from the query point.","format":"double","type":"number"},"place":{"$ref":"#/components/schemas/Place","description":"The place, exactly as uploaded."}},"required":["place","distance_m"],"type":"object"},"PlaceNearestResponse":{"description":"Response for `GET /places/nearest`.","properties":{"count":{"description":"Number of results returned.","minimum":0,"type":"integer"},"results":{"description":"Hits ordered nearest first. Empty when no dataset is stored.","items":{"$ref":"#/components/schemas/PlaceNearestHit"},"type":"array"}},"required":["count","results"],"type":"object"},"PlaceSearchHit":{"description":"One `GET /places/search` hit.","properties":{"distance_m":{"description":"Haversine distance in metres from `lat`/`lon`, when given.","format":"double","type":["number","null"]},"place":{"$ref":"#/components/schemas/Place","description":"The matched place, exactly as uploaded."},"score":{"description":"Relevance score (text match × importance × proximity decay);\nlarger is better, comparable only within one response.","format":"float","type":"number"}},"required":["place","score"],"type":"object"},"PlaceSearchResponse":{"description":"Response for `GET /places/search`.","properties":{"count":{"description":"Number of results returned.","minimum":0,"type":"integer"},"query":{"description":"The query text as interpreted.","type":"string"},"results":{"description":"Ranked hits, best first. Empty when no dataset is stored.","items":{"$ref":"#/components/schemas/PlaceSearchHit"},"type":"array"}},"required":["query","count","results"],"type":"object"},"PlacesDataset":{"description":"The stored dataset (`<dir>/<key_id>/dataset.json`), returned by\n`GET /places`.","properties":{"count":{"description":"Number of places stored.","minimum":0,"type":"integer"},"places":{"description":"Every stored place.","items":{"$ref":"#/components/schemas/Place"},"type":"array"},"updated_at":{"description":"RFC 3339 timestamp of the last upload.","type":"string"}},"required":["count","updated_at","places"],"type":"object"},"PlanCount":{"description":"Identities carrying one plan label.","properties":{"identities":{"description":"Identities on that plan.","format":"int64","type":"integer"},"plan":{"description":"The plan label as stored on `identities.plan`; `NULL`/blank is\nreported as [`FREE_PLAN`]. Never mapped to a price here — the\ngateway holds no price list, so the console annotates these counts\nwith published pricing and labels the result an estimate.","type":"string"}},"required":["plan","identities"],"type":"object"},"PlanResponse":{"description":"Response for `POST /admin/identities/{email}/plan`.","properties":{"duplicate":{"description":"`true` when the reason had already been applied (idempotent replay;\nnothing changed).","type":"boolean"},"email":{"description":"Canonical (lowercased) identity email.","type":"string"},"plan":{"description":"The plan now recorded for this reason.","type":"string"},"stripe_customer_id":{"description":"The Stripe customer the identity is linked to after the change.","type":["string","null"]}},"required":["email","plan","duplicate"],"type":"object"},"PointcloudsOverview":{"description":"Response for `GET /admin/pointclouds`.","properties":{"datasets":{"description":"Every dataset with a readable manifest.","items":{"$ref":"#/components/schemas/DatasetSummary"},"type":"array"},"hosting_fees":{"description":"Hosting fees grouped by owner — the invoice lines. Report only:\nnothing here debits a ledger (see the module docs).","items":{"$ref":"#/components/schemas/HostingFeeLine"},"type":"array"}},"required":["datasets","hosting_fees"],"type":"object"},"PrepaidRevenue":{"description":"Revenue signals the database genuinely holds: settled x402 payments,\nhow many identities are linked to a Stripe customer, and the plan\ndistribution.\n\nDeliberately **not** Stripe MRR: the gateway stores no subscription\namounts, prices or invoices, so any monthly-recurring figure would be\nfabricated. `stripe_customers` and `plans` are the honest inputs, and\nthe console multiplies plan counts by published list prices under an\nexplicit \"estimate\" label.\nPrepaid-rail money for the commercial snapshot. Credits only — the\nledger journals credits and not debits, so consumption has no time\naxis yet and is deliberately absent rather than approximated.","properties":{"consumed_this_month_pence":{"description":"Prepaid credit spent on calls this calendar month, in pence\n(`ledger_spend`, migration 0026). Reporting-grade: a crash between\na debit and its spend row undercounts, never overcounts.","format":"int64","type":"integer"},"month":{"description":"The calendar month reported (`YYYY-MM`, UTC).","type":"string"},"other_credited_pence":{"description":"Operator/manual credits this month (every other reason), in pence.","format":"int64","type":"integer"},"outstanding_balance_pence":{"description":"Unspent prepaid balance across all identities, in pence.","format":"int64","type":"integer"},"settlement_failures_24h":{"description":"Per-call x402 settlements that FAILED in the last 24h. Service was\nalready rendered when one of these fails, so anything above zero\nis revenue quietly leaking and worth a look the same day.","format":"int64","type":"integer"},"stripe_credited_pence":{"description":"Card top-ups journalled this month (reasons `stripe:…`), in pence.","format":"int64","type":"integer"},"x402_credited_pence":{"description":"Crypto top-ups journalled this month (reasons `x402:…`), in pence.","format":"int64","type":"integer"}},"required":["month","stripe_credited_pence","x402_credited_pence","other_credited_pence","consumed_this_month_pence","outstanding_balance_pence","settlement_failures_24h"],"type":"object"},"ProbeOptInRequest":{"description":"Body for `PUT /admin/keys/{id}/opt-in`.","properties":{"opted_in":{"description":"`true` to opt the key in to aggregate probe contribution, `false` to\nopt it back out. Off by default for every key.","type":"boolean"}},"required":["opted_in"],"type":"object"},"ProbeOptInResponse":{"description":"Response for `PUT /admin/keys/{id}/opt-in`.","properties":{"key_id":{"description":"Key whose flag was set.","type":"string"},"probe_opt_in":{"description":"The flag's value after the change.","type":"boolean"}},"required":["key_id","probe_opt_in"],"type":"object"},"PutPlacesRequest":{"description":"Body for `PUT /places`.","properties":{"places":{"description":"The full dataset; replaces whatever was stored before.","items":{"$ref":"#/components/schemas/Place"},"type":"array"}},"required":["places"],"type":"object"},"PutPlacesResponse":{"description":"Response for `PUT /places`.","properties":{"count":{"description":"Number of places stored.","minimum":0,"type":"integer"},"updated_at":{"description":"RFC 3339 timestamp of this upload.","type":"string"}},"required":["count","updated_at"],"type":"object"},"QaOut":{"description":"The bake's QA block as published, plus nothing computed here.","properties":{"cells_overhead":{"description":"Cells carrying a validated overhead surface.","format":"int64","minimum":0,"type":"integer"},"cells_road":{"description":"Cells carrying a road surface.","format":"int64","minimum":0,"type":"integer"},"cells_total":{"description":"Cells in the grid.","format":"int64","minimum":0,"type":"integer"},"road_z_mad_m":{"description":"Median absolute deviation of the road **surface height** across the\ngrid, metres, exactly as the bake published it.\n\nNot a quality score, and it should not be read as one: it measures\nthe terrain rather than the fit, so it runs to metres wherever a\nramp climbs and to centimetres on the flat. A gradient is not a bad\nbake.","format":"double","type":"number"},"synthetic_checks_passed":{"description":"Whether the bake's own synthetic checks passed.","type":"boolean"}},"required":["cells_total","cells_road","cells_overhead","road_z_mad_m","synthetic_checks_passed"],"type":"object"},"QuotaResponse":{"description":"Response for `POST /admin/identities/{email}/quota`.","properties":{"duplicate":{"description":"`true` when the reason had already been applied (idempotent\nreplay; nothing changed).","type":"boolean"},"email":{"description":"Canonical (lowercased) identity email.","type":"string"},"keys_updated":{"description":"Number of keys updated (0 on an idempotent replay).","format":"int64","minimum":0,"type":"integer"},"monthly_quota":{"description":"The quota recorded for this reason.","format":"int64","type":"integer"}},"required":["email","monthly_quota","keys_updated","duplicate"],"type":"object"},"RetentionMonth":{"description":"One calendar month of the month-on-month retention **proxy**.\n\nA proxy, not cohort retention: `usage_counters.day` is per-day but the\nonly durable identity-level activity signal is \"did this identity make a\nmetered call in month M\", so this measures month-over-month overlap of\nactive identities, not the survival of a signup cohort. True cohort\nretention needs per-identity daily activity retained for the whole\ncohort window; nothing in the schema stores that today.","properties":{"active":{"description":"Identities that made at least one metered call this month.","format":"int64","type":"integer"},"month":{"description":"The calendar month (`YYYY-MM`, UTC).","type":"string"},"previous_active":{"description":"Identities active the previous calendar month (the denominator).","format":"int64","type":"integer"},"retained":{"description":"Identities active this month that were also active the previous\ncalendar month.","format":"int64","type":"integer"},"retention_rate":{"description":"`retained / previous_active` in `0.0..=1.0`; `0.0` when nobody was\nactive the previous month.","format":"double","type":"number"}},"required":["month","active","retained","previous_active","retention_rate"],"type":"object"},"RetroListResponse":{"description":"Response for `GET /admin/retros`.","properties":{"retros":{"description":"One page of retros, newest first.","items":{"$ref":"#/components/schemas/RetroRecord"},"type":"array"},"total":{"description":"Total stored retros (across all pages).","format":"int64","type":"integer"}},"required":["retros","total"],"type":"object"},"RetroProblem":{"description":"One problem hit during the integration.","properties":{"area":{"description":"Platform area, one of [`PROBLEM_AREAS`].","type":"string"},"description":{"description":"What went wrong (at most 1000 bytes).","type":"string"},"workaround_found":{"description":"Whether a workaround was found.","type":"boolean"}},"required":["area","description","workaround_found"],"type":"object"},"RetroRecord":{"description":"One stored retro, as the admin listing returns it.","properties":{"created_at":{"description":"RFC 3339 submission timestamp.","type":"string"},"id":{"description":"Retro id (UUID v4).","type":"string"},"key_id":{"description":"API key id that submitted it.","type":"string"},"payload":{"description":"The stored retro payload (untrusted content — display only)."},"status":{"description":"Triage status, one of [`RETRO_STATUSES`].","type":"string"}},"required":["id","key_id","created_at","status","payload"],"type":"object"},"RouteClearanceReport":{"description":"The full response for `POST /v1/clearance/along`.","properties":{"advisories":{"description":"Notes on stretches of the route.","items":{"$ref":"#/components/schemas/Advisory"},"type":"array"},"basis":{"description":"Always `surveyed_geometry_not_signage`.","type":"string"},"clearance_enforcement":{"$ref":"#/components/schemas/ClearanceEnforcement","description":"The honesty block."},"datasets":{"description":"The datasets drawn on.","items":{"$ref":"#/components/schemas/DatasetRef"},"type":"array"},"explanation":{"description":"One-line honest summary. British English, no claim words, no em\ndashes; see `explain.rs`, which enforces that rather than asking.","type":"string"},"height":{"$ref":"#/components/schemas/RouteVerdictOut","description":"The height axis."},"route":{"$ref":"#/components/schemas/RouteRef","description":"The route the question was asked about."},"vehicle":{"$ref":"#/components/schemas/VehicleQuery","description":"The vehicle, echoed back including its margin."},"width":{"$ref":"#/components/schemas/WidthOut","description":"The width axis."}},"required":["basis","clearance_enforcement","route","vehicle","height","width","datasets","advisories","explanation"],"type":"object"},"RouteRef":{"description":"The route the question was asked about.","properties":{"distance_m":{"description":"Route length, metres.","format":"double","type":"number"},"geometry_polyline6":{"description":"The corridor as supplied, a six-digit-precision encoded polyline.","type":"string"}},"required":["distance_m","geometry_polyline6"],"type":"object"},"RouteSection":{"description":"A contiguous run of route stations, by distance along the route.","properties":{"end_m":{"description":"Distance along the route at which the run ends, metres.","format":"double","type":"number"},"polyline":{"description":"`[lng, lat]` pairs for display. Note the order: shapes arrive as\n`(lat, lon)` and are flipped once, here, for GeoJSON compatibility.","items":{"items":{"format":"double","type":"number"},"type":"array"},"type":"array"},"start_m":{"description":"Distance along the route at which the run starts, metres.","format":"double","type":"number"}},"required":["start_m","end_m","polyline"],"type":"object"},"RouteVerdictOut":{"description":"The verdict for the height axis.\n\nSee the module docs. `Pass` takes [`CompleteCoverage`] and nothing else,\nand `NoVerdict` takes [`PartialCoverage`] and nothing else. The first is\nthe safety invariant; the second is its mirror, stated here because the\ncontract only implied it: \"no verdict\" over ground that was fully\nassessed would be a contradiction, and a caller reading `no_verdict` is\nentitled to find the gaps that caused it.","oneOf":[{"description":"The vehicle clears every assessed station by more than the stated\nbound, and every metre of the route was assessed.","properties":{"coverage":{"$ref":"#/components/schemas/CompleteCoverage","description":"Complete coverage. There is no variant of this carrying gaps."},"tightest":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LimitingPointOut","description":"How close it was, because a pass with little to spare is worth\nseeing.\n\nAbsent where the survey found no overhead surface above the\ncorridor at all. The contract makes this field mandatory, which\nis a state a real corridor can fail to have: a surveyed stretch\nof open road has no tightest point, and inventing one would put\na fabricated headroom in a safety report. Serialisation omits\nthe field rather than emitting a null."}]},"verdict":{"enum":["pass"],"type":"string"}},"required":["coverage","verdict"],"type":"object"},{"description":"The vehicle does not clear a measured headroom somewhere.","properties":{"all_failures":{"description":"**Every point that fails, worst first, including `limiting`.**\n\n`all_failures[0]` and `limiting` are the same point. The contract\nleft the relationship unstated, which leaves a renderer to guess\nwhether to draw the list, the limiting point, or both, and one of\nthose guesses draws the worst obstruction twice. Pinned here: the\nlist is complete, `limiting` is a convenience, and a caller that\ndraws only the list has drawn everything.","items":{"$ref":"#/components/schemas/LimitingPointOut"},"type":"array"},"coverage":{"$ref":"#/components/schemas/CoverageOut","description":"Coverage, either kind: a failure is a failure whether or not the\nrest of the route was surveyed."},"limiting":{"$ref":"#/components/schemas/LimitingPointOut","description":"The worst point."},"verdict":{"enum":["fail"],"type":"string"}},"required":["coverage","limiting","all_failures","verdict"],"type":"object"},{"description":"The vehicle sits inside the uncertainty band, or something else\nprevents an answer that is not a coverage gap.","properties":{"contested":{"description":"The points that could not be called, worst first.","items":{"$ref":"#/components/schemas/LimitingPointOut"},"type":"array"},"coverage":{"$ref":"#/components/schemas/CoverageOut","description":"Coverage, either kind."},"reasons":{"description":"Why, machine-readable, sorted and free of repeats.","items":{"$ref":"#/components/schemas/IndeterminateReason"},"type":"array"},"resolution_hint":{"description":"What would resolve it, in one sentence, for a human.","type":"string"},"verdict":{"enum":["indeterminate"],"type":"string"}},"required":["coverage","contested","reasons","resolution_hint","verdict"],"type":"object"},{"description":"Coverage gaps prevented an answer.","properties":{"coverage":{"$ref":"#/components/schemas/PartialCoverage","description":"Partial coverage. A complete-coverage no-verdict is not a state."},"reason":{"$ref":"#/components/schemas/NoVerdictReason","description":"Which kind of gap."},"verdict":{"enum":["no_verdict"],"type":"string"}},"required":["coverage","reason","verdict"],"type":"object"}]},"SelectionRequest":{"properties":{"id":{"description":"The `id` property of the chosen feature.","type":"string"},"query":{"description":"The query as typed.","type":"string"}},"required":["query","id"],"type":"object"},"SelectionResponse":{"properties":{"status":{"description":"Always `\"recorded\"`.","type":"string"}},"required":["status"],"type":"object"},"SelfResponse":{"description":"Response for `GET /v1/keys/self`.","properties":{"credits_millipence":{"description":"Prepaid credit balance in internal millipence (1p = 1000\nmillipence) — full precision for fractional, tiered per-call prices.","format":"int64","type":"integer"},"credits_pence":{"description":"Prepaid credit balance of the owning identity, in pence (floor of\nthe internal millipence balance).","format":"int64","type":"integer"},"download_allowance_mib":{"description":"Monthly offline-download allowance in MiB (identity-bound). A plan\nor the SDK licence raises this above the free evaluation default.","format":"int64","type":"integer"},"download_used_mib":{"description":"Offline download bytes served this calendar month, in MiB (rounded\nup), identity-wide.","format":"int64","type":"integer"},"email":{"description":"Delivery email of the owning identity — the key holder already\nproved control of the key, and the website's checkout derives the\nbilling identity from this rather than trusting client input.\nAbsent on operator (`admin`) keys, which have no identity.","type":["string","null"]},"expires_at":{"description":"RFC 3339 expiry — present on provisional keys only.","type":["string","null"]},"first_success_at":{"description":"RFC 3339 timestamp of this key's first successful (2xx) metered\nAPI call — the activation event — or `null` if it has never had\none. The stamp is written by a buffered task, so it can lag the\nfirst success by a moment.","type":["string","null"]},"identity_keys":{"description":"Number of active keys sharing this identity (including this one).","format":"int64","type":"integer"},"key_id":{"description":"Key id (UUID).","type":"string"},"mau_included":{"description":"Included monthly active users per calendar month (identity-bound).\nA plan or the admin API raises this above the free default.","format":"int64","type":"integer"},"mau_overdrawn":{"description":"Whether a new distinct user beyond the included count could not be\ncovered by the prepaid ledger this month. **Fail-open:** requests\nkeep being served regardless; sustained overdraw is a commercial\nconversation, not a hard stop.","type":"boolean"},"mau_this_month":{"description":"Distinct monthly active users (`X-MapMap-User` identifiers)\nrecorded this calendar month, identity-wide. Flushed rows only —\nthe buffered writer lags by at most the flush interval.","format":"int64","type":"integer"},"monthly_quota":{"description":"Requests allowed per calendar month (identity-bound for\nself-served keys).","format":"int64","type":"integer"},"remaining":{"description":"Free-tier requests remaining this month.","format":"int64","type":"integer"},"state":{"description":"Lifecycle state: `admin`, `provisional` or `verified`.","type":"string"},"used_this_month":{"description":"Requests used this month (identity-wide for self-served keys,\nincluding not-yet-flushed ones for this key).","format":"int64","type":"integer"}},"required":["key_id","state","monthly_quota","used_this_month","remaining","credits_pence","credits_millipence","identity_keys","download_allowance_mib","download_used_mib","mau_this_month","mau_included","mau_overdrawn"],"type":"object"},"ServerErrorDetail":{"description":"Where this month's server errors actually happened — enough for the\nconsole's triage ribbon to tell a live fire from a finished burst\nwithout anyone opening a database. Present only when the month has a\n5xx at all.","properties":{"last_day":{"description":"The most recent UTC day (`YYYY-MM-DD`) any 5xx was recorded.","type":"string"},"top_endpoint":{"description":"The endpoint with the most 5xx this month (ties: first by name).","type":"string"},"top_endpoint_count":{"description":"5xx recorded against that endpoint this month.","format":"int64","type":"integer"}},"required":["top_endpoint","top_endpoint_count","last_day"],"type":"object"},"SessionExchangeRequest":{"description":"Body for `POST /v1/console/session`.","properties":{"token":{"description":"The single-use token from the emailed sign-in link.","type":"string"}},"required":["token"],"type":"object"},"SessionResponse":{"description":"Response for `POST /v1/console/session`.","properties":{"email":{"description":"Delivery email of the identity (the user just proved control of\nthis mailbox).","type":"string"},"expires_at":{"description":"RFC 3339 session expiry.","type":"string"},"session":{"description":"The session bearer (`sns_…`). The website stores it in an httpOnly\ncookie on its own origin; it is never an API credential.","type":"string"}},"required":["session","expires_at","email"],"type":"object"},"SetDownloadAllowanceRequest":{"description":"Body for `POST /admin/identities/{email}/download-allowance`.","properties":{"mib":{"description":"New monthly offline-download allowance for the identity, in MiB\n(>= 0). Plans raise it; cancellation resets it to the free eval\ndefault.","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe-sub:sub_…:1750000000`. Repeating a\nreason is a no-op returning the recorded allowance.","type":"string"}},"required":["mib","reason"],"type":"object"},"SetMauIncludedRequest":{"description":"Body for `POST /admin/identities/{email}/mau-included`.","properties":{"mau":{"description":"New included monthly active users for the identity (>= 0). Plans\nraise it; cancellation resets it to the free default.","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe-sub:sub_…:1750000000`. Repeating a\nreason is a no-op returning the recorded value.","type":"string"}},"required":["mau","reason"],"type":"object"},"SetPlanRequest":{"description":"Body for `POST /admin/identities/{email}/plan`.","properties":{"plan":{"description":"Coarse plan label to record on the identity: 1–[`MAX_PLAN_LEN`]\ncharacters of lowercase `a-z`, `0-9`, `-` or `_`. Cancellation\nwrites [`FREE_PLAN`] back.","type":"string"},"reason":{"description":"Idempotency reason, e.g. `stripe-sub:sub_…:1750000000`. Repeating a\nreason is a no-op returning the recorded plan. Share it with the\nquota / download-allowance / included-MAU calls for the same Stripe\nevent so all four move together.","type":"string"},"stripe_customer_id":{"description":"The Stripe customer that paid (`cus_…`), when the change came from\nStripe. Optional so sales/comp plans can be set by hand; omitting it\n(or sending `null`) leaves any existing link untouched — it is never\ncleared, so a lapsed account stays traceable to its past invoices.","type":["string","null"]}},"required":["plan","reason"],"type":"object"},"SetQuotaRequest":{"description":"Body for `POST /admin/identities/{email}/quota`.","properties":{"monthly_quota":{"description":"New monthly quota for every non-revoked verified key of the\nidentity (>= 0). Provisional keys keep their allowance.","format":"int64","type":"integer"},"reason":{"description":"Idempotency reason, e.g. `stripe-sub:sub_…:1750000000`. Repeating\na reason is a no-op returning the recorded state.","type":"string"}},"required":["monthly_quota","reason"],"type":"object"},"SetSubscriptionRequest":{"description":"Body for `POST /admin/identities/{email}/subscription` — what Stripe\nACTUALLY charges, so recurring revenue stops being an estimate.","properties":{"billed_pence":{"description":"What the last invoice collected, in minor units (pence).","format":"int64","type":"integer"},"billing_interval":{"description":"Billing interval as Stripe names it: `month`, `year`.","type":"string"},"currency":{"description":"ISO currency, lowercase (`gbp`).","type":"string"},"current_period_end":{"description":"RFC 3339 end of the paid period, when known.","type":["string","null"]},"reason":{"description":"Idempotency reason — the Stripe EVENT id (`stripe-evt:evt_…`),\nunique per delivery and identical across retries of it.","type":"string"},"status":{"description":"Lifecycle status as Stripe reports it: `active`, `trialing`,\n`past_due`, `unpaid`, `canceled` …","type":"string"},"stripe_subscription_id":{"description":"The Stripe subscription (`sub_…`).","type":"string"}},"required":["stripe_subscription_id","status","billed_pence","currency","billing_interval","reason"],"type":"object"},"SettledRevenue":{"description":"Settled x402 revenue over one window, split by asset.","properties":{"by_asset":{"description":"Per-(network, asset) totals, biggest earner first.","items":{"$ref":"#/components/schemas/AssetRevenue"},"type":"array"},"settlements":{"description":"Successful settlements in the window, across every asset.","format":"int64","type":"integer"}},"required":["settlements","by_asset"],"type":"object"},"SigninRequest":{"description":"Body for `POST /v1/console/signin`.","properties":{"email":{"description":"Email address to send the sign-in link to.","type":"string"}},"required":["email"],"type":"object"},"SigninResponse":{"description":"Uniform response for sign-in link requests.","properties":{"status":{"description":"Always `\"sent-if-exists\"` — the response is identical whether or\nnot the address has an account, so it cannot be used to probe\nwhich emails are MapMap customers.","type":"string"}},"required":["status"],"type":"object"},"SourceCount":{"description":"One signup channel's tally.","properties":{"last_30d":{"description":"Of those, created in the last 30 days.","format":"int64","type":"integer"},"source":{"description":"The sanitised channel tag as stored.","type":"string"},"total":{"description":"Identities carrying it, all-time.","format":"int64","type":"integer"}},"required":["source","total","last_30d"],"type":"object"},"StyleMeta":{"description":"Per-style metadata (`<dir>/<id>/meta.json`).","properties":{"created_at":{"description":"RFC 3339 creation timestamp.","type":"string"},"id":{"description":"Style id (kebab-case slug + 6 hex chars).","type":"string"},"latest_version":{"description":"Highest published version; `GET /styles/{id}.json` serves it.","format":"int64","minimum":0,"type":"integer"},"name":{"description":"Human-readable style name.","type":"string"},"owner":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/StyleOwner","description":"Who may list and publish over this style.\n\n`None` for styles created before ownership was recorded: they keep\nserving public reads, are listed to nobody and writable by nobody\nuntil an operator adopts them (see the module docs). Optional so the\non-disk documents written by earlier gateway versions still parse —\nthe migration is a backfill, never a reformat."}]},"updated_at":{"description":"RFC 3339 timestamp of the latest publish.","type":"string"}},"required":["id","name","latest_version","created_at","updated_at"],"type":"object"},"StyleOwner":{"description":"Who owns a hosted style: the API key that created it, and the identity\nthat key belongs to when it is a self-served key.\n\nThe identity is what ownership is really checked against wherever it\nexists — a self-served account may hold up to five sibling keys and they\nshare one quota, so they must share one set of styles too, or rotating a\nkey would silently strand every style it had published. Operator-issued\nkeys have no identity and fall back to the key id.","properties":{"identity_id":{"description":"Owning identity id, for self-served keys. `None` for\noperator-issued keys, which have no identity.","type":["string","null"]},"key_id":{"description":"Id of the API key that created the style.","type":"string"}},"required":["key_id"],"type":"object"},"StyleSummary":{"description":"One entry of `GET /styles`.","properties":{"id":{"description":"Style id.","type":"string"},"latest_version":{"description":"Highest published version.","format":"int64","minimum":0,"type":"integer"},"name":{"description":"Human-readable style name.","type":"string"},"style_url":{"description":"Immutable URL of the latest compiled style.","type":"string"}},"required":["id","name","latest_version","style_url"],"type":"object"},"StyleVersionResponse":{"description":"Response for `POST /styles/{id}`.","properties":{"id":{"description":"Style id.","type":"string"},"style_url":{"description":"Immutable URL of the compiled style at this version.","type":"string"},"version":{"description":"The newly published, immutable version.","format":"int64","minimum":0,"type":"integer"}},"required":["id","version","style_url"],"type":"object"},"SubscriptionResponse":{"description":"Response for `POST /admin/identities/{email}/subscription`.","properties":{"billed_pence":{"format":"int64","type":"integer"},"duplicate":{"type":"boolean"},"email":{"type":"string"},"status":{"type":"string"}},"required":["email","status","billed_pence","duplicate"],"type":"object"},"TerritoryPresence":{"description":"One territory's presence across the hosted surfaces.","properties":{"id":{"description":"Territory id, e.g. `uk`.","type":"string"},"package_versions":{"description":"Published update-channel package versions (from `SN_CHANNEL_DIR`),\nsorted; empty when the territory has no offline package staged.","items":{"type":"string"},"type":"array"},"tiles":{"description":"Whether a hosted-tiles PMTiles archive is staged (`SN_TILES_DIR`).","type":"boolean"}},"required":["id","package_versions","tiles"],"type":"object"},"TruckDimensions":{"description":"Doc-only mirror of [`sn_adr::TruckDimensions`].","properties":{"axle_count":{"description":"Number of axles, if known.","format":"int32","minimum":0,"type":["integer","null"]},"axle_load_t":{"description":"Heaviest single-axle load in metric tonnes, if known.","format":"double","type":["number","null"]},"gross_weight_t":{"description":"Gross combination weight in metric tonnes.","example":40.0,"format":"double","type":"number"},"height_m":{"description":"Vehicle height in metres.","example":4.0,"format":"double","type":"number"},"length_m":{"description":"Vehicle length in metres.","example":16.5,"format":"double","type":"number"},"width_m":{"description":"Vehicle width in metres.","example":2.55,"format":"double","type":"number"}},"required":["height_m","width_m","length_m","gross_weight_t"],"type":"object"},"UpdateMapIssueRequest":{"description":"Body for `PATCH /admin/map-issues/{id}`.","properties":{"admin_note":{"description":"Triage note (trimmed, at most 1000 bytes). Omitted leaves the\nstored note unchanged.","type":["string","null"]},"status":{"description":"New triage status, one of the accepted statuses.","type":"string"}},"required":["status"],"type":"object"},"UpdateRetroRequest":{"description":"Body for `PATCH /admin/retros/{id}`.","properties":{"status":{"description":"New triage status, one of [`RETRO_STATUSES`].","type":"string"}},"required":["status"],"type":"object"},"UpdateStyleRequest":{"description":"Body for `POST /styles/{id}`.","properties":{"theme":{"description":"The full sn-style theme document to publish as the next version."}},"required":["theme"],"type":"object"},"UsageDay":{"description":"One day of metered calls, split by price class.\n\nThe class split is derived from the stored endpoint labels: `/adr/check`\nis the premium-by-path endpoint, every other label counts as standard.\nTruck/ADR calls on mixed endpoints (classified per request body at bill\ntime) are indistinguishable in `usage_counters` and count as standard, so\nthe premium figure is a floor, not an exact billing total.","properties":{"day":{"description":"ISO `YYYY-MM-DD` UTC day.","type":"string"},"premium":{"description":"Premium-class calls recorded that day (see the type docs for the\nendpoint-label approximation).","format":"int64","type":"integer"},"standard":{"description":"Standard-class calls recorded that day.","format":"int64","type":"integer"}},"required":["day","standard","premium"],"type":"object"},"UsageOverview":{"description":"Metered-call figures for the overview. Flushed rows only: the buffered\nusage writer lags by at most the flush interval.","properties":{"by_endpoint":{"description":"Current-month metered calls split by collapsed endpoint, busiest\nfirst — turns the coarse standard/premium split into real\nproduct-usage understanding.","items":{"$ref":"#/components/schemas/EndpointUsage"},"type":"array"},"days":{"description":"Per-day counts for the last [`OVERVIEW_USAGE_DAYS`] days, oldest\nfirst. Days with no recorded usage are omitted.","items":{"$ref":"#/components/schemas/UsageDay"},"type":"array"},"month":{"$ref":"#/components/schemas/MonthUsage","description":"Current-calendar-month totals."}},"required":["days","month","by_endpoint"],"type":"object"},"UsageRow":{"description":"One `usage_counters` row.","properties":{"count":{"description":"Number of authenticated requests recorded.","format":"int64","type":"integer"},"day":{"description":"ISO `YYYY-MM-DD` UTC day.","type":"string"},"endpoint":{"description":"Endpoint label, e.g. `/route` or `/route/v1`.","type":"string"}},"required":["day","endpoint","count"],"type":"object"},"ValidateGeodataRequest":{"description":"Doc-only mirror of the `/geodata/validate` request body.","properties":{"coordinates":{"description":"A sample of the dataset's raw coordinates. A few dozen is plenty:\nthe check is about ranges and spans, not volume.","items":{"$ref":"#/components/schemas/GeodataCoordinate"},"type":"array"},"declared_crs":{"description":"The CRS the dataset claims, e.g. `\"EPSG:4326\"` or `\"EPSG:32610\"`.","example":"EPSG:32610","type":"string"}},"required":["declared_crs","coordinates"],"type":"object"},"ValidateGeodataResponse":{"description":"Doc-only mirror of [`sn_crs::ValidateOutput`].","properties":{"extent":{"$ref":"#/components/schemas/GeodataExtent","description":"Observed extent of the sample, in the dataset's own units."},"interpreted_as":{"description":"The CRS family the declaration was understood as.","example":"UTM zone 10N","type":"string"},"problems":{"description":"Plain-language findings, most important first. Empty when consistent.","items":{"type":"string"},"type":"array"},"suggestions":{"description":"What the numbers look like when the declaration does not fit.","items":{"type":"string"},"type":"array"},"verdict":{"description":"`consistent`, `suspect` or `impossible`.","example":"impossible","type":"string"}},"required":["verdict","interpreted_as","problems","suggestions","extent"],"type":"object"},"VehicleQuery":{"description":"The vehicle the question was asked about.","properties":{"height_m":{"description":"Vehicle height, metres. Required; there is no default vehicle.","format":"double","type":"number"},"margin_m":{"description":"The caller's operating margin, metres, added to the height. Defaults\nto zero: nobody's compliance policy is baked in here, both numbers\nare surfaced, and the margin is echoed back.","format":"double","type":"number"},"width_m":{"description":"Vehicle width, metres. The width axis is assessed only when given.","format":"double","type":["number","null"]}},"required":["height_m","margin_m"],"type":"object"},"WeatherAlongRequest":{"description":"Request body.","properties":{"depart_at":{"description":"Departure time, RFC 3339; defaults to now.","type":["string","null"]},"duration_s":{"description":"Total route duration in seconds, used to compute each sample's ETA\nby arc-length fraction. Omit to fall back to a generic\n[`DEFAULT_AVERAGE_SPEED_KPH`] estimate (flagged in the response as\n`duration_source: \"estimated_default_speed\"`); pass the real value\nfrom `/route` for accurate alignment.","format":"double","type":["number","null"]},"sample_interval_m":{"description":"Route-sampling interval in metres; default 25,000, clamped to\n5,000–100,000, and the point count is separately capped at\n[`MAX_SAMPLES`].","format":"double","type":["number","null"]},"shape":{"description":"The route shape as a polyline6 string.","type":"string"}},"required":["shape"],"type":"object"},"WeatherAlongResponse":{"description":"Response body.","properties":{"attribution":{"description":"Present when at least one sample carries real data.","type":["string","null"]},"depart_at":{"description":"The departure time actually used (RFC 3339 UTC).","type":"string"},"duration_s":{"description":"The route duration actually used, seconds.","format":"double","type":"number"},"duration_source":{"description":"`provided` | `estimated_default_speed` — see [`DurationSource`].","type":"string"},"sample_interval_m":{"description":"The sampling interval actually used, metres (after clamping/the\n[`MAX_SAMPLES`] cap — may be coarser than requested on a long\nroute).","format":"double","type":"number"},"samples":{"description":"Route samples, `arc_m` ascending.","items":{"$ref":"#/components/schemas/WeatherSample"},"type":"array"}},"required":["samples","depart_at","duration_s","duration_source","sample_interval_m"],"type":"object"},"WeatherSample":{"description":"One route sample's weather, aligned to its own ETA.","properties":{"arc_m":{"description":"Distance along the route to this sample, metres.","format":"double","type":"number"},"available":{"description":"Whether real forecast data is present below. `false` means every\nweather field is omitted — never a fabricated fallback value.","type":"boolean"},"eta":{"description":"RFC 3339 UTC estimated time of arrival at this point.","type":"string"},"forecast_hour":{"description":"`eta` rounded to the nearest hour, UTC — the forecast hour actually\nserved. Absent only when no fetch was even attempted for this\nsample's cell (an upstream failure on a different cell in the same\nrequest never affects this field).","type":["string","null"]},"lat":{"format":"double","type":"number"},"lon":{"format":"double","type":"number"},"precipitation_mm":{"format":"double","type":["number","null"]},"precipitation_probability_pct":{"format":"double","type":["number","null"]},"precipitation_type":{"description":"`\"rain\"`, `\"snow\"`, `\"mixed\"` or `\"none\"`, derived from `rain_mm`\nand `snowfall_cm`.","type":["string","null"]},"rain_mm":{"format":"double","type":["number","null"]},"severe":{"description":"See the module docs, \"What severe means here\". Omitted (not\n`false`) when the sample is unavailable.","type":["boolean","null"]},"snowfall_cm":{"format":"double","type":["number","null"]},"temperature_c":{"format":"double","type":["number","null"]},"unavailable_reason":{"description":"Present only when `available` is `false`.","type":["string","null"]},"visibility_m":{"format":"double","type":["number","null"]},"weather_code":{"description":"The source's raw WMO weather code, for callers who want their own\nmapping.","format":"double","type":["number","null"]},"wind_gusts_kph":{"description":"Gust speed, km/h — the figure that matters for high-sided vehicles.","format":"double","type":["number","null"]},"wind_speed_kph":{"description":"Sustained wind speed, km/h.","format":"double","type":["number","null"]}},"required":["lat","lon","arc_m","eta","available"],"type":"object"},"WidthOut":{"description":"The width axis, which can decline to answer where the height axis still\ncan.","oneOf":[{"$ref":"#/components/schemas/WidthVerdictOut","description":"A verdict about the width of the clear corridor."},{"$ref":"#/components/schemas/NotAssessed","description":"Not assessed, with the reason spelled out."}]},"WidthVerdictOut":{"description":"The verdict for the width axis.\n\nStructurally identical to [`RouteVerdictOut`] and deliberately a separate\ntype: its points carry a [`MeasuredLateral`], so a width answer can never\nbe read as a headroom. The same two coverage invariants hold here.","oneOf":[{"description":"A corridor wide enough for the vehicle is clear at every assessed\nstation, and every metre of the route was assessed.","properties":{"coverage":{"$ref":"#/components/schemas/CompleteCoverage","description":"Complete coverage. There is no variant of this carrying gaps."},"tightest":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/LateralPointOut","description":"The narrowest point, absent where nothing narrowed the corridor\ninside the assessed band."}]},"verdict":{"enum":["pass"],"type":"string"}},"required":["coverage","verdict"],"type":"object"},{"description":"The corridor closes below the vehicle's width somewhere.","properties":{"all_failures":{"description":"Every point that fails, worst first, including `limiting`.","items":{"$ref":"#/components/schemas/LateralPointOut"},"type":"array"},"coverage":{"$ref":"#/components/schemas/CoverageOut","description":"Coverage, either kind."},"limiting":{"$ref":"#/components/schemas/LateralPointOut","description":"The worst point."},"verdict":{"enum":["fail"],"type":"string"}},"required":["coverage","limiting","all_failures","verdict"],"type":"object"},{"description":"The width sits inside the uncertainty band, or something else\nprevents an answer that is not a coverage gap.","properties":{"contested":{"description":"The points that could not be called, worst first.","items":{"$ref":"#/components/schemas/LateralPointOut"},"type":"array"},"coverage":{"$ref":"#/components/schemas/CoverageOut","description":"Coverage, either kind."},"reasons":{"description":"Why, machine-readable, sorted and free of repeats.","items":{"$ref":"#/components/schemas/IndeterminateReason"},"type":"array"},"resolution_hint":{"description":"What would resolve it, in one sentence, for a human.","type":"string"},"verdict":{"enum":["indeterminate"],"type":"string"}},"required":["coverage","contested","reasons","resolution_hint","verdict"],"type":"object"},{"description":"Coverage gaps prevented an answer.","properties":{"coverage":{"$ref":"#/components/schemas/PartialCoverage","description":"Partial coverage."},"reason":{"$ref":"#/components/schemas/NoVerdictReason","description":"Which kind of gap."},"verdict":{"enum":["no_verdict"],"type":"string"}},"required":["coverage","reason","verdict"],"type":"object"}]},"X402Overview":{"description":"x402 machine-payment status and settled-revenue figures for the\noverview, read from config and the settlement journal (migration 0010).","properties":{"enabled":{"description":"Whether x402 is configured on this deployment.","type":"boolean"},"network":{"description":"The primary network advertised (e.g. `base`, `base-sepolia`), or\nempty when x402 is unconfigured.","type":"string"},"settled_atomic_this_month":{"description":"Sum of settled `amount_atomic` this month, in the asset's base units\n(decimal string; USDC 6-dp, so `\"15000\"` = $0.015). String because\nthe total can exceed `i64`.","type":"string"},"settled_this_month":{"description":"Successful settlements journalled this calendar month.","format":"int64","type":"integer"},"settled_total":{"description":"Successful settlements journalled all-time.","format":"int64","type":"integer"}},"required":["enabled","network","settled_this_month","settled_atomic_this_month","settled_total"],"type":"object"}},"securitySchemes":{"admin_token":{"scheme":"bearer","type":"http"},"api_key_bearer":{"scheme":"bearer","type":"http"},"api_key_query":{"in":"query","name":"api_key","type":"apiKey"}}},"info":{"description":"Truck and dangerous-goods (ADR) routing API: Valhalla-compatible and OSRM-compatible route endpoints plus a standalone ADR tunnel-compliance check, metered by API key.","license":{"name":"Proprietary"},"title":"sn-gateway","version":"0.3.0"},"openapi":"3.1.0","paths":{"/":{"get":{"operationId":"index","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Service name, version and documentation links"}},"summary":"Minimal service index.","tags":["meta"]}},"/admin/clearance":{"get":{"description":"Read-only, and unfiltered by customer visibility: this is the operator's\nview of the deployment.","operationId":"admin_overview","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClearanceOverview"}}},"description":"Clearance artefacts, staleness and QA stats"},"401":{"description":"Missing or wrong admin token"}},"security":[{"admin_bearer":[]}],"summary":"`GET /admin/clearance` — the clearance artefact inventory: what is\npublished, whether it still matches its sources, and what the bake said\nabout its own quality.","tags":["admin"]}},"/admin/commercial":{"get":{"description":"Every figure here is something the database actually holds. In\nparticular there is **no Stripe MRR**: the gateway stores no\nsubscription amounts, so the response carries plan counts and lets the\nconsole annotate them with published list prices under an explicit\nestimate label. `retention_proxy` is a proxy by construction — see\n[`RetentionMonth`].","operationId":"commercial","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommercialResponse"}}},"description":"Settled revenue, plan distribution, activation funnel, usage mix and the month-on-month retention proxy"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Commercial and valuation metrics, derived read-only from tables the\ngateway already writes: the x402 settlement journal (migration 0011),\nthe identity plan/Stripe columns (0012), `api_keys.first_success_at`\n(0015) and `usage_counters` (0001).","tags":["admin"]}},"/admin/identities":{"get":{"operationId":"list_identities","parameters":[{"description":"Page size (1–200, default 50).","in":"query","name":"limit","required":false,"schema":{"format":"int64","type":"integer"}},{"description":"Rows to skip (>= 0, default 0).","in":"query","name":"offset","required":false,"schema":{"format":"int64","type":"integer"}},{"description":"Sort order: `recent` (newest signup first, default) or `usage`\n(highest month-to-date calls first).","in":"query","name":"sort","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityListResponse"}}},"description":"One page of identities"},"400":{"content":{"application/problem+json":{}},"description":"Invalid limit, offset or sort"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Pages through all identities with per-identity keys, month usage, MAU\nand balance.","tags":["admin"]}},"/admin/identities/credit":{"post":{"operationId":"credit_identity","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditIdentityRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}},"description":"New (or unchanged, when the reason was replayed) balance"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"Credits an identity ledger by email. Idempotent by `reason`.","tags":["admin"]}},"/admin/identities/{email}":{"get":{"operationId":"identity_detail","parameters":[{"description":"Identity email (case-insensitive)","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IdentityDetailResponse"}}},"description":"Identity, keys, balance and history"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"Inspects an identity: keys, ledger and credit history.","tags":["admin"]}},"/admin/identities/{email}/download-allowance":{"post":{"operationId":"set_identity_download_allowance","parameters":[{"description":"Identity email (case-insensitive)","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDownloadAllowanceRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DownloadAllowanceResponse"}}},"description":"Allowance applied (or unchanged, when the reason was replayed)"},"400":{"content":{"application/problem+json":{}},"description":"Invalid mib or reason"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"Sets the monthly offline-download allowance (MiB) on an identity\n(subscription plans / SDK licence). Idempotent by `reason`.","tags":["admin"]}},"/admin/identities/{email}/mau-included":{"post":{"operationId":"set_identity_mau_included","parameters":[{"description":"Identity email (case-insensitive)","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetMauIncludedRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MauIncludedResponse"}}},"description":"Included MAU applied (or unchanged, when the reason was replayed)"},"400":{"content":{"application/problem+json":{}},"description":"Invalid mau or reason"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"Sets the included monthly active users on an identity (subscription\nplans / sales). Idempotent by `reason`, mirroring the quota endpoint.","tags":["admin"]}},"/admin/identities/{email}/plan":{"post":{"description":"**The identity ↔ Stripe customer join.** The path email is the primary\nkey: it is the identity's own address, read out of the gateway by\ncheckout and mirrored into our Stripe metadata, and it resolves through\n[`identity::identity_by_email`]'s alias-collapsed `canonical_key` — so a\nsubscription bought as `you+work@gmail.com` reaches the identity that\nsigned up as `you@gmail.com`. `stripe_customer_id` is *stamped onto*\nthat row, not joined on, because Checkout mints a fresh Stripe customer\nper session: customer → identity is many-to-one, so it could never be\nthe primary join. When the email matches nothing but the supplied\ncustomer id is already linked to an identity, that link is used as a\nfallback (the customer edited their email in Stripe's billing portal),\nrather than 404-ing for Stripe's whole retry window.","operationId":"set_identity_plan","parameters":[{"description":"Identity email (case-insensitive)","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPlanRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanResponse"}}},"description":"Plan applied (or unchanged, when the reason was replayed)"},"400":{"content":{"application/problem+json":{}},"description":"Invalid plan, stripe_customer_id or reason"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email, and no identity linked to the supplied Stripe customer"}},"security":[{"admin_token":[]}],"summary":"Records an identity's plan and its Stripe customer link (subscription\nactivation, renewal, cancellation, or a sales/comp plan set by hand).\nIdempotent by `reason`, mirroring the quota endpoint.","tags":["admin"]}},"/admin/identities/{email}/quota":{"post":{"operationId":"set_identity_quota","parameters":[{"description":"Identity email (case-insensitive)","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetQuotaRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuotaResponse"}}},"description":"Quota applied (or unchanged, when the reason was replayed)"},"400":{"content":{"application/problem+json":{}},"description":"Invalid quota or reason"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"Sets the monthly quota on every non-revoked verified key of an\nidentity (subscription plans). Idempotent by `reason`.","tags":["admin"]}},"/admin/identities/{email}/subscription":{"post":{"operationId":"set_identity_subscription","parameters":[{"description":"Identity email","in":"path","name":"email","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetSubscriptionRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionResponse"}}},"description":"Recorded (or an idempotent replay)"},"400":{"content":{"application/problem+json":{}},"description":"Malformed request"},"404":{"content":{"application/problem+json":{}},"description":"Unknown identity email"}},"security":[{"admin_token":[]}],"summary":"`POST /admin/identities/{email}/subscription` — record the billed\ntruth for an identity's subscription. Operator-only, written by the\nStripe webhook; idempotent per Stripe event id.","tags":["admin"]}},"/admin/keys":{"get":{"operationId":"list_keys","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/ApiKeyRecord"},"type":"array"}}},"description":"All keys, newest first"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Lists all keys (never includes key material or hashes).","tags":["admin"]},"post":{"operationId":"create_key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateKeyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedKeyResponse"}}},"description":"Key created; the `key` field is shown once only"},"400":{"content":{"application/problem+json":{}},"description":"Invalid name, quota or rate"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Creates a metered API key.","tags":["admin"]}},"/admin/keys/{id}":{"delete":{"operationId":"revoke_key","parameters":[{"description":"Key id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Key revoked (or already revoked)"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown key id"}},"security":[{"admin_token":[]}],"summary":"Revokes a key. Revoked keys fail authentication immediately; revocation\nis idempotent.","tags":["admin"]}},"/admin/keys/{id}/credit":{"post":{"operationId":"credit_key","parameters":[{"description":"Key id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditKeyRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditResponse"}}},"description":"New (or unchanged, when the reason was replayed) balance"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown key id"},"409":{"content":{"application/problem+json":{}},"description":"Key is operator-issued and has no identity ledger"}},"security":[{"admin_token":[]}],"summary":"Credits the identity ledger behind a key. Idempotent by `reason`.","tags":["admin"]}},"/admin/keys/{id}/opt-in":{"put":{"operationId":"set_key_opt_in","parameters":[{"description":"Key id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProbeOptInRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProbeOptInResponse"}}},"description":"Flag set"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown key id"}},"security":[{"admin_token":[]}],"summary":"Sets a key's probe contribution opt-in flag — the per-key consent gate on\n`POST /v1/probe`. A key contributes aggregate road-segment statistics only\nwhile this is `true`; it is `false` for every key by default. Idempotent.","tags":["admin"]}},"/admin/keys/{id}/usage":{"get":{"operationId":"key_usage","parameters":[{"description":"Key id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyUsageResponse"}}},"description":"Per-day, per-endpoint usage counters"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown key id"}},"security":[{"admin_token":[]}],"summary":"Usage counters for a key.","tags":["admin"]}},"/admin/map-issues":{"get":{"operationId":"list_issues","parameters":[{"description":"Restrict to one triage status (one of the accepted statuses);\nomitted lists every status.","in":"query","name":"status","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapIssueListResponse"}}},"description":"Reports newest first, capped at 200"},"400":{"content":{"application/problem+json":{}},"description":"Unknown status filter"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Lists reports for triage, newest first (at most 200), optionally\nfiltered by status.","tags":["admin"]}},"/admin/map-issues/{id}":{"patch":{"operationId":"update_issue","parameters":[{"description":"Report id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMapIssueRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MapIssueRecord"}}},"description":"The updated report"},"400":{"content":{"application/problem+json":{}},"description":"Unknown status or over-long admin note"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown report id"}},"security":[{"admin_token":[]}],"summary":"Updates a report's triage status (and optionally the triage note).","tags":["admin"]}},"/admin/mcp-map-issues":{"get":{"operationId":"list_mcp_map_issues","parameters":[{"description":"Page size (1–500, default 50).","in":"query","name":"limit","required":false,"schema":{"format":"int64","type":"integer"}},{"description":"Rows to skip (>= 0, default 0).","in":"query","name":"offset","required":false,"schema":{"format":"int64","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/McpMapIssueListResponse"}}},"description":"One page of queued MCP observations, newest first; empty when SN_MAP_ISSUES_DIR is unset or empty"},"400":{"content":{"application/problem+json":{}},"description":"Invalid limit or offset"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Lists the MCP `report_map_issue` NDJSON queue (`SN_MAP_ISSUES_DIR`),\nnewest first. An unset variable or empty/missing directory is an empty\nlisting, never an error. Distinct from the DB-backed\n`GET /admin/map-issues` triage surface, which lists user reports filed\nthrough `POST /map-issues`.","tags":["admin"]}},"/admin/overview":{"get":{"operationId":"overview","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OverviewResponse"}}},"description":"Aggregated signups, keys, usage, MAU, downloads, credit and territories"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"One dashboard snapshot: signups, keys, metered usage, MAU, downloads,\ncredit and territory presence, derived read-only from the existing\ntables (and the staged territory directories), no new writes.","tags":["admin"]}},"/admin/pointclouds":{"get":{"operationId":"admin_overview","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PointcloudsOverview"}}},"description":"Datasets and hosting-fee lines"},"401":{"description":"Missing or wrong admin token"}},"security":[{"admin_bearer":[]}],"summary":"`GET /admin/pointclouds` — datasets, declared sizes and computed\nmonthly hosting fees (£1/GB-month, £10 minimum), for invoice-line\nbilling. Read-only: this endpoint never moves money.","tags":["admin"]}},"/admin/retros":{"get":{"operationId":"list_retros","parameters":[{"description":"Page size (1–500, default 50).","in":"query","name":"limit","required":false,"schema":{"format":"int64","type":"integer"}},{"description":"Rows to skip (>= 0, default 0).","in":"query","name":"offset","required":false,"schema":{"format":"int64","type":"integer"}},{"description":"Restrict to one triage status (one of [`RETRO_STATUSES`]), or\n`open` for everything not yet done; omitted lists every status.","in":"query","name":"status","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetroListResponse"}}},"description":"One page of retros, newest first"},"400":{"content":{"application/problem+json":{}},"description":"Invalid limit, offset or status filter"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"}},"security":[{"admin_token":[]}],"summary":"Lists integration retros, newest first, optionally filtered by status.","tags":["admin"]}},"/admin/retros/{id}":{"patch":{"operationId":"update_retro","parameters":[{"description":"Retro id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRetroRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetroRecord"}}},"description":"The updated retro"},"400":{"content":{"application/problem+json":{}},"description":"Unknown status"},"401":{"content":{"application/problem+json":{}},"description":"Missing or wrong admin token"},"404":{"content":{"application/problem+json":{}},"description":"Unknown retro id"}},"security":[{"admin_token":[]}],"summary":"Updates a retro's triage status.","tags":["admin"]}},"/admin/styles":{"get":{"description":"The operator's view of the migration: what still needs adopting, and by\nwhom. Behind the admin bearer token like every other `/admin/*` route.","operationId":"admin_list_styles","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/StyleMeta"},"type":"array"}}},"description":"Every hosted style with its owner"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid admin token"},"404":{"description":"Hosted styles not configured on this gateway"}},"security":[{"admin_token":[]}],"summary":"`GET /admin/styles` — every hosted style with its owner, including the\nownerless ones left by gateway versions that did not record one.","tags":["admin"]}},"/admin/styles/{id}/owner":{"post":{"description":"The per-style half of the legacy migration (the bulk half is\n`SN_STYLES_LEGACY_OWNER_KEY`, see [`adopt_legacy_styles`]). Also the\nsupported way to hand a style over when a customer's key is rotated or\nan account is merged, so ownership is never a one-way door.\n\nThe key id is looked up so a typo cannot strand a style behind an owner\nthat does not exist; the key's identity is recorded alongside it, so\nsibling keys of the same account inherit the style exactly as they would\nhave had they created it.","operationId":"admin_set_style_owner","parameters":[{"description":"Style id","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdoptStyleRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StyleMeta"}}},"description":"The style's updated metadata"},"400":{"content":{"application/problem+json":{}},"description":"Unknown key id"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid admin token"},"404":{"description":"Hosted styles not configured or unknown style"}},"security":[{"admin_token":[]}],"summary":"`POST /admin/styles/{id}/owner` — give one style an owner.","tags":["admin"]}},"/adr/check":{"post":{"operationId":"post_check","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdrCheckRequest"}}},"description":"ADR vehicle profile plus the tunnel category to check","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Decision"}}},"description":"Compliance decision under the worst-case ADR 8.6.4 reading"},"400":{"content":{"application/problem+json":{}},"description":"Invalid profile or tunnel category"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"ADR tunnel-entry compliance check (see module docs).","tags":["compliance"]}},"/boundary":{"get":{"operationId":"get_boundary","parameters":[{"description":"Latitude (WGS84), required","in":"query","name":"lat","required":true,"schema":{"format":"double","type":"number"}},{"description":"Longitude (WGS84), required","in":"query","name":"lon","required":true,"schema":{"format":"double","type":"number"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{point: {lat, lon}, admins: [{admin_level, iso_code, name, name_en, drive_on_right}, …]}`, sorted ascending by `admin_level` (country before sub-division). An empty `admins` array is a valid answer (open sea, or a genuine gap in this deployment's admin-boundary coverage — see docs/API.md), never an error"},"400":{"content":{"application/problem+json":{}},"description":"Missing or malformed `lat`/`lon`"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"500":{"content":{"application/problem+json":{}},"description":"The admin-boundaries database could not be read"},"501":{"content":{"application/problem+json":{}},"description":"Admin boundary lookup not enabled (SN_ADMIN_BOUNDARIES_DB unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /boundary?lat=<f64>&lon=<f64>`.","tags":["boundaries"]}},"/centroid":{"post":{"operationId":"post_centroid","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla centroid request JSON: the same shape as `POST /route` — `locations` (at least two), `costing`, optional `costing_options`. \"Meet in the middle\": finds the point in the road graph where paths from every location converge for the least total cost, and returns one path per location to that shared point — not available from Google, Mapbox, HERE, TomTom or NextBillion","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"A Valhalla route response: `trip` is the path from locations[0] to the centroid, and each `alternates[i]` is the path from locations[i+1] to the SAME centroid — every path's last location shares an identical lat/lon, which is the meeting point. `costing` (e.g. `truck`) applies identically to every participant's path"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request, or fewer than two locations (Valhalla error_code 120)"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"SN_ROUTING_ENGINE=graphhopper: GraphHopper has no centroid service"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Finds the least-cost meeting point of several locations (see module\ndocs).","tags":["routing"]}},"/elevation":{"post":{"operationId":"post_elevation","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{\"points\": [{\"lat\", \"lon\"}, …]}`, at most 500 points","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{\"results\": [{\"lat\", \"lon\", \"elevation_m\", \"source\", \"resolution_m\"}, …]}`, one entry per input point in order; `elevation_m`/`source`/`resolution_m` are `null` together wherever the engine has no DEM coverage"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Elevation not enabled on this deployment (SN_ELEVATION_ENABLED unset; the hosted gateway sets it true — this is a self-hosting gate for a Valhalla with no DEM tiles staged)"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Point-elevation endpoint (see module docs).","tags":["elevation"]}},"/elevation/along":{"post":{"operationId":"post_elevation_along","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Along-route elevation profile: a route (`geometry_polyline6`, or the full `route` response `POST /route` returns), and at most one of `interval_m` (default 100 m) or `sample_count`","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{samples: [{distance_m, lat, lon, elevation_m, source, resolution_m, grade_percent}], total_distance_m, total_ascent_m, total_descent_m, coverage_fraction, sample_count}`. `grade_percent` on sample i is the percent grade of the segment ending at i (null on sample 0, and whenever either endpoint lacks elevation). `total_ascent_m`/`total_descent_m` are null (not 0) when no consecutive pair of samples both have elevation"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Elevation not enabled on this deployment (SN_ELEVATION_ENABLED unset; the hosted gateway sets it true — this is a self-hosting gate for a Valhalla with no DEM tiles staged)"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Along-route elevation profile endpoint (see module docs).","tags":["elevation"]}},"/fonts/{fontstack}/{range}":{"get":{"description":"`fontstack` is percent-decoded by the router (e.g. `Noto%20Sans%20Regular`\narrives as `Noto Sans Regular`) and validated so it can never escape the\nglyphs directory; `range` must be `<start>-<end>.pbf`.","operationId":"get_glyphs","parameters":[{"description":"Font stack name, e.g. `Noto Sans Regular` (URL-encoded)","in":"path","name":"fontstack","required":true,"schema":{"type":"string"}},{"description":"Glyph range plus extension, e.g. `0-255.pbf`","in":"path","name":"range","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/x-protobuf":{}},"description":"One SDF glyph range"},"404":{"description":"Map assets not configured, unknown fontstack or range"}},"summary":"`GET /fonts/{fontstack}/{range}.pbf` — one SDF glyph range.","tags":["assets"]}},"/geocode":{"get":{"operationId":"get_geocode","parameters":[{"description":"Free-text place query (required)","in":"query","name":"q","required":true,"schema":{"type":"string"}},{"description":"Maximum results, 1–10 (Photon default when omitted)","in":"query","name":"limit","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Result language. Must be one of the deployment's supported languages (`SN_PHOTON_LANGS`, default `en`, `de`, `fr`) or `default`; any other value is a 400 `unsupported-lang` problem listing the supported set","in":"query","name":"lang","required":false,"schema":{"type":"string"}},{"description":"Location bias as `lon,lat` (WGS84), e.g. `-0.1278,51.5074`","in":"query","name":"bias","required":false,"schema":{"type":"string"}},{"description":"Map zoom (0-20) controlling bias strength: zoomed in favours nearby results, zoomed out favours prominence. Only meaningful with `bias`; defaults to a gentle city-scale bias","in":"query","name":"zoom","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Hard bounding-box filter as `minLon,minLat,maxLon,maxLat` (WGS84, GeoJSON bbox axis order — the same convention as Mapbox/Google/HERE `bbox`/`bounds` parameters), e.g. `-0.489,51.286,0.236,51.686` for Greater London. Every returned feature's coordinate must fall inside the box; unlike `bias` (a ranking preference) this can exclude an otherwise-top-ranked result outright. `minLat` must be ≤ `maxLat`. `minLon` may be greater than `maxLon` to describe a box that crosses the antimeridian (per RFC 7946 §5) — e.g. `170,-20,-170,-15`; both `minLon` and `maxLon` must still be within [-180, 180]. Applies to free-text search on either backend (the first-party index filters in the query; the Photon proxy path pushes the box upstream where that is safe, fetches a deeper page, and filters the answer regardless). Ignored by the two fast paths that run before any text search: a pasted coordinate pair still returns that point, and a category browse (`coffee` + `bias`) still browses around the bias","in":"query","name":"bbox","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Photon-compatible GeoJSON FeatureCollection (locality-aware reranked, entity-deduplicated: one row per real-world place). First-party hits carry a stable `id`; POI hits add a `details` object of whitelisted OSM display tags (opening_hours, phone, website, brand, cuisine, wheelchair, wikidata, socket:*, …). Two query fast paths: a pasted coordinate pair (`51.5074, -0.1278`) returns a single `type: \"coordinate\"` feature at that point labelled with the nearest address; a pure category query (`coffee`, `petrol station`, `pharmacy`) with a `bias` returns the nearest POIs of that category — a browse with `distance_m` on each hit, not a name match"},"400":{"content":{"application/problem+json":{}},"description":"Missing `q`, a malformed parameter, or an unsupported `lang` (`urn:sn-gateway:problem:unsupported-lang`, body lists `supported_langs`)"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Geocoding not enabled (neither SN_GEOCODE_DIR nor SN_PHOTON_URL set)"},"502":{"content":{"application/problem+json":{}},"description":"Photon upstream failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /geocode?q=<text>&limit=<n≤10>&lang=<code>&bias=<lon>,<lat>&bbox=<minLon>,<minLat>,<maxLon>,<maxLat>`.","tags":["geocoding"]}},"/geocode/reverse":{"get":{"operationId":"get_geocode_reverse","parameters":[{"description":"Longitude (WGS84), required","in":"query","name":"lon","required":true,"schema":{"format":"double","type":"number"}},{"description":"Latitude (WGS84), required","in":"query","name":"lat","required":true,"schema":{"format":"double","type":"number"}},{"description":"Restrict hits to these document kinds, comma-separated: `address`, `street`, `locality`, `poi`, `postcode` (e.g. `kinds=poi` for nearest-POI lookups). First-party index only; ignored on the Photon proxy path.","in":"query","name":"kinds","required":false,"schema":{"type":"string"}},{"description":"Restrict hits to POIs of these categories, comma-separated — a category browse: the nearest cafés/fuel/parking to the point, nearest first. The index's category vocabulary is the lowercased value of the POI's first matching OSM tag (`amenity`, `shop`, `tourism`, `railway`, `leisure`, …), e.g. `cafe`, `fuel`, `charging_station`, `parking`, `pharmacy`, `supermarket`, `hotel`, `restaurant`, `fast_food`, `atm`, `bakery`, `hospital`, `station`. Common colloquial phrases are normalised to their canonical token (`coffee` → `cafe`; `ev_charging`, `ev charging`, `charging station` → `charging_station`; `petrol` → `fuel`; `chemist` → `pharmacy`); anything else is passed through lowercased, and a category the index does not carry simply matches nothing — an empty FeatureCollection, never an error. Implies `kinds=poi` unless `kinds` is given explicitly. First-party index only: on the Photon proxy path this parameter answers 501 `reverse-filtering-not-enabled` (Photon's reverse cannot filter by category).","in":"query","name":"categories","required":false,"schema":{"type":"string"}},{"description":"Restrict hits to places of this name or brand, nearest first — the \"nearest Lloyds bank\" question (`name=Lloyds&categories=bank`), which forward text search cannot answer: `GET /geocode?q=Lloyds Bank` ranks every branch in the country and only biases by proximity, so a namesake in another city outranks the branch 100 m away. Matched the same way the forward search matches names: lowercased, diacritic-folded, split into words, and EVERY word must match the place's name or one of its alternative names (`alt_name`, `short_name`, `official_name`, `name:en`), with the last word also matching as a prefix so partial input works (`name=Sains` finds Sainsbury's). It follows that `name=Lloyds` matches \"Lloyds Bank\" but `name=Lloyds Bank` does not match a branch mapped as just \"Lloyds\" — combine `name` with `categories` for that. Not a kind filter: streets and localities of that name match too unless `kinds` says otherwise. Brand and operator tags are NOT searchable (they are returned in `details` but not indexed), so a branch whose `name` never mentions the brand is not reachable by `name`. First-party index only: on the Photon proxy path this parameter answers 501 `reverse-filtering-not-enabled` (Photon's reverse cannot filter by name).","in":"query","name":"name","required":false,"schema":{"type":"string"}},{"description":"Maximum straight-line distance of any hit from the point, in metres (1–100000; the search ladder is capped and hits beyond the radius are dropped). Without it a category or name browse expands 2.5 km → 25 km → 100 km until it fills the page. First-party index only: on the Photon proxy path this parameter answers 501 `reverse-filtering-not-enabled`.","in":"query","name":"radius_m","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Maximum results, 1–10 (default 5). Honoured on both the first-party and Photon proxy paths.","in":"query","name":"limit","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Photon-compatible GeoJSON FeatureCollection. First-party hits add `distance_m`, a stable `id`, and a `details` object on POI hits (whitelisted OSM display tags); the Photon proxy path passes the upstream body through verbatim"},"400":{"content":{"application/problem+json":{}},"description":"Missing or malformed `lon`/`lat`, or a malformed `kinds`/`categories`/`name`/`radius_m`/`limit`"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Geocoding not enabled (neither SN_GEOCODE_DIR nor SN_PHOTON_URL set), or `categories`/`name`/`radius_m` on a Photon-proxy deployment (`urn:sn-gateway:problem:reverse-filtering-not-enabled` — clients feature-detect on this status)"},"502":{"content":{"application/problem+json":{}},"description":"Photon upstream failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /geocode/reverse?lon=<f64>&lat=<f64>[&kinds=..&categories=..&name=..&radius_m=..&limit=..]`.","tags":["geocoding"]}},"/geocode/selection":{"post":{"operationId":"record_selection","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelectionRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelectionResponse"}}},"description":"Selection recorded"},"400":{"content":{"application/problem+json":{}},"description":"Missing or over-long query/id"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"This key has hit its daily selection cap"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`POST /geocode/selection` — never charged; capped per key per day.","tags":["geocode"]}},"/geodata/validate":{"post":{"operationId":"post_validate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateGeodataRequest"}}},"description":"A declared CRS and a sample of the dataset's own coordinates","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateGeodataResponse"}}},"description":"Whether the declaration and the coordinates agree"},"400":{"content":{"application/problem+json":{}},"description":"Empty or non-finite coordinate sample"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Coordinate-reference-system sanity check (see module docs).","tags":["compliance"]}},"/health":{"get":{"operationId":"health","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Gateway and backend healthy"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Routing backend unreachable or failing"}},"summary":"Backend-aware health check: 200/`ok` when the primary routing backend\nanswers, 503/`degraded` when it does not.","tags":["meta"]}},"/isochrone":{"post":{"operationId":"post_isochrone","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla IsochroneRequest JSON: `locations`, `costing`, `contours` (each `time` minutes or `distance` km), optional `polygons`, `denoise`, `generalize` and `costing_options`. Scenario analysis: optional `exclude_polygons` — areas whose intersecting roads are avoided, as a JSON array of rings of `[lon, lat]` pairs (note the lon-first order), e.g. `[[[-0.1,51.5],[-0.11,51.5],[-0.11,51.52]]]` — and `exclude_locations` — `{lat, lon}` objects each mapped to its nearest road(s), which are then excluded — so \"close this bridge and recompute reachability\" is one request. Valhalla engine only: a GraphHopper-backed deployment ignores both fields","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"GeoJSON FeatureCollection of contour geometries"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Isochrone endpoint (see module docs).","tags":["routing"]}},"/llms.txt":{"get":{"operationId":"llms_txt","responses":{"200":{"content":{"text/plain":{}},"description":"llms.txt description of the API"}},"summary":"`llms.txt` (<https://llmstxt.org/>) describing the API for agent\nconsumers, with runnable curl examples.","tags":["meta"]}},"/locate":{"post":{"operationId":"post_locate","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla locate request JSON: `locations`, `costing`, optional `costing_options`, `verbose` (default false) and `units`","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"One entry per input location: the closest edges (way id, correlated point, side of street, percent along) and, in `verbose` mode, nodes with a dense administrative/access/geometry attribute tree. A location with no nearby graph edges (open sea, an unmapped area) answers an entry with empty `edges`/`nodes` arrays, never an error"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"SN_ROUTING_ENGINE=graphhopper: GraphHopper has no locate service"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Snaps each location to the road graph (see module docs).","tags":["routing"]}},"/map-issues":{"post":{"operationId":"create_issue","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMapIssueRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedMapIssueResponse"}}},"description":"Report stored for triage"},"400":{"content":{"application/problem+json":{}},"description":"Invalid category, out-of-bounds coordinates or over-long note/contact"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"This key has hit its daily report cap"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`POST /map-issues` — file a map-error report. Never edits map data:\nreports are triaged by the team and verified fixes flow into the\noverride file and upstream OSM.","tags":["map-issues"]}},"/matrix":{"post":{"operationId":"post_matrix","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla matrix request JSON: `sources`, `targets`, `costing`, optional `units` and `costing_options`. Scenario analysis: optional `exclude_polygons` — areas whose intersecting roads are avoided, as a JSON array of rings of `[lon, lat]` pairs (note the lon-first order), e.g. `[[[-0.1,51.5],[-0.11,51.5],[-0.11,51.52]]]` — and `exclude_locations` — `{lat, lon}` objects each mapped to its nearest road(s), which are then excluded — so \"close this bridge and recompute the matrix\" is one request. Valhalla engine only: a GraphHopper-backed deployment ignores both fields","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{durations, distances}`: row-major seconds and metres, null when unreachable"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"422":{"content":{"application/problem+json":{}},"description":"More than 10,000 source×target elements, or a source/target pair further apart than the engine's `max_matrix_distance` (400 km by default — not chunkable around)"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Matrix endpoint (see module docs).","tags":["routing"]}},"/openapi.json":{"get":{"operationId":"openapi_json","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OpenAPI 3.1 document"}},"summary":"The generated OpenAPI 3.1 document, with the configured public URL as\nthe server entry.","tags":["meta"]}},"/optimise":{"post":{"operationId":"post_optimise","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Optimisation problem: `vehicles` (id, start/end, capacity, skills, time_window, plus `breaks`, `max_travel_time`, `max_tasks` and a `costs` model), `jobs` (id, location, service_s, delivery, pickup, skills, time_windows), optional `shipments` (pickup+delivery pairs), `costing` (`auto`|`truck`|`bicycle`|`pedestrian`|`motor_scooter`), optional Valhalla-style `costing_options` and an optional top-level `adr` vehicle profile (as on `/route`). Set `eu_drivers_hours` (`true` or `{driving_before_break_s, break_duration_s}`) to auto-generate EC 561/2006 rest breaks for windowed vehicles without explicit breaks (single-shift approximation). `/optimize` is accepted as an alias. Fair use: at most 200 unique locations per request, and no wider than the routing engine's matrix span (400 km by default)","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Optimised plan: `summary`, `unassigned` and `routes[]` with ordered `steps[]` (durations in seconds, distances in metres, locations echoed as `{lat, lon}`), plus the matrix `profile` used"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request, geometry requested, or the solver rejected the problem"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"422":{"content":{"application/problem+json":{}},"description":"More than 200 unique locations (fair-use cap), or a problem spread wider than the routing engine's `max_matrix_distance` (400 km by default — the internal matrix is chunked to fit the engine's pair ceiling, but its span cannot be chunked around)"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"The VROOM solver is unreachable or failed"},"503":{"content":{"application/problem+json":{}},"description":"Optimisation is not enabled on this deployment (SN_VROOM_URL unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Route-optimisation endpoint (see module docs). `/optimize` is an alias.","tags":["routing"]}},"/places":{"delete":{"operationId":"delete_places","responses":{"204":{"description":"Dataset and index removed (or none existed)"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"Customer places not configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`DELETE /places` — remove the caller's dataset and index. Idempotent:\ndeleting an empty slate is still a 204.","tags":["places"]},"get":{"operationId":"get_places","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlacesDataset"}}},"description":"The stored dataset"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"No dataset stored for this key, or customer places not configured"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /places` — the caller's stored dataset.","tags":["places"]},"put":{"operationId":"put_places","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutPlacesRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PutPlacesResponse"}}},"description":"Dataset stored and indexed"},"400":{"content":{"application/problem+json":{}},"description":"Malformed request body"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"Customer places not configured on this gateway"},"413":{"content":{"application/problem+json":{}},"description":"More than the per-key place cap"},"422":{"content":{"application/problem+json":{}},"description":"One or more places failed validation; the body's `problems` list names each offender"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`PUT /places` — replace the caller's place dataset and rebuild its\nsearch index.","tags":["places"]}},"/places/nearest":{"get":{"operationId":"nearest_places","parameters":[{"description":"Latitude (WGS84), required","in":"query","name":"lat","required":true,"schema":{"format":"double","type":"number"}},{"description":"Longitude (WGS84), required","in":"query","name":"lon","required":true,"schema":{"format":"double","type":"number"}},{"description":"Maximum results, 1–50 (default 10)","in":"query","name":"limit","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceNearestResponse"}}},"description":"Places ordered nearest first (empty when no dataset is stored)"},"400":{"content":{"application/problem+json":{}},"description":"Missing or malformed `lat`/`lon`"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"Customer places not configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /places/nearest?lat=…&lon=…&limit=…` — the caller's places\nnearest to a point, by haversine distance (no text query, no index).","tags":["places"]}},"/places/search":{"get":{"operationId":"search_places","parameters":[{"description":"Free-text query over name, alternative names, street, locality, region and postcode (required)","in":"query","name":"q","required":true,"schema":{"type":"string"}},{"description":"Focus latitude for proximity-biased ranking; requires `lon`","in":"query","name":"lat","required":false,"schema":{"format":"double","type":"number"}},{"description":"Focus longitude for proximity-biased ranking; requires `lat`","in":"query","name":"lon","required":false,"schema":{"format":"double","type":"number"}},{"description":"Maximum results, 1–50 (default 10)","in":"query","name":"limit","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Restrict hits to places carrying this category","in":"query","name":"category","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlaceSearchResponse"}}},"description":"Ranked hits (empty when no dataset is stored)"},"400":{"content":{"application/problem+json":{}},"description":"Missing `q` or a malformed parameter"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"Customer places not configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /places/search?q=…&lat=…&lon=…&limit=…&category=…` — full-text\nsearch over the caller's own places, proximity-biased when a focus\npoint is given.","tags":["places"]}},"/pointclouds/{dataset}/{file}":{"get":{"description":"Key-authenticated and scoped to the dataset's manifest (owning\nidentity, listed keys, or `public` — anything else 404s). Supports a\nsingle `bytes=` `Range` (206 with `Content-Range`); metered in chunk\nunits at the point-cloud discount — see the module docs.","operationId":"get_file","parameters":[{"description":"Dataset id, e.g. `acme-depot`","in":"path","name":"dataset","required":true,"schema":{"type":"string"}},{"description":"Payload or sidecar file, e.g. `corridor-a.bin` or `corridor-a.json`","in":"path","name":"file","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/octet-stream":{}},"description":"The whole file"},"206":{"content":{"application/octet-stream":{}},"description":"The requested byte range"},"304":{"description":"Not modified (If-None-Match matched the ETag)"},"404":{"description":"Point clouds not configured, unknown dataset/file, or this key is not allowed this dataset"},"416":{"description":"Requested range not satisfiable"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /pointclouds/{dataset}/{file}` — one hosted payload (`.bin`) or\nsidecar (`.json`).","tags":["pointclouds"]}},"/route":{"post":{"operationId":"post_route","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla RouteRequest JSON (`costing` may be `auto`, `truck`, `bicycle`, `pedestrian`, `motor_scooter`, `bus` or `motorcycle`; per-mode `costing_options` such as pedestrian `use_lit`/`type`/`max_hiking_difficulty` or bicycle `bicycle_type`/`use_roads`/`use_living_streets`/`avoid_bad_surfaces`/`use_hills` pass through), optionally extended with a top-level `adr` vehicle profile object and a top-level `rationale: true` flag (truck, auto, bicycle, pedestrian and motor_scooter; opt-in because it costs up to 1 + N extra engine calls, and it draws one call of its class per engine computation it performs, at most 8 and typically fewer, reported back in `rationale.metering`) for a structured `rationale.avoided[]` block derived by route divergence, with live incidents on the route attached as context where incident data is configured, and a top-level `landmarks: true` flag for turn instructions anchored to recognisable places (\"Turn right just after the Shell garage\"), added alongside each manoeuvre's own `instruction` as `landmark_instruction` and never replacing it, and a top-level `traffic: true` flag for a per-leg `traffic` object (`covered_pct`, `confidence` in HERE's bands, `sources`) saying what speed data stood behind the ETA — computed from the returned geometry, so it costs no extra engine call and is not metered separately. At most 500 `locations` (waypoints) per request. The routing engine takes 20 per call, so longer lists are cut at `break` waypoints, routed concurrently and spliced into one continuous route, and billed one call per started block of 20 waypoints","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla route response, with an `adr` echo when an ADR profile was applied, a `rationale` block when requested (`avoided[]` entries carry basis: \"route_divergence\" — see the module docs for the method's honesty limits), and, when `landmarks` was requested, a `landmark_instruction` on each manoeuvre where a recognisable place cleared the salience bar plus a `landmarks` summary block reporting how many were annotated, and, when `traffic` was requested, a `traffic` object on each leg plus a top-level `traffic` summary carrying the route aggregate, the coverage snapshot's freshness and the method's caveats"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request, conflicting costing_options, or more than 500 locations"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"422":{"content":{"application/problem+json":{}},"description":"More waypoints than the routing engine takes in one call, AND the route cannot be split faithfully: a run of more than 20 consecutive `through`/`via`/`break_through` waypoints, a time-dependent route (`date_time.type` 0, 1 or 2), or a request for `alternates`. `detail` says which and what to change"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Valhalla-compatible route endpoint (see module docs).","tags":["routing"]}},"/route/along":{"post":{"operationId":"post_route_along","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Along-route search: a route (`geometry_polyline6`, or `origin` + `destination` + `costing`), a free-text `query`, optional `max_detour_minutes` (default 10), `max_results` (default 5, max 25), `source` (`geocode` default | `places`), `category` (an OSM category token for `geocode`, the customer taxonomy for `places`), `sort` (`cheapest_diesel` | `cheapest_petrol`, fuel-enriched results by price then detour), Valhalla `costing_options` and an optional top-level `adr` profile","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Places along the route, each with its honest engine-computed detour: `{query, source, costing, route, max_detour_minutes, candidates: {considered, costed, cap}, results[]: {place, detour_minutes, detour_km, along_route_position, off_route_m}}`, sorted by detour. When the geocode source browsed the first-party index by category — an explicit `category`, or a `query` that is entirely category phrases (`fuel`, `petrol station`, `coffee near me`), which promotes automatically — the response also carries `matched_categories` naming the normalised tokens browsed. With the fuel-price store configured (SN_FUEL_PRICES), fuel-station results also carry `fuel_prices`, `fuel_brand` and `fuel_updated_at`, and the response a `fuel_attribution` string"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"content":{"application/problem+json":{}},"description":"`source: \"places\"` on a gateway without customer places"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"`source: \"geocode\"` on a gateway without a geocoder (SN_PHOTON_URL unset), or an explicit `category` on a gateway without the first-party geocode index (SN_GEOCODE_DIR unset — `urn:sn-gateway:problem:reverse-filtering-not-enabled`, the same feature-detect status `GET /geocode/reverse` uses for its first-party-only filters). A category *promoted* from the query degrades to free-text search instead of failing"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine or geocoder failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Along-route search endpoint (see module docs).","tags":["routing"]}},"/route/report":{"post":{"operationId":"post_route_report","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Either `{\"locations\": [{lat,lon}, …], \"costing\": \"auto\"}` (route mode: computes the route internally) or `{\"shape\": [{lat,lon}, …]}` / `{\"encoded_polyline\": \"…\"}` (trace mode: reports over an existing trace) — exactly one of `locations` or `shape`/`encoded_polyline`. Optional `costing_options`","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{distance_m, duration_s, by_road_class: {<class>: {distance_m, duration_s, edge_count}}, by_admin: [{country_code, country_text, state_code, state_text, distance_m, duration_s, edge_count}], toll: {edge_count, distance_m}, bridge: {…}, tunnel: {…}, by_surface: {<surface>: {distance_m, edge_count}}, edge_count}`. Distances in metres, durations in seconds. An edge with no resolvable admin area is grouped under a null-keyed entry in `by_admin`, never guessed at"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request, or neither/both of `locations` and a trace given"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Report endpoint (see module docs).","tags":["routing"]}},"/route/v1/{profile}/{coordinates}":{"get":{"operationId":"get_route","parameters":[{"description":"Routing profile: `driving` (aliases `car`/`auto`), `truck`, `bus`, `bicycle` (`bike`/`cycling`), `walking` (`foot`/`pedestrian`), `scooter` or `motorcycle` (`motorbike`)","in":"path","name":"profile","required":true,"schema":{"type":"string"}},{"description":"`lon,lat` pairs separated by `;`, e.g. `-0.1,51.5;-1.9,52.5`","in":"path","name":"coordinates","required":true,"schema":{"type":"string"}},{"description":"`true`, `false` or a number of alternates (max 3)","in":"query","name":"alternatives","required":false,"schema":{"type":"string"}},{"description":"Include turn-by-turn steps (default false)","in":"query","name":"steps","required":false,"schema":{"type":"boolean"}},{"description":"`polyline` (default), `polyline6` or `geojson`","in":"query","name":"geometries","required":false,"schema":{"type":"string"}},{"description":"`simplified` (default), `full` or `false`","in":"query","name":"overview","required":false,"schema":{"type":"string"}},{"description":"Avoid toll roads (Valhalla `use_tolls=0`; motorised profiles only). Hosted gateway only — the public demo router ignores it","in":"query","name":"avoid_tolls","required":false,"schema":{"type":"boolean"}},{"description":"Avoid motorways/highways (Valhalla `use_highways=0`; motorised profiles only). Hosted gateway only","in":"query","name":"avoid_motorways","required":false,"schema":{"type":"boolean"}},{"description":"Avoid ferries (Valhalla `use_ferry=0`; all profiles). Hosted gateway only","in":"query","name":"avoid_ferries","required":false,"schema":{"type":"boolean"}},{"description":"Prefer the shortest path over the fastest (Valhalla `shortest=true`; all profiles). Hosted gateway only","in":"query","name":"shortest","required":false,"schema":{"type":"boolean"}},{"description":"With `steps=true`: add Mapbox-shaped `voiceInstructions` (plain + SSML, distance-triggered) to each step","in":"query","name":"voice_instructions","required":false,"schema":{"type":"boolean"}},{"description":"With `steps=true`: add Mapbox-shaped `bannerInstructions` (and lane sub-banners where OSM has `turn:lanes`) to each step","in":"query","name":"banner_instructions","required":false,"schema":{"type":"boolean"}},{"description":"With `steps=true`: add a `landmark_instruction` to each step that passes a recognisable place — \"Turn right just after the Shell garage\" — beside the step's own fields, which are never replaced. Nothing is named unless it is recognisable from the road, within 40 m of the junction and not tagged as closed, so many steps carry none. Needs the first-party place index (`SN_GEOCODE_DIR`); a deployment without one returns no landmarks","in":"query","name":"landmarks","required":false,"schema":{"type":"boolean"}},{"description":"BCP 47 narration language, e.g. `en-GB`, `es-ES` (also sets the route `voiceLocale`)","in":"query","name":"language","required":false,"schema":{"type":"string"}},{"description":"Vendor: vehicle height in metres (truck profile)","in":"query","name":"height","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: vehicle width in metres (truck profile)","in":"query","name":"width","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: vehicle length in metres (truck profile)","in":"query","name":"length","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: gross weight in tonnes (truck profile)","in":"query","name":"weight","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: carrying dangerous goods (truck profile)","in":"query","name":"hazmat","required":false,"schema":{"type":"boolean"}},{"description":"Vendor: ADR 8.6.4 tunnel restriction code, e.g. `B/D` (truck profile; implies hazmat)","in":"query","name":"tunnel_code","required":false,"schema":{"type":"string"}},{"description":"Vendor: preference for lit streets, 0–1 (walking profile; 1 = prefer lit as strongly as possible)","in":"query","name":"use_lit","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: `wheelchair` (avoids steps/kerbs/steep grades where mapped) or `blind` (richer guidance) — Valhalla pedestrian `type` (walking profile)","in":"query","name":"pedestrian_type","required":false,"schema":{"type":"string"}},{"description":"Vendor: maximum hiking-trail difficulty, OSM sac_scale 1–6 (walking profile; default 1)","in":"query","name":"max_hiking_difficulty","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Vendor: `road`, `hybrid` (default), `city`, `cross` or `mountain` (bicycle profile)","in":"query","name":"bicycle_type","required":false,"schema":{"type":"string"}},{"description":"Vendor: willingness to ride roads alongside traffic, 0–1 (bicycle profile; 0 = prefer cycleways)","in":"query","name":"use_roads","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: preference for living/shared streets, 0–1 (bicycle profile)","in":"query","name":"use_living_streets","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: avoidance of surfaces unsuited to the bicycle type, 0–1 (bicycle profile; 1 = strictly avoid)","in":"query","name":"avoid_bad_surfaces","required":false,"schema":{"format":"double","type":"number"}},{"description":"Vendor: willingness to take hills, 0–1 (bicycle profile; 0 = avoid climbs)","in":"query","name":"use_hills","required":false,"schema":{"format":"double","type":"number"}},{"description":"Time-dependent routing: specified departure time, ISO 8601 `YYYY-MM-DDThh:mm` local (Valhalla `date_time.type=1`). Mutually exclusive with `arrive_by`","in":"query","name":"depart_at","required":false,"schema":{"type":"string"}},{"description":"Time-dependent routing: specified arrival time, ISO 8601 `YYYY-MM-DDThh:mm` local (Valhalla `date_time.type=2`). Mutually exclusive with `depart_at`","in":"query","name":"arrive_by","required":false,"schema":{"type":"string"}},{"description":"Comma-separated speed sources to use: `freeflow`, `constrained`, `predicted`, `current` (Valhalla `costing_options.<costing>.speed_types`)","in":"query","name":"speed_types","required":false,"schema":{"type":"string"}},{"description":"Curbside preference per coordinate, `;`-separated: `curb` (Valhalla `preferred_side=same`) or `unrestricted` (`either`); empty entry = no preference. Must have one entry per coordinate","in":"query","name":"approaches","required":false,"schema":{"type":"string"}},{"description":"Curbside: `street_side_tolerance` in metres, applied to every location (Valhalla default 5)","in":"query","name":"street_side_tolerance","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Curbside: `street_side_max_distance` in metres, applied to every location (Valhalla default 1000)","in":"query","name":"street_side_max_distance","required":false,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Curbside: `street_side_cutoff` road class (motorway…service_other), applied to every location","in":"query","name":"street_side_cutoff","required":false,"schema":{"type":"string"}},{"description":"Avoid-areas: JSON array of `[lon, lat]` rings, e.g. `[[[-0.1,51.5],[-0.11,51.5],[-0.11,51.52]]]` (Valhalla `exclude_polygons`)","in":"query","name":"exclude_polygons","required":false,"schema":{"type":"string"}},{"description":"Avoid-locations: `lon,lat` pairs separated by `;` (Valhalla `exclude_locations`)","in":"query","name":"exclude_locations","required":false,"schema":{"type":"string"}},{"description":"Opt-in route safety alerts: `true` adds a top-level `safety_alerts[]` array (speed cameras, average-speed zones, French danger zones, railway level crossings along the primary route, ordered by distance along it) behind the per-country legality gate. Only effective on gateways where the operator configured `SN_SAFETY_DIR`; the field never appears otherwise","in":"query","name":"safety_alerts","required":false,"schema":{"type":"boolean"}},{"description":"`optin` asserts the END USER explicitly enabled enforcement alerts, unlocking countries whose law requires them off by default (e.g. DE — StVO §23(1c) is a driver-use ban). Without it those countries' cameras/zones are suppressed; forbidden and zones-only regimes are unaffected (never unlockable)","in":"query","name":"safety_alerts_enforcement","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OSRM route response (`code: Ok`)"},"400":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"OSRM error envelope, e.g. `InvalidQuery`, `InvalidValue`, `NoSegment`, `NoRoute`"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"OSRM-compatible route endpoint (see module docs).","tags":["routing"]}},"/sprite/{file}":{"get":{"operationId":"get_sprite","parameters":[{"description":"Sprite file: `sprite[@2x].json|png` or `marker-sprite[@2x].json|png`","in":"path","name":"file","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The sprite index (JSON) or atlas (PNG)"},"404":{"description":"Map assets not configured or no such sprite file"}},"summary":"`GET /sprite/{file}` — a sprite-sheet file: `<sheet>.json`,\n`<sheet>.png`, `<sheet>@2x.json` or `<sheet>@2x.png` (the four names\nMapLibre resolves from a style's `sprite` base URL), for either sheet in\n[`SPRITE_STEMS`].","tags":["assets"]}},"/styles":{"get":{"description":"Authenticated and scoped: a key sees the styles it (or a sibling key of\nthe same identity) created, and nothing else. It was previously public\nand unscoped, which published every style id and name on the deployment\nto anyone who asked — see the module docs.","operationId":"list_styles","responses":{"200":{"content":{"application/json":{"schema":{"items":{"$ref":"#/components/schemas/StyleSummary"},"type":"array"}}},"description":"The calling key's hosted styles"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"404":{"description":"Hosted styles not configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /styles` — the CALLER'S hosted styles with their latest versions.","tags":["styles"]},"post":{"operationId":"create_style","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateStyleRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatedStyleResponse"}}},"description":"Style created as version 1"},"400":{"content":{"application/problem+json":{}},"description":"Malformed request or name"},"404":{"description":"Hosted styles not configured on this gateway"},"422":{"content":{"application/problem+json":{}},"description":"Theme failed sn-style validation; the body's `problems` list names the offending slot or layer"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`POST /styles` — create a style: validate and compile the theme, store\nit as version 1 under a freshly generated id.","tags":["styles"]}},"/styles/{spec}":{"delete":{"description":"Same ownership rule as publishing (`check_publish_allowed`), taken\nINSIDE the lock. Deletion breaks every map still embedding one of the\nstyle's capability URLs — that is the owner's call to make, and it is\nthe only way the \"immutable versions are kept forever\" storage story\nstays bounded now that creation is capped rather than unlimited.","operationId":"delete_style","parameters":[{"description":"Style id","in":"path","name":"spec","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Style and all versions removed"},"403":{"description":"Not the style's owner"},"404":{"description":"Hosted styles not configured or unknown style"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`DELETE /styles/{id}` — remove a style and every version of it.","tags":["styles"]},"get":{"description":"`spec` is `<id>@<version>.json` for an immutable version (cached\nforever) or `<id>.json` for the latest (60 s TTL).","operationId":"get_style_json","parameters":[{"description":"`<id>@<version>.json` (immutable) or `<id>.json` (latest)","in":"path","name":"spec","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The compiled MapLibre style (spec v8)"},"404":{"description":"Hosted styles not configured, unknown style or version"}},"summary":"`GET /styles/{spec}` — a compiled MapLibre style.","tags":["styles"]},"post":{"operationId":"update_style","parameters":[{"description":"Style id","in":"path","name":"spec","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStyleRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StyleVersionResponse"}}},"description":"New version published"},"400":{"content":{"application/problem+json":{}},"description":"Malformed request"},"404":{"description":"Hosted styles not configured or unknown style"},"422":{"content":{"application/problem+json":{}},"description":"Theme failed sn-style validation; the body's `problems` list names the offending slot or layer"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`POST /styles/{id}` — publish a new immutable version of an existing\nstyle (existing versions are never modified).","tags":["styles"]}},"/styles/{spec}/theme":{"get":{"operationId":"get_theme","parameters":[{"description":"Style id","in":"path","name":"spec","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The latest theme document"},"404":{"description":"Hosted styles not configured or unknown style"}},"summary":"`GET /styles/{id}/theme` — the latest theme document (the editable\nsource a new version is published from; never cached).","tags":["styles"]}},"/terms":{"get":{"operationId":"terms","responses":{"200":{"content":{"text/plain":{}},"description":"Plain-text terms pointer (no `SN_TERMS_URL` configured)"},"307":{"description":"Redirect to the configured `SN_TERMS_URL`"}},"summary":"Terms of service: a 307 redirect to `SN_TERMS_URL` when the operator\nhosts the terms elsewhere (e.g. the website's `/terms` page),\notherwise a plain-text pointer so the `tos` URL echoed by self-serve\nsignup never dangles.","tags":["meta"]}},"/territories":{"get":{"operationId":"get_index","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The channel index (index.json, exact signed bytes)"},"304":{"description":"Not modified (If-None-Match matched the BLAKE3 ETag)"},"404":{"description":"No update channel configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /territories` — the signed channel index (`index.json`), byte\nexact so the detached signature verifies over the response body.","tags":["territories"]}},"/territories/index.sig":{"get":{"operationId":"get_index_sig","responses":{"200":{"content":{"text/plain":{}},"description":"Detached signature over index.json"},"404":{"description":"No update channel configured on this gateway"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /territories/index.sig` — detached base64 ed25519 signature over\nthe exact bytes of `/territories`.","tags":["territories"]}},"/territories/{id}/{version}/layers/{addr}/{file}":{"get":{"description":"Unlike the small metadata routes, layer blobs are ~1 GB each, so they\nare byte-metered against the calling identity's monthly download\nallowance ([`DownloadMeter`]). Provisional keys are blocked (403);\noperator (`admin`) keys, which carry no identity, are not metered.","operationId":"get_layer","parameters":[{"description":"Territory id","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Published package version","in":"path","name":"version","required":true,"schema":{"type":"string"}},{"description":"Blob address: 16 lowercase hex chars of the layer's BLAKE3","in":"path","name":"addr","required":true,"schema":{"type":"string"}},{"description":"Blob file name, e.g. `valhalla.tar.zst`","in":"path","name":"file","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/octet-stream":{}},"description":"The layer blob (tar + zstd)"},"206":{"content":{"application/octet-stream":{}},"description":"Partial content for a Range request"},"304":{"description":"Not modified"},"402":{"content":{"application/json":{}},"description":"Monthly offline-download allowance exhausted; body carries a machine-readable `download_allowance_exceeded` upgrade pointer"},"403":{"content":{"application/problem+json":{}},"description":"Provisional key: territory downloads require a verified account"},"404":{"description":"Unknown territory, version or blob"},"416":{"description":"Unsatisfiable Range"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /territories/{id}/{version}/layers/{addr}/{file}` — a\ncontent-addressed layer blob, streamed with a strong BLAKE3 ETag,\nsingle-range `Range` support and immutable cache headers.","tags":["territories"]}},"/territories/{id}/{version}/manifest":{"get":{"operationId":"get_manifest","parameters":[{"description":"Territory id, e.g. `uk`","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Published package version, e.g. `2.0.0`","in":"path","name":"version","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The signed package manifest (exact bytes)"},"304":{"description":"Not modified"},"404":{"description":"Unknown territory or version"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /territories/{id}/{version}/manifest` — a version's signed\npackage manifest (`manifest.json`, exact signed bytes).","tags":["territories"]}},"/territories/{id}/{version}/manifest.sig":{"get":{"operationId":"get_manifest_sig","parameters":[{"description":"Territory id","in":"path","name":"id","required":true,"schema":{"type":"string"}},{"description":"Published package version","in":"path","name":"version","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"text/plain":{}},"description":"Detached signature over the manifest"},"404":{"description":"Unknown territory or version"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /territories/{id}/{version}/manifest.sig` — the manifest's\ndetached base64 ed25519 signature.","tags":["territories"]}},"/tiles/{territory}/style.json":{"get":{"description":"A compact light style over the territory's vector source (equivalent in\nspirit to the web SDK's `buildStyle`), pointing its vector source at this\nterritory's TileJSON. OSM attribution is retained.","operationId":"get_style","parameters":[{"description":"Territory id, e.g. `uk`","in":"path","name":"territory","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{}},"description":"MapLibre GL style JSON"},"404":{"description":"Tiles not configured or unknown territory"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /tiles/{territory}/style.json` — a ready-to-render MapLibre style.","tags":["tiles"]}},"/tiles/{territory}/tiles.json":{"get":{"description":"The zoom range, bounds and centre come from the PMTiles header; the tile\ntemplate and attribution are filled in for the client.","operationId":"get_tilejson","parameters":[{"description":"Territory id, e.g. `uk`","in":"path","name":"territory","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{}},"description":"TileJSON 3.0 document"},"404":{"description":"Tiles not configured or unknown territory"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /tiles/{territory}/tiles.json` — a TileJSON 3.0 document.","tags":["tiles"]}},"/tiles/{territory}/{z}/{x}/{y}":{"get":{"description":"Reads the (z, x, y) tile straight from the territory PMTiles and returns\nits bytes with the MVT content-type, the archive's tile compression as\n`Content-Encoding` (PMTiles stores MVT gzip-compressed by convention), a\nstrong BLAKE3 ETag and immutable cache headers.\n\n- Unknown territory, tiles not configured, out-of-range coordinates, or a\n  bad file extension: **404**.\n- A tile absent from the archive (in range, no data): **204 No Content** —\n  the empty-tile convention MapLibre treats as a blank tile.","operationId":"get_tile","parameters":[{"description":"Territory id, e.g. `uk`","in":"path","name":"territory","required":true,"schema":{"type":"string"}},{"description":"Zoom level","in":"path","name":"z","required":true,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Tile column","in":"path","name":"x","required":true,"schema":{"format":"int32","minimum":0,"type":"integer"}},{"description":"Tile row plus extension, e.g. `21.mvt` (or `.pbf`)","in":"path","name":"y","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/vnd.mapbox-vector-tile":{}},"description":"One Mapbox Vector Tile"},"204":{"description":"No data for this tile (empty tile)"},"304":{"description":"Not modified (If-None-Match matched the ETag)"},"404":{"description":"Tiles not configured, unknown territory, or out-of-range tile"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`GET /tiles/{territory}/{z}/{x}/{y}.mvt` (also `.pbf`) — one vector tile.","tags":["tiles"]}},"/timezone":{"get":{"operationId":"get_timezone","parameters":[{"description":"Latitude in decimal degrees","in":"query","name":"lat","required":true,"schema":{"format":"double","type":"number"}},{"description":"Longitude in decimal degrees","in":"query","name":"lon","required":true,"schema":{"format":"double","type":"number"}},{"description":"RFC 3339 or Unix seconds; defaults to now","in":"query","name":"timestamp","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"`{tzid, utc_offset_s, dst_offset_s, abbreviation, timestamp}`. `tzid` and every derived field are `null` together when no loaded zone covers the point — an honest \"no data\", never a guess"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Timezone lookup not enabled (SN_TIMEZONE_SQLITE unset or failed to load)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Timezone-at-coordinate endpoint (see module docs).","tags":["timezone"]}},"/trace_attributes":{"post":{"operationId":"post_trace_attributes","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla trace request JSON: `shape` (points) or `encoded_polyline`, `costing`, optional `shape_match`, `filters` and `costing_options`","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Matched edges and their attributes (open JSON document)"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Map matching to per-edge attributes (see module docs).","tags":["routing"]}},"/trace_route":{"post":{"operationId":"post_trace_route","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla trace request JSON: `shape` (points) or `encoded_polyline`, `costing`, optional `shape_match` and `costing_options`","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Valhalla route response for the matched trace"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Map matching to a route (see module docs).","tags":["routing"]}},"/v1/cameras/along":{"post":{"operationId":"post_cameras_along","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CamerasAlongRequest"}}},"description":"Route shape as polyline6 plus an optional corridor buffer (`buffer_m`, default 40 m, max 100 m)","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CamerasAlongResponse"}}},"description":"Cameras along the route (arc-ordered) and the applied jurisdiction policy"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Camera sidecars are not enabled (SN_CAMERAS_DIR unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Safety cameras along a route shape (see module docs).","tags":["routing"]}},"/v1/clearance/along":{"post":{"operationId":"post_clearance_along","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClearanceAlongRequest"}}},"description":"Route shape as polyline6 plus the vehicle's height in metres (optional width, operating margin and dataset narrowing)","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RouteClearanceReport"}}},"description":"The clearance report: verdict per axis, limiting point with its bound, coverage, datasets read"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"No clearance artefacts are configured on this deployment"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Measured overhead clearance along a route shape (see module docs).","tags":["clearance"]}},"/v1/console/claims/{token}":{"get":{"operationId":"resolve_claim","parameters":[{"description":"Claim pointer (`snc_…`)","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimResolveResponse"}}},"description":"Masked email of the identity behind the pointer"},"404":{"content":{"application/problem+json":{}},"description":"Unknown, expired or revoked pointer"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"summary":"Resolve a claim pointer to its masked email (read-only).","tags":["console"]}},"/v1/console/claims/{token}/signin":{"post":{"operationId":"claim_signin","parameters":[{"description":"Claim pointer (`snc_…`)","in":"path","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SigninResponse"}}},"description":"Uniform acknowledgement; a link was sent if budgets allow"},"404":{"content":{"application/problem+json":{}},"description":"Unknown, expired or revoked pointer"},"429":{"content":{"application/problem+json":{}},"description":"Per-IP request velocity exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"summary":"The claim page's \"email me a sign-in link\" action.","tags":["console"]}},"/v1/console/self":{"get":{"operationId":"console_self","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsoleSelfResponse"}}},"description":"Identity view for the session"},"401":{"content":{"application/problem+json":{}},"description":"Missing, unknown, revoked or expired session"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"summary":"The identity account view for a session bearer. Read-only: key\nmetadata, balance, usage and credit history — never key plaintext,\nwhich does not exist server-side.","tags":["console"]}},"/v1/console/session":{"delete":{"operationId":"revoke_session","responses":{"204":{"description":"Session revoked (idempotent)"},"401":{"content":{"application/problem+json":{}},"description":"No session bearer presented"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"summary":"Sign out: revokes the session server-side (cookie clearing alone would\nleave the bearer live).","tags":["console"]},"post":{"operationId":"exchange_session","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionExchangeRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResponse"}}},"description":"Session minted"},"401":{"content":{"application/problem+json":{}},"description":"Unknown, used or expired sign-in token"},"429":{"content":{"application/problem+json":{}},"description":"Per-IP exchange velocity exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"summary":"Exchange an emailed sign-in token for a console session. POST-only by\ndesign: a scanner prefetching the emailed link consumes nothing.","tags":["console"]}},"/v1/console/signin":{"post":{"operationId":"request_signin","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SigninRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SigninResponse"}}},"description":"Uniform acknowledgement; a link was sent only if the address has an account and budgets allow"},"400":{"content":{"application/problem+json":{}},"description":"Syntactically invalid email"},"429":{"content":{"application/problem+json":{}},"description":"Per-IP request velocity exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured (SN_CONSOLE_URL unset)"}},"summary":"Request a sign-in link by email. Uniform 202 regardless of whether the\naddress has an account.","tags":["console"]}},"/v1/feedback":{"post":{"operationId":"submit_feedback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationRetroRequest"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IntegrationRetroResponse"}}},"description":"Retro stored"},"400":{"content":{"application/problem+json":{}},"description":"Schema violation: missing what_built, unknown problem area, over-long field or over-long list"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"This key has hit its daily retro cap"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"`POST /v1/feedback` — store one integration retro. Never charged;\ncapped at [`RETROS_PER_KEY_PER_DAY`] per key per rolling 24 hours.","tags":["feedback"]}},"/v1/fuel/along":{"post":{"operationId":"post_fuel_along","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Cheapest-fuel search: a route (`geometry_polyline6`, or `origin` + `destination` + `costing`), a `fuel` code (default `diesel`), optional `max_detour_minutes` (default 10, max 120), `max_results` (default 5, max 25), `fill_litres` (adds `saving_total` per result), Valhalla `costing_options` and an optional top-level `adr` profile","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Stations selling the requested fuel within the detour budget, cheapest first: `{fuel, costing, route, max_detour_minutes, candidates: {considered, costed, cap}, coverage: {stations, countries, sources}, note (only when results is empty, naming which of the several causes applies), baseline: {<currency>: {value, station_id, brand, name, updated_at}}, results[]: {station_id, source, brand, name, lat, lon, price: {value, currency, updated_at}, stale, detour_minutes, detour_km, along_route_position, off_route_m, saving_per_litre, saving_total}, fuel_attribution}. Detours are engine-computed, never crow-flies"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"The fuel-price dataset is not enabled (SN_FUEL_PRICES unset)"},"502":{"content":{"application/problem+json":{}},"description":"Upstream routing engine failed"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Cheapest fuel along a route (see module docs).","tags":["routing"]}},"/v1/incidents/along":{"post":{"operationId":"post_incidents_along","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentsAlongRequest"}}},"description":"Route shape as polyline6 plus an optional corridor buffer (`buffer_m`, default 60 m, max 500 m)","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IncidentsAlongResponse"}}},"description":"Incidents along the route (arc-ordered) and every configured source's coverage metadata"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Incident data is not enabled (SN_INCIDENTS_DIR unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Traffic incidents and closures along a route shape (see module docs).","tags":["routing"]}},"/v1/keys":{"post":{"operationId":"issue_key","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssueKeyRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssuedKeyResponse"}}},"description":"Provisional key issued; the `key` field is shown once only"},"400":{"content":{"application/problem+json":{}},"description":"ToS not accepted (body echoes the `tos` URL) or invalid/disposable email"},"409":{"content":{"application/problem+json":{}},"description":"Identity already has the maximum number of active keys"},"429":{"content":{"application/problem+json":{}},"description":"Per-IP issuance velocity exceeded"}},"summary":"One-call signup: issues a provisional key and emails a magic\nverification link.","tags":["self-serve"]}},"/v1/keys/claim":{"post":{"operationId":"mint_claim","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClaimMintResponse"}}},"description":"Fresh zero-authority claim pointer"},"401":{"content":{"application/problem+json":{}},"description":"Missing, unknown, revoked or expired key"},"501":{"content":{"application/problem+json":{}},"description":"Console not configured"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Mint a fresh claim pointer for the calling key's identity — the agent's\nmid-conversation \"let my human see billing\" action.","tags":["console"]}},"/v1/keys/self":{"get":{"operationId":"self_status","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SelfResponse"}}},"description":"State, quota, usage, credits and expiry of the calling key"},"401":{"content":{"application/problem+json":{}},"description":"Missing, unknown, revoked or expired key"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Key status for the calling agent (quota-free — polling it never\nconsumes the free tier).","tags":["self-serve"]}},"/v1/keys/verify":{"get":{"operationId":"verify_key","parameters":[{"description":"Single-use verify token from the emailed link","in":"query","name":"token","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Keys upgraded to `verified`; HTML page, or `{\"state\":\"verified\",\"monthly_quota\":50000}` with `Accept: application/json`"},"400":{"description":"Missing, unknown, used or expired token"}},"summary":"Magic-link verification: upgrades every provisional key of the\nidentity to the verified free tier. Answers HTML for browsers/email\nclients and JSON when `Accept: application/json`.","tags":["self-serve"]}},"/v1/probe":{"post":{"operationId":"post_probe","requestBody":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"A gzipped-or-plain JSON probe batch (`probe_format: 1`): per-segment aggregates, coarse events, and an optional ~5 km geohash origin/destination record. Never a trajectory.","required":true},"responses":{"202":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"Batch accepted and appended to the probe store"},"400":{"content":{"application/problem+json":{}},"description":"Malformed batch, wrong probe_format, or a raw-coordinate-looking OD cell"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"403":{"content":{"application/problem+json":{}},"description":"The key is not opted in to probe contribution"},"501":{"content":{"application/problem+json":{}},"description":"Probe collection is not enabled (SN_PROBE_DIR unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Probe ingest endpoint (see module docs). Consent-gated, aggregate-only,\nmetered as a free contribution.","tags":["collection"]}},"/v1/weather/along":{"post":{"operationId":"post_weather_along","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeatherAlongRequest"}}},"description":"Route shape as polyline6, optional `depart_at` (RFC 3339, default now), optional `duration_s` (route duration in seconds; omit for a generic average-speed estimate) and optional `sample_interval_m` (default 25,000, 5,000–100,000)","required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WeatherAlongResponse"}}},"description":"Per-sample weather aligned to each point's estimated time of arrival"},"400":{"content":{"application/problem+json":{}},"description":"Invalid request"},"401":{"content":{"application/problem+json":{}},"description":"Missing or invalid API key"},"429":{"content":{"application/problem+json":{}},"description":"Quota or rate limit exceeded"},"501":{"content":{"application/problem+json":{}},"description":"Weather-along-route is not enabled (SN_OPEN_METEO_URL unset)"}},"security":[{"api_key_bearer":[]},{"api_key_query":[]}],"summary":"Forecast weather along a route, aligned to each point's ETA (see\nmodule docs).","tags":["routing"]}},"/v1/x402/topup":{"post":{"operationId":"post_topup","parameters":[{"description":"Bundle from the configured allowlist (USD), e.g. `5` or `0.10`","in":"query","name":"usd","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{}},"description":"Settled and credited: `{status, creditedMillipence, balanceMillipence, tx, network, fxMicroUsdPerPence}`; `status: \"replay\"` reports an already-processed authorization without settling twice"},"400":{"content":{"application/problem+json":{}},"description":"Unknown bundle (body lists the allowlist) or operator key without an identity ledger"},"402":{"content":{"application/json":{}},"description":"x402 payment required: `accepts[0].maxAmountRequired` is the bundle's exact atomic amount; `extra` carries `bundleUsd`, `creditMillipenceEstimate` and `fxMicroUsdPerPence`. Retry with `X-PAYMENT` signing exactly that amount"},"404":{"content":{"application/problem+json":{}},"description":"Top-up not enabled on this deployment (needs bundles + the FX knob)"}},"summary":"`POST /v1/x402/topup?usd={bundle}`.","tags":["self-serve"]}}},"servers":[{"url":"https://api.mapmap.ai"}],"tags":[{"description":"Metered routing endpoints","name":"routing"},{"description":"Administrative boundary lookup: point-in-polygon against the Valhalla admins.sqlite. Standard price class, metered per request. 501 unless the operator sets SN_ADMIN_BOUNDARIES_DB. OSM-derived (ODbL) — see docs/THIRD-PARTY-NOTICES.md","name":"boundaries"},{"description":"Point and along-route elevation via Valhalla's native /height action. Honest per-sample source/resolution metadata and a null (never a guess) wherever the engine has no DEM tile coverage. Standard price class, metered per request. 501 unless the operator sets SN_ELEVATION_ENABLED=true (off by default until DEM tiles are staged)","name":"elevation"},{"description":"Timezone-at-coordinate: IANA zone id, UTC offset, DST offset and abbreviation at a given timestamp. Backed by Valhalla's own timezones.sqlite boundary database and chrono-tz's IANA rules. 501 unless the operator sets SN_TIMEZONE_SQLITE. Standard price class, metered per request","name":"timezone"},{"description":"Metered Photon geocoding proxy (forward and reverse): GeoJSON passthrough, Standard price class. 501 unless the operator sets SN_PHOTON_URL","name":"geocoding"},{"description":"Metered ADR compliance endpoints","name":"compliance"},{"description":"Metered territory update-channel downloads (signed static files; devices verify signatures and hashes themselves)","name":"territories"},{"description":"Hosted vector-tiles API: per-tile MVT, TileJSON and a MapLibre style from the territory PMTiles. Standard price class, metered per request","name":"tiles"},{"description":"Hosted style API (MapMap Studio): versioned, immutable MapLibre styles compiled from sn-style theme documents. Compiled-style and theme reads are public and unmetered (browser map clients fetch them by bare URL); listing is authenticated and scoped to the caller's own styles; publishes are metered at the Standard price class and enforce ownership","name":"styles"},{"description":"Customer places (\"bring your own places\"): per-key proprietary place datasets with full-text and nearest search over the caller's own tantivy index. Never merged into OSM-derived data (ODbL boundary). All routes key-authenticated and metered at the Standard price class","name":"places"},{"description":"User map-error reports (\"Report a map issue\"): a pin, a category and an optional note, key-authenticated and metered at the Standard price class. Reports never edit map data directly — the team triages them (see the admin surface) and verified fixes flow into the deployment's OSM-overrides file and upstream OpenStreetMap contributions","name":"map-issues"},{"description":"Static map assets: SDF font glyph ranges and sprite sheets referenced by compiled styles. Public, unmetered, immutable caching","name":"assets"},{"description":"Agent-first self-serve keys: one-call signup, magic-link verification, quota-free status poll. Beyond the free tier, 402 responses use the x402 machine-payment wire format","name":"self-serve"},{"description":"Integration retros: the structured end-of-integration report an agent (or human) submits once — what was built, problems, gotchas, wins and docs gaps. Key-authenticated, never charged, rate-limited per key. Content is stored verbatim and treated as untrusted text","name":"feedback"},{"description":"Measured overhead clearance along a route, from baked survey geometry (crates/sn-clearance). A within-survey difference with a stated uncertainty bound, never a signed or legal clearance and never a pass over ground the survey did not cover. Standard price class, never premium: a safety check carries no extra charge. 501 unless the deployment publishes at least one clearance artefact","name":"clearance"},{"description":"Key management and credit ledger, admin token required","name":"admin"},{"description":"Unauthenticated service metadata","name":"meta"}]}