API reference

Infractiv Platform API

Water-infrastructure risk, satellite leak detection, and capital-planning intelligence for asset and work-management systems.

Version 1.0.0 Spec OpenAPI 3.1.0 Base https://api.infractiv.com/v1 Endpoints 24

The Infractiv Platform API exposes the analytical outputs of the Infractiv water-infrastructure intelligence platform as a machine-readable contract, so that an enterprise asset-management or work-order system can consume Infractiv findings as work, and return what the field actually found.

The contract is deliberately vendor-neutral. It speaks the vocabulary every work-management system shares — service requests, work orders, inspections, labour and material cost — rather than any one vendor's object names. Mappings for specific systems are published under info.x-infractiv-integrations, so that adding the next system is a mapping exercise and not a rewrite.

The loop this API closes

  1. Infractiv → your system. Every pipe segment carries a modelled failure risk. Every satellite acquisition produces ranked leak candidates. Both are published here as stable, geolocated, incrementally-queryable resources.
  2. Your system → the field. The consuming system turns a detection into a service request or work order and dispatches a crew against its own business rules, its own asset registry, and its own mobile application. Infractiv does not decide what becomes work; it decides what is worth considering.
  3. Field → your system → Infractiv. When the work order closes, the crew's finding — leak confirmed, not found, already repaired — is posted back to /observations, and the repair's labour, equipment and material cost is posted to /cost-actuals.
  4. Infractiv recalibrates. Confirmed and rejected findings are recorded as ground truth against the detection they disposition, and realised repair costs are recorded against the segment. Consuming them automatically to retune the detector on the next cycle is designed and documented, but the automated consumer is not yet wired — today that evidence is applied through Infractiv's own model-refresh cycle. We state this rather than imply a loop that already closes itself.

Steps 3 and 4 are the part a one-way feed does not do. A detection feed that only pushes in one direction produces the same quality of finding in year three as in month one.

Design conventions

ConcernConvention
VersioningMajor version in the path (/v1). Additive changes only within a major version. Deprecation policy below.
PaginationOpaque forward cursors, keyed on each collection's own record identifier. limit + cursor, nextCursor in the envelope, plus a standard Link header.
Incremental syncupdatedSince on every utility-scoped analytical collection — assets, risk scores, detections, observations, cost actuals, work recommendations, asset context and zones. It answers at collection granularity, not row granularity; read the parameter before sizing a delta sync. /scans filters on acquisition date instead, and /layers, /changes, /utilities and /subscriptions do not carry it. Row-level deltas with explicit deletes come from the dedicated /changes feed, which emits upsert and delete operations against a monotonic checkpoint.
ErrorsRFC 9457 application/problem+json, with the problem-type vocabulary published in the Problem schema rather than left to discovery.
IdempotencyIdempotency-Key request header on every method that creates a record; replays return the original response, and a replay with a different body is rejected.
CachingStrong ETag and If-None-Match on the collections a connector polls — utilities, assets, risk scores, detections, detection evidence, asset context and zones, each in both list and single-record form where both exist. Declared per operation, and deliberately not offered on the rest; check the operation rather than assuming. Every 304 returns the validator, so the next poll is conditional too.
Rate limitsRateLimit-Limit / RateLimit-Remaining / RateLimit-Reset on every 2xx response, and Retry-After on 429. The budget is a property of the issued credential and is read from those headers; this contract does not fix a number it has not committed to.
CorrelationInfractiv-Trace-Id on every response, including those with no body, and the same value as traceId inside a Problem.
GeometryGeoJSON (RFC 7946), WGS 84 / EPSG:4326, via Accept: application/geo+json or ?format=geojson. format wins when the two disagree.
TimeRFC 3339 UTC timestamps; dates as YYYY-MM-DD.
UnitsDeclared per field. Lengths in miles, diameters in inches, flow in US gallons per minute, volume in US gallons, distance-to-pipe in metres, area in square metres. Units are never inferred from a name.

Versioning and deprecation

A Marketplace listing is a multi-year commitment, so the evolution rules are part of the contract rather than a support answer.

Additive within a major version. Within /v1 Infractiv may add operations, add optional request parameters, add response properties, and add members to an enumeration that describes a server-produced value. It will not remove or rename a property, narrow a type, tighten a constraint, make an optional request parameter required, or change the meaning of an existing value. Treat unknown response properties and unknown enum members as forward compatibility, not as errors — a client that rejects them is the one that breaks first.

What is a major version. Anything the list above excludes, and one thing specific to this domain: a change in the estimand behind a risk-bearing field. If the quantity currentRisk expresses changes — a different horizon, a conditional rather than an unconditional formulation, a different unit of ranking — that is a new major version even though the field name and its JSON type are unchanged, because a consumer's threshold would silently mean something else. Field names and types are not a sufficient contract for a modelled value.

Deprecation. A deprecated operation or property is marked deprecated in this document and answered with the Deprecation and Sunset response headers (RFC 9745 and RFC 8594) naming the date it stops being served, so a connector learns of a retirement from the responses it is already receiving rather than from a mailing list. The notice period between those two dates, and the period /v1 continues to be served after a /v2 exists, are set in the partner agreement rather than fixed in this document — they are commercial commitments and this contract does not state numbers nobody has signed. Nothing in this document is deprecated today.

Discovering what you are talking to. GET /health returns the running build identity, and GET /utilities/{utilityId} returns per-lane freshness and the scopes the calling credential holds on that utility. Between them a connector can tell which capabilities are live for a given customer without hard-coding per-utility assumptions. There is no minor-version discovery endpoint, because within a major version every change is additive and a client should feature-detect on the field rather than on a version number.

Authentication

Two mechanisms, both suited to unattended machine-to-machine integration and both standard authentication types in the connector frameworks that consume APIs like this one:

  • OAuth 2.0 client credentials (RFC 6749 §4.4) — the default. A partner receives a client identifier and secret scoped to the utilities it is authorised for, and exchanges them at /v1/oauth/token for a short-lived bearer token.
  • API key — a long-lived key presented in the X-Api-Key header, for connector frameworks that prefer static credentials. Keys are per-partner, per-utility, and revocable independently.

Both mechanisms resolve to the same authorisation model: a token or key grants access to an explicit set of utility identifiers and an explicit set of scopes. There is no cross-utility read, and no implicit scope widening.

Data-protection posture

Platform compute and storage run in United States Azure regions, and the AI inference path is pinned to United States processing at the model provider. There is no inbound public network path to any utility environment.

Each utility runs in its own resource group, compute plan, subnet with a dedicated network security group, private endpoint and key vault, under its own workload identities. Diagnostic and application logs from every utility are collected into a single shared workspace in the same United States region, under role-based access control — stated because "isolated at every layer" would not be true of the observability plane, and a partner's security reviewer will ask.

One further bound is stated deliberately. The public edge that fronts platform surfaces is a commercial global content-delivery network; the controls that would pin its TLS termination and log storage to the United States are an enterprise add-on that has been requested and is not yet in place. Until it is, a recorded decision limits that edge to public content and the authenticated staff application, the tunnel connector is pinned to United States regions, and the edge's own authentication records are exported hourly to United States-resident storage.

Verifying these claims rather than believing them

Security statements in vendor documentation are usually assertions. Three of the ones above are commands, and we would rather you ran them than took our word. From a clone of the repository this contract lives in:

  • bash api/lint.sh clones Trimble's own MIT-licensed API-standard ruleset, pins it to a commit, and lints this document against it. It prints the resolved ruleset revision so a published result can be tied to the exact rules that produced it, and it fails on any warning that is not declared as a deviation in info.x-infractiv-deviations.
  • Rscript api/plumber/tests/self-test.R runs the reference implementation's assertion suite over the helpers that decide who may read what, what a malformed request does, and what is published when a column is missing.
  • node scripts/checks/no-external-subresources.mjs api/docs/index.html walks the published reference's markup and proves it loads nothing from a third-party host — it renders offline, behind an egress filter, with no external party observing who reads our documentation.

Two properties of that list matter more than any individual result. The checks run in continuous integration on every change, so they cannot rot quietly. And each is designed to be able to fail: the parser guard was proven to fail against a deliberately planted defect before it was trusted, which is the difference between a gate and a green tick.

What this API can and cannot evidence for an assurance programme

Stated plainly, because a control that has never run produces no evidence and an auditor will establish that in the first hour.

This contract and its reference implementation evidence control design: the authorisation model, the credential lifecycle including revocation, input validation, the error and audit surface, and change control over all of it. They do not yet evidence operating effectiveness, because no host is serving this contract — see x-release-status. Statements below about the platform's existing production surfaces are a different matter: those are as-built, dated, and carry run identifiers.

Where the honest answer is "not yet", this document says so rather than describing an intention in the present tense.

Release status

Read this before you plan against it

This document is a published contract, not an as-built record. Endpoints are specification-complete and backed by a reference implementation that reads the platform's existing committed data contracts. No production host is serving this contract at the date of publication. Infractiv states this explicitly rather than implying availability it has not yet demonstrated.

ItemState
Contract1.0.0 — specification complete
Reference implementationR/Plumber skeleton committed at api/plumber/
Hosted sandboxTBD pending verification — scheduled per the partner integration plan
Hosted productionTBD pending verification — scheduled per the partner integration plan

Getting started

Four steps from nothing to a first call. Everything below links to the section that specifies it; read the release status above before you plan a date against any of it.

1 · Request access

There is no self-serve signup, deliberately. A credential is issued against an explicit list of utility identifiers and an explicit list of scopes, and which utilities a partner may read is the utility's decision rather than the caller's. Get in touch through https://infractiv.com, naming the utilities you are integrating, the scopes you need, and whether your connector framework wants OAuth client credentials or an API key.

2 · Exchange the credential for a token

The client-credentials request is shown in full under Authentication. The token endpoint is not modelled as a path in this document — the reason is declared under Standards conformance — so it is exactly the RFC 6749 shape: a form-encoded request and a §5.1 JSON token response. The token is short-lived; cache it no longer than the lifetime that response reports. Connectors using an API key skip this step entirely.

3 · Confirm the credential before building on it

GET /health takes no credential and answers only whether the service is reachable. Then GET /utilities returns exactly the utilities your credential covers — as far as that credential is concerned, no others exist. If it comes back empty, or missing one you expected, the grant is wrong and no later call will work.

4 · Read the feed, then write back

GET /detections with minClass=High is the call the integration exists for. Poll it with updatedSince and follow nextCursor to the end. When a page comes back empty, read /scans — "clean scan" and "no scan" are different facts and only one of them means there is no work. When the resulting work order closes, post the crew's finding to /observations. That last step is the one a one-way feed does not have.

Integrating with Trimble Unity Maintain

The contract above is vendor-neutral by design. This section is where one system's object names and quirks live, so that integrating a different system never means reading around someone else's vocabulary.

Unity Maintain is GIS-centric by architecture: Trimble states it requires ArcGIS and that the utility's GIS remains the authoritative source of truth for asset data. This API is built for that. It never asks to own the asset registry, publishes findings keyed to the utility's own segment identifier, and returns null asset geometry by design so that a second copy cannot drift from the authoritative one.

Resource mapping

InfractivDirectionTrimble Unity MaintainNote
detections out → Service Request, promoted to Work Order One record per qualifying detection. The consuming system's own rules decide what qualifies.
riskScores out → Asset attributes Written onto asset records so a planner filtering assets sees Infractiv risk without leaving Unity.
assets out → Asset reference Join on the utility's own segment identifier; the GIS facility identifier where the source data carries one.
workRecommendations out → Project, with child Work Orders
observations ← in Work Order closeout, or Inspection The write-back. Map the crew's completion code to the observation status.
costActuals ← in Labor Cost, Equipment Cost, Material Cost Consumed as an input, not just recorded. Realised repair cost replaces assumed unit cost in the capital-planning model.

Connector path

Preferred. App Xchange connector built with the Connector SDK. Its incremental data reader needs a source that exposes a cursor or timestamp and that surfaces deletions explicitly — which is exactly what GET /changes provides, with the same upsert and delete operations the SDK models.

Alternative. A direct REST client writing to the utility's own Unity Maintain site through its per-site authenticated endpoint, with authorisation granted by the customer's own administrators.

Authentication types

  • OAuth2ClientCredentials — the SDK type takes TokenUrl, ClientId, ClientSecret, Scope and ClientAuthentication (default BasicAuthHeader); this API's token endpoint supports all of them.
  • ApiKeyAuth — sends the X-Api-Key header this API accepts by name.
Conformance

This document declares x-trimble-api-standard R2026.1 and lints clean against Trimble's own published Spectral ruleset — zero errors and one warning, the accepted server-URL deviation. See info.x-infractiv-deviations for that deviation, for the token endpoint that is declared rather than modelled, and for the one pagination rule that does not evaluate a cursor-paginated document at all.

Standards conformance

This API is built to published standards rather than to house style: OpenAPI 3.1.0, RFC 9457 for errors, RFC 7946 for geometry, RFC 6749 for authentication, RFC 9110 for conditional requests.

Conformance is also verified against a partner's own rules rather than asserted. The document declares x-trimble-api-standard: R2026.1 and is linted against Trimble's MIT-licensed Spectral ruleset (trimble-oss/openapi-spectral-rules) by a committed, re-runnable script — bash api/lint.sh. Where another partner publishes a machine-checkable standard, this API will be linted against that too, and the result published the same way.

Result: zero errors. Two deviations are declared in the specification rather than left for a reviewer to discover. Trimble's standard explicitly permits documented deviations.

tas-api-server-url-invalid — accepted

The rule requires servers to be hosted on Trimble infrastructure. This is a partner-hosted API, so the condition cannot be met by construction. Every other server-related requirement — HTTPS only, major version in the path, no verbs in path segments — is met.

oauth-token-endpoint-not-modelled — accepted

The OAuth 2.0 token endpoint is not modelled as a path in this document. RFC 6749 mandates snake_case parameter names, a form-encoded request body, and a 200 response. Those conflict with three rules in this standard: camelCase schema properties, structured request data, and 201-or-202 on POST. Rather than claim a conformance the endpoint cannot have, it is declared through the securityScheme tokenUrl and documented in the reference's Authentication section.

tas-pagination-links-structure — not-evaluated

