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 —
coreonly, orcore+ 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
| Field | Type | Required | Description |
|---|---|---|---|
location_type | string | Yes | plot for this workflow |
commodity_type | string | Yes | cattle, cocoa, coffee, palm, rubber, soy, timber, shrimp |
metrics | string[] | No | Subset of deforestation, emissions, biodiversity, water — leave off here; metrics are triggered in step 6 |
tags | string[] | No | ≤ 50 tags, each ≤ 100 chars |
end_date | string | No | YYYY-MM-DD; Dec 31 of a prior year, or any date in the current year up to today (UTC) |
assessments | object | No | Omit 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— yourasset_task_id. Captured into{{plot_lifecycle_task_id}}for steps 2 → 8.meta.client_actions[0]—type: uploadaction descriptor withmethod,href,headers, and apollblock telling the client how to wait for the task to leavepending.data.attributes.overall_status—pendinguntil 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_URLismeta.client_actions[0].hreffrom 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 leavespending— which is what Step 2 polls for.
▶Frontend Usage
- Store
data.idasplot_lifecycle_task_id - Read
meta.client_actions[0]and callxhr.open(action.method, action.href) - On 2xx, poll
action.poll.hrefeveryaction.poll.interval_msuntildata.attributes[action.poll.attribute]is inaction.poll.terminal_states
▶Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing/malformed JSON field; unknown or duplicate metrics |
| 401 | MISSING_API_KEY / INVALID_API_KEY | Auth missing or invalid |
| 403 | FORBIDDEN | API key lacks ingestions:write |
| 415 | UNSUPPORTED_MEDIA_TYPE | Signed-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.