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:

#KeyPrefers
1has_metricA location with metric results over one with none
2monitoring_endThe later monitoring window
3is_assessedAn assessed location over an unassessed one
4used_ccdcA CCDC-backed result
5ingestion_dateThe more recently ingested metric run
6link_created_atThe 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

Same query parameters, same four response formats, same asset resource shape. Two differences:

  • Superseded records are excluded.
  • The plot fold is always on. ?rollup=true and ?rollup=task are accepted and inert, so a client already calling /ingestions/assets?rollup=true can move to this path without editing its query. ?rollup=unit keeps the dedupe and moves the fold to the supplier unit — see List All Assets for what a unit row carries. ?rollup=false returns 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> (or filter[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/assets returns 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.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 seeMeaning
Same footprint, different commodityExpected. One catchment supplying several commodities is recorded once per commodity.
Different footprintsGenuinely separate catchments — distinct watersheds, or areas split by a border or a mountain range. Rare.
Same footprint, same commodityRedundant: 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