Recorded so that "lints clean" is not read as saying more than it does. This rule only inspects an operation that declares x-trimble-pagination or a pageIndex/pageSize query parameter, so it never evaluates this document: pagination here is opaque forward cursors, not page offsets. The shape it would require — items plus a links object with self and first — is an offset pattern. Cursor pagination is deliberate for an incrementally-synced analytical dataset: an offset walk over a collection that is rebuilt as a unit re-reads and skips rows across the rebuild boundary, while a keyset cursor does not, and a self/first link pair implies a stable page numbering this data does not have. The envelope carries limit, nextCursor and totalEstimate, and every paginated operation also emits the standard RFC 8288 Link header with rel="next". Declaring x-trimble-pagination to make the rule fire would be worse than the gap: it would assert an offset contract this API does not honour.

Authentication

Two mechanisms are accepted. Both are unattended machine-to-machine flows, and both are supported natively by the connector frameworks that consume this API — so no custom credential handling is required to integrate.

OAuth 2.0 client credentials — the default

curl -s -X POST https://api.infractiv.com/v1/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=$INFRACTIV_CLIENT_ID \
  -d client_secret=$INFRACTIV_CLIENT_SECRET \
  -d scope="detections:read observations:write"
curl -s "https://api.infractiv.com/v1/utilities/examplewater/detections?minClass=High&updatedSince=2026-07-01T00%3A00%3A00Z" \
  -H "Authorization: Bearer $ACCESS_TOKEN"

API key — for connectors that prefer static credentials

curl -s "https://api.infractiv.com/v1/utilities/examplewater/detections?minClass=High" \
  -H "X-Api-Key: $INFRACTIV_API_KEY"

Scopes

ScopeGrants
utilities:readRead utility profiles and freshness.
assets:readRead pipe segments and geometry.
risk:readRead risk scores, drivers, and projections.
detections:readRead satellite leak detections and evidence.
planning:readRead work recommendations and the water audit.
observations:readRead field observations.
observations:writeRecord field observations.
costs:writePost realised cost actuals.
subscriptions:writeManage webhook subscriptions.
Authorisation model

A credential is issued against an explicit list of utility identifiers and an explicit list of scopes. Every request is checked against both before any data is read. There is no wildcard utility and no administrative scope, so a credential cannot reach a utility it was not issued for — including by accident.

Conventions

These hold across every endpoint. Read them once and the rest of this reference is mostly field names.

Incremental synchronisation

Every collection accepts updatedSince and an opaque forward cursor. For a connector framework that checkpoints, /changes is the endpoint to build against: it emits a single ordered feed with an explicit upsert or delete operation per entry.

Deletes are emitted explicitly and deliberately. A checkpointed reader cannot infer a removal, so without them a retracted detection would sit in the consuming system as an open work order indefinitely.

Pagination

Cursors encode the sort position of the last row returned, not an offset. Offset pagination over a collection that is being written to silently skips and duplicates rows — for a feed that generates field work, that means missed leaks and duplicate work orders.

Stop when nextCursor is null. Do not stop on totalEstimate, which is an estimate and says so.

Idempotency

Every unsafe method requires an Idempotency-Key. Replaying a key returns the original response with status 200 rather than creating a second record, so a connector can retry a timeout without needing deduplication logic of its own.

Caching

Collections and items carry a strong ETag. Between data refreshes a polling loop that sends If-None-Match receives 304 and transfers nothing.

Errors and retries

Every error is RFC 9457 application/problem+json. One shape across the whole surface, so a partner writes one handler.

{
  "type": "https://api.infractiv.com/problems/unprocessable",
  "title": "The request was well-formed but semantically rejected",
  "status": 422,
  "detail": "assetId 'WM-99999' is not a segment in this utility's inventory",
  "traceId": "20260831T174455.123-4f2a9c1b7e30",
  "errors": [{ "pointer": "/assetId", "detail": "unknown asset" }]
}

Quote traceId when reporting an issue; it resolves to the exact request in Infractiv's logs.

Retry on 429 after Retry-After, and on 503 with exponential backoff and jitter. Do not retry 400, 403, or 422 — they will not succeed on a second attempt, and retrying them is how a connector turns one bad record into a rate-limit incident.

403 means the credential is valid but is not granted this utility or this scope. It is safe to be explicit here, because a partner already knows which utilities its credential covers — telling it so reveals nothing it did not supply.

404 is returned identically whether a resource inside a granted utility does not exist or is not visible to the credential. That distinction is deliberately collapsed: a 404 that differed from a 403 at the record level would let a caller enumerate records it cannot read.

Assets

Pipe segments and their attributes. The join surface to a GIS or EAM asset registry.

GET /v1/utilities/{utilityId}/assets

List pipe segments with attributes (geometry is null in v1).

listAssets assets:read

The asset collection is the join surface between Infractiv and a GIS or EAM asset registry. assetId is the utility's own segment identifier as supplied in its source data, not an Infractiv surrogate — so a consuming system can match on the identifier it already holds.

application/geo+json (or ?format=geojson) returns a GeoJSON FeatureCollection. Geometry is null in v1: the utility's GIS is the authoritative source of asset geometry, and a consuming system running on that same GIS already has it. Emitting a second copy would create two geometries that can disagree. Join on assetId. Geometry passthrough is a v1.1 option for consumers that do not share the utility's GIS.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

material query array<string>

Filter by canonical material family code. Repeatable. Codes, not display labels — see the Asset.material schema for the vocabulary.

pressureZone query array<string>

Filter by pressure zone as the utility records it. Repeatable. These are the utility's own zone labels, not a canonicalised vocabulary.

minDiameterInches query number

Return only segments whose nominal diameter is at or above this value, in inches.

maxDiameterInches query number

Return only segments whose nominal diameter is at or below this value, in inches.

installYearFrom query integer

Return only segments installed in or after this year.

installYearTo query integer

Return only segments installed in or before this year.

includeInAnalysisOnly query boolean

When true (the default) only model-covered segments are returned. Set false to retrieve the full mirrored inventory, including inventory-only segments that carry no risk, survival, or failure-prediction coverage.

Responses

StatusWhenBody
200 OK

A page of assets.

application/jsonPage<Asset>application/geo+jsonAssetFeatureCollection
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

The request was malformed or a parameter was invalid.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblemapplication/problem+jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblemapplication/problem+jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/assets/{assetId}

Retrieve one pipe segment.

getAsset assets:read

Fetches a single segment by the utility's own identifier. Unlike the collection, this returns inventory-only segments too — a consuming system that holds an asset should be able to ask about it and receive an answer, even if that answer is "this segment carries no model coverage".

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

assetIdrequired path string

The utility's own pipe-segment identifier, carried through unchanged. Infractiv does not control its character set, so no pattern is declared: a GIS facility identifier in the wild can contain spaces, slashes and colons. Percent-encode the value in path position (RFC 3986 §2.1) — a client that does not will misroute the moment it meets a utility whose identifiers are not alphanumeric.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

The asset.

application/jsonAssetapplication/geo+jsonAssetFeature
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Cost actuals

Realised repair and replacement cost written back from the work-management system.

GET /v1/utilities/{utilityId}/cost-actuals

List posted cost actuals.

listCostActuals planning:read

Reads back the realised costs that have been posted, so a connector can reconcile what it sent against what was accepted, and so a utility can audit which work orders have fed the cost basis its capital plan is built on.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

Responses

StatusWhenBody
200 OK

A page of cost actuals.

application/jsonPage<CostActual>
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
POST /v1/utilities/{utilityId}/cost-actuals

Post realised repair or replacement cost from the work-management system.

createCostActual costs:write

Infractiv's capital-planning model is only as honest as its unit costs. Out of the box those costs are assumed values from the utility's own cost basis. A work-management system already holds the truth — the labor, equipment, and material actually booked against each work order.

Posting that back replaces an assumption with a measurement. It is the single highest-value field a work-management integration can return, and it maps directly onto the cost entities work-management systems already expose.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

Idempotency-Keyrequired header string

Client-generated unique key, recommended a UUID. Replaying a key returns the original response with status 200 instead of creating a duplicate.

Scope. A key is unique within the tuple (credential, operation, utility — the last omitted where the operation is not utility-scoped). The same key presented on a different operation, or against a different utility, is a different key — so a connector that mints one key per work order and posts both an observation and a cost actual under it gets two records, which is what it wanted. Do not rely on the reverse: a key that is unique on your side is safe under any scoping rule, and a key that is only unique per work order is not.

Replay with a different body. A key replayed with a body that does not match the original request is rejected with 409 and the problem type idempotency-key-reuse. Correcting a field and re-posting under the same key does not amend the stored record; post the correction as a new record with a new key.

Retention and durability. Keys are retained for 24 hours. In the current deployment shape the record is per-instance: a retry that lands on a different instance or after a revision may not be recognised as a replay. On the write-back resources, carry your own correlation value in externalReference so a duplicate remains detectable on your side. A retry past the 24-hour window is a new request by definition.

Request body

Content typeSchemaNotes
application/jsonrequired CostActualCreate
Emergency repair of a failed main
{
  "assetId": "WM-14827",
  "workType": "reactive_repair",
  "completedDate": "2026-08-28",
  "laborCost": 4820,
  "equipmentCost": 1975.5,
  "materialCost": 1140.25,
  "otherCost": 0,
  "currency": "USD",
  "crewHours": 26.5,
  "externalReference": {
    "system": "acme-work-management",
    "entity": "WorkOrder",
    "id": "WO-2026-004412"
  }
}

Responses

StatusWhenBody
200 OK

Idempotent replay.

application/jsonCostActual
201 Created

Cost actual recorded.

application/jsonCostActual
400 Bad Request

The request was malformed or a parameter was invalid.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblemapplication/problem+jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
409 Conflict

The Idempotency-Key was already used for a different request body. Problem type idempotency-key-reuse. Post the corrected record under a new key; the original is unchanged.

application/jsonProblemapplication/problem+jsonProblem
422 Unprocessable Content

Syntactically valid but semantically rejected — for example a cost actual referencing an asset that is not in this utility's inventory, or a completedDate in the future.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
503 Service Unavailable

The record could not be durably stored and was NOT accepted. Retry with the same Idempotency-Key. Nothing has been recorded — treat this as a failed write, never as a slow success.

application/jsonProblemapplication/problem+jsonProblem

Detections

Satellite-derived leak candidates. The near-term source of new work.

GET /v1/utilities/{utilityId}/detections

Satellite-derived leak candidates from the latest processing cycle.

listDetections detections:read

The primary source of new work. Each detection is a pipe segment that carries anomalous surface-moisture evidence in the most recent synthetic-aperture-radar acquisition over the utility's service area, scored and classified.

A consuming work-management system should normally poll with minClass=High and updatedSince, and create one Service Request or Work Order per returned detection. To avoid re-raising work already dispositioned, read /observations and skip detections carrying a terminal status. A server-side openOnly filter is planned for v1.1 and is deliberately not documented here until it exists.

A zero-length collection is a meaningful result: it means the most recent cycle ran and found nothing, not that the feed is broken. Use /scans to distinguish "clean scan" from "no scan".

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

minClass query LeakLikelihoodClass

Return only detections at or above this severity class.

minLikelihood query number

Return only detections with a fused likelihood at or above this value.

detectionDate query stringdate

Restrict to a single acquisition date.

bbox query string

Spatial filter, as minLon,minLat,maxLon,maxLat in WGS 84 decimal degrees — the RFC 7946 axis order, so the same four numbers a GeoJSON bbox carries.

Declared after an audit found the reverse of the usual drift: the parameter was implemented and wired in the reference implementation while the contract did not declare it, so a working capability was invisible to anyone integrating from the specification. An earlier pass had removed it from v1 believing it was documented-but-unbuilt; that was backwards.

A malformed value is a 400 rather than a silently ignored filter. Latitudes outside [-90, 90], longitudes outside [-180, 180], or a minimum greater than its maximum are all malformed. Boxes crossing the antimeridian are not supported in v1 and are rejected rather than silently reinterpreted.

subsidenceOnly query boolean

Return only detections co-located with measured ground subsidence.

Returns an empty page against current data. The filter reads groundMotion.subsidenceFlag, and the step that populates it is configured but has never run — the field is marked x-infractiv-availability: planned for that reason. The parameter is documented because the filter is real and the shape will not change when the fusion lands, but a connector must not read an empty result as "no subsidence-co-located leaks this cycle". Ground-motion evidence that is populated today lives on /asset-context.

Responses

StatusWhenBody
200 OK

A page of detections.

application/jsonPage<Detection>application/geo+jsonDetectionFeatureCollection
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
404 Not Found

This utility has no satellite detection lane, so there is nothing here to serve. Distinguished from an empty collection deliberately: "the lane is not enabled" and "the lane ran and found nothing" are different facts, and a connector that conflated them would report an all-clear it never received.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/detections/{detectionId}

Retrieve one detection.

getDetection detections:read

Fetches a single detection by its composite identifier. Use this to refresh a detection a work order was raised from, before dispatching, in case a later acquisition has changed its class.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

detectionIdrequired path string

Composite detection identifier, <assetId>:<detectionDate>.

The assetId part is the utility's own identifier and may itself contain a colon, so the split rule is on the last colon: everything after it is the YYYY-MM-DD acquisition date, everything before it is the assetId. Percent-encode the whole value in path position. Treat the identifier as opaque where you can; the composition is documented so a value read from a log can be understood, not so clients construct one.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

The detection.

application/jsonDetectionapplication/geo+jsonDetectionFeature
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/detections/{detectionId}/evidence

The decomposed evidence behind a detection.

getDetectionEvidence detections:read

Returns the independent signal channels that were fused into the detection's likelihood, together with the filter lineage recording which vetoes and corrections were applied to the contributing pixels, and the history of this segment's prior detections.

This is what a crew lead or engineer needs in order to trust — or correctly distrust — a dispatch. It is deliberately a separate call: the collection endpoint stays small enough to sync cheaply.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

detectionIdrequired path string

Composite detection identifier, <assetId>:<detectionDate>.

The assetId part is the utility's own identifier and may itself contain a colon, so the split rule is on the last colon: everything after it is the YYYY-MM-DD acquisition date, everything before it is the assetId. Percent-encode the whole value in path position. Treat the identifier as opaque where you can; the composition is documented so a value read from a log can be understood, not so clients construct one.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

The evidence bundle.

application/jsonDetectionEvidence
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Environment

Soil, terrain, land cover, moisture and ground-motion context — per segment, as analytical polygon zones, and as map layers for the utility's GIS.

GET /v1/utilities/{utilityId}/asset-context

Soil, terrain, land cover, weather and ground-motion attributes, per segment.

listAssetContext assets:read

