<!-- Source: https://docs.geopera.com/api-reference/guides/clipping · Markdown for LLMs -->

# 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](/api-reference/operations) and [Concepts](/api-reference/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](/api-reference/scopes) for how grants map to operations.

## Lifecycle

A clip job moves through three phases:

1. **Create / execute** — `clip.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](/api-reference/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`.

```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](/api-reference/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](/api-reference/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](/api-reference/errors) for the schema and [Rate limits](/api-reference/rate-limits) for `429` handling. For authentication and token formats, see [Authentication](/api-reference/authentication).
