List All Ingestion Assets
List all assets across all ingestion tasks.
▶Use Cases
- Get a global view of all uploaded files.
- Monitor asset processing across multiple ingestions.
▶Filters
Use filter[field]=condition:value to narrow results. See Query Parameters for full syntax.
filter[assets.task_id]=eq:<uuid>— scope to a specific ingestion taskfilter[tags]=eq:verified— filter by tag namefilter[source_file]=eq:locations.csv— filter by original file namefilter[normalised_data.id]=eq:<uuid>— filter by normalised location IDfilter[normalised_data.commodity_type]=eq:timber— filter by commodityfilter[assets.data.CITY]=eq:London— filter by source data fields using dot-notationfilter[assets.data.ASSET_NAME]=contains:Aberdeen— substring match on source datafilter[compliance]=eq:non_compliant— filter by per-location EUDR deforestation status (non_compliant/pending/compliant/excluded/not_assessed); see Deforestation roll-up below
▶Deforestation roll-up (include=deforestation)
include=deforestation)Add include=deforestation (or ?deforestation=<view>) to attach a per-location EUDR deforestation roll-up under attributes.deforestation on each item — the compliance summary for that item's location_id:
GET /ingestions/assets?include=deforestation
"deforestation": {
"total_plots": 12, "compliant_plots": 9, "non_compliant_plots": 2,
"unknown_plots": 1, "total_area_ha": 84.3, "compliant_area_ha": 61.0
}
A facility/shed aggregates its whole catchment; a plot location rolls up the parts of its latest upload. Facility/shed roll-ups are read from a cache written at task completion (no live catchment scan); plot verdicts are computed live.
Pick the variant with ?deforestation= (also enables the block):
ccdc— CCDC (continuous change detection) metrics onlyeudr— EUDR monitoring-window metrics onlytrue/user(or omit) — org default: prioritised fallback (prefer a variant with an assessed metric, then CCDC, then EUDR window)
Filter the list to a status with filter[compliance]=eq:non_compliant|pending|compliant|excluded|not_assessed — same vocabulary as the task-level filter[compliance] on GET /ingestions, evaluated per location against the selected variant. meta.total reflects the filter. See Query Parameters → EUDR compliance verdict.
▶Supersession surface — what a row replaced
Upload the same facility three times and this endpoint returns it three times, because each upload is its own record. Every row says where it sits in that history.
On a per-record row (facility, shed, or an unfolded plot):
| Field | Meaning |
|---|---|
attributes.superseded_count | How many earlier records this one replaced. 0 means it is the location's only record. |
attributes.asset_task_ids | Every upload task in the group: this row's own task first, then the tasks it replaced, newest first. |
links.superseded | The whole group — this record plus the ones it replaced. |
"attributes": {
"asset_task_id": "<this row's task>",
"superseded_count": 2,
"asset_task_ids": ["<this task>", "<the one before>", "<the first>"]
},
"links": {
"superseded": "/ingestions/assets?filter[location_id]=eq:<location_id>&filter[organization_id]=eq:<org_id>"
}
Following links.superseded returns superseded_count + 1 rows — the record
and its predecessors. It carries both filters deliberately:
superseded_count counts one (organization, location) pair, so without the org
a parent whose two children hold the same location would get both children's
records back and the listing would disagree with the count.
superseded_count counts the whole history, not the one generation
directly before it. Re-election points every dead record in a group at the
live one, so a location uploaded three times gives the survivor 2, not 1.
The count also ignores the request's other filters, for the same reason the
link is org-scoped: a filter[status] that excludes the predecessors does not
change it, so the number and the link always describe the same group.
A row with no resolved location has no group, so it gets no links.superseded
(its superseded_count is 0).
▶On a folded plot row
A folded row (?rollup=true) spans many locations, so one superseded_count
could not describe it. It reports the split instead, and omits the per-record
fields entirely — an absent field means "does not apply to this row kind":
| Field | Meaning |
|---|---|
attributes.live_plot_count | Plots of this task that are still the current record for their location. |
attributes.superseded_plot_count | Plots a later upload of the same location has replaced. |
Both count located plots only. An unlocated row has no location to be
current for, so it belongs to neither. A folded row gets no links.superseded
either — it takes its location_id from its representative first plot, and a
link built from that would return one plot's group while the row folds a whole
task.
Under ?rollup=unit a folded row reports live_plot_count as its unit's
own plot total, and omits superseded_plot_count: attributing a superseded
plot to a unit needs the ownership ladder that only runs at task completion.
▶row_id and links.history — the record's own handle
row_id and links.history — the record's own handlesuperseded_count and links.superseded describe the group as a count and a
filter query. To read the history itself, every per-record row carries:
| Field | Meaning |
|---|---|
attributes.row_id | This record's own id — one upload's view of one location, which is the grain supersession works at. |
links.history | /ingestions/assets/latest/{row_id}/history — the same records ordered by supersession, each flagged is_live, each with its monitoring_end. |
Prefer links.history when the question is "what happened to this location":
it is ordered, it names the live record, and it carries the monitoring window
that decided which record is current. links.superseded remains the way to
apply the endpoint's filters and formats to the group.
row_id is not the resource id. A row's id is its asset_entity id,
shared wherever the uploaded attributes are identical — including across
organizations. A parent organization reading two children's rows for one
location sees the same id twice, so id cannot address a record and row_id
can. Passing a superseded record's row_id to the history endpoint returns the
same history, which is what makes a stale id from an export still useful.
A folded plot row is not one record, so it carries neither field.
See Get Record History for the full reference.
▶include=superseded — the records themselves
include=superseded — the records themselvessuperseded_count says how many; ?include=superseded says which.
"relationships": {
"superseded": {
"data": [
{ "id": "<row_id>", "asset_task_id": "…", "source_file": "may.csv",
"status": "completed", "created_at": "…",
"monitoring_end": "2026-05-31T00:00:00.000Z",
"record_superseded_at": "2026-06-01T10:00:00.000Z" }
],
"meta": { "total": 12, "truncated": true }
}
}
Newest-replaced first — record_superseded_at descending, which is why each
element carries it. Each id is that record's row_id, so you can follow any
of them to Get Record History.
data.length matches attributes.superseded_count up to the cap of 10, and
meta.total stays exact past it. The two always agree because both read the
same supersession pointer.
Past the cap, links.history returns the whole set paginated. Note it resolves
through the location group rather than the pointer, so it is the authoritative
set and can return more than meta.total if a deletion left a pointer
stale.
A folded plot row carries none of this — it spans many locations, so it has no one predecessor set.
▶Metrics (include=metrics)
include=metrics)Attach the latest metric per category for each row's location, batched across the page:
GET /ingestions/assets?include=metrics # all four categories
GET /ingestions/assets?include=metrics.deforestation # one
GET /ingestions/assets?include=metrics.emissions,metrics.water_stress
| Include | Categories loaded |
|---|---|
metrics | deforestation, biodiversity, emissions, water_stress |
metrics.deforestation | deforestation |
metrics.biodiversity | biodiversity |
metrics.emissions | emissions |
metrics.water_stress | water_stress |
The dotted values union, and bare metrics means all four — so mixing them
yields all four. An unknown category is a 400 naming the allowed values.
Each element is the same metric resource the /metrics/... routes return.
links.metrics_<category> points at that route for each requested category.
metrics.deforestationis notattributes.deforestation. The relationship is the measured deforestation metric for the location. The attribute is the EUDR compliance roll-up — plot counts and areas — from?include=deforestation. Reading one and reporting the other is the mistake this note exists to prevent.
▶Ask for the category you need
Selection is a cost control, not a convenience. One statement runs per category per location type on the page:
| Request | Statements on a facility+shed page | On a page that also has plot rows |
|---|---|---|
metrics | 8 | 12 |
metrics.deforestation | 2 | 3 |
GET /ingestions/assets/latest always folds plot tasks, so it is the
two-type case. The unfolded list and the per-task results carry per-record plot
rows, so they are the three-type case.
Body size scales the same way: a metric resource carries every field of its category plus its tile ids, collection id, parents, commodity and links. Four of those on a 500-row page is a large response. A page that renders one figure should ask for one category.
▶What is absent, and why
- A category with no run for a location is missing from the object.
- A location with no run at all omits the relationship entirely — rather than an empty object, which would read as "measured, nothing found".
- A folded plot row carries neither the relationship nor the
metrics_<category>links: it spans many locations, so it has no single metric row.
▶DDS records (include=audit_trails)
include=audit_trails)Add include=audit_trails to attach every EUDR audit trail (DDS record)
covering a row's location, with each one's status:
"audit_trails": {
"data": [
{ "id": "…", "name": "Coffee — BR",
"lifecycle_status": "accepted",
"reference_number": "25NLxxxxxxxxx",
"created_at": "2026-05-01T00:00:00.000Z",
"pinned_from_asset_task_id": "<the upload the filing pinned>",
"pinned_record_is_live": false }
],
"meta": { "total": 3, "truncated": false }
}
▶pinned_record_is_live — was the filing made against current data?
pinned_record_is_live — was the filing made against current data?This is the field the include exists for, and no other field in the API carries it.
A DDS pins the plots it declares at the moment its version is cut, from a specific upload. If that location is uploaded again, the record the filing was pinned from is superseded — the filing still exists, and it may no longer describe what you now hold.
| Value | Meaning |
|---|---|
true | The filing was pinned from the location's current record. |
false | A later upload replaced the record it was pinned from. The filing may not describe current data. |
null | The pinning upload has no record for this location, so there is nothing whose currency to read. |
false is deliberately conservative: where one version pinned plots from
several uploads, a single replaced ancestor is enough. A false reassurance on
an audit field costs more than a false alarm.
▶lifecycle_status
lifecycle_statusThe same vocabulary and the same derivation
GET /regulations/eudr/audit-trail uses — draft,
awaiting_submission, submitting, submitted, accepted, rejected,
withdrawn, timed_out, stale — so the two endpoints cannot disagree about
a record. stale is in the vocabulary but unreachable today: the staleness
input is hardcoded false everywhere until GAP-10 ships.
reference_number is the primary TRACES reference (statement 0) of the
record's current version, and is null until the record is filed.
▶Resolved over the location's whole upload history
The set is resolved across the row's supersession lineage: its own upload plus
every upload it replaced. That is what makes a filing pinned from a superseded
record visible at all — the scalar audit_trail_id (below) would report null
for it.
▶The cap
At most 10 records per row, with meta.total staying exact so you can see
what is missing. For the whole set, unbounded:
GET /regulations/eudr/audit-trail?filter[location_id]=eq:<location_id>
▶Relationship to audit_trail_id
audit_trail_idinclude=audit_trail (singular) still returns a scalar audit_trail_id — one
representative record, matched on the row's own upload task. It is
unchanged, for drop-in parity, and this include supersedes it: the plural is
the set, carries status, and sees filings pinned from superseded records.
▶It is opt-in, and it should stay that way
Resolving it drills the location's catchment plots — a facility-heavy cost — and then joins the DDS version, its TRACES statements and its audit log on top. It is strictly more expensive than the scalar. Request it for a page you are showing DDS status on; do not add it to a bulk read.
A folded plot row carries no audit_trails at all: it spans many locations, so
a set resolved from one of them — and pinned_record_is_live above all — would
read as a claim about the whole upload.
▶Identifier surface — the ids a row resolves to
A row names every id needed to follow it, so grouping and drilling take no second request.
▶The row's own entity
Inside normalised_data (so ?include=normalised_data is required), exactly
one of three fields carries the V1 entity the row resolved to. Which one
matches location_type; the other two are null:
location_type | Populated field | Value |
|---|---|---|
facility | facility_id | facility.id — a string, not a UUID |
shed | shed_id | shed.id |
plot | plot_id | plot.id |
All three are null on a folded plot row (?rollup=true), which resolves no
single entity, and on a row with no resolved location.
▶The parents
Two top-level attributes, so grouping rows under a parent needs no
include:
| Field | Set for | Resolves at |
|---|---|---|
parent_shed_location_id | plots | /locations/sheds/{id} |
parent_facility_location_id | plots and sheds | /locations/facilities/{id} |
Both are location_id values (staged locations), so they match the
location_id of the parent's own row on this endpoint and on /locations —
that is what lets a client group children under a parent. links.parent_shed
and links.parent_facility carry the same targets as URLs, and each appears
only when its id is non-null.
"attributes": {
"location_id": "<this plot's location>",
"location_type": "plot",
"parent_shed_location_id": "<the shed's location>",
"parent_facility_location_id": "<the facility's location>"
},
"links": {
"parent_shed": "/locations/sheds/<the shed's location>",
"parent_facility": "/locations/facilities/<the facility's location>"
}
A facility row reports both as null — it is the top of the hierarchy. A
folded plot row omits them entirely, because it spans many locations and
has no single parent. (The entity ids above are null rather than absent on a
folded row: they are long-standing non-optional fields of normalised_data.)
▶Every id is resolved within the row's own organization
A location is deduplicated across organizations, while the entities derived
from it are per-organization. So a location that two of your child orgs both
uploaded has a facility_id per org and a parent per org, and each row names
its own — never a sibling's. A parent_shed_location_id you receive is
therefore always a shed you can read.
▶Collection hashes (collection_ids)
collection_ids)Each row carries attributes.collection_ids — the supply-shed / plot
collection hashes (bq_hash, the PMTiles / batch keys) for that row's
resolved location:
- facility → its catchment shed hash(es)
- shed → the shed's own hash
- plot → the plot's collection hash
Under ?rollup=true a folded plot row lists the distinct hashes across the
whole task. The field is omitted where the sco2 collection tables aren't
available in the environment, and under ?rollup=unit, where it is not
defined at the supplier-unit grain.
▶Rollup (?rollup=true / ?rollup=task) — fold a plot task's rows into one
?rollup=true / ?rollup=task) — fold a plot task's rows into one?rollup=task is the explicit spelling of ?rollup=true. The two return
identical responses. For one row per supplier unit, see
Rollup by supplier unit below.
A plot upload resolves to many asset rows (one per plot). Pass ?rollup=true
to fold every plot task (location_type = 'plot') into a single row per
task; facility/shed rows stay individual.
GET /ingestions/assets?rollup=true&include=deforestation
On a rolled row:
id= the plotasset_task_id.asset_datais folded across the task's plots: each key is a scalar when identical on every plot, else an array of the distinct values. Arrays are capped; keys whose value set was truncated (or"*"when the per-task entity scan itself was capped) are listed under a reserved_truncatedentry.deforestation(when included) is the asset_task (task-grain) summary — the task's roll-up — not a per-plot verdict. Facility/shed rows keep their per-location roll-up. Since EPO-2331 that roll-up counts only the plots a current record owns, so a plot whose record a later upload superseded is excluded. Note this row set is not deduplicated (that isGET /ingestions/assets/latest), so on this endpoint a superseded row's block can read zero while the row itself is still listed.normalised_data,assessments, andlocation_id/location_typeare taken from the task's first (representative) plot (the earliest resolved plot). So the fold shows one plot's location detail + assessments whileasset_dataspans all plots. In geo formats the rolled row is aFeaturewith that representative plot's geometry (ornullif none resolved yet).
{
"type": "asset", "id": "<plot_task_id>",
"attributes": {
"location_id": "<first plot's staged_location id>",
"location_type": "plot",
"asset_data": {
"country_code": "BR", // uniform → scalar
"producer_name": ["Farm X", "Farm Y"], // varies → distinct array
"_truncated": ["some_high_cardinality_key"]
},
"deforestation": { "total_plots": 42, "compliant_plots": 40, ... }
},
"relationships": {
"normalised_data": { /* first plot */ },
"assessments": { /* first plot */ }
}
}
Supported across offset and cursor pagination and all four response
formats — JSON, application/geo+json, and the streaming
application/x-ndjson / application/x-ndgeo+json variants (the collapse is
done in SQL, so meta.total and page[cursor] operate on the folded row set;
streaming pages that folded set under a cursor so memory stays bounded to one
page). ?sort= and filter[...] are both honoured (EPO-1627). Filters apply
at plot grain: a plot task appears when at least one of its plots matches,
and the folded row summarises only the matching plots. Facility and shed rows
filter directly.
▶Rollup by supplier unit (?rollup=unit)
?rollup=unit)?rollup=unit folds onto the supplier unit instead of the task: one row
per (organization_id, asset_task_id, supplier unit). A plot task that holds
three supplier units returns three rows, and each folds only its own plots.
The same unit under two tasks stays two rows. Two organisations never share
a row.
GET /ingestions/assets?rollup=unit&include=normalised_data,deforestation
The unit is asset_task_entity.supplier_unit, assigned by validation at
ingest. A row with no unit falls back:
- A plot task with no units folds to one row, the same row
?rollup=truereturns. - A facility or shed record with no unit stays its own row, with its own
asset
id— the same row?rollup=truereturns. Records that share a unit fold into one row.
Every task ingested before 2026-09-16 carries no units, so it reads exactly
as it does under ?rollup=true.
On a unit row:
idis a deterministic uuid derived from (organisation, task, unit). It is stable across reads and works inpage[cursor], but it is not a task id. Useattributes.asset_task_idto link to the task.relationships.normalised_data.attributes.supplier_unitcarries the unit id. Requestinclude=normalised_data.deforestationis the unit's own roll-up, from the per-unit summary (EPO-3088). A unit that is missing from a task that does have per-unit rows reports zeros. A task with no per-unit rows at all (not yet backfilled) uses its task-grain summary only when it has one unit; with several, the block andlive_plot_countare absent, because no per-unit figure exists.live_plot_countis the unit's plot total, with or withoutinclude=deforestation. A folded unit row omitscollection_idsandsuperseded_plot_count; an unfolded facility or shed record keeps its owncollection_ids.asset_data, the representative plot, filters, sort, pagination and all four response formats behave as under?rollup=true, at the unit grain.
Known limitation (EPO-3076). Two different suppliers can in rare cases receive the same unit id. Their plots then fold into one row.
▶Geometry (include_geometries)
include_geometries)Geometry lives inside the normalised_data relationship and is opt-in. To
get it on the JSON response you need both include=normalised_data and
include_geometries=true:
GET /ingestions/assets?include=normalised_data&include_geometries=true
Without the flag, normalised_data is returned without its geometry.
Passing include_geometries=true without include=normalised_data is a no-op
(the relationship that carries the geometry isn't expanded). A GeoJSON Accept
format (application/geo+json) always includes geometry. See
Query Parameters → Geometry.
▶Match methods
| Value | Description |
|---|---|
hash | Matched via attributes_hash — identical source row was previously linked. |
address_key | Matched via deterministic address identity key for address-only facilities. |
spatial_single | Single spatial candidate within 500m with matching commodity and location type (or exact geometry match for plots/sheds). |
spatial_distance | Multiple candidates; closest was within 10m. |
spatial_normalized | Multiple candidates; matched by normalized fields (name, city, admin1, country). |
added | No match found — a new staged location was created. |
▶Cross-Organization Matching & Visibility
- Cross-organization name gate: A facility is linked to an existing location that only another organization can see only when the submitted facility name shares the same distinctive words (ignoring legal entity suffixes, punctuation, and common stopwords). If name evidence does not match, the upload creates a new staged location with its own name row rather than linking across organizations.
- Parent and child organizations: A parent organization querying locations can see locations belonging to its child organizations. When reading names, the parent's own normalization row takes priority, followed by its child organizations' rows (
own → unowned → other). Specifyingx-acting-as: <child-org-id>orfilter[organization_id]=<child-org-id>narrows the query scope to that specific child organization.
Required scope:
ingestions:read