The environmental context of every modelled segment, keyed to the utility's own asset identifier so it can be written straight onto a GIS feature or an asset record.

A note on radar bands, because partners ask: the operational detection cycle runs on C-band. Infractiv has built and exercised an L-band lane — archived synthetic-aperture imagery, the NISAR mission, and L-band radiometer soil moisture — but it is a research lane whose output does not reach this API. Cross-calibration measured strong spatial agreement between the bands and weak temporal co-movement, which is the open problem a fused multi-band score would have to solve. Nothing here is derived from L-band today.

This is the layer utilities usually cannot assemble for themselves: the moisture regime a segment sits in, the soil persistence term that regime is scored against, and — where the ground-motion lane is enabled — how fast the ground under the pipe is moving and how hard it is being strained. A planner who can see that a main sits in persistent soil in an active subsidence zone has a different conversation from one looking at a risk score alone.

Read the availability markers before binding to this resource. The ground-motion and moisture blocks are populated per segment today. Soil texture, land cover and topographic wetness are real layers that the models consume, but they are attributed to an area rather than to a segment — /zones returns them with the polygon, and the per-asset fields are reserved. Two fields are deliberately withheld for stated reasons rather than merely missing. We would rather a partner know which third of this resource is load-bearing than discover it in integration.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

include query array<soil · terrain · landCover · moisture · groundMotion>

Which attribute families to return. Repeatable. Omit for all available. Requesting a family the utility has not licensed or that has not been run returns it as absent rather than as an error.

Responses

StatusWhenBody
200 OK

A page of per-segment environmental context.

application/jsonPage<AssetContext>
304 Not Modified

The representation is unchanged since the supplied ETag.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/zones

Analytical polygon zones, as GIS-ready features.

listZones assets:read

Areas rather than assets: subsidence basins and differential-strain neighbourhoods, delivered as polygons a GIS can add as a layer.

Each zone carries the identifiers of every modelled segment inside it, so a zone is not merely a shape on a map — it is a named set of assets a utility can act on together. That is what makes an area layer useful in an asset-management system rather than only in a picture.

Request application/geo+json (or ?format=geojson) for a FeatureCollection in EPSG:4326, ready to load as a feature layer. Unlike /assets, geometry here is emitted: these polygons are Infractiv's own analytical product and have no counterpart in the utility's GIS to disagree with. Read ZoneFeature before binding — one of the two zone types persists a polygon today and the other does not, so geometry is nullable and a renderer must handle that.

Replace, do not merge. Zones are recomputed per processing period and a zone that stops persisting is simply absent from the next set — there is no retraction signal, and /changes does not carry this resource. A consumer materialising zones as a GIS layer should replace the whole set for each detectedFor period. Merging accumulates analytical polygons that no longer exist, which surfaces later as a GIS administrator's support ticket rather than as an error.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

zoneType query array<ZoneType>

Filter by zone type. Repeatable. Omit for all types.

asOf query stringdate

Return the zone set detected for this period. Zones are recomputed per processing period, so omitting this returns the most recent set.

Responses

StatusWhenBody
200 OK

A page of zones.

application/jsonPage<Zone>application/geo+jsonZoneFeatureCollection
304 Not Modified

The representation is unchanged since the supplied ETag.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/layers

Catalogue of raster and vector layers available for the utility's GIS.

listLayers assets:read

What map layers exist for this utility, what each one represents, how current it is, and how to fetch it.

A catalogue rather than a bulk download, because these are large and a GIS wants to pull what it needs on its own schedule. Each entry declares its format, resolution, coordinate reference system, the period it covers, and — importantly — its provenance and redistribution terms, because some of these layers are derived from third-party sources whose licences travel with them.

Release status: this catalogue is specified and not yet served. The underlying products exist — the platform's warmup and analysis lanes build roughly thirty derived raster products, most of them written directly as cloud-optimised GeoTIFF: moisture climatology, anomaly frequency, change-point score/date/delta, topographic wetness, velocity mosaics and strain fields — but they are produced today for Infractiv's own map rather than as GIS-consumable exports, and what reaches the deployed application is a set of downsampled preview images, not the rasters themselves. Publishing them as GeoTIFF and GeoJSON is a bounded piece of work, sequenced in the integration plan. We list the endpoint because a partner should be able to design against it, and we mark it clearly rather than implying it is live.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

kind query raster · vector

Filter to raster or vector layers.

Responses

StatusWhenBody
200 OK

The layer catalogue.

application/jsonPage<LayerDescriptor>
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Observations

Field outcomes written back from work-order closeout. The ground-truth loop.

GET /v1/utilities/{utilityId}/observations

Field observations recorded against detections.

listObservations observations:read

Reads back what has been recorded, from any source — this API, the Infractiv dashboard's own inline feedback form, or a bulk import. A connector should read this before raising work, so that a detection an operator has already dispositioned in the dashboard does not become a second work order in the field.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

assetId query string

Filter to observations recorded against a single pipe segment.

status query array<ObservationStatus>

Filter by observation status. Repeatable.

Responses

StatusWhenBody
200 OK

A page of observations.

application/jsonPage<Observation>
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
POST /v1/utilities/{utilityId}/observations

Record what the field actually found.

createObservation observations:write

The write-back that closes the loop. A work-management system calls this on work-order closeout, mapping the crew's completion code to status.

Every accepted observation is durably recorded as ground truth against the detection it dispositions. A false_positive is as valuable as a confirmed_leak: both inform the model, and a partner that returns only confirmations biases it.

Honest note on timing: the automated consumer that folds observations into the next detection cycle is designed but not yet wired. Recorded observations are applied through Infractiv's model-refresh cycle today.

The call is idempotent on Idempotency-Key. Re-posting the same key with the same body returns the original observation rather than creating a duplicate; re-posting it with a different body is rejected with 409. Read the parameter's own description for the scope of a key and for the durability bound on the replay record — the guarantee is real but it is not unconditional, and a connector that carries its own externalReference can detect a duplicate whatever happens.

Observations are append-only. There is no PATCH or DELETE: a correction is a new observation for the same assetId and detectionDate, and the most recently submitted one is authoritative for that pair. updatedAt therefore equals submittedAt on every record. This is deliberate — ground truth that can be edited in place is ground truth a model cannot trust — and it is why the change feed can carry observation upserts without any operation here that mutates one.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

Idempotency-Keyrequired header string

Client-generated unique key, recommended a UUID. Replaying a key returns the original response with status 200 instead of creating a duplicate.

Scope. A key is unique within the tuple (credential, operation, utility — the last omitted where the operation is not utility-scoped). The same key presented on a different operation, or against a different utility, is a different key — so a connector that mints one key per work order and posts both an observation and a cost actual under it gets two records, which is what it wanted. Do not rely on the reverse: a key that is unique on your side is safe under any scoping rule, and a key that is only unique per work order is not.

Replay with a different body. A key replayed with a body that does not match the original request is rejected with 409 and the problem type idempotency-key-reuse. Correcting a field and re-posting under the same key does not amend the stored record; post the correction as a new record with a new key.

Retention and durability. Keys are retained for 24 hours. In the current deployment shape the record is per-instance: a retry that lands on a different instance or after a revision may not be recognised as a replay. On the write-back resources, carry your own correlation value in externalReference so a duplicate remains detectable on your side. A retry past the 24-hour window is a new request by definition.

Request body

Content typeSchemaNotes
application/jsonrequired ObservationCreate
Crew confirmed and repaired a leak
{
  "assetId": "WM-14827",
  "detectionDate": "2026-08-24",
  "status": "repair_confirmed",
  "source": "field_crew",
  "groundTruthLeakDate": "2026-08-27",
  "leakLatitude": 34.4231,
  "leakLongitude": -117.4788,
  "leakRateGpmObserved": 11.4,
  "repairDate": "2026-08-28",
  "repairNotes": "Longitudinal crack, 6-inch AC main. Full-circle clamp.",
  "confidence": 0.95,
  "externalReference": {
    "system": "acme-work-management",
    "entity": "WorkOrder",
    "id": "WO-2026-004412"
  }
}
Crew investigated and found nothing
{
  "assetId": "WM-09113",
  "detectionDate": "2026-08-24",
  "status": "false_positive",
  "source": "field_crew",
  "notes": "Landscape irrigation overspray on the easement. No main leak. Acoustic survey clean.",
  "confidence": 0.8,
  "externalReference": {
    "system": "acme-work-management",
    "entity": "WorkOrder",
    "id": "WO-2026-004418"
  }
}

Responses

StatusWhenBody
200 OK

Idempotent replay — the original observation is returned unchanged.

application/jsonObservation
201 Created

Observation recorded.

application/jsonObservation
400 Bad Request

The request was malformed or a parameter was invalid.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblemapplication/problem+jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
409 Conflict

The Idempotency-Key was already used for a different request body. Problem type idempotency-key-reuse. Post the corrected record under a new key; the original is unchanged.

application/jsonProblemapplication/problem+jsonProblem
422 Unprocessable Content

Syntactically valid but semantically rejected — for example an observation referencing an unknown asset.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
503 Service Unavailable

The record could not be durably stored and was NOT accepted. Retry with the same Idempotency-Key. Nothing has been recorded — treat this as a failed write, never as a slow success.

application/jsonProblemapplication/problem+jsonProblem

Operational

Liveness and build identity. Per-utility capability is answered by GET /utilities/{utilityId} — its freshness block and scope list are what a connector branches on — not by a discovery endpoint here.

GET /v1/health

Liveness and data-freshness probe.

getHealth no credential required

Unauthenticated liveness probe. Returns the service status and the build identity of the running instance. Does not disclose utility data.

Responses

StatusWhenBody
200 OK

Service is live.

application/jsonHealth
503 Service Unavailable

The service is temporarily unavailable. Retry with exponential backoff and jitter.

application/jsonProblemapplication/problem+jsonProblem

Planning

Replacement prioritisation, capital schedule, and the AWWA M36 water audit.

GET /v1/utilities/{utilityId}/work-recommendations

Ranked replacement and inspection recommendations.

listWorkRecommendations planning:read

The planned-work counterpart to /detections. Where a detection is evidence of a leak happening now, a recommendation is a modeled judgment that a segment should be replaced, inspected, or monitored — carrying its rank, its scheduled year in the applied capital scenario, and its estimated cost.

These are not a sorted list. A capacity-constrained allocator walks segments in computed priority order and assigns each a schedule year, a funding source and a scheduled cost against an annual capital calendar, under simultaneous constraints on dollars, miles and segment count — so the programme is deliverable, not merely ranked.

A consuming system can materialise these as a Project with child Work Orders, or hold them as a planning backlog. Recommendations are stable across cycles: recommendationId persists while the underlying judgment does, so a connector can update rather than re-create.

Geometry, unlike /assets. The application/geo+json variant does carry the segment centreline. That is not a contradiction of the null-geometry rule on /assets: the rule exists so a second copy of the utility's authoritative asset geometry cannot drift from the first, and a capital programme has no counterpart feature class in the utility's GIS to drift from. Request application/json and join on assetId if you would rather not carry centrelines at all.

One applied scenario. scheduledYear and the funding assignment come from the capital scenario the utility currently has adopted. The levers behind it — budget cap, replacement rates, level-of-service goal, discount rate, horizon — are set in the platform, not through this API, and v1 publishes the adopted programme rather than what-if variants: a work-order system of record should hold the plan the utility is executing, not a hypothesis. Changing the applied scenario produces a new build, so it is visible as a change in updatedAt and updatedBy on every recommendation; there is no scenario identifier in v1, and a consumer that needs to compare two programmes should compare two updatedBy builds. Scenario comparison as a first-class resource is deliberately out of the v1 surface.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

format query json · geojson

Response shape. Equivalent to content negotiation via Accept; provided because some connector frameworks cannot set request headers per call.

Precedence. When format is present it wins over Accept, because the caller who set it is the caller who could not set the header. When it is absent, Accept decides. The parameter is declared only on the operations that actually offer both representations, so there is no case where it selects something the operation cannot produce; everywhere else the single JSON representation is served regardless of either mechanism.

action query array<RecommendedAction>

Filter by recommended action. Repeatable.

scheduledYear query integer

Return only recommendations scheduled for this year in the currently applied capital scenario.

maxRank query integer

Return only the top N ranked recommendations.

Responses

StatusWhenBody
200 OK

A page of recommendations.

application/jsonPage<WorkRecommendation>application/geo+jsonRecommendationFeatureCollection
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/water-loss

AWWA M36 water balance, validity grading, and the value of real losses.

getWaterLoss planning:read

The utility's water audit as a machine-readable balance — supplied volume, authorised consumption, apparent losses, real losses, non-revenue water — with a data-validity grade behind each component and the annual cost of real losses in dollars.

This is the money end of the platform. The cost of water lost is also one component of the reactive cost stream the capital value case is built on, so a utility's water-loss position and its replacement programme are two views of one calculation. Exposed here so a reporting layer in the consuming system can surface audit position alongside work.

Three things to understand before using these numbers.

It is system-wide, not per asset. The audit balances a system, not a pipe. There is no per-segment water-loss attribution, and this API does not imply one.

Real losses are the utility's own figure. The platform reports the real -loss volume the utility carries in its audit and factsheet, valued at the utility's production cost. It computes the AWWA top-down residual internally as a cross-check but does not publish it as the headline. If you need the residual, ask — do not infer it from these components.

The validity weighting is Infractiv's. Component grades follow the AWWA 1–10 convention. The roll-up that turns 19 graded inputs into a single score — weighted 70 volume, 20 system, 10 cost — is Infractiv's own construction, not part of the AWWA software, and is labelled as such rather than presented as a standard output.

Infractiv does not compute ILI or UARL.

An audit without a workbook is mostly defaults. When a utility has supplied a completed audit workbook, these components and their grades come from it. When it has not, the platform seeds the balance components as fixed fractions of imported water and falls back to labelled placeholder grades. inputProvenance on each component says which applies, and a consuming system should surface that rather than presenting a defaulted balance as a completed audit.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

auditYear query integer

The audit year to return. Defaults to the most recent completed audit.

Responses

StatusWhenBody
200 OK

The water balance.

application/jsonWaterLossAudit
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Risk

Modelled likelihood of failure per segment, and the projections built on it. The risk surface is produced by Infractiv's model pipeline and consumed by the platform as a versioned deliverable — the application serves it, it does not fit it, which is why a model refresh is a release rather than a recalculation. Consequence is not scored here; it enters economically, through replacement cost and avoided reactive cost, in the planning resources.

