Ingest Assets

Create an asset ingestion task and receive a short-lived signed upload URL the client uses to PUT the file directly to object storage.

Recommended for new integrations. The legacy multipart upload variant is documented at Ingest Assets Multipart and continues to work, but emits Deprecation: true and a Link: …; rel="successor-version" response header.

How it works

  1. Request the URL — POST /ingestions/assets with a JSON body describing the asset. No bytes yet, and no filename / content_type — those are resolved from the uploaded object once storage finalises. The response carries an upload_url (valid for 15 minutes) and a task id in pending.
  2. Upload the bytes — PUT the raw file to upload_url with Content-Type: application/octet-stream. The signed URL is single-object, single-shot, and scoped to that exact path.
  3. Storage finalises — when GCS confirms the upload, an OBJECT_FINALIZE event triggers an internal webhook that dispatches the ingestion task and records the real filename / MIME type on the task. No further client call is needed.
  4. Poll — GET /ingestions/{id} reports task progress: pending → processing → staged → completed.

Request body

FieldTypeRequiredNotes
location_typeenumYesfacility, plot, or shed. Promoted to a first-class task attribute and returned on every read. After processing it is reconciled to the type actually detected in the file, so the value read back may differ from the one requested.
commodity_typestringNocattle, cocoa, coffee, palm, rubber, soy, timber, shrimp. Common aliases (e.g. palm oil / oil palm → palm) are accepted and normalised to the canonical value on ingest. When omitted, the commodity is auto-detected per location during validation and surfaced as commodity on the results (locations + assessments).
namestringNoHuman-friendly task name, ≤ 255 chars. Returned on every task read. When omitted it is derived from tags (joined with , ), falling back to the filename. Supply it to override the derivation.
descriptionstringNoFree-form notes, ≤ 2000 chars. Never derived — null unless you set it.
metricsstring[]NoSubset of deforestation, emissions, biodiversity, water. Unique values only.
tagsstring[]NoTags for the upload. Each ≤ 100 chars. A tag with no placeholder applies to every location in the file; a tag embedding a {Column Header} placeholder is expanded per row from that column's cell value. See Per-row tags from columns below.
end_datestringNoYYYY-MM-DD. Must be Dec 31 of a prior year, or any date in the current year up to today (UTC).
forcebooleanNoBypass duplicate-upload rejection. Defaults to false. See Duplicate uploads below.
assessmentsobjectNoPer-group toggles for the assessments persisted on this upload. See Enabling assessments below. Omit to run the default — core only.
create_artifactbooleanNoFast path. When true, the ingestion worker auto-generates a GeoParquet artifact scoped to exactly this upload once it finishes processing — no second call. See Fast path below. Requires metrics to include at least one metric.
signed_url_ttl_secondsnumberNoLifetime (seconds, 1–86400) for the artifact dashboard signed URL surfaced on the single-task read. Only valid alongside create_artifact: true (otherwise 400). See Fast path below.
create_audit_trailbooleanNoFast path. When true, the ingestion worker auto-creates an EUDR audit-trail record from this upload once it finishes processing — no second call. See Fast path below. Requires metrics to include deforestation.
audit_trail_operatorobjectNoOperator block stamped onto the auto-created audit-trail record. Same shape as the operator block on POST /regulations/eudr/audit-trail. Only valid when create_audit_trail: true (otherwise 400). When omitted, the record falls back to the org's default operator.
audit_trail_commodityobjectNoCommodity block stamped onto the auto-created audit-trail record. Same shape as the commodity block on POST /regulations/eudr/audit-trail; hs_code may be omitted; it is never defaulted and a blank one blocks submission. Only valid when create_audit_trail: true (otherwise 400). When omitted, the record stores a NULL commodity block (set later via PATCH).

Per-row tags from columns

A plain tag applies the same label to every location in the upload. A tag that embeds a {Column Header} placeholder instead becomes a per-row tag: the placeholder is replaced with that row's cell value for the named column. This is the way to carry a per-location identifier (part number, internal site id, sales region, …) through ingestion so you can later filter or group locations by it.

Write the tag as namespace:{Column Header} — the text before the first : is the namespace (exactly as for a static namespaced tag), and the brace-delimited column header supplies the value:

