Locations

Locations & Metrics

The API uses a single, stable identifier for each physical location, simplifying how you interact with locations and their metrics.

Key concepts

Location ID

Every physical site (mill, farm, sourcing area) is assigned a stable location_id when it is first ingested. This ID persists across processing runs — the same mill always resolves to the same location_id, even when new facilities, sheds, or plots are created underneath it.

GET /locations
→ [{ "type": "location", "id": "81895eee-...", "attributes": { "name": "Test Mill", ... } }]

Process ID

Each processing run produces a process_id (maps to a shed internally). When you request metrics for a location, the process_id tells you which run produced the result.

GET /metrics/deforestation
→ [{ "attributes": { "location_id": "81895eee-...", "process_id": "c359cbd3-...", ... } }]

Latest vs All Runs

EndpointWhat you get
GET /metrics/deforestationAll processing runs (history)
GET /metrics/deforestation/latestOne metric per location (most recent run)
GET /locations/:location_id/deforestationLatest metric(s) for a specific location

Use filter[process_id]=latest as an alternative to the /latest path.

Location types

Each location has a location_type attribute indicating the underlying entity:

TypeDescription
facilityProcessing or storage facility (mill, factory)
shedSupply shed (sourcing area around a facility)
plotIndividual plot or farm

Use convenience routes to filter by type:

GET /locations/facilities
GET /locations/sheds
GET /locations/plots
GET /locations/commodity/palm

Opt-in includes

Location list and single-location responses can attach related payloads under a relationships object via the include query parameter (comma-separate to combine, e.g. ?include=assessment,deforestation):

ValueAddsScope
assessmentrelationships.assessment — per-assessment-group payloadsfacilities, sheds, plots
deforestationrelationships.deforestation — cached EUDR compliance roll-upfacilities & sheds only
GET /locations/facilities?include=deforestation
→ { "type": "location", "id": "...", "attributes": { ... },
    "relationships": {
      "deforestation": {
        "total_plots": 10, "compliant_plots": 7, "non_compliant_plots": 2,
        "unknown_plots": 1, "total_area_ha": 100.5, "compliant_area_ha": 70.25
      }
    } }

The deforestation block is the same roll-up returned on GET /ingestions/assets?include=deforestation, but read from a per-location cache and reflecting the latest figure across all tasks rather than a single asset-task. Facilities and sheds only for now — plot rows omit the block, and a location with no cached roll-up yet omits it too (rather than emitting all-zeros). Plot support will follow.

Filtering by compliance

Narrow a location list to a single EUDR status with filter[compliance]=eq:<status> (same derivation as the include=deforestation roll-up). The statuses are checked in this order, and the first that applies wins:

StatusMatches
non_compliantAt least one catchment plot is non-compliant
pendingNo non-compliant plot, and at least one awaits adjudication review
not_assessedNot a verdict: some plot has no deforestation result yet (not screened, still running, or failed), or there is no cached roll-up
compliantEvery plot that is not excluded has a compliant result, and at least one does
excludedThe operator excluded every plot from the filing
# Non-compliant facilities, with the roll-up attached
GET /locations/facilities?filter[compliance]=eq:non_compliant&include=deforestation

# Compliant sheds
GET /locations/sheds?filter[compliance]=eq:compliant

Facilities and sheds read the cached roll-up. A plot location reads every plot of its latest upload (a multi-part plot has several), each with its latest deforestation result and operator decision. The five statuses partition the full location set exactly. Plot statuses are evaluated per request, so on /locations/plots add filter[assets.task_id]=eq:<id> to evaluate one ingestion's plots rather than the whole organisation's.

Filtering and sorting by headline risk level

The typed routes also accept the two headline risk levels promoted onto the assessment relationship (legality_risk_level — Article 9 adverse-media / legality; article10_risk_level — Article 10 country-risk) as filters and sort keys:

# High legality-risk facilities, with the assessment block attached
GET /locations/facilities?filter[legality_risk_level]=eq:high&include=assessment

# Facilities ranked worst-first by Article 10 country-risk
GET /locations/facilities?sort=-article10_risk_level

Values are low / medium / high (eq / in). The filter matches the promoted level the row displays under include=assessment; locations with no visible art9/art10 assessment never match, and sort to the tail in both directions. See Query Parameters for details.

Metric endpoints

Metrics are namespaced by location type because each level returns different fields (facility metrics include aggregated summaries, plot metrics have per-plot detail).

GET /metrics/{location_type}/{metric}
GET /metrics/{location_type}/{metric}/latest
GET /metrics/{location_type}/{metric}/{commodity}
GET /metrics/{location_type}/{metric}/{commodity}/latest

Location types

TypeExampleDescription
facilities/metrics/facilities/deforestationAggregated metrics with plot_count, noncompliance_area_perc
sheds/metrics/sheds/deforestationMid-level supply shed metrics
plots/metrics/plots/deforestationPer-plot detail with uncertainty_rrmse

Metric categories

CategoryDescription
deforestationForest loss and compliance
biodiversityCanopy diversity index
emissionsLand use change CO2e
water_stressWater availability index

Location-scoped metrics

For a single location, use the location-scoped endpoint which auto-detects the correct level:

GET /locations/:location_id/deforestation
GET /locations/:location_id/biodiversity
GET /locations/:location_id/emissions
GET /locations/:location_id/water_stress

Filtering metrics by location_id

The filter[location_id] query parameter (and the :location_id path param on singular-location metric routes) is type-agnostic and drills down: pass any location_id — facility, shed, or plot — and the API returns metrics for that location and any descendants matching the route's level.