GET /v1/utilities/{utilityId}/risk-scores

Current modeled failure risk by asset.

listRiskScores risk:read

A narrow, high-cardinality projection designed to be written onto asset records in the consuming system as attributes — so that a planner filtering assets in the work-management system sees Infractiv risk without leaving it.

currentRisk is a probability in [0,1]. riskPercentile places the segment against the utility's own modeled population, which is the number a prioritisation rule should threshold on, because an absolute probability is not comparable between utilities.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

updatedSince query stringdate-time

Return only records whose updatedAt is at or after this instant.

Read this before sizing a delta sync. A utility's analytical artifact is rebuilt as a unit and is immutable between builds, so every record in it carries the same updatedAt — the build's own timestamp. This parameter is therefore honest at collection granularity, not row granularity: an unchanged artifact returns an empty page, and a changed one returns the whole collection. It answers "has anything changed", which is the cheap poll a connector wants, and it does not answer "what changed". For record-level deltas with explicit deletes, use /changes.

Combine with cursor to walk the returned collection resumably.

Any RFC 3339 timestamp is accepted, with Z or a numeric offset. A value that does not parse is a 400 naming the parameter — never a request served with the filter quietly dropped, which would return a full collection that looks like a delta.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

minPercentile query number

Return only segments at or above this risk percentile (0-100).

Responses

StatusWhenBody
200 OK

A page of risk scores.

application/jsonPage<RiskScore>
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}/risk-scores/{assetId}

Risk detail for one asset, with cohort context and projections.

getRiskScore risk:read

Includes the per-asset survival curve and the year-by-year risk projection, plus where the segment sits against its own material, diameter, era, and zone cohorts — so a planner can answer "why is this segment ranked here" without a second call.

Note what this does not include: per-feature model attribution. Infractiv does not publish it, and cohort position is not presented as though it were.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

assetIdrequired path string

The utility's own pipe-segment identifier, carried through unchanged. Infractiv does not control its character set, so no pattern is declared: a GIS facility identifier in the wild can contain spaces, slashes and colons. Percent-encode the value in path position (RFC 3986 §2.1) — a client that does not will misroute the moment it meets a utility whose identifiers are not alphanumeric.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

The risk detail.

application/jsonRiskDetail
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Subscriptions

Outbound webhook registration for push delivery.

GET /v1/subscriptions

List this credential's webhook subscriptions.

listSubscriptions subscriptions:write

Returns only the subscriptions owned by the calling credential. Signing secrets are never returned — they are shown once, at creation.

Paginated on the same envelope as every other collection even though a credential rarely holds many subscriptions: growing a bare array into a paginated one after launch would be a breaking change, and this contract promises additive-only evolution within /v1.

Parameters

NameInTypeDescription
limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

Responses

StatusWhenBody
200 OK

A page of subscriptions.

application/jsonPage<Subscription>
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
POST /v1/subscriptions

Register an endpoint for push delivery.

createSubscription subscriptions:write

Push delivery is offered as an alternative to polling for partners that want detections to arrive as they are published rather than on a sync interval. Polling remains fully supported and is the default; a partner need not implement webhooks at all.

Deliveries are signed. See the detectionPublished webhook definition for the signature scheme and the replay-protection rules.

Parameters

NameInTypeDescription
Idempotency-Keyrequired header string

Client-generated unique key, recommended a UUID. Replaying a key returns the original response with status 200 instead of creating a duplicate.

Scope. A key is unique within the tuple (credential, operation, utility — the last omitted where the operation is not utility-scoped). The same key presented on a different operation, or against a different utility, is a different key — so a connector that mints one key per work order and posts both an observation and a cost actual under it gets two records, which is what it wanted. Do not rely on the reverse: a key that is unique on your side is safe under any scoping rule, and a key that is only unique per work order is not.

Replay with a different body. A key replayed with a body that does not match the original request is rejected with 409 and the problem type idempotency-key-reuse. Correcting a field and re-posting under the same key does not amend the stored record; post the correction as a new record with a new key.

Retention and durability. Keys are retained for 24 hours. In the current deployment shape the record is per-instance: a retry that lands on a different instance or after a revision may not be recognised as a replay. On the write-back resources, carry your own correlation value in externalReference so a duplicate remains detectable on your side. A retry past the 24-hour window is a new request by definition.

Request body

Content typeSchemaNotes
application/jsonrequired SubscriptionCreate

Responses

StatusWhenBody
200 OK

Idempotent replay — the original subscription is returned unchanged, including the signing secret issued on the first call. This is the one case where the secret is returned again, and it is returned to the caller that already received it.

application/jsonSubscriptionCreated
201 Created

Subscription created. The signing secret is returned once and never again.

application/jsonSubscriptionCreated
400 Bad Request

The request was malformed or a parameter was invalid.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
409 Conflict

The Idempotency-Key was already used for a different request body. Problem type idempotency-key-reuse. Post the corrected record under a new key; the original is unchanged.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
DELETE /v1/subscriptions/{subscriptionId}

Remove a webhook subscription.

deleteSubscription subscriptions:write

Deletes the subscription. Events continue to be available by polling; removing a subscription stops delivery, never the data.

Parameters

NameInTypeDescription
subscriptionIdrequired path string

Identifier of the subscription to remove.

Responses

StatusWhenBody
204 No Content

Deleted.

401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Sync

Incremental change feed and scan metadata for connector frameworks.

GET /v1/utilities/{utilityId}/scans

Satellite processing history and cadence.

listScans detections:read

One entry per processing cycle: the acquisition date, when it was processed, how many detections it produced, and its terminal status.

This endpoint is how a connector distinguishes states that an empty /detections response cannot: a clean scan that found nothing, a cycle that was skipped because no new acquisition was available, and a cycle that failed. Treating "no detections" as "no leaks" without checking /scans is the integration error this endpoint exists to prevent.

One limit, stated rather than left to be discovered: this collection holds one entry per logged cycle. A cycle that never ran leaves no row at all rather than a not_run row, so detecting a missing cycle means comparing the returned dates against the expected cadence. not_run appears on a utility's freshness summary, where there is a single expected-latest to compare against — not here.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

since query stringdate

Return only cycles whose acquisition date is on or after this date.

Responses

StatusWhenBody
200 OK

A page of scans.

application/jsonPage<Scan>
400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
404 Not Found

This utility has no satellite detection lane, so there is nothing here to serve. Distinguished from an empty collection deliberately: "the lane is not enabled" and "the lane ran and found nothing" are different facts, and a connector that conflated them would report an all-clear it never received.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
503 Service Unavailable

The satellite lane is enabled but its processing history is temporarily unreadable. Problem type scan-history-unavailable. Retry; do not interpret this as a clean scan, which is an empty collection and a different answer.

application/jsonProblemapplication/problem+jsonProblem
GET /v1/utilities/{utilityId}/changes

Incremental change feed with explicit deletes.

listChanges assets:readrisk:readdetections:readplanning:readobservations:read

Scope behaviour, stated first because it changes what you receive. This feed carries five resource types and each is gated by the same scope that gates its own collection. A credential is never refused for holding a subset: the feed returns only the resource types the credential is scoped for, so a connector with assets:read and detections:read sees asset and detection changes and no others. Read resource on every entry rather than assuming the set — a grant widened later changes what appears here, with no change at your end.

A single ordered feed of every mutation across the resources a partner subscribes to, designed for the incremental-reader pattern that connector frameworks use.

Each entry carries an operation of upsert or delete. Deletes are emitted explicitly because an incremental reader cannot infer a removal from a checkpointed query — a superseded or retracted detection would otherwise linger in the consuming system as an open work order forever.

Call with the checkpoint returned by the previous run. The first call omits checkpoint and receives a backfill of the current state, paged by limit exactly like any other collection and walked to completion with the checkpoint from each response — "bounded" means bounded per response, not truncated. hasMore tells a connector to call again immediately rather than wait for its next interval.

Retention, and how to size an outage. Change history is kept for a finite window; a checkpoint older than it answers 410 and the connector must re-backfill. The window is a property of the deployment and is published on every response as retentionDays — read it and size the poll interval and outage tolerance from it rather than hard-coding a figure. It is not fixed in this document because no host is serving this contract yet, and a number stated here would be one nobody had committed to.

Scope of the feed. It covers assets, risk scores, detections, work recommendations and observations. It does not cover /asset-context or /zones. Zones in particular are recomputed per processing period and a zone that stops persisting carries no retraction signal here, so a consumer materialising them as a GIS layer must replace the full zone set for each detectedFor period rather than merge into the previous one — otherwise stale analytical polygons accumulate. Asset context is keyed one-to-one on assets and follows the asset's own lifecycle.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

checkpoint query string

Opaque checkpoint from the previous run. Omit for an initial backfill.

resource query array<asset · riskScore · detection · workRecommendation · observation>

Restrict the feed to specific resource types. Repeatable. Omit for all subscribed resources.

limit query integer

Page size. Defaults to 100, maximum 1000.

Responses

StatusWhenBody
200 OK

A page of change entries plus the next checkpoint.

application/jsonChangeFeed
400 Bad Request

The request was malformed or a parameter was invalid.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
410 The supplied checkpoint is older than the retained change history. The client must perform a full resynchronisation. Returned rather than silently skipping mutations. application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Utilities

The utilities a credential is authorised to read, and their data freshness.

GET /v1/utilities

List the utilities this credential may read.

listUtilities utilities:read

Returns exactly the utilities in the credential's grant. A partner integrating a single utility sees a single entry. There is no enumeration of utilities the caller cannot access.

Parameters

NameInTypeDescription
limit query integer

Page size. Defaults to 100, maximum 1000.

cursor query string

Opaque forward cursor from the previous page's nextCursor. Do not construct or parse.

Order and stability. A collection is ordered by its own record identifier, ascending, and the cursor is a keyset on that identifier: assetId on assets, risk scores and asset context; the (assetId, detectionDate) pair that composes detectionId on detections; the corresponding identifier elsewhere. The identifier is unique within a utility, so the order is total and there is no tie to break. It is deliberately not ordered by updatedAt — every record in a build carries the same timestamp (see updatedSince), so an updatedAt keyset would have no stable order at all.

One collection is ordered differently and says so rather than being quietly inconsistent: /scans is ordered by acquisition date descending, newest cycle first, because the question asked of scan history is almost always "what happened most recently".

The cursor is a live keyset, not a snapshot. A walk is consistent for as long as the underlying artifact is unchanged, which is the normal case because an artifact is rebuilt as a unit rather than edited. If a rebuild lands mid-walk, the remaining pages come from the new build: records are never skipped or duplicated by identifier, but a page may carry newer values than the pages before it. A client that needs a point-in-time read should compare the updatedAt on the first and last page and re-walk if they differ.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

A page of utilities.

application/jsonPage<Utility>
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

400 Bad Request

A parameter was malformed or invalid — an unparseable timestamp, a value outside a declared enumeration, or a non-numeric value where a number is required. errors[] on the problem body names the offending parameter; the request is never silently applied with the bad parameter ignored.

application/jsonProblemapplication/problem+jsonProblem
401 Unauthorized

No valid credential was presented.

application/jsonProblemapplication/problem+jsonProblem
403 Forbidden

The credential is valid but does not hold the required scope, or is not granted access to this utility. Returned in preference to 404 so that probing cannot be used to enumerate which utilities exist.

application/jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblemapplication/problem+jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem
GET /v1/utilities/{utilityId}

Utility profile, model coverage, and data freshness.

getUtility utilities:read

The freshness block is the field a connector should poll before a sync run: it states when each dataset was last rebuilt and, for the satellite lane, the most recent acquisition actually processed.

Parameters

NameInTypeDescription
utilityIdrequired path string

Stable utility identifier from the Infractiv fleet registry.

If-None-Match header string

Conditional request. Supply the previous ETag to receive 304 when nothing changed.

Responses

StatusWhenBody
200 OK

The utility.

application/jsonUtility
304 Not Modified

The representation is unchanged since the supplied ETag. No body is returned.

401 Unauthorized

No valid credential was presented.

application/jsonProblemapplication/problem+jsonProblem
403 Forbidden

The credential is valid but is not granted this utility or scope.

application/jsonProblemapplication/problem+jsonProblem
404 Not Found

No such resource within a utility this credential is granted. Authorisation is checked before existence, so a utility the credential does not hold answers 403 regardless of whether it exists.

application/jsonProblemapplication/problem+jsonProblem
429 Too Many Requests

Rate limit exceeded. Wait for Retry-After seconds before retrying.

application/jsonProblem
500 Internal Server Error

An unhandled condition. The response carries a traceId; quote it when reporting the failure and the corresponding server-side log line can be retrieved. Safe to retry an idempotent request; for a write, retry with the same Idempotency-Key rather than a fresh one.

application/jsonProblem

Webhooks

Push delivery is optional. Polling is fully supported and is the default; a partner need not implement webhooks at all. A webhook is an accelerator, never the system of record — if deliveries fail, the events remain available by polling.

POST your endpoint ← detectionPublished

A processing cycle has published new or updated detections.

onDetectionPublished

Delivered to every subscription registered for detection.published.

Signature. Each delivery carries Infractiv-Signature: t=<unix>,v1=<hex> where v1 is the lowercase hex HMAC-SHA256 of <t>.<raw request body> keyed with the subscription's signing secret. Verify by recomputing over the raw body — not a re-serialised parse — and comparing in constant time.

Replay protection. Reject a delivery whose t is more than five minutes from your clock.

Delivery semantics. At-least-once. Deduplicate on deliveryId. Respond 2xx within ten seconds; anything else is retried with exponential backoff for twenty-four hours, after which the subscription is marked degraded and the events remain available by polling. A webhook is an accelerator, never the system of record.

Ordering is not guaranteed. Retries and parallel delivery mean a later event can arrive before an earlier one — a detection.retracted can land ahead of the detection.published it retracts, and a handler that applies deliveries in arrival order will re-open work that was retracted. Order on occurredAt and discard any event older than the last one you applied for the same resource and identifier. Where order matters more than latency, poll /changes: its checkpoint is monotonic and its entries are ordered, which is the guarantee a push channel cannot make.

Request body

Content typeSchemaNotes
application/json WebhookEnvelope

Responses

StatusWhenBody
200 OK

Acknowledged. Any 2xx is treated as success; anything else is retried. If your endpoint publishes rate-limit headers, Infractiv honours them when scheduling the next delivery.