"tags": ["part_number:{Part Number}", "region:{Sales Region}"]

Given a row where the Part Number cell is 12345 and Sales Region is EMEA, that row's location is tagged part_number:12345 and region:EMEA.

Behaviour:

  • Comma-split into multiple values — a placeholder cell is split on commas, so one row can carry several values under the same namespace: a Part Number cell of 1,2,3 with part_number:{Part Number} tags that row part_number:1, part_number:2, and part_number:3. Each part is trimmed; blank parts (e.g. from 1,,2) are skipped.
  • Missing column / blank cell is skipped — a row with no cell for the referenced column (or a blank one) gets no tag from that template; other rows and tags are unaffected.
  • Namespace — as with any tag, the namespace is the text before the first : and is normalised to lowercase snake_case when stored for grouping; the substituted value keeps its original case.
  • Column-name matching is space-insensitive — a {Part Number} placeholder matches a Part Number header in the file.
  • The per-row tags a template produces count against the normal tag limits.

The resulting tags behave like any other tag — they propagate to the row's location, are returned in the asset's tags array, and can be renamed in bulk with PATCH /tags/namespaces/:namespace.

Fast path: auto-create an audit trail

Set create_audit_trail: true to chain ingestion → deforestation → EUDR audit-trail record in a single upload. Once the task reaches a successful terminal (completed / completed_with_errors), the worker dispatches the same create task as POST /regulations/eudr/audit-trail, scoped to this upload's plots (filter: assets.task_id == <this task>).

  • metrics must include deforestation (validated — a request without it returns 400), so the created record snapshots the deforestation signal rather than being empty.
  • Operator + commodity: supply audit_trail_operator and/or audit_trail_commodity to stamp them onto the created record. Both are optional:
    • audit_trail_operator omitted → the record uses the org's default operator (resolved live at read time).
    • audit_trail_commodity omitted → the record stores a NULL commodity block; set it via PATCH /regulations/eudr/audit-trail/:recordId before submitting (the commodity_missing blocker enforces this). When supplied, its hs_code may be omitted; it is never defaulted, and a blank one blocks submission.
    • Both are only accepted alongside create_audit_trail: true; sending either without it returns 400.
  • Auto-split still applies: one record per (country_code, commodity). A supplied audit_trail_commodity block is applied only when the upload resolves to a single (country_code, commodity) group; multi-group uploads store a NULL commodity on every record (populate each via PATCH).
  • Poll this task; once it carries attributes.audit_trail_task_id, poll that audit-trail task for the created record id. A failed ingestion creates nothing.
  • Split flow: the same opt-in is available on Process Ingestion Assets (PUT), so you can upload first, review, then create the audit trail when you kick off deforestation via the reprocess. See that endpoint's Fast path section.

Fast path: auto-create an artifact

Set create_artifact: true to chain ingestion → metric processing → a GeoParquet artifact of just this upload in a single request — no second POST /artifacts call, and no filter to author. Once the task reaches a non-failed terminal state (completed / completed_with_errors), the worker auto-dispatches a generateArtifact task scoped to this upload (filter: assets.task_id == <this task>), so the bundle contains precisely the locations from this upload and nothing else. See the Artifacts endpoint docs for what the bundle contains and how to consume it.

  • metrics must include at least one metric (validated — a request without any returns 400), so the exported artifact carries metric/assessment slices rather than being empty.
  • It fires once. The dispatch is guarded, so a retried or re-run upload does not mint a second artifact — if generation is retried, the same artifact is reused.
  • Follow it via the task. Poll this ingestion task; once it carries attributes.artifact_id, fetch the artifact at /v1/artifacts/:id (or list by scope — GET /artifacts?filter[assets.task_id]=<this task>). A failed ingestion creates nothing.
  • Downloads are not pre-minted. The fast path creates and generates the artifact; it does not pre-mint download URLs. Once the artifact is completed, call POST /artifacts/:id/signed-urls (or open the dashboard link) exactly as for an on-demand artifact.
  • Optional dashboard link. Set signed_url_ttl_seconds (1–86400) alongside create_artifact: true to have the single-task GET /ingestions/{id} surface a freshly-minted attributes.artifact_dashboard_url + attributes.artifact_signed_url_expires_at once the artifact succeeds. The URL is minted on read, never stored. Sending signed_url_ttl_seconds without create_artifact: true returns 400.