Endpointfacility location_idshed location_idplot location_id
/metrics/facilities/...direct match(no match)(no match)
/metrics/sheds/...sheds under the facilitydirect match(no match)
/metrics/plots/...plots under the facilityplots under the sheddirect match

The same drill-down applies to the singular-ID routes (e.g. GET /metrics/plots/<facility_location_id>/deforestation returns plot metrics for every plot under that facility).

# All plots' deforestation under a given facility
GET /metrics/plots/deforestation?filter[location_id]=eq:<facility_location_id>

# All sheds' deforestation under a given facility
GET /metrics/sheds/deforestation?filter[location_id]=eq:<facility_location_id>

# Latest deforestation for plots under a shed (singular-ID route)
GET /metrics/plots/<shed_location_id>/deforestation

Drill-down is one-directional — supplying a plot location_id to /metrics/facilities/... does not return the parent facility's metrics. To walk up the hierarchy, use the parent_*_location_id fields below (no extra request needed) or /locations/:id.

Relating plots, sheds & facilities

Every location and metric resource carries its parents' location_ids, so you can group a mixed result set into its hierarchy client-side, from the response bodies alone — no follow-up request:

FieldPresent onValue
parent_shed_location_idplotslocation_id of the plot's parent shed
parent_facility_location_idplots, shedslocation_id of the parent facility

These are plain location_id values (null where not applicable — a facility has neither, a shed has only parent_facility_location_id). Match them to a parent's own location_id:

// GET /metrics/plots/deforestation
{ "attributes": {
    "location_id": "<plot location_id>",
    "parent_shed_location_id": "<shed location_id>",
    "parent_facility_location_id": "<facility location_id>"
} }
// Group plot metrics under a facility you already fetched
const plotsForFacility = plotMetrics.filter(
  (p) => p.attributes.parent_facility_location_id === facility.attributes.location_id,
);

Use whichever direction fits: drill-down (filter[location_id]) when you want the server to return a facility's descendants; parent_*_location_id when you've already pulled a set of plot/shed metrics and want to roll them up locally.

Joining to map tiles — tile_ids

Each location and metric resource also exposes a tile_ids object with the stable identifier of the underlying entity. Only the field matching the resource's own type is populated:

// a plot resource / plot metric
"tile_ids": { "plot_id": "<id>", "supply_shed_id": null, "facility_id": null }

// a shed resource / shed metric
"tile_ids": { "plot_id": null, "supply_shed_id": "<id>", "facility_id": null }

// a facility resource / facility metric
"tile_ids": { "plot_id": null, "supply_shed_id": null, "facility_id": "<id>" }

These IDs match the feature properties on Epoch's plot vector tiles (PMTiles plot_id / supply_shed_id / facility_id). If you render that map layer, join a tile feature straight to its API resource:

// clicked map feature → its plot metric
feature.properties.plot_id === plotMetric.attributes.tile_ids.plot_id;

For callers that don't use the tiles, tile_ids is simply a stable per-entity identifier you can use as a join/dedup key across responses.

Reaching the collection a metric row came from

Every metric resource names its collection — the ingestion task whose upload produced the row:

"attributes": {
  "asset_task_id": "<task id>",          // the collection
  "parent_supply_shed_id": "<shed id>",  // plot rows: the parent shed's shed.id
  // ...
}

asset_task_id echoes filter[assets.task_id] when you supplied it, otherwise it is the most recent ingestion task for that location. It is null on the /history and streaming variants, and on a parent-id drill-down, where resolving it would mean an unbounded scan.

Use it to fetch the collection's data export:

GET /v1/artifacts?filter[assets.task_id]=eq:<asset_task_id>&filter[status]=eq:succeeded

Results are already newest-first. If none exists, POST /v1/artifacts with that same task scope filter generates one; pass an Idempotency-Key so concurrent callers share a single job. See Artifacts.

parent_supply_shed_id is the parent shed's shed.id, not a location_id — it is the value that joins to supply_shed_id on plot vector-tile features and to sheds.shed_id inside an artifact export. Use parent_shed_location_id when you instead want to join to the parent's own shed metric.

links.dashboard

Metric rows and ingestion-task rows also carry a dashboard link — a deep link into the Epoch platform app for that collection, focused on the row's shed (facility rows: its facility):

"links": {
  "self": "/metrics/deforestation",
  "dashboard": "https://platform.epoch.blue/artifact?task=<task>&shed=<shed>"
}

The link is a plain string built from ids; serving it materialises nothing. The artifact is generated when the link is first opened, so following it may show a brief preparing state.

This link needs a platform app session. It is not a shareable URL, and it is not usable from an API integration. If you are consuming the API programmatically, ignore dashboard and use asset_task_id with /v1/artifacts as shown above. The link is absent when the collection could not be resolved.

Organisation isolation

Locations are scoped to your organisation. Two organisations can monitor the same physical site independently — each sees only their own data. Parent organisations can see their children's data; use x-acting-as header or filter[organization_id] to narrow the scope.

Content negotiation

All endpoints support four response formats via the Accept header:

Accept headerFormat
application/json (default)Paginated JSON envelope
application/geo+jsonGeoJSON FeatureCollection
application/x-ndjsonStreaming NDJSON
application/x-ndgeo+jsonStreaming NDGeoJSON

Geometry is opt-in on JSON formats. application/json and application/x-ndjson omit the geo attribute by default — pass ?include_geometries=true to include it. The GeoJSON formats (application/geo+json, application/x-ndgeo+json) always carry geometry, so the flag isn't needed there. See Query Parameters → Geometry.