401 Unauthorized

Your endpoint rejected the delivery as unauthenticated. Infractiv presents no credential of its own — a delivery is authenticated by verifying the Infractiv-Signature HMAC, or by embedding a secret path segment in the subscription URL. Treated as a failed delivery and retried, so a misconfigured endpoint recovers once it is fixed rather than losing the events.

application/jsonProblem
403 Forbidden

Your endpoint refused the delivery. Retried on the standard backoff; persistent refusal marks the subscription degraded and the events remain available by polling.

application/jsonProblem
429 Too Many Requests

Your endpoint is throttling. Infractiv waits for Retry-After before the next attempt rather than applying its own backoff, so a subscriber can pace deliveries.

application/jsonProblem
500 Internal Server Error

Your endpoint failed. Retried with exponential backoff for twenty-four hours. Nothing is lost by failing here — every event delivered by webhook is also readable from its collection and from /changes.

application/jsonProblem

Schemas

Fields annotated maps from name the column in the utility's curated analytical artifact that the value is drawn from, so any figure returned here can be traced to its origin.

Problem

RFC 9457 problem detail.

type is the field to branch on. Switching on status alone cannot separate two 422s with different causes, so the vocabulary below is published as part of the contract rather than left for a partner to discover from response bodies. It is a closed set for v1: a new problem class is an additive change, and an existing URI never changes meaning.

FieldTypeDescription
typerequired stringuri

Stable URI identifying the problem class. The registry, under the prefix https://api.infractiv.com/problems/:

  • unauthenticated — no credential, or one that did not resolve. Accompanies 401 with a WWW-Authenticate challenge.
  • utility-not-granted — the credential is valid but does not hold this utility, or lacks the scope the operation requires. Returned in preference to 404 so that probing cannot enumerate which utilities exist. Accompanies 403.
  • not-found — the addressed record does not exist within a utility the credential does hold. Accompanies 404.
  • invalid-request — malformed syntax, an unparseable parameter, or a body that fails schema validation. Accompanies 400; errors[] locates the failure.
  • unprocessable — syntactically valid and semantically rejected, such as a write referencing an asset that is not in this utility's inventory. Accompanies 422.
  • idempotency-key-reuse — an Idempotency-Key replayed with a different body. Accompanies 409.
  • rate-limited — the credential's request budget for the current window is exhausted. Accompanies 429; honour Retry-After.
  • writeback-unavailable — the write-back store could not accept the record. The request was not applied; retry with the same Idempotency-Key. Accompanies 503.
  • scan-history-unavailable — the satellite lane is enabled but its processing history could not be read. Accompanies 503 and is transient; retry. Do not interpret it as "no scans", which is a different answer and is served as an empty collection. A utility with no satellite lane at all answers 404, not this.
  • internal-error — an unhandled condition. Accompanies 500; quote traceId.

Each URI is planned to resolve to a human-readable page once a host is serving this contract; today the meanings above are the authority. Treat an unrecognised type as its status class.

titlerequired string
statusrequired integer
detail string
instance stringuri
traceId string

Correlation identifier. Quote this when reporting an issue. The same value is returned in the Infractiv-Trace-Id response header on every response, including those with no body.

errors array<object>

Input-level validation failures, when applicable. Exactly one of pointer or parameter is present on each entry — a body member is located by pointer, a query, path or header input by name — so a client can report the offending input without parsing detail.

   pointer stringnullable

JSON Pointer (RFC 6901) to the offending member of the request body.

   parameter stringnullable

Name of the offending query, path or header input.

   in query · path · header · null

Where parameter was read from. Null when pointer is used.

   detail string

Page

FieldTypeDescription
datarequired array<object>
paginationrequired object
   limitrequired integer
   nextCursor stringnullable

Cursor for the next page, or null on the last page.

   totalEstimate integernullable

Approximate total matching records. Null when the estimate would be expensive. Never treat as exact — cursor exhaustion, not this figure, is the end-of-collection signal.

Health

FieldTypeDescription
statusrequired ok · degraded
versionrequired string
buildDigest string

Container image digest of the running instance.

serverTime stringdate-time

Utility

FieldTypeDescription
utilityIdrequired string
namerequired string
state string

Two-letter US state code.

timezone string
totalMainMiles numbernullable

Total distribution main length in the utility's inventory, miles.

modeledMainMiles numbernullable

Length covered by the risk, survival, and failure-prediction models, miles. Reported separately from the inventory total because analytics are computed on the modeled subset and conflating the two misstates every rate and share.

modeledAssetCount integernullable
coordinateReferenceSystem "EPSG:4326"

The CRS of every coordinate this API returns as GeoJSON, fixed by RFC 7946 and therefore a constant rather than a per-utility setting. It says nothing about how raster layers are stored — those declare their own CRS per layer on /layers, and it is often a projected one.

freshnessrequired object

When each dataset was last rebuilt. Poll before a sync run.

   assetsUpdatedAt stringdate-timenullable
   riskUpdatedAt stringdate-timenullable
   planningUpdatedAt stringdate-timenullable
   latestScanDate stringdatenullable

Most recent satellite acquisition processed.

   latestScanStatus ScanStatus

skipped is a real platform outcome, not an error — it is what a cycle reports when no new acquisition was available. A consumer that treats it as a failure will page someone at every satellite outage; one that treats it as a clean scan will report "no leaks" for a period nobody looked at.

scopes array<string>

Scopes the calling credential holds on this utility.

LeakLikelihoodClass

Severity classification of a leak candidate. Class boundaries are tuned per utility against that utility's own signal distribution and confirmed history; a High at one utility is not numerically identical to a High at another, which is why the class, not the raw score, is the dispatch trigger.

Ordering. The classes are ordinal, ascending: Low < Moderate < High < Very High < Critical. The minClass filter is inclusive at the class named. The values are the labels utilities already use on their own dispatch boards, which is why they are title-cased rather than snake_cased like the rest of the contract's enumerations — Very High therefore contains a space and must be percent-encoded in query position (minClass=Very%20High).

Low Moderate High Very High Critical

ObservationStatus

Disposition of a detection after a field visit.

confirmed_leak, false_positive and repair_confirmed are terminal: the detection has been answered and a connector should stop raising work against it. field_visit_pending and uncertain are not terminal — the question is still open. v1 has no server-side filter for this; a connector reads /observations and skips detections carrying a terminal status itself.

confirmed_leak false_positive uncertain field_visit_pending repair_confirmed

ObservationSource

The five sources an operator or an integration may submit.

field_crew acoustic_survey resident_report scada_correlation operator_review

ObservationSourceRead

What may appear when reading observations back. Adds warmup_changepoint_detection, which the platform writes itself when its archive change-point detector seeds a historical event. A consumer must accept it; it can never be submitted.

field_crew acoustic_survey resident_report scada_correlation operator_review warmup_changepoint_detection

RecommendedAction

Version 1.0 emits replace only — the capital replacement programme is the recommendation set the platform produces today. The remaining members are reserved so that adding them later is an additive change rather than a breaking one. A consuming system should switch on this value rather than assume it.

replace rehabilitate inspect monitor acoustic_survey

ScanStatus

skipped is a real platform outcome, not an error — it is what a cycle reports when no new acquisition was available. A consumer that treats it as a failure will page someone at every satellite outage; one that treats it as a clean scan will report "no leaks" for a period nobody looked at.

completed completed_no_detections skipped in_progress failed not_run

Asset

FieldTypeDescription
assetIdrequired string

The utility's own segment identifier, carried through unchanged.

What happens when it changes. Utilities re-segment their mains — a split after a capital project is routine — and when they do, the old identifier stops appearing and one or more new ones take its place. v1 does not model that transition: there is no previousAssetId, and the change feed carries it as a delete plus unrelated upserts with no link between them. A consuming system should treat a disappeared assetId as work to reconcile against its own GIS rather than as a closure signal, because open work orders raised against the old identifier will otherwise be orphaned and an observation posted against it will be rejected as unknown. Carrying supersession explicitly is a known gap, named here rather than discovered at the fourth utility.

maps from pipes.pipe_id

gisFacilityId stringnullable

The identifier this segment carries in the utility's GIS. This is the join key that makes the integration work, and at the reference utility it is not merely present but identical to assetId — 5,651 distinct values, zero blank, matching the analytics identifier verbatim.

Which GIS column it came from varies by utility, and the contract does not yet let a tenant declare which. FACILITYID is the usual source and the one to assume; where a utility does not carry it, the platform resolves the first equivalent column its source data does carry, which at some utilities is a different column entirely. Confirm the mapping per tenant during onboarding rather than hard-coding a target attribute in a connector, and note that in the Esri water data model FACILITYID is an editable text attribute — it is a good join key, not an immutable one. Surfacing the resolved column name, and the immutable GlobalID alongside it, is a named v1.1 addition.

Where a utility's GIS carries no usable facility identifier, Infractiv derives a stable segment identifier instead, this field is null, and assetId is the join key. A consuming system should prefer gisFacilityId where it is present and fall back to assetId, never the reverse.

maps from gis_distribution_lines.facilityid — the runtime binds pipe_id to it directly

utilityIdrequired string
material stringnullable

Canonical material family. Source systems spell materials inconsistently and differently between utilities; this field is the canonical family the platform computes at build time, and materialRaw preserves what the utility actually recorded.

Values are short codes, not display labels: AC, PVC, CI, DIP, GI, Steel, Copper, Concrete, HDPE, Other, Unknown. Filters match the code.

maps from pipes.material_family

materialRaw stringnullable

The material string exactly as the utility recorded it — for example PVC C900, CML_DIP, STL, UNK. Preserved so a utility can recognise its own data, and so a canonicalisation error stays visible rather than being silently absorbed.

maps from pipes.material

diameterInches numbernullable

Nominal diameter in inches, normalised to a number. Utility source schemas vary — some record diameter as text, some as a numeric field in different units. Normalising it here is deliberate: it is the difference between a filter that works across a fleet and one that works at one utility.

maps from pipes.diameter

installYear integernullable
ageYears numbernullable
pressureZone stringnullable

The utility's own pressure-zone label, as recorded in its source data.

maps from pipes.pressure_zone

lengthMiles numbernullable

maps from pipes.pipe_length_miles

estimatedReplacementCost numbernullable

Modeled replacement cost, USD.

maps from pipes.est_replacement_cost

modelCoverage object

Which models cover this segment. A consuming system should not display a risk-derived field for a segment whose coverage flag is false — the absence is a data fact, not a zero.

   hasRiskModel boolean

maps from pipes.has_risk_model

   hasSurvivalModel boolean

maps from pipes.has_survival_model

   hasFailurePrediction boolean

maps from pipes.has_failure_prediction

   includeInAnalysis boolean

maps from pipes.include_in_analysis

   exclusionReason stringnullable

maps from pipes.analysis_exclusion_reason

geometry LineString | null

GeoJSON LineString in EPSG:4326. Null in v1 — the utility's GIS holds the authoritative geometry. See the collection description.

externalIds object

Foreign keys to the utility's other systems, carried through from its GIS export where they exist.

This matters more than its size suggests. A utility's pipe inventory frequently already contains the asset-management identifiers a consuming system needs — at the reference utility the source extract carries Lucity, Tyler and Lynx identifiers alongside CIP status and as-built links. The join an integration would otherwise have to construct is often already in the data.

Status: planned for v1.1. The current pipeline selects a working subset of the source columns and does not carry these through, so this field is empty today. Surfacing them is a small, bounded change, and it is named here because a partner should be able to plan for it.

updatedAtrequired stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

RiskScore

FieldTypeDescription
assetIdrequired string
currentRisk numbernullable

The model's current-year failure score for this segment, on a 0–1 scale.

Use it to rank, not to forecast a count. The value orders segments against each other reliably, which is what a prioritisation decision needs. Its absolute magnitude is not calibrated: Infractiv's own model provenance records that the delivered values overstate absolute risk, and a corrected conditional formulation exists but is not yet the shipped deliverable. Summing these values across a network to obtain "expected failures per year" is therefore not a supported use of this field, and we would rather say so here than have someone discover it in a board meeting.

riskPercentile is the safer field to threshold on, because a percentile is invariant to the calibration question.

The estimand is declared, not implied. x-infractiv-estimand below names the exact quantity this number is, because "current-year failure score" is a label and not a definition, and two different quantities can wear the same label and the same JSON type. Today it is an unconditional first-break probability by year. A corrected conditional formulation is the direction of travel; when it becomes the shipped quantity, the estimand string changes and — per the versioning policy in the document description — that is a major version, not a silent improvement. Bind to the estimand string if your system stores a threshold against this field.

maps from pipes.current_risk

riskPercentile numbernullable

Percentile rank within this utility's own modeled population.

Deliberately the only ranking this resource publishes. The platform has no Low/Moderate/High banding for failure risk, so the API does not invent one — a band would look authoritative while encoding a threshold Infractiv never set. Threshold on the percentile, at a level the utility chooses.

predictedFailureYear integernullable

Modelled year of expected failure for this segment.

A past-dated year is a real output, not an error, and a consuming system must be built to expect it. On the reference utility's current build, 1,030 of the 4,854 segments carrying a prediction — 21.2% — are dated before the present year. That is the model saying a segment is already beyond its expected life, which for a distribution network with mid-century installations is a substantive finding rather than a glitch. It is also why a naive yearsRemaining = predictedFailureYear - currentYear renders as a negative number on a fifth of the network, and why any UI binding to this field needs a deliberate treatment for that case rather than a default numeric formatter.

Read it as a position in an ordering, consistent with the calibration limits described on currentRisk. Do not aggregate these years into a forecast of how many failures a given year will bring; the model is not calibrated to support that and we do not claim it.

Coverage is not universal: 4,854 of 5,651 segments (85.9%) carry a prediction. The remainder are inventory-only, and modelCoverage.includeInAnalysis on the asset record is where a consumer reads why.

maps from pipes.predicted_failure_year

yearsRemaining numbernullable

Modelled remaining service life in years; negative means past due.

Currently a placeholder model. The platform's remaining-life figure is derived from a simple tolerance model, not from the survival model, and it is labelled as a placeholder in the platform's own data contract. It is published here because it is useful for coarse sorting and because withholding it would not make anyone's planning better — but do not present it to a utility as a modelled service-life estimate, and do not threshold capital decisions on it. predictedFailureYear and riskPercentile are the load-bearing fields.

modelVersionplanned stringnullable

Build identity of the model run that produced this score.

