List Latest Ingestion Assets
List the current record for every location — one row per location.
Upload the same facility three times and GET /ingestions/assets returns it
three times, because each upload is its own record. This endpoint returns only
the record that is current: records superseded by a later upload of the same
location are excluded, and meta.total counts the deduped set.
▶What "latest" means
Not "the newest upload". Records are grouped by organization and location, and within a group the current one is chosen by the same six-key precedence the DDS filing uses to pin its plots:
| # | Key | Prefers |
|---|---|---|
| 1 | has_metric | A location with metric results over one with none |
| 2 | monitoring_end | The later monitoring window |
| 3 | is_assessed | An assessed location over an unassessed one |
| 4 | used_ccdc | A CCDC-backed result |
| 5 | ingestion_date | The more recently ingested metric run |
| 6 | link_created_at | The more recently linked pivot row |
The shared ordering is the point. This endpoint, the DDS plot pin,
filter[compliance] and the dashboard roll-up all resolve currency this way, so
they name the same record. If you reconcile two Epoch numbers and they disagree,
that is a defect to report rather than two definitions of "latest".
The practical consequence: a re-upload carrying a later end_date takes
currency at key 2. A re-upload with the same window may not — keys 1 and 3 can
keep the earlier record current when it already has metric results and
assessments and the new one does not.
Currency is stored, not computed per request. Each record carries a stamp
written as its task's pivot rows are inserted, which is why this endpoint and
/history can never disagree about which record leads.
▶Relationship to GET /ingestions/assets
GET /ingestions/assetsSame query parameters, same four response formats, same asset resource
shape. Two differences:
- Superseded records are excluded.
- The plot fold is always on.
?rollup=trueand?rollup=taskare accepted and inert, so a client already calling/ingestions/assets?rollup=truecan move to this path without editing its query.?rollup=unitkeeps the dedupe and moves the fold to the supplier unit — see List All Assets for what a unit row carries.?rollup=falsereturns 400 — the endpoint is the folded read, so it cannot honour that request, and silently ignoring the parameter would return the deduped set to a caller who asked for the full one.
GET /ingestions/assets is unchanged and remains the way to see every
upload of a location, including the superseded ones.
▶Reaching a superseded record
Two routes stay open, because a record's history is not the same question as a location's current state:
filter[assets.task_id]=eq:<uuid>(orfilter[asset_task_id]) drills into one upload. Scoped that way, this endpoint returns that task's rows whether or not a later upload replaced them — a task is the historical record of what it ingested.GET /ingestions/assetsreturns every record.
▶Each row says what it replaced
Deduping hides a location's history, so every row carries it. A facility or
shed row gets attributes.superseded_count, attributes.asset_task_ids, and a
links.superseded that returns the whole group — the record plus the
superseded_count records it replaced. A folded plot row spans many locations,
so it reports attributes.live_plot_count and
attributes.superseded_plot_count instead.
"attributes": {
"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>"
}
Note the link points at GET /ingestions/assets, not at this endpoint — the
group is exactly the set this endpoint excludes.
▶The records themselves, and the metrics
?include=superseded returns the records this row replaced inline — capped at
10, with meta.total exact past the cap and each element carrying its
monitoring_end and record_superseded_at. It is the detail behind
superseded_count, and the two always agree.
?include=metrics attaches the latest metric per category for the row's
location, and ?include=metrics.<category> attaches one. Ask for the category
you need: one statement runs per category per location type, so bare metrics
is 8 statements on this endpoint against 2 for a single category — and four
metric resources per row rather than one.
Because this endpoint always folds plot tasks, only facility and shed rows carry either relationship. A folded row spans many locations, so it has no one predecessor set and no single metric row.
See List All Ingestion Assets for the full
reference on both, including why metrics.deforestation is not the same thing
as attributes.deforestation.
▶Prefer links.history for the history itself
links.history for the history itselflinks.superseded is a filter query, so it returns the group in upload order
with no indication of which record is live. Every per-record row also carries
attributes.row_id and a links.history pointing at
Get Record History, which returns the same
records ordered by supersession, each flagged is_live and carrying its
monitoring_end — the key that decided which one is current.
"attributes": { "row_id": "<this record>" },
"links": { "history": "/ingestions/assets/latest/<row_id>/history" }
row_id is the record's own id. It is not the resource id, which is the
shared asset_entity id and repeats across organizations that uploaded
identical attributes.
A folded plot row is not one record, so it carries neither. Take a plot
location's history from a per-record row on GET /ingestions/assets.
The fields are decided by the row, not the endpoint, so they appear on
GET /ingestions/assets too. The difference is that every row here is a
current record, while that endpoint returns a mix: a superseded row there
reports its own (usually 0) count.
See List All Ingestion Assets for the full
reference, including why the link carries filter[organization_id] and what
the counts deliberately ignore.
▶Was this location's DDS filed against current data?
include=audit_trails matters most on this endpoint, because deduping is what
hides the problem it reveals.
A DDS pins its plots from one upload. When a location is uploaded again, this
endpoint returns the new record — and a filing pinned from the record it
replaced becomes invisible: the scalar audit_trail_id matches on the row's own
upload, so it reports null while the predecessor still carries the filed
statement.
The plural include resolves the set across the row's whole lineage and flags each one:
"audit_trails": {
"data": [
{ "id": "…", "lifecycle_status": "accepted",
"reference_number": "25NLxxxxxxxxx",
"pinned_record_is_live": false }
],
"meta": { "total": 1, "truncated": false }
}
pinned_record_is_live: false says a later upload replaced the record this
filing was pinned from, so the filing may not describe the data this row now
reports. Nothing else in the API says that.
It is opt-in and strictly more expensive than the scalar. Folded plot rows carry none of it. See List All Ingestion Assets for the full reference, including the cap and the unbounded listing past it.
▶Each row names the ids it resolves to
A deduped row stands for a whole group of records, so it also names what a
caller must follow next: its own V1 entity id in normalised_data
(facility_id, shed_id or plot_id — whichever matches location_type),
and the parent locations as top-level parent_shed_location_id /
parent_facility_location_id, with a link each.
Every id is resolved within the row's own organization, which matters most here: this endpoint returns one row per location, and a location two of your child orgs both uploaded still yields one row per org, each naming its own entity and its own parents.
A folded plot row names none of it — see List All Ingestion Assets for the full reference.
▶Several shed rows under one facility
A facility commonly has more than one shed row, and this endpoint does not fold them: each shed is its own location, so each has its own group and its own current record. They all appear.
There are two reasons for it, and they are not equally benign. Check which one you are looking at before treating the extra rows as expected:
| What you see | Meaning |
|---|---|
Same footprint, different commodity | Expected. One catchment supplying several commodities is recorded once per commodity. |
| Different footprints | Genuinely separate catchments — distinct watersheds, or areas split by a border or a mountain range. Rare. |
Same footprint, same commodity | Redundant: one catchment recorded more than once. Not something to reconcile against — see below. |
Compare geo (with include_geometries=true) together with commodity to tell
them apart. parent_facility_location_id is the same on all of them, so it
cannot.
A re-run that replaced a shed's record is a different thing again, and does
not add a row: it shows as superseded_count: 1 or more on a single row.
On our production data, 42% of facilities that have sheds have more than one shed row — but only a small number of those are genuinely distinct catchments. Most are one footprint recorded more than once, about a third of them with the same commodity. So if you count shed rows and suspect duplication, you are usually right. Measured 14 August 2026, across all organizations; your own data may differ.
▶Rows with no location
A record that never resolved to a location has no dedupe key, so it is never
superseded and always appears, with location_id and location_type null.
A plot task therefore still appears while it holds at least one unresolved
plot, even when every resolved plot's record has been superseded.
▶The deforestation block agrees with the row set
With include=deforestation, a rolled plot row's attributes.deforestation
counts only the plots a current record owns (EPO-2331). A plot whose record a
later upload superseded is excluded, so the block and the rows around it
describe the same set of records.
"Owns" follows the record, not the plot: a plot record owns its own plot, a shed record owns its shed's plots, and a facility record owns the plots of every shed under it. So superseding a facility's record removes its whole catchment from that task's roll-up.
The numbers are cached, not computed per request — the roll-up is written in the ingestion task and refreshed whenever record currency changes. Computing it live would cost 495–705 ms warm and 6.8 s cold on one org's first page (50 tasks, 6,116 plots), against 0.3 ms for the cached read.
Because the same cache serves them, GET /ingestions, GET /ingestions/{id},
filter[compliance] and GET /ingestions/assets now all describe current
records too. One consequence is worth knowing: a task's numbers can change
without the task changing, because a later upload of one of its locations moved
which record is current.
What this does not fix is cross-generation double counting, for a different reason — see What this endpoint does not dedupe below.
▶What this endpoint does not dedupe
The dedupe is not total, and the two gaps sit at a grain record currency cannot see. Both are worth knowing before reconciling a count.
A shed whose geometry shifts between runs is not superseded. Currency tracks uploaded records against a location. A re-run that re-derives a facility's catchment can mint a new location rather than a new record of the old one — and two locations are two groups, so both stay current and both appear. Tracked as EPO-1277 Gap B.
The deforestation roll-up still double counts across entity generations. One current record's location can own two generations of plots, and both are counted, so an affected location counts its area twice. A record can be current while the footprint beneath it has gained a generation. Tracked as EPO-1899.
Neither is a bug in the ordering above. Both are the same shape of problem: this endpoint is authoritative about which record is current, and says nothing about which entity generation beneath it is. See Record Supersession and Deforestation Methodology.
▶Everything else
Filters, sorting, pagination, include, geometry opt-in, collection_ids,
and the fold's asset_data / _truncated semantics are identical to
List All Ingestion Assets. See that endpoint for the full reference.
One counting note, since meta.total here counts locations rather than records:
counts are approximate by default. The server caps the count at 10,000 and sets
meta.total_is_approx: true when the cap was hit, so a page whose response omits
that field carries an exact total. Pass ?meta[count]=exact to force an
unbounded count — slower at scale. The flag is never set on a cursor page,
because a cursor read does not count at all.
Required scope:
ingestions:read