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 task
  • filter[tags]=eq:verified — filter by tag name
  • filter[source_file]=eq:locations.csv — filter by original file name
  • filter[normalised_data.id]=eq:<uuid> — filter by normalised location ID
  • filter[normalised_data.commodity_type]=eq:timber — filter by commodity
  • filter[assets.data.CITY]=eq:London — filter by source data fields using dot-notation
  • filter[assets.data.ASSET_NAME]=contains:Aberdeen — substring match on source data
  • filter[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)

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 only
  • eudr — EUDR monitoring-window metrics only
  • true / 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):

FieldMeaning
attributes.superseded_countHow many earlier records this one replaced. 0 means it is the location's only record.
attributes.asset_task_idsEvery upload task in the group: this row's own task first, then the tasks it replaced, newest first.
links.supersededThe 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":

FieldMeaning
attributes.live_plot_countPlots of this task that are still the current record for their location.
attributes.superseded_plot_countPlots 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

superseded_count and links.superseded describe the group as a count and a filter query. To read the history itself, every per-record row carries:

FieldMeaning
attributes.row_idThis 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

superseded_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)

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
IncludeCategories loaded
metricsdeforestation, biodiversity, emissions, water_stress
metrics.deforestationdeforestation
metrics.biodiversitybiodiversity
metrics.emissionsemissions
metrics.water_stresswater_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.deforestation is not attributes.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:

RequestStatements on a facility+shed pageOn a page that also has plot rows
metrics812
metrics.deforestation23

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)

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?

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.

ValueMeaning
trueThe filing was pinned from the location's current record.
falseA later upload replaced the record it was pinned from. The filing may not describe current data.
nullThe 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

The 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

include=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_typePopulated fieldValue
facilityfacility_idfacility.id — a string, not a UUID
shedshed_idshed.id
plotplot_idplot.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:

FieldSet forResolves at
parent_shed_location_idplots/locations/sheds/{id}
parent_facility_location_idplots 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)

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=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 plot asset_task_id.
  • asset_data is 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 _truncated entry.
  • 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 is GET /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, and location_id / location_type are taken from the task's first (representative) plot (the earliest resolved plot). So the fold shows one plot's location detail + assessments while asset_data spans all plots. In geo formats the rolled row is a Feature with that representative plot's geometry (or null if 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 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=true returns.
  • A facility or shed record with no unit stays its own row, with its own asset id — the same row ?rollup=true returns. 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:

  • id is a deterministic uuid derived from (organisation, task, unit). It is stable across reads and works in page[cursor], but it is not a task id. Use attributes.asset_task_id to link to the task.
  • relationships.normalised_data.attributes.supplier_unit carries the unit id. Request include=normalised_data.
  • deforestation is 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 and live_plot_count are absent, because no per-unit figure exists.
  • live_plot_count is the unit's plot total, with or without include=deforestation. A folded unit row omits collection_ids and superseded_plot_count; an unfolded facility or shed record keeps its own collection_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)

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

ValueDescription
hashMatched via attributes_hash — identical source row was previously linked.
address_keyMatched via deterministic address identity key for address-only facilities.
spatial_singleSingle spatial candidate within 500m with matching commodity and location type (or exact geometry match for plots/sheds).
spatial_distanceMultiple candidates; closest was within 10m.
spatial_normalizedMultiple candidates; matched by normalized fields (name, city, admin1, country).
addedNo 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). Specifying x-acting-as: <child-org-id> or filter[organization_id]=<child-org-id> narrows the query scope to that specific child organization.

Required scope: ingestions:read