Not emitted yet, and the reason is worth stating because the field looked populated until it was checked. The reference implementation was filling it with the API container image digest — a different artifact entirely, built downstream of the pipeline that produced the score. A partner pinning an analysis to that value would have been pinning to the wrong thing, and would not have found out.

The published artifact carries no model-run identifier today, so the field returns null rather than something plausible. GET /health returns buildDigest if what you want is the identity of the service answering you; that is a genuinely different question.

updatedAtrequired stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

RiskDetail

FieldTypeDescription
assetIdrequired string
currentRisk numbernullable

The model's current-year failure score for this segment, on a 0–1 scale.

Use it to rank, not to forecast a count. The value orders segments against each other reliably, which is what a prioritisation decision needs. Its absolute magnitude is not calibrated: Infractiv's own model provenance records that the delivered values overstate absolute risk, and a corrected conditional formulation exists but is not yet the shipped deliverable. Summing these values across a network to obtain "expected failures per year" is therefore not a supported use of this field, and we would rather say so here than have someone discover it in a board meeting.

riskPercentile is the safer field to threshold on, because a percentile is invariant to the calibration question.

The estimand is declared, not implied. x-infractiv-estimand below names the exact quantity this number is, because "current-year failure score" is a label and not a definition, and two different quantities can wear the same label and the same JSON type. Today it is an unconditional first-break probability by year. A corrected conditional formulation is the direction of travel; when it becomes the shipped quantity, the estimand string changes and — per the versioning policy in the document description — that is a major version, not a silent improvement. Bind to the estimand string if your system stores a threshold against this field.

maps from pipes.current_risk

riskPercentile numbernullable

Percentile rank within this utility's own modeled population.

Deliberately the only ranking this resource publishes. The platform has no Low/Moderate/High banding for failure risk, so the API does not invent one — a band would look authoritative while encoding a threshold Infractiv never set. Threshold on the percentile, at a level the utility chooses.

predictedFailureYear integernullable

Modelled year of expected failure for this segment.

A past-dated year is a real output, not an error, and a consuming system must be built to expect it. On the reference utility's current build, 1,030 of the 4,854 segments carrying a prediction — 21.2% — are dated before the present year. That is the model saying a segment is already beyond its expected life, which for a distribution network with mid-century installations is a substantive finding rather than a glitch. It is also why a naive yearsRemaining = predictedFailureYear - currentYear renders as a negative number on a fifth of the network, and why any UI binding to this field needs a deliberate treatment for that case rather than a default numeric formatter.

Read it as a position in an ordering, consistent with the calibration limits described on currentRisk. Do not aggregate these years into a forecast of how many failures a given year will bring; the model is not calibrated to support that and we do not claim it.

Coverage is not universal: 4,854 of 5,651 segments (85.9%) carry a prediction. The remainder are inventory-only, and modelCoverage.includeInAnalysis on the asset record is where a consumer reads why.

maps from pipes.predicted_failure_year

yearsRemaining numbernullable

Modelled remaining service life in years; negative means past due.

Currently a placeholder model. The platform's remaining-life figure is derived from a simple tolerance model, not from the survival model, and it is labelled as a placeholder in the platform's own data contract. It is published here because it is useful for coarse sorting and because withholding it would not make anyone's planning better — but do not present it to a utility as a modelled service-life estimate, and do not threshold capital decisions on it. predictedFailureYear and riskPercentile are the load-bearing fields.

modelVersionplanned stringnullable

Build identity of the model run that produced this score.

Not emitted yet, and the reason is worth stating because the field looked populated until it was checked. The reference implementation was filling it with the API container image digest — a different artifact entirely, built downstream of the pipeline that produced the score. A partner pinning an analysis to that value would have been pinning to the wrong thing, and would not have found out.

The published artifact carries no model-run identifier today, so the field returns null rather than something plausible. GET /health returns buildDigest if what you want is the identity of the service answering you; that is a genuinely different question.

updatedAtrequired stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

asset Asset
cohortContext array<object>

Where this segment sits against its own cohorts — its material, its diameter class, its installation era, its pressure zone — expressed as the cohort's median risk and the segment's rank within it.

This is cohort comparison, not model attribution. Infractiv does not publish per-feature attribution for the failure model, and does not present cohort position as if it were. A planner can use it to say "this segment is high because its cohort is high" or "this segment is high for its cohort", both of which are defensible statements about the data.

   cohort string
   value string
   cohortMedianRisk numbernullable
   rankWithinCohort integernullable
   cohortSize integernullable
survivalCurve array<object>

Modeled survival probability by year.

   year integer
   survivalProbability number
riskProjection array<object>

Projected annual failure probability.

   year integer
   risk number

Detection

FieldTypeDescription
detectionIdrequired string

Composite of asset identifier and acquisition date.

assetIdrequired string

maps from satellite_leak_detections.pipe_id

utilityId string
detectionDaterequired stringdate

Date of the synthetic-aperture-radar acquisition this detection was derived from.

maps from satellite_leak_detections.detection_date

leakLikelihood numbernullable

Fused likelihood score. Blends independent evidence channels with the segment's modeled failure risk, so it is a dispatch-ranking score and not a standalone probability of a leak.

One channel is currently degraded and we would rather state it than let a partner find it. The soil-persistence channel carries 20% of the fusion weight and is affected by the unit-convention defect described on DetectionEvidence.channels.soilScore. Measured on the current build, it injects a near-constant offset of +0.090 (standard deviation 0.005) into this value, so the absolute magnitude is biased upward and that channel contributes almost no discriminating signal.

The ordering — the only use this field supports — is materially unaffected, and we measured that rather than asserting it: rank correlation between the published values and the corrected computation is 0.986 (Spearman), the top 25 segments are identical, and 48 of the top 50 are shared. A dispatch list built from this field is sound; a threshold set against its absolute value is not.

maps from satellite_leak_detections.leak_likelihood

leakLikelihoodClassrequired LeakLikelihoodClass

Severity classification of a leak candidate. Class boundaries are tuned per utility against that utility's own signal distribution and confirmed history; a High at one utility is not numerically identical to a High at another, which is why the class, not the raw score, is the dispatch trigger.

Ordering. The classes are ordinal, ascending: Low < Moderate < High < Very High < Critical. The minClass filter is inclusive at the class named. The values are the labels utilities already use on their own dispatch boards, which is why they are title-cased rather than snake_cased like the rest of the contract's enumerations — Very High therefore contains a space and must be percent-encoded in query position (minClass=Very%20High).

meanConfidence numbernullable

Mean per-pixel detector confidence across the anomaly clusters attributed to this segment, 0 to 1. A property of the imagery evidence, not of the fused dispatch score.

maps from satellite_leak_detections.mean_confidence

maxConfidence numbernullable

Highest per-pixel detector confidence within those clusters, 0 to 1. Same scale as meanConfidence; the gap between the two says how concentrated the evidence is.

maps from satellite_leak_detections.max_confidence

estimatedRateGpm numbernullable

Estimated flow rate, US gallons per minute. Derived from detected near-surface excess water and therefore a lower bound — water below the radar support layer is not observable.

maps from satellite_leak_detections.estimated_rate_gpm

estimatedVolumeGal numbernullable

Total volume in US gallons over the detection window, not a rate. The window is a configured number of days matched to the radar revisit interval, and estimatedRateGpm is this figure divided by it — so the two are one estimate expressed two ways, and only one of them should be carried into a downstream calculation.

maps from satellite_leak_detections.estimated_volume_gal

volumeConfidence High · Medium · Low · null

How many independent layers confirmed the anomaly behind this volume estimate, banded and ordered Low < Medium < High. The three are detector confidence above its threshold, co-located ground subsidence, and an anomaly falling within the pipe buffer. Title-cased to match leakLikelihoodClass, which is the label utilities already read.

High is unreachable on current builds, because it requires all three and one of the three is the ground-motion co-location that has never run (see groundMotion on this schema). A connector should treat Medium as the current ceiling rather than waiting for a High that cannot arrive.

maps from satellite_leak_detections.volume_confidence

anomalyClusterCount integernullable

Number of distinct anomaly clusters attributed to this segment in this acquisition. More than one means the evidence is spatially separated along the main rather than a single spot.

maps from satellite_leak_detections.anomaly_cluster_count

totalAreaSqm numbernullable

Combined surface area of those clusters, square metres.

maps from satellite_leak_detections.total_area_sqm

centroid object

Anomaly centroid in EPSG:4326 — the point a crew should be dispatched to.

   longitude numbernullable

maps from satellite_leak_detections.centroid_lon

   latitude numbernullable

maps from satellite_leak_detections.centroid_lat

minDistanceToPipeM numbernullable

Closest distance from the anomaly to the pipe centreline, metres.

maps from satellite_leak_detections.min_distance_to_pipe_m

groundMotion object

Co-located ground-displacement evidence. Subsidence at a leak candidate raises both the likelihood that the anomaly is subsurface water and the consequence of leaving it.

Empty on every current detection, and marked accordingly. The step that fuses displacement into a detection record is configured but has no cache path, so it has never run: displacementMmPerYear is non-null in 0 of the reference utility's 177 current detections. The ground-motion data itself is real and substantial — it is attributed to 4,854 segments across roughly a decade — but it lives on /asset-context, which is where a consumer should read it. These two fields are retained because the fusion is a bounded piece of work, not a redesign.

   subsidenceFlagplanned booleannullable

maps from satellite_leak_detections.insar_subsidence_flag

   displacementMmPerYearplanned numbernullable

Line-of-sight velocity, millimetres per year. Negative indicates subsidence.

maps from satellite_leak_detections.insar_displacement_mmyr

assetContext object

Denormalised segment attributes, so a work order can be raised from one call.

   material stringnullable
   diameterInches numbernullable
   pressureZone stringnullable
   lengthMiles numbernullable
   currentRisk numbernullable
   predictedFailureYear integernullable
   estimatedReplacementCost numbernullable
waterAuditCrosscheck within_bounds · potential_overestimate · potential_underestimate · m36_unavailable · null

Whether this cycle's aggregate estimated loss is consistent with the utility's AWWA M36 real-loss position. A cycle that implies more loss than the audit can account for is flagged rather than silently published.

maps from satellite_leak_detections.m36_crosscheck_flag

observationStatus ObservationStatus | null

The most recent field observation recorded against this detection, if any.

geometry Point | null

GeoJSON Point at the anomaly centroid. Present in GeoJSON form.

updatedAtrequired stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

DetectionEvidence

The independent channels fused into a detection, its filter lineage, and its history. Exposed so that a finding can be interrogated rather than taken on faith.

FieldTypeDescription
detectionId string
channels object

Decomposed evidence. A null channel means that channel was absent for this cycle, which is a stated fact and not a zero.

   anomalyZ numbernullable

Pixel-weighted mean anomaly z-score over the detection's pixels.

maps from satellite_leak_detections.mean_anomaly_z

   moistureExcess numbernullable

Pixel-weighted mean signed soil-moisture excess, cubic metres per cubic metre.

maps from satellite_leak_detections.mean_moisture_excess

   entropyBits numbernullable

Pixel-weighted mean posterior entropy, bits.

maps from satellite_leak_detections.mean_entropy_bits

   soilScoreplanned numbernullable

Soil-persistence term, 0 sand to 1 clay.

A known unit-convention defect currently affects this value and it is withheld rather than published. The soil grid stores saturated hydraulic conductivity as log10(cm/hr); the code path that turns it into this term applies log10 a second time and compares the result against thresholds expressed in linear cm/hr. Measured on the current build: the term reads a median 0.44 where the corrected computation gives 0.00, and 76% of segments that should read "free-draining" instead read mid-range. See saturatedConductivity on AssetContext for the same root cause.

A second reason this is withheld rather than published with a caveat: where the conductivity grid is unavailable the term is imputed to a neutral 0.5 rather than left null, so a published value would not distinguish "measured mid-range soil" from "no soil data for this segment" — and the channel guarantee above says a null means absent. When it lands it will carry an explicit provenance flag, in the vocabulary AuditComponent.inputProvenance already uses.

maps from satellite_leak_detections.soil_score

   changepointScore numbernullable

Archive change-point score at the cluster centroid.

maps from satellite_leak_detections.changepoint_score

filterLineage object

Which vetoes and corrections were applied to the contributing pixels.

maps from satellite_leak_detections.filter_provenance

   polarisationMasked boolean
   rainVetoed boolean
   imperviousContext boolean
   drainageReduced boolean
   soilAdjusted boolean
volumeBasis legacy_geometry · moisture_excess · null

Which volume estimator produced this cycle's figures.

maps from satellite_leak_detections.volume_basis

history array<object>

Prior detections on this segment across cycles. A segment detected repeatedly is a different operational proposition from a first-time detection, and this is where a connector reads that.

   detectionDate stringdate
   leakLikelihood numbernullable
   leakLikelihoodClass LeakLikelihoodClass

Severity classification of a leak candidate. Class boundaries are tuned per utility against that utility's own signal distribution and confirmed history; a High at one utility is not numerically identical to a High at another, which is why the class, not the raw score, is the dispatch trigger.

Ordering. The classes are ordinal, ascending: Low < Moderate < High < Very High < Critical. The minClass filter is inclusive at the class named. The values are the labels utilities already use on their own dispatch boards, which is why they are title-cased rather than snake_cased like the rest of the contract's enumerations — Very High therefore contains a space and must be percent-encoded in query position (minClass=Very%20High).

   estimatedRateGpm numbernullable
   subsidenceFlag booleannullable
modelVersionplanned stringnullable

Build identity of the model run behind this detection. Not emitted yet — see the same field on RiskScore for why, and use GET /health if what you need is the identity of the service.

ObservationBase

FieldTypeDescription
assetIdrequired string
detectionDate stringdatenullable

The acquisition this observation dispositions. Omit for an observation not tied to a specific detection — for example a break found during unrelated work, which is still valuable ground truth.

statusrequired ObservationStatus

Disposition of a detection after a field visit.

confirmed_leak, false_positive and repair_confirmed are terminal: the detection has been answered and a connector should stop raising work against it. field_visit_pending and uncertain are not terminal — the question is still open. v1 has no server-side filter for this; a connector reads /observations and skips detections carrying a terminal status itself.

groundTruthLeakDate stringdatenullable
leakLatitude numbernullable
leakLongitude numbernullable
leakAreaM2 numbernullable
leakRateGpmObserved numbernullable

Field-measured flow rate, US gallons per minute. The single most valuable field for calibrating volume estimation.