POST /ingestions/assets
Content-Type: application/json

{
  "location_type": "facility",
  "commodity_type": "palm",
  "metrics": ["deforestation"],
  "create_artifact": true,
  "signed_url_ttl_seconds": 3600
}

Enabling assessments

Ingestion is for ingestion-scope assessment groups: core, hazard, and infrastructure. EUDR-scope assessments (art9, art10) belong to a DDS submission, not an upload, and are driven on the DDS create path — see Assessments.

Each include_* flag is independent — set the ones you want and leave the rest off:

FlagDefaultPersists
include_corefalseThe core validation row — confidence score + level + plot- and facility-validation flags.
include_hazardfalseNatural-hazard risk score / level row.
include_infrastructurefalseInfrastructure nearest-feature enrichments — nearest_water_km / nearest_port_km / nearest_road_km plus port metadata (subject to the access checks below).

Normalisation always runs regardless of these flags — that is what stages the location itself (parsed coordinates, geocoding, commodity detection). The flags only govern which validation results are persisted alongside it.

Migration note: The include_eudr / include_art9 / include_art10 flags previously accepted here are no longer wired up on ingestion. Stale callers can keep sending them — the API silently strips the keys rather than returning 400 — but no EUDR-scope row will be persisted. Move EUDR-scope opt-in to the DDS create path.

See docs/assessment-fields.md for the field-by-field reference of every key each group can carry, and Assessments for the readiness check / fill-missing endpoints used during the DDS workflow.

Default (omit the block)

Omit assessments to run with the defaults: no validation results are persisted. Normalisation still runs and stages the location. This is the right shape for tagging or metric processing flows that don't need risk scoring or core confidence flags.

POST /ingestions/assets
Content-Type: application/json

{
  "filename": "supply_chain_data.geojson",
  "content_type": "application/geo+json",
  "location_type": "facility",
  "commodity_type": "palm"
}

After the upload finalises, the resulting locations exist but carry no validation results. The core, hazard, and infrastructure slots will be null in the response. EUDR-scope assessments (art9, art10) are not surfaced on this response shape at all — they are exposed through the audit-trail record (GET /regulations/eudr/audit-trail/:recordId/plots). Set include_core: true to opt in to the core confidence/flags assessment.

Top-level flags

FieldTypeDefaultDescription
include_corebooleanfalsePersist the core validation row (confidence + flags) and pivot it to your org. Set true to opt in. Honoured at upload time on POST /ingestions/assets and post-hoc on PUT /ingestions/assets (filter-scoped). Both paths share the same cache-aware runner — a covering core row short-circuits to a pivot insert.
include_hazardbooleanfalseRun natural-hazard risk scoring. Persists the hazard validation row pivoted to your org.
include_infrastructurebooleanfalseRun infrastructure nearest-feature enrichments — adds nearest_water_km / nearest_port_km / nearest_road_km plus port metadata (port_road_km, nearest_port_name / nearest_port_address, GeoJSON nearest_port_point / nearest_water_point), subject to the access checks below. Persists the infrastructure validation row pivoted to your org.

Access checks (check_*)

The check_* flags are cross-cutting toggles that govern the water / port / road access checks. They are recorded on both core and infrastructure rows, because the same assessment call produces:

  • core — boolean access flags: water_access, port_proximity, road_access.
  • infrastructure — numeric distances in km: nearest_water_km, nearest_port_km, nearest_road_km.

Because they are shared, the request flags live at the top level of the assessments block — not nested under any group.

FieldTypeDefaultDrives (in core)Drives (in infrastructure)
check_water_accessbooleanfalsewater_access booleannearest_water_km
check_port_proximitybooleanfalseport_proximity booleannearest_port_km
check_road_accessbooleanfalseroad_access booleannearest_road_km

Each row records the check_* snapshot on its sub_options column, so a later request that asks for an access check not covered by the cached row is treated as a cache miss and refreshes both groups.

Opting in to core

include_core: true writes the core assessment, scoped to your org — confidence score, level, and plot-validation flags persisted. Normalisation runs either way (the location is staged), and the other ingestion-scope add-ons are independent. Use this when:

  • You want confidence scoring or geometric validation flags surfaced on the asset response.
  • You need the core row available for later DDS readiness / fill-missing flows.
  • You're on a tier or contract that includes core scoring.
