01 Upload Plots

Step 1: Upload Plots Without Validation

Mint a signed upload URL and ship the plot bytes — no validation rows written at upload time. Set up exactly like the canonical Asset Ingestion / 1 Upload Assets flow: a JSON POST returns the task id and a meta.client_actions[0] upload descriptor; you then PUT the file bytes to the signed URL in that descriptor (Step 1b below). The docs UI's Upload tab does this for you, but a server-to-server integrator issues the PUT as an explicit request — no UI step is required.

Because the body omits assessments, no validation results are persisted. Normalisation still runs and the locations exist after step 2 — you can opt into core + EUDR later by asset_task_id in step 3.

What this step does

It registers the upload and hands back a signed URL to PUT the plot bytes into plus the asset_task_id that stitches the whole workflow together. Because assessments is omitted, this is the cheapest possible ingestion: the file is staged and normalised into staged_location rows, but no validation (core, EUDR) or metrics work is triggered. The response is only the handle — the bytes themselves travel over the signed URL in meta.client_actions[0], and the task stays pending until GCS finalises that upload.

Why it's needed

Everything downstream is keyed by asset_task_id, and that id does not exist until this call creates it. Nothing can be inspected, assessed, or metered before the plots are staged, so this is the step that turns raw polygons into addressable locations. Splitting the byte upload out behind a signed URL also keeps the API request small and lets large plot files go straight to storage rather than through the API.

Why you'd choose the upload-first shape

Omitting assessments here is a deliberate choice: it defers all validation cost until after you've seen what actually staged. Reach for this path when:

  • Plots arrive ahead of any compliance decision and you want to review the staged geometry (steps 4/5) before spending on assessments.
  • You want to opt in selectively later — core only, or core + EUDR — rather than committing at upload time.

If instead you know up front you want validation and metrics in a single call, use Quick Ingestion, which sends assessments and metrics on the POST.

Request

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

FieldTypeRequiredDescription
location_typestringYesplot for this workflow
commodity_typestringYescattle, cocoa, coffee, palm, rubber, soy, timber, shrimp
metricsstring[]NoSubset of deforestation, emissions, biodiversity, water — leave off here; metrics are triggered in step 6
tagsstring[]No≤ 50 tags, each ≤ 100 chars
end_datestringNoYYYY-MM-DD; Dec 31 of a prior year, or any date in the current year up to today (UTC)
assessmentsobjectNoOmit for the upload-first flow (default = no validation rows)

Note: filename and content_type are not sent. They are resolved from the uploaded object's metadata when GCS finalises.

Response (201)

See the Examples tab for the full envelope. Key fields:

  • data.id — your asset_task_id. Captured into {{plot_lifecycle_task_id}} for steps 2 → 8.
  • meta.client_actions[0] — type: upload action descriptor with method, href, headers, and a poll block telling the client how to wait for the task to leave pending.
  • data.attributes.overall_status — pending until the GCS finalize webhook fires (which happens once the byte upload completes).

Step 1b: PUT the file bytes to the signed URL

This is the explicit byte upload — a plain HTTP PUT straight to GCS, with no x-api-key. Take the descriptor from meta.client_actions[0] (mirrored on data.attributes.upload_url) and send the raw file bytes with the headers the descriptor lists:

# The header values below are illustrative. Copy them verbatim from your
# own response's `client_actions[0].headers` — a minted descriptor may
# differ, and GCS 403s if any header does not match what it signed.
curl -X PUT "$UPLOAD_URL" \
  -H "content-type: application/octet-stream" \
  -H "x-goog-content-length-range: 0,104857600" \
  -H "x-goog-if-generation-match: 0" \
  --data-binary @plots.csv
  • $UPLOAD_URL is meta.client_actions[0].href from the 201 response.
  • Send exactly the headers in client_actions[0].headers — GCS rejects the PUT (403 SignatureDoesNotMatch) if the signed headers do not match. The values shown above are only the descriptor's defaults for this example.
  • The URL is single-use and expires at expires_at. Re-POST step 1 to mint a fresh one if it lapses.
  • On a 2xx, GCS finalises the object and the task leaves pending — which is what Step 2 polls for.

Frontend Usage

  • Store data.id as plot_lifecycle_task_id
  • Read meta.client_actions[0] and call xhr.open(action.method, action.href)
  • On 2xx, poll action.poll.href every action.poll.interval_ms until data.attributes[action.poll.attribute] is in action.poll.terminal_states

Errors

StatusCodeCause
400VALIDATION_ERRORMissing/malformed JSON field; unknown or duplicate metrics
401MISSING_API_KEY / INVALID_API_KEYAuth missing or invalid
403FORBIDDENAPI key lacks ingestions:write
415UNSUPPORTED_MEDIA_TYPESigned-URL flow not enabled in this environment

Next Step

→ Step 2: Wait For Plots Staged — poll GET /ingestions/{task_id} until overall_status reaches staged.