repairDate stringdatenullable
repairNotes stringnullable
notes stringnullable
confidence numbernullable

The observer's own confidence. Recorded, never inferred.

operatorId stringnullable

Identifier of the person or service recording the observation, as known to the calling system. Do not send personal data beyond a stable internal identifier.

attachments array<object>

References to evidence held in the calling system. Infractiv stores the reference, not the file.

   kind photo · document · audio · other
   uri stringuri
   caption stringnullable
externalReference ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

ObservationCreate

FieldTypeDescription
assetIdrequired string
detectionDate stringdatenullable

The acquisition this observation dispositions. Omit for an observation not tied to a specific detection — for example a break found during unrelated work, which is still valuable ground truth.

statusrequired ObservationStatus

Disposition of a detection after a field visit.

confirmed_leak, false_positive and repair_confirmed are terminal: the detection has been answered and a connector should stop raising work against it. field_visit_pending and uncertain are not terminal — the question is still open. v1 has no server-side filter for this; a connector reads /observations and skips detections carrying a terminal status itself.

groundTruthLeakDate stringdatenullable
leakLatitude numbernullable
leakLongitude numbernullable
leakAreaM2 numbernullable
leakRateGpmObserved numbernullable

Field-measured flow rate, US gallons per minute. The single most valuable field for calibrating volume estimation.

repairDate stringdatenullable
repairNotes stringnullable
notes stringnullable
confidence numbernullable

The observer's own confidence. Recorded, never inferred.

operatorId stringnullable

Identifier of the person or service recording the observation, as known to the calling system. Do not send personal data beyond a stable internal identifier.

attachments array<object>

References to evidence held in the calling system. Infractiv stores the reference, not the file.

   kind photo · document · audio · other
   uri stringuri
   caption stringnullable
externalReference ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

sourcerequired ObservationSource

The five sources an operator or an integration may submit.

Observation

FieldTypeDescription
assetIdrequired string
detectionDate stringdatenullable

The acquisition this observation dispositions. Omit for an observation not tied to a specific detection — for example a break found during unrelated work, which is still valuable ground truth.

statusrequired ObservationStatus

Disposition of a detection after a field visit.

confirmed_leak, false_positive and repair_confirmed are terminal: the detection has been answered and a connector should stop raising work against it. field_visit_pending and uncertain are not terminal — the question is still open. v1 has no server-side filter for this; a connector reads /observations and skips detections carrying a terminal status itself.

groundTruthLeakDate stringdatenullable
leakLatitude numbernullable
leakLongitude numbernullable
leakAreaM2 numbernullable
leakRateGpmObserved numbernullable

Field-measured flow rate, US gallons per minute. The single most valuable field for calibrating volume estimation.

repairDate stringdatenullable
repairNotes stringnullable
notes stringnullable
confidence numbernullable

The observer's own confidence. Recorded, never inferred.

operatorId stringnullable

Identifier of the person or service recording the observation, as known to the calling system. Do not send personal data beyond a stable internal identifier.

attachments array<object>

References to evidence held in the calling system. Infractiv stores the reference, not the file.

   kind photo · document · audio · other
   uri stringuri
   caption stringnullable
externalReference ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

observationIdrequired stringuuid
sourcerequired ObservationSourceRead

What may appear when reading observations back. Adds warmup_changepoint_detection, which the platform writes itself when its archive change-point detector seeds a historical event. A consumer must accept it; it can never be submitted.

utilityId string
submittedAtrequired stringdate-time
updatedAt stringdate-time

Always equal to submittedAt. Observations are append-only — see the operation description — so this field exists for envelope symmetry with the read resources, not because a record is ever amended.

recordedBy string

The credential that submitted this record. Not a pipeline build like updatedBy on the read resources: a write-back is attributed to whoever wrote it, which is what makes an observation usable as ground truth.

modelVersionplanned stringnullable

Model build the dispositioned detection came from, captured at write time. Null until the platform emits a model-run identifier — see the same field on RiskScore. It is stored on the observation rather than looked up later precisely so that a confirmation stays attached to the model that produced the prediction, once there is an identifier to attach.

CostActualCreate

FieldTypeDescription
assetIdrequired string
workTyperequired reactive_repair · planned_replacement · rehabilitation · inspection · leak_survey
completedDaterequired stringdate
laborCost numbernullable
equipmentCost numbernullable
materialCost numbernullable
otherCost numbernullable
currencyrequired USD
crewHours numbernullable
lengthReplacedFeet numbernullable
notes stringnullable
externalReference ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

CostActual

FieldTypeDescription
assetIdrequired string
workTyperequired reactive_repair · planned_replacement · rehabilitation · inspection · leak_survey
completedDaterequired stringdate
laborCost numbernullable
equipmentCost numbernullable
materialCost numbernullable
otherCost numbernullable
currencyrequired USD
crewHours numbernullable
lengthReplacedFeet numbernullable
notes stringnullable
externalReference ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

costActualIdrequired stringuuid
utilityId string
totalCost numbernullable

Server-computed sum of the supplied components.

recordedAtrequired stringdate-time
updatedAt stringdate-time

Always equal to recordedAt. Cost actuals are append-only for the same reason observations are; a correction is a new record.

recordedBy string

The credential that submitted this record.

WorkRecommendation

FieldTypeDescription
recommendationIdrequired string
assetIdrequired string
utilityId string
actionrequired RecommendedAction

Version 1.0 emits replace only — the capital replacement programme is the recommendation set the platform produces today. The remaining members are reserved so that adding them later is an additive change rather than a breaking one. A consuming system should switch on this value rather than assume it.

rankrequired integer

Position in the utility's prioritised programme. 1 is highest priority.

maps from replacement_priority

scheduledYear integernullable

Year in the currently applied capital scenario.

estimatedCost numbernullable

USD.

lengthMiles numbernullable
rationale stringnullable

Plain-language basis for the recommendation, suitable for a work-order description field.

priorityScore numbernullable

Composite prioritisation score behind the rank.

maps from replacement_priority.priority_score

geometry LineString | null
updatedAtrequired stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

WaterLossAudit

AWWA M36 water balance components with data-validity grading.

FieldTypeDescription
utilityId string
auditYear integer
methodology "AWWA M36"
units "acre-feet per year"

The platform's water balance is denominated in acre-feet per year, the basis its source audit uses. The API does not convert, because a silent unit conversion is how a water audit stops reconciling with the utility's own filed numbers.

components object
   waterSupplied AuditComponent
   authorizedConsumption AuditComponent
   apparentLosses AuditComponent
   realLosses AuditComponent
   nonRevenueWater AuditComponent
dataValidityScore integernullable

Overall validity score for the audit, 0–100, from 19 graded inputs weighted 70 volume / 20 system / 10 cost, banded into five tiers.

Published because an audit number without its grade is not a defensible figure. The weighting is Infractiv's own construction, not an AWWA output — the component grades follow the AWWA 1–10 convention, the roll-up does not. Stated here so nobody presents it to a regulator as a standard score.

annualCostOfRealLosses numbernullable

The value of real losses, USD per year — real-loss volume multiplied by the utility's variable production cost per acre-foot. The AWWA M36 production-cost basis.

At a utility whose factsheet carries a single water unit rate, the production-cost and retail-value bases resolve to the same figure until a completed audit workbook separates them. Where that is the case, costBasis says so.

costBasis production_cost · retail_value · unseparated · null

Which basis the value above was computed on. unseparated means the utility's inputs do not yet distinguish production cost from retail value, so the two are numerically identical — a real condition worth surfacing rather than hiding behind a single number.

updatedAt stringdate-time
updatedBy string

Identity of the process that last wrote this record — the Infractiv pipeline build, expressed as its image digest. Records are produced by a pinned, reproducible build rather than by a person, and this field says so rather than leaving the question open. The form is infractiv-pipeline@sha256:<digest>. No example is declared here, because the metadata convention this document lints against expects a vendor-namespaced resource name, and Infractiv is not a resource in that vendor's namespace. Asserting one would be a false claim about where the record came from.

AuditComponent

FieldTypeDescription
value numbernullable
inputProvenance source_backed · workbook · operator_entered · assumed_default · null

Where this component's value came from. assumed_default means the platform seeded it because the utility supplied nothing — a real and common condition at onboarding, surfaced rather than hidden. Never present a component to a regulator without checking this field.

dataGrade integernullable

AWWA data-grading value for this component.

basis stringnullable

How the figure was derived.

ZoneType

subsidence_hotspot — a regional basin where the ground is moving, clustered from the absolute-velocity field. strain_zone — a neighbourhood where multiple segments share elevated differential ground motion, which is what actually breaks a pipe: uniform settlement is far less damaging than differential settlement across a joint.

subsidence_hotspot strain_zone

AssetContext

Environmental context attributed to a single pipe segment.

Every field carries x-infractiv-availability, because this resource mixes data that is populated today with fields that are reserved. The three values mean exactly what they say:

  • populated — verified non-null in the current build for the reference utility. Safe to bind a workflow to.
  • area-level — the underlying layer exists and is used inside the models, but it is attributed to an area rather than to a segment. Read it from /zones, which returns the polygon. The per-asset field is reserved and returns null today.
  • planned — not emitted. Present so the shape is stable when it lands, and so that nobody has to guess where it will go.

A field that is area-level or planned returns null rather than being absent, so a client that binds to it will not break when it begins to carry values.

Scope of the convention, so an unmarked field is not misread. The marker appears wherever a field's availability is anything other than "populated as documented" — on this schema, and on the handful of fields elsewhere in the contract that are known-empty or known-degraded (Detection.groundMotion.* and DetectionEvidence.channels.soilScore). Absence of a marker is not a guarantee; it means we know of nothing to declare. Where a field has a limitation we have measured, it is written into that field's own description rather than left to a marker.

FieldTypeDescription
assetIdrequired string
utilityId string
soil object

Soil properties from a national soil-properties grid. Only the persistence score is attributed per segment; texture and conductivity are area-level and are served on /zones.

   textureDominantarea-level sand · silt · clay · null

Dominant texture class. Attributed at area level, not per segment — read it from /zones.

   saturatedConductivityplanned numbernullable

Saturated hydraulic conductivity. It matters twice over: it governs how a leak plume moves, and it is a corrosion and bedding-stability factor in its own right.

Deliberately not emitted yet, and the reason is a unit convention rather than a missing input. The source grid stores this variable as log10(cm/hr). One consumer path in the reference utility's chain compares it against thresholds expressed in linear cm/hr without de-scaling first, so a value published from that path today would be numerically wrong while looking entirely plausible. The field lands once the scaling is asserted at load time the way the neighbouring retention parameters already are.

   persistenceScoreplanned numbernullable

The soil-persistence term used in leak scoring, 0 sand to 1 clay.

A known unit-convention defect currently affects this value and it is withheld rather than published. The soil grid stores saturated hydraulic conductivity as log10(cm/hr); the code path that turns it into this term applies log10 a second time and compares the result against thresholds expressed in linear cm/hr. Measured on the current build: the term reads a median 0.44 where the corrected computation gives 0.00, and 76% of segments that should read "free-draining" instead read mid-range. See saturatedConductivity on AssetContext for the same root cause.

Being dimensionless does not make it safe: it is computed from the mis-scaled conductivity, so it inherits the defect. It is held back with its parent rather than shipped because it looks reasonable.

maps from satellite_leak_detections.soil_score

terrain object
   slopeDegreesMeanplanned numbernullable
   topographicWetnessIndexarea-level numbernullable

Higher values mark natural drainage lines. Used inside the detection chain to damp leak confidence where surface water collects for reasons that have nothing to do with a pipe. Computed as a raster; not currently attributed per segment.

landCover object
   classCodearea-level integernullable

National land-cover class code for the segment's surface. Held as a raster and consumed by the detection filters; not currently attributed per segment.

   imperviousPercentplanned numbernullable

Percent impervious surface. Not emitted, and worth stating why: the cached raster in the reference pipeline carries a legacy filename suggesting an impervious-percent product, but the pixel values are land-cover class codes. Publishing a percent from it would be a category error. A real impervious product has to be ingested before this field means anything.

moisture object

The segment's moisture regime, from the radar time series rather than from a single scene.

   excessCurrentpopulated numbernullable

Signed moisture excess against the segment's own climatology, cubic metres per cubic metre.

maps from satellite_leak_detections.mean_moisture_excess

   anomalyZCurrentpopulated numbernullable

maps from satellite_leak_detections.mean_anomaly_z

   changepointScorepopulated numbernullable

Archive change-point score — evidence that the moisture regime stepped rather than drifted.

maps from satellite_leak_detections.changepoint_score

groundMotion object

Present where the ground-motion lane is enabled for the utility. Derived from interferometric radar and validated against continuously operating reference stations. This block is the most completely populated on the resource: it covers the modelled segment population rather than only segments with a current detection.

   losVelocityMmPerYearpopulated numbernullable

Line-of-sight velocity. Negative is subsidence.

   axialStrainRatepopulated numbernullable

Strain along the pipe axis, microstrain per year.

   transverseStrainRatepopulated numbernullable

Strain across the pipe axis, microstrain per year.

   strainMagnitudepopulated numbernullable

Combined strain magnitude, microstrain per year. The variable trend statistics run against.

   strainRankClasspopulated stringnullable

The segment's strain rank within this utility's own population.

   trendDirectionpopulated increasing · stable · decreasing · insufficient_data · null

Non-parametric trend over the rolling-window strain series.

updatedAtrequired stringdate-time
updatedBy string

Pipeline build that produced this record, as infractiv-pipeline@sha256:<digest>.

Zone

FieldTypeDescription
zoneIdrequired string

Stable across recomputation while the same zone persists.

utilityId string
zoneTyperequired ZoneType

subsidence_hotspot — a regional basin where the ground is moving, clustered from the absolute-velocity field. strain_zone — a neighbourhood where multiple segments share elevated differential ground motion, which is what actually breaks a pipe: uniform settlement is far less damaging than differential settlement across a joint.

detectedForrequired stringdate

The processing period this zone set was computed for.

areaSqm numbernullable
centroid object
   longitude numbernullable
   latitude numbernullable
assetIds array<string>

Every modelled segment whose geometry falls inside the zone. This is what turns a polygon into an actionable set — a consuming system can raise one investigation covering all of them, or filter its own asset layer to exactly this list.

assetCount integernullable
classification stringnullable

For subsidence hot spots — whether the basin is actively moving, rebounding, or carrying signal without a confident trend.