POST /ingestions/assets
Content-Type: application/json

{
  "filename": "supply_chain_data.geojson",
  "content_type": "application/geo+json",
  "location_type": "facility",
  "commodity_type": "palm",
  "assessments": {
    "include_core": true,
    "include_hazard": true,
    "include_infrastructure": true
  }
}

Above: the core, hazard, and infrastructure rows are persisted. EUDR-scope rows are written by the DDS create path and are not surfaced on this response (see GET /regulations/eudr/audit-trail/:recordId/plots).

Disabling assessments

There is no separate run flag. To disable any group, leave its include_* flag absent (the default for every group, including core) or set it to false. Sub-options on a group whose include_* is false are ignored.

Resolution rules

Body shapeEffective groups persisted
No assessments blockNone — location staged only
"assessments": {}None — location staged only
"assessments": { "include_core": true }core only
"assessments": { "include_hazard": true }hazard only
"assessments": { "include_infrastructure": true, "check_water_access": true }infrastructure group with nearest_water_km populated
"assessments": { "include_core": true, "include_hazard": true, "include_infrastructure": true }core + hazard + infrastructure
"assessments": { "include_eudr": true }None — EUDR-scope keys are silently stripped on ingestion; use the DDS create path

Adding assessments later

If you uploaded without an assessments block (or with a smaller subset) and want to fill in missing add-ons later, use Process Ingestion Assets (PUT /ingestions/assets) with the same assessments block. The runner is cache-aware — assessments any org has already produced for the same location are reused, granting your org visibility instead of making a fresh assessment call.

Duplicate uploads

To avoid re-running the same work, an upload is rejected as a duplicate when its file content and its processing parameters match a task that has already completed for your organization (or a child org).

  • What counts as identical: the file bytes (content hash) plus location_type, commodity_type, metrics, and assessments / metric options. Labelling fields — name, description, tags, supplier info — are not part of the match, so re-uploading the same file just to change a tag is still treated as a duplicate.
  • end_date: matched separately. When you set an end_date, only a prior completed task with the same end_date counts as a duplicate. When you omit it, the upload is considered a duplicate only of another open-ended task completed in the last 14 days — after that window a re-upload is allowed so it re-runs against fresher data.
  • Only completed tasks block: a task still pending / processing / staged does not trigger rejection.

On the signed-URL flow the duplicate is detected once the bytes finalise, so the task is marked failed with duplicate_of_asset_task_id pointing at the original — poll GET /ingestions/{id} to see it. (The multipart variant rejects synchronously with 409.)

Overriding with force

Set "force": true to bypass the check and process the upload anyway — e.g. to deliberately re-run the same file, or re-ingest the same locations under a different tag:

POST /ingestions/assets
Content-Type: application/json

{
  "filename": "supply_chain_data.geojson",
  "location_type": "facility",
  "commodity_type": "palm",
  "metrics": ["deforestation"],
  "force": true
}

Why this flow

  • One client request to the API; the file bytes never pass through it.
  • The signed URL is scoped to one object path with a 15-minute TTL — safe to hand to less-trusted clients (MCP, browser).
  • A failed or expired upload leaves a pending task that times out cleanly, no half-ingested file.

Supported file formats

CSV, GeoJSON / NDJSON, Parquet, GeoPackage, Shapefile (.zip), Excel (.xlsx/.xls). Up to 100 MB.

Errors

StatusCodeCause
400VALIDATION_ERRORMissing/malformed JSON field; unknown or duplicate metrics; create_artifact: true without at least one metrics entry; signed_url_ttl_seconds set without create_artifact: true.
401MISSING_API_KEY / INVALID_API_KEYAuth missing or invalid.
403FORBIDDENAPI key lacks ingestions:write.
409CONFLICTDuplicate upload — identical file + parameters already processed. The original task id is in data.details.duplicate_of_asset_task_id. Multipart only; the signed-URL flow surfaces this asynchronously as a failed task. Pass force: true to override. See Duplicate uploads.
415UNSUPPORTED_MEDIA_TYPESigned-URL flow not enabled in this environment.

Required scope: ingestions:write