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
| Endpoint | What you get |
|---|---|
GET /metrics/deforestation | All processing runs (history) |
GET /metrics/deforestation/latest | One metric per location (most recent run) |
GET /locations/:location_id/deforestation | Latest 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:
| Type | Description |
|---|---|
facility | Processing or storage facility (mill, factory) |
shed | Supply shed (sourcing area around a facility) |
plot | Individual 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):
| Value | Adds | Scope |
|---|---|---|
assessment | relationships.assessment — per-assessment-group payloads | facilities, sheds, plots |
deforestation | relationships.deforestation — cached EUDR compliance roll-up | facilities & 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:
| Status | Matches |
|---|---|
non_compliant | At least one catchment plot is non-compliant |
pending | No non-compliant plot, and at least one awaits adjudication review |
not_assessed | Not a verdict: some plot has no deforestation result yet (not screened, still running, or failed), or there is no cached roll-up |
compliant | Every plot that is not excluded has a compliant result, and at least one does |
excluded | The 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
| Type | Example | Description |
|---|---|---|
facilities | /metrics/facilities/deforestation | Aggregated metrics with plot_count, noncompliance_area_perc |
sheds | /metrics/sheds/deforestation | Mid-level supply shed metrics |
plots | /metrics/plots/deforestation | Per-plot detail with uncertainty_rrmse |
▶Metric categories
| Category | Description |
|---|---|
deforestation | Forest loss and compliance |
biodiversity | Canopy diversity index |
emissions | Land use change CO2e |
water_stress | Water 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
location_idThe 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.
| Endpoint | facility location_id | shed location_id | plot location_id |
|---|---|---|---|
/metrics/facilities/... | direct match | (no match) | (no match) |
/metrics/sheds/... | sheds under the facility | direct match | (no match) |
/metrics/plots/... | plots under the facility | plots under the shed | direct 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:
| Field | Present on | Value |
|---|---|---|
parent_shed_location_id | plots | location_id of the plot's parent shed |
parent_facility_location_id | plots, sheds | location_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
tile_idsEach 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
links.dashboardMetric 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
dashboardand useasset_task_idwith/v1/artifactsas 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 header | Format |
|---|---|
application/json (default) | Paginated JSON envelope |
application/geo+json | GeoJSON FeatureCollection |
application/x-ndjson | Streaming NDJSON |
application/x-ndgeo+json | Streaming NDGeoJSON |
Geometry is opt-in on JSON formats.
application/jsonandapplication/x-ndjsonomit thegeoattribute by default — pass?include_geometries=trueto 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.