motion object
   meanVelocityMmPerYear numbernullable
   maxVelocityMmPerYear numbernullable
   trendAcceleration numbernullable

Year-over-year change in mean velocity — a basin getting worse reads differently from a stable one.

strain object
   meanMagnitude numbernullable
   maxMagnitude numbernullable
   thresholdPercentile numbernullable

The strain percentile used to gate zone membership, published so the zone definition is auditable rather than opaque.

context object

Dominant environmental character inside the zone.

   soilTextureDominant stringnullable
   landCoverClassDominant integernullable
   slopeDegreesMean numbernullable
detectionCountInside integernullable

Satellite leak detections on segments inside this zone, where the module is licensed.

geometry Polygon | null

GeoJSON Polygon in EPSG:4326. Present in GeoJSON form.

updatedAtrequired stringdate-time
updatedBy string

Pipeline build that produced this zone, as infractiv-pipeline@sha256:<digest>.

Polygon

RFC 7946 §3.1.6 polygon. The first ring is the exterior ring; any further rings are interior. Rings are closed — the last position repeats the first — which is why the minimum ring length is four positions.

Winding is normalised on emit — exterior rings counterclockwise, interior rings clockwise, as §3.1.6 requires — because the analytical sources these polygons come from do not all produce that orientation and a strict validator will reject the wrong one. Degenerate rings (collinear, zero-area, or self-intersecting) are not served: they are a defect at the point they are computed, and passing one through would fail validation in ArcGIS or PostGIS on the consumer's side rather than ours.

FieldTypeDescription
typerequired "Polygon"
coordinatesrequired array<array<array<number>>>

ZoneFeature

RFC 7946 Feature. geometry is nullable, which is deliberate and conformant — §3.2 of the RFC permits a Feature with null geometry, and a consumer must handle it here rather than assume a polygon.

Which zone types carry geometry today: differential-strain zones do. Subsidence hot spots do not — the detector computes a polygon but the current writer persists only the centroid and area, so the feature arrives with geometry: null and its extent described by properties.areaSqm and properties.centroid. Persisting that polygon is tracked work; until it lands, a client that renders zone geometry should skip null-geometry features rather than fail on them, and the intersected_pipe_ids on properties remain the actionable payload in both cases.

FieldTypeDescription
typerequired "Feature"
id string
geometryrequired Polygon | null

The zone boundary, or null where the zone type does not yet persist one. See the schema description for which types carry geometry.

propertiesrequired Zone
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

ZoneFeatureCollection

FieldTypeDescription
typerequired "FeatureCollection"
featuresrequired array<ZoneFeature>
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

pagination object

LayerDescriptor

FieldTypeDescription
layerIdrequired string
namerequired string
description string
kindrequired raster · vector
format geotiff · cog · geojson · pmtiles · null
resolutionMetres numbernullable
coordinateReferenceSystem string

The CRS the layer is actually stored in, as an EPSG code — per layer, and not a constant.

Vector layers delivered as GeoJSON are EPSG:4326 because RFC 7946 requires it. Raster layers are a different matter: the ground-motion products are analysed and written on the utility's metric grid and are stored in that grid's projected CRS, because reprojecting a velocity or strain field for storage resamples a physical measurement. This field states what the file is, never what a consumer might prefer it to be; a GIS that needs a different projection should reproject on read.

cadence stringnullable

How often the layer is rebuilt — per processing cycle, monthly, or static.

coversPeriod object
   start stringdatenullable
   end stringdatenullable
href stringurinullable

Where to fetch it. Null while the layer is catalogued but not yet served.

statusrequired available · planned

planned means the underlying product exists but is not yet published in a GIS-consumable format. Declared rather than omitted so a partner can see the roadmap without asking.

provenance object

Where the layer came from and what may be done with it. Infractiv's derived products are Infractiv's to license to the utility. Layers derived from third-party sources carry those sources' terms, and this block names them so a customer's GIS administrator is not left to guess. Where a source's terms do not permit redistribution, Infractiv delivers its derived product rather than the source data.

A per-layer terms review is a precondition of publishing any layer. Infractiv's records today carry explicit terms for only a few sources — including one requiring attribution, one whose free tier is non-commercial, and one that is commercially restricted — and do not yet record terms for the remainder. No layer is published through this endpoint until its own terms are established and stated in this block.

   derivedFrom array<string>
   attribution stringnullable
   redistribution infractiv_derived · attribution_required · source_terms_apply · null

Scan

FieldTypeDescription
detectionDaterequired stringdate

Acquisition date.

processedAt stringdate-timenullable
statusrequired ScanStatus

skipped is a real platform outcome, not an error — it is what a cycle reports when no new acquisition was available. A consumer that treats it as a failure will page someone at every satellite outage; one that treats it as a clean scan will report "no leaks" for a period nobody looked at.

detectionCount integernullable
anomalyMethod stringnullable

Which detection method ran for this cycle.

maps from satellite_detection_history.anomaly_method

precipitationVetoSource stringnullable

Source of the precipitation veto applied to this cycle. When unavailable, rain-driven false positives are less strongly suppressed, and a consuming system may reasonably raise its dispatch threshold for that cycle.

maps from satellite_detection_history.precip_veto_source

configHash stringnullable

Hash of the detection-shaping configuration for this cycle. Changes here explain changes in output.

ChangeFeed

FieldTypeDescription
changesrequired array<ChangeEntry>
checkpointrequired string

Opaque, monotonic. Supply on the next call. Persist it.

hasMore boolean

True when more changes are immediately available; call again without waiting for the next interval.

retentionDays integernullable

How long a checkpoint stays valid, in days. Read this rather than assuming: it is what determines how long a connector can be down before it must re-backfill, and it is a deployment property. Null only where a deployment has not declared one.

ChangeEntry

FieldTypeDescription
operationrequired upsert · delete

Maps directly onto the upsert and delete operations of an incremental-reader framework. A delete on a detection means the finding has been retracted or superseded and any open work raised from it should be closed, not left orphaned.

resourcerequired asset · riskScore · detection · workRecommendation · observation
idrequired string
occurredAtrequired stringdate-time
reason stringnullable

Why the change occurred, when a meaningful cause exists.

data Asset | RiskScore | Detection | WorkRecommendation | Observation

The full current representation on upsert. Absent on delete.

Which schema appears is determined by resource, one-for-one: assetAsset, riskScoreRiskScore, detectionDetection, workRecommendationWorkRecommendation, observationObservation. A generated client should switch on resource rather than attempting structural inference. The union is enumerated below so that a code generator produces a typed member instead of a free-form object.

SubscriptionCreate

FieldTypeDescription
urlrequired stringuri

HTTPS endpoint. Plain HTTP is rejected.

Destination restrictions, stated here because the contract is what the delivery worker will be built from and an authenticated caller supplying an arbitrary URL is a confused-deputy path into the platform's own network. The destination must be a public unicast address. Loopback, RFC 1918 private ranges, link-local (169.254.0.0/16, which includes the cloud instance-metadata address), unique-local IPv6, and Infractiv's own address ranges are rejected. The hostname is resolved at registration and re-checked at delivery time, so a name that later resolves into a blocked range stops being delivered to rather than being trusted on its registration-time answer. Redirects are not followed: a 3xx from the endpoint is a failed delivery, not a hop.

eventsrequired array<SubscriptionEvent>
utilityIds array<string>

Restrict deliveries to these utilities. Defaults to all granted.

description stringnullable

Subscription

FieldTypeDescription
urlrequired stringuri

HTTPS endpoint. Plain HTTP is rejected.

Destination restrictions, stated here because the contract is what the delivery worker will be built from and an authenticated caller supplying an arbitrary URL is a confused-deputy path into the platform's own network. The destination must be a public unicast address. Loopback, RFC 1918 private ranges, link-local (169.254.0.0/16, which includes the cloud instance-metadata address), unique-local IPv6, and Infractiv's own address ranges are rejected. The hostname is resolved at registration and re-checked at delivery time, so a name that later resolves into a blocked range stops being delivered to rather than being trusted on its registration-time answer. Redirects are not followed: a 3xx from the endpoint is a failed delivery, not a hop.

eventsrequired array<SubscriptionEvent>
utilityIds array<string>

Restrict deliveries to these utilities. Defaults to all granted.

description stringnullable
subscriptionId string
status active · degraded · disabled
createdAt stringdate-time
lastDeliveryAt stringdate-timenullable
consecutiveFailures integer

SubscriptionCreated

FieldTypeDescription
urlrequired stringuri

HTTPS endpoint. Plain HTTP is rejected.

Destination restrictions, stated here because the contract is what the delivery worker will be built from and an authenticated caller supplying an arbitrary URL is a confused-deputy path into the platform's own network. The destination must be a public unicast address. Loopback, RFC 1918 private ranges, link-local (169.254.0.0/16, which includes the cloud instance-metadata address), unique-local IPv6, and Infractiv's own address ranges are rejected. The hostname is resolved at registration and re-checked at delivery time, so a name that later resolves into a blocked range stops being delivered to rather than being trusted on its registration-time answer. Redirects are not followed: a 3xx from the endpoint is a failed delivery, not a hop.

eventsrequired array<SubscriptionEvent>
utilityIds array<string>

Restrict deliveries to these utilities. Defaults to all granted.

description stringnullable
subscriptionId string
status active · degraded · disabled
createdAt stringdate-time
lastDeliveryAt stringdate-timenullable
consecutiveFailures integer
signingSecret string

Returned exactly once. Store it; it cannot be retrieved again.

Rotating it in v1 is register-then-retire, not an in-place operation: create a second subscription for the same URL and event set, confirm your endpoint verifies deliveries signed with the new secret, then delete the first. Both are active in the overlap, so your endpoint receives each event twice for that window and deduplicates on deliveryId exactly as it already does for retries — no delivery is lost. A dedicated rotate operation with a server-side overlap window is a named v1.1 addition; it is described this way rather than promised as an endpoint that does not exist.

WebhookEnvelope

FieldTypeDescription
deliveryIdrequired stringuuid

Deduplicate on this.

eventrequired SubscriptionEvent

Which event this delivery carries. It determines the shape of data below.

occurredAtrequired stringdate-time
utilityIdrequired string
datarequired array<Detection> | array<RetractedRef> | array<RiskScore> | array<WorkRecommendation> | Scan

The event payload, determined by event. Every subscribable event has a payload contract — none is left to be discovered at runtime:

  • detection.published → array of Detection
  • detection.retracted → array of RetractedRef (the identifiers only; the detections themselves are gone, which is the point)
  • riskScore.updated → array of RiskScore
  • workRecommendation.updated → array of WorkRecommendation
  • scan.completed → single Scan
  • scan.failed → single Scan (with a non-ok status and the reason on the record)

Payloads are capped; when a cycle exceeds the cap the envelope carries a cursor into the corresponding collection rather than truncating silently.

cursor stringnullable

Present when the payload was capped. Continue via /detections?cursor=.

SubscriptionEvent

The closed set of subscribable events. Named rather than written inline because the value a caller subscribes with and the value that arrives on the delivery envelope must be the same vocabulary — while they were two separate declarations, one was an enum and the other a bare string, so a generated client saw String for the field that routes the whole payload.

detection.published detection.retracted riskScore.updated workRecommendation.updated scan.completed scan.failed

RetractedRef

A retraction carries identifiers, not a representation — the detection it names no longer exists. A consumer uses this to close work raised from the finding rather than to update a record.

FieldTypeDescription
detectionIdrequired string
assetIdrequired string
retractedAtrequired stringdate-time
reason stringnullable

ExternalReference

A pointer back to the record in the calling system. Recorded so that any Infractiv finding can be traced to the work order it produced, and any work order to the finding that caused it — in both directions, by either party, without a spreadsheet.

FieldTypeDescription
systemrequired string

Identifier of the calling system.

entity stringnullable

Entity type within that system.

idrequired string
url stringurinullable

Point

FieldTypeDescription
typerequired "Point"
coordinatesrequired array<number>

[longitude, latitude] in EPSG:4326.

LineString

FieldTypeDescription
typerequired "LineString"
coordinatesrequired array<array<number>>

BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

AssetFeature

RFC 7946 Feature wrapping the Asset resource.

properties is the resource verbatim, which means the resource's own geometry member is repeated inside it. feature.geometry is the authoritative one; properties.geometry is a duplicate that GDAL and ArcGIS will materialise as a stray attribute column on conversion. Drop it on read. The same shape applies to the detection, zone and recommendation features.

FieldTypeDescription
typerequired "Feature"
id string
geometryrequired LineString | null

Null in v1 — see the collection description. Join on assetId.

propertiesrequired Asset
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

AssetFeatureCollection

FieldTypeDescription
typerequired "FeatureCollection"
featuresrequired array<AssetFeature>
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

pagination object

DetectionFeature

RFC 7946 Feature wrapping the Detection resource. As with AssetFeature, properties carries the resource's own geometry member as a duplicate of feature.geometry; read the outer one.

FieldTypeDescription
typerequired "Feature"
id string
geometryrequired Point | null

Point at the anomaly centroid, or null. Detection.centroid is itself nullable, so a detection with no centroid is a modelled state and must be representable here; RFC 7946 §3.2 permits it.

propertiesrequired Detection
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

DetectionFeatureCollection

FieldTypeDescription
typerequired "FeatureCollection"
featuresrequired array<DetectionFeature>
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

pagination object

RecommendationFeature

RFC 7946 Feature carrying a work recommendation.

Geometry here is the segment centreline, and it is the one place this contract emits it — /assets deliberately returns null geometry so that the utility's GIS stays the single authoritative copy. The distinction is that a capital programme is an Infractiv product with no counterpart feature class in the utility's GIS, so drawing it creates no second copy of anything: a planner needs the programme to be mappable, and there is no authoritative version of "the 2031 replacement list" to drift from. A consumer that wants to avoid carrying centrelines at all can request application/json and join on assetId, exactly as it does for assets.

FieldTypeDescription
typerequired "Feature"
id string
geometryrequired LineString | null

Segment centreline, or null where the source geometry is absent.

propertiesrequired WorkRecommendation
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

RecommendationFeatureCollection

FieldTypeDescription
typerequired "FeatureCollection"
featuresrequired array<RecommendationFeature>
bbox BoundingBox

RFC 7946 §5 bounding box, [west, south, east, north] in EPSG:4326. Optional on any Feature or FeatureCollection, and present on collections so a desktop GIS can compute an extent without reading every feature.

pagination object