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.

OperationSide-effectScopeUse
clip.create_from_itemexternal_spendprocessing:processClip a source item’s assets to an AOI
clip.create_from_areaexternal_spendclip:writeClip a delivery-bucket area + mosaics to an AOI
clip.jobs.listreadclip:readList clip jobs for an organization
clip.job.getreadclip:readGet a single clip job by id
clip.job.downloadsreadclip:readList downloadable files for a completed job
clip.job.downloadreadclip:readStream one clip file (egress-enforced)
clip.job.tile_inforeadclip:readTile-rendering params for a completed job
clip.job.deletedestructiveclip:destroyDelete 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:

  1. Create / executeclip.create_from_item resolves the source item’s raster assets, reserves compute units atomically, creates a cog_clip job, dispatches the GDAL warp, and returns immediately with the job already marked running.
  2. Poll — call clip.job.get (or clip.jobs.list) until status is completed.
  3. Retrieve — call clip.job.downloads for a descriptor of each output file, then clip.job.download to 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

FieldTypeRequiredDescription
source_item_idstringyesItem whose raster assets are clipped
project_idstringyesProject that receives the output (must belong to your org)
aoi_geometryGeoJSON geometryyesPolygon/MultiPolygon { "type", "coordinates" } to clip to
asset_keysstring[]noRestrict to specific asset keys; omit to clip all raster assets
aoi_namestringnoLabel for the AOI
output_namestringnoName for the output
target_collection_idstringnoCollection 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.

http
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
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.

http
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
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.

http
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.

http
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
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.

http
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

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

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

bash
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.tif

Edge cases and gotchas

  • The create call returns running, not the file. It only reserves credits and dispatches the warp. You must poll clip.job.get and then call the download operations.
  • Idempotency-Key is mandatory. clip.create_from_item is external_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 match asset_keys), the create call fails with a 400 problem.
  • Project has no storage bucket → 400. The output project_id must have a configured storage bucket.
  • Quota exceeded → 402. If reserving compute units would exceed your quota, the create call returns 402 Payment Required with 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, and clip.job.tile_info require status == "completed".
  • Egress limits → 429. clip.job.download is rate-limited per asset and guarded by a circuit breaker; back off and retry on 429.

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.