Clipping
Cut a source item’s rasters down to your area of interest. Clipping is an asynchronous job: you create it, poll it to completion, then read a download descriptor — never a stream of raw bytes from the create call.
Clipping is one of Geopera’s 227 typed operations. Like every operation, each is invoked as POST /v1/op/{operation_id} with a JSON body and a Bearer token — there are no GET/PUT/DELETE verbs and no path or query parameters. The list/get/delete meaning lives in the operation name. See Operations and Concepts for the model.
The clip operations
There are two ways to create a clip job, plus a set of read operations to track and retrieve it. This guide focuses on clip.create_from_item — clipping a data-platform item you already own.
| Operation | Side-effect | Scope | Use |
|---|---|---|---|
clip.create_from_item | external_spend | processing:process | Clip a source item’s assets to an AOI |
clip.create_from_area | external_spend | clip:write | Clip a delivery-bucket area + mosaics to an AOI |
clip.jobs.list | read | clip:read | List clip jobs for an organization |
clip.job.get | read | clip:read | Get a single clip job by id |
clip.job.downloads | read | clip:read | List downloadable files for a completed job |
clip.job.download | read | clip:read | Stream one clip file (egress-enforced) |
clip.job.tile_info | read | clip:read | Tile-rendering params for a completed job |
clip.job.delete | destructive | clip:destroy | Delete a clip job and its files |
clip.create_from_item requires the processing:process scope and is the path covered here. The clip.area.* and clip:*-scoped operations cover the separate delivery-bucket workflow. See Scopes for how grants map to operations.
Lifecycle
A clip job moves through three phases:
- Create / execute —
clip.create_from_itemresolves the source item’s raster assets, reserves compute units atomically, creates acog_clipjob, dispatches the GDAL warp, and returns immediately with the job already markedrunning. - Poll — call
clip.job.get(orclip.jobs.list) untilstatusiscompleted. - Retrieve — call
clip.job.downloadsfor a descriptor of each output file, thenclip.job.downloadto stream the bytes.
The create call does not block on the warp. It returns a job object you poll; the rasters are produced on a worker.
Creating a clip job
clip.create_from_item is an external_spend operation: it can reserve compute units, so it must carry an Idempotency-Key. A retried request with the same key never reserves credits or dispatches a duplicate warp. See Idempotency for how keys are scoped and how long they are retained.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
source_item_id | string | yes | Item whose raster assets are clipped |
project_id | string | yes | Project that receives the output (must belong to your org) |
aoi_geometry | GeoJSON geometry | yes | Polygon/MultiPolygon { "type", "coordinates" } to clip to |
asset_keys | string[] | no | Restrict to specific asset keys; omit to clip all raster assets |
aoi_name | string | no | Label for the AOI |
output_name | string | no | Name for the output |
target_collection_id | string | no | Collection to register outputs into |
The charged org is taken from your authenticated principal, not the request body. Your org must own both the output project_id and the source item’s project, or the call returns 403.
POST /v1/op/clip.create_from_item HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
Idempotency-Key: 4e1c8b2a-9f0d-4c3e-bf6a-0a1b2c3d4e5f
{
"source_item_id": "3a9c7f10-2b44-4d11-9c8e-1f2a3b4c5d6e",
"project_id": "b21e6d44-7c0a-4e2f-9a11-8c7d6e5f4a3b",
"aoi_geometry": {
"type": "Polygon",
"coordinates": [
[[151.20, -33.86], [151.24, -33.86], [151.24, -33.84], [151.20, -33.84], [151.20, -33.86]]
]
},
"asset_keys": ["data"],
"output_name": "harbour_clip"
}HTTP/1.1 200 OK
Content-Type: application/json
{
"job": {
"id": "77a1c9e2-4b3d-4f5a-8c6b-9d0e1f2a3b4c",
"status": "running",
"job_type": "cog_clip",
"organization_id": "..."
},
"assets_to_clip": 1,
"asset_keys": ["data"]
}The output carries assets_to_clip (the count resolved) and asset_keys (which keys were clipped). Capture job.id — you poll on it.
Polling the job
Call clip.job.get with the job_id until status is completed. The job report includes the output_artifacts once finished.
POST /v1/op/clip.job.get HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
{ "job_id": "77a1c9e2-4b3d-4f5a-8c6b-9d0e1f2a3b4c" }HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "77a1c9e2-4b3d-4f5a-8c6b-9d0e1f2a3b4c",
"status": "completed",
"output_artifacts": {
"clips": [
{
"mosaic_type": "rgb",
"filename": "harbour_clip_rgb.tif",
"size_bytes": 18234112,
"gcs_path": "clips/77a1.../harbour_clip_rgb.tif"
}
]
}
}Poll with backoff rather than a tight loop. clip.job.get and the other read operations require the clip:read scope, which is granted alongside the clip cluster.
To find jobs without an id, use clip.jobs.list with the organization_id and an optional status filter. It is paginated with limit (default 50) and offset — see Pagination.
POST /v1/op/clip.jobs.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
{ "organization_id": "...", "status": "running", "limit": 50, "offset": 0 }Retrieving the result
Once the job is completed, clip.job.downloads returns a descriptor for each output file — not the bytes. Each entry gives you mosaic_type, filename, size_bytes, and the storage gcs_path.
POST /v1/op/clip.job.downloads HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
{ "job_id": "77a1c9e2-4b3d-4f5a-8c6b-9d0e1f2a3b4c" }HTTP/1.1 200 OK
Content-Type: application/json
[
{
"mosaic_type": "rgb",
"filename": "harbour_clip_rgb.tif",
"size_bytes": 18234112,
"gcs_path": "clips/77a1.../harbour_clip_rgb.tif"
}
]To fetch the actual file, call clip.job.download with the job_id and the mosaic_type you want. This operation streams the COG bytes with Content-Type: image/tiff and a Content-Disposition filename, and is egress-enforced: per-asset rate limits and a circuit breaker apply, so a throttled request returns 429. Calling clip.job.downloads or clip.job.download before the job is completed returns 400.
POST /v1/op/clip.job.download HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_...
Content-Type: application/json
{ "job_id": "77a1c9e2-4b3d-4f5a-8c6b-9d0e1f2a3b4c", "mosaic_type": "rgb" }Worked example
Create a clip, poll it to completion, and download the first output.
Python
import os
import time
from geopera import Client
client = Client(api_key=os.environ["GEOPERA_API_KEY"])
aoi = {
"type": "Polygon",
"coordinates": [
[[151.20, -33.86], [151.24, -33.86], [151.24, -33.84], [151.20, -33.84], [151.20, -33.86]]
],
}
created = client.op(
"clip.create_from_item",
{
"source_item_id": "3a9c7f10-2b44-4d11-9c8e-1f2a3b4c5d6e",
"project_id": "b21e6d44-7c0a-4e2f-9a11-8c7d6e5f4a3b",
"aoi_geometry": aoi,
"asset_keys": ["data"],
"output_name": "harbour_clip",
},
idempotency_key="4e1c8b2a-9f0d-4c3e-bf6a-0a1b2c3d4e5f",
)
job_id = created["job"]["id"]
# Poll until completed.
while True:
job = client.op("clip.job.get", {"job_id": job_id})
if job["status"] in ("completed", "failed"):
break
time.sleep(5)
if job["status"] != "completed":
raise RuntimeError(f"clip job {job_id} ended as {job['status']}")
downloads = client.op("clip.job.downloads", {"job_id": job_id})
first = downloads[0]
print(f"{first['filename']} — {first['size_bytes']} bytes ({first['mosaic_type']})")
# Stream the actual COG bytes.
data = client.op_raw(
"clip.job.download",
{"job_id": job_id, "mosaic_type": first["mosaic_type"]},
)
with open(first["filename"], "wb") as fh:
fh.write(data)TypeScript
import { Geopera } from '@geopera/sdk';
import { writeFile } from 'node:fs/promises';
const client = new Geopera({ apiKey: process.env.GEOPERA_API_KEY! });
const aoi = {
type: 'Polygon',
coordinates: [
[
[151.2, -33.86],
[151.24, -33.86],
[151.24, -33.84],
[151.2, -33.84],
[151.2, -33.86]
]
]
};
const created = await client.op(
'clip.create_from_item',
{
source_item_id: '3a9c7f10-2b44-4d11-9c8e-1f2a3b4c5d6e',
project_id: 'b21e6d44-7c0a-4e2f-9a11-8c7d6e5f4a3b',
aoi_geometry: aoi,
asset_keys: ['data'],
output_name: 'harbour_clip'
},
{ idempotencyKey: '4e1c8b2a-9f0d-4c3e-bf6a-0a1b2c3d4e5f' }
);
const jobId = created.job.id;
let job;
for (;;) {
job = await client.op('clip.job.get', { job_id: jobId });
if (job.status === 'completed' || job.status === 'failed') break;
await new Promise((r) => setTimeout(r, 5000));
}
if (job.status !== 'completed') {
throw new Error(`clip job ${jobId} ended as ${job.status}`);
}
const downloads = await client.op('clip.job.downloads', { job_id: jobId });
const first = downloads[0];
const bytes = await client.opRaw('clip.job.download', {
job_id: jobId,
mosaic_type: first.mosaic_type
});
await writeFile(first.filename, Buffer.from(bytes));curl
JOB_ID=$(curl -s -X POST https://api.geopera.com/v1/op/clip.create_from_item \
-H "Authorization: Bearer $GEOPERA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 4e1c8b2a-9f0d-4c3e-bf6a-0a1b2c3d4e5f" \
-d '{
"source_item_id": "3a9c7f10-2b44-4d11-9c8e-1f2a3b4c5d6e",
"project_id": "b21e6d44-7c0a-4e2f-9a11-8c7d6e5f4a3b",
"aoi_geometry": {"type":"Polygon","coordinates":[[[151.20,-33.86],[151.24,-33.86],[151.24,-33.84],[151.20,-33.84],[151.20,-33.86]]]},
"asset_keys": ["data"],
"output_name": "harbour_clip"
}' | jq -r '.job.id')
# Poll until completed.
until [ "$(curl -s -X POST https://api.geopera.com/v1/op/clip.job.get \
-H "Authorization: Bearer $GEOPERA_API_KEY" -H "Content-Type: application/json" \
-d "{\"job_id\":\"$JOB_ID\"}" | jq -r '.status')" = "completed" ]; do sleep 5; done
# List downloadable files, then stream the rgb output to disk.
curl -s -X POST https://api.geopera.com/v1/op/clip.job.downloads \
-H "Authorization: Bearer $GEOPERA_API_KEY" -H "Content-Type: application/json" \
-d "{\"job_id\":\"$JOB_ID\"}"
curl -s -X POST https://api.geopera.com/v1/op/clip.job.download \
-H "Authorization: Bearer $GEOPERA_API_KEY" -H "Content-Type: application/json" \
-d "{\"job_id\":\"$JOB_ID\",\"mosaic_type\":\"rgb\"}" \
-o harbour_clip_rgb.tifEdge cases and gotchas
- The create call returns
running, not the file. It only reserves credits and dispatches the warp. You must pollclip.job.getand then call the download operations. - Idempotency-Key is mandatory.
clip.create_from_itemisexternal_spend. Reuse the same key on a retry so you never double-charge or double-dispatch. See Idempotency. - No raster assets resolved →
400. If the source item has no raster assets (or none matchasset_keys), the create call fails with a400problem. - Project has no storage bucket →
400. The outputproject_idmust have a configured storage bucket. - Quota exceeded →
402. If reserving compute units would exceed your quota, the create call returns402 Payment Requiredwith the used/limit counts in the detail. - Cross-org access →
403. Your org must own both the output project and the source item’s project; the charged org always comes from the token, never the body. - Downloading before completion →
400.clip.job.downloads,clip.job.download, andclip.job.tile_inforequirestatus == "completed". - Egress limits →
429.clip.job.downloadis rate-limited per asset and guarded by a circuit breaker; back off and retry on429.
All errors are RFC 9457 application/problem+json. See Errors for the schema and Rate limits for 429 handling. For authentication and token formats, see Authentication.