<!-- Source: https://docs.geopera.com/api-reference/stac-and-ogc · Markdown for LLMs -->

# STAC & OGC compatibility

Geopera speaks the open geospatial standards your tools already know — STAC 1.0.0, OGC WMTS, and OGC API - Processes — while keeping every capability behind the same typed `POST /v1/op/{operation_id}` envelope as the rest of the platform.

## How compatibility works

The kernel is RPC-over-HTTP: there are no GET/PUT/DELETE verbs and no path or query parameters for operations. Standards compatibility is therefore delivered two ways. First, **kernel operations return standards-shaped payloads** — `items.get_stac` hands back a STAC Feature, `stac.search` returns a STAC `FeatureCollection`, `items.tile.wmts_capabilities` returns OGC WMTS XML. Second, a thin **provider-compatible REST facade** is mounted at clean root paths (e.g. `/assets/stac/search`, `/api/v1/items/{id}/tiles/...`, `/processing`) that simply builds the caller principal and forwards to the very same operations, so legacy STAC/WMTS/UP42 clients work unchanged. Every example below shows the canonical kernel call; see [Operations](/api-reference/operations) for the exact per-op request and response schemas.

## STAC 1.0.0

The catalog is STAC 1.0.0 compliant. Items are GeoJSON `Feature` objects, Collections are STAC `Collection` objects, and search follows the STAC API item-search shape including CQL2-JSON filters.

### Items as STAC Features

`items.get_stac` returns one catalog item as a full STAC 1.0.0 `Feature` with embedded `assets` and `links`. The response media type is `application/geo+json`.

```bash
curl -X POST https://api.geopera.com/v1/op/items.get_stac \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": "itm_8f3c0a2e",
    "base_url": "https://api.geopera.com"
  }'
```

`item_id` is required; `base_url` is optional and only controls the absolute hrefs written into the item's `links`. The returned feature carries the standard STAC fields plus Geopera-namespaced provenance claims projected at read time:

```json
{
	"type": "Feature",
	"stac_version": "1.0.0",
	"stac_extensions": [
		"https://api.geopera.com/stac-extensions/geopera-provenance/v1.0.0/schema.json"
	],
	"id": "S2B_MSIL2A_20240611",
	"collection": "col_sentinel2",
	"geometry": { "type": "Polygon", "coordinates": [] },
	"bbox": [11.1, 46.6, 11.9, 47.1],
	"properties": {
		"datetime": "2024-06-11T10:12:19Z",
		"eo:cloud_cover": 4.2,
		"gsd": 10,
		"geopera:source": "TASKING",
		"geopera:item_id": "itm_8f3c0a2e"
	},
	"assets": {
		"cog": {
			"href": "https://api.geopera.com/...",
			"type": "image/tiff; application=geotiff",
			"roles": ["data"]
		}
	},
	"links": []
}
```

The `geopera:source` claim is derived from the item's origin: `ARCHIVE` for uploaded items, `PROCESSING` for items produced by a processing job, and `TASKING` otherwise. These claims are projected on read and never stored on the item.

### Collections

`stac.collections.list` returns the org-scoped collections envelope — `{ "collections": [...], "links": [...] }` — where each entry is a minimal valid STAC `Collection`. The optional `base_url` builds the `self` / `root` / `items` links. The operation is organization-scoped: a principal with no organization receives `403`.

```bash
curl -X POST https://api.geopera.com/v1/op/stac.collections.list \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "base_url": "https://api.geopera.com" }'
```

### Search with CQL2-JSON, bbox, intersects, and datetime

`stac.search` performs a cross-collection, organization-scoped search and returns a STAC `FeatureCollection`:

```json
{ "type": "FeatureCollection", "features": [], "links": [], "context": {} }
```

The body is a subset of the STAC API item-search schema:

| Field         | Type       | Notes                                                  |
| ------------- | ---------- | ------------------------------------------------------ |
| `collections` | `string[]` | Restrict to these collection ids                       |
| `ids`         | `string[]` | Restrict to these STAC item ids                        |
| `datetime`    | `string`   | Single instant, or an open/closed interval `start/end` |
| `bbox`        | `number[]` | Exactly 4 elements `[west, south, east, north]`        |
| `intersects`  | GeoJSON    | A GeoJSON `Polygon`; max 999 vertices                  |
| `filter`      | object     | CQL2-JSON filter expression                            |
| `filter-lang` | `string`   | Defaults to `cql2-json`; other values are rejected     |
| `sortby`      | object[]   | `{ "field": "...", "direction": "asc" \| "desc" }`     |
| `limit`       | integer    | 1–10000, default 10                                    |
| `page`        | integer    | Zero-based page index, default 0                       |

A spatial + temporal + CQL2 search:

```bash
curl -X POST https://api.geopera.com/v1/op/stac.search \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "collections": ["col_sentinel2"],
    "datetime": "2024-06-01T00:00:00Z/2024-06-30T23:59:59Z",
    "bbox": [11.1, 46.6, 11.9, 47.1],
    "filter": {
      "op": "<=",
      "args": [ { "property": "eo:cloud_cover" }, 10 ]
    },
    "sortby": [ { "field": "datetime", "direction": "desc" } ],
    "limit": 50
  }'
```

Gotchas worth knowing:

- **`bbox` and `intersects` are mutually exclusive.** Sending both returns `400`.
- **`bbox` must be exactly 4 numbers** and `intersects` must be a GeoJSON `Polygon` — otherwise `422`.
- **`filter-lang` must be `cql2-json`** (the default). Any other value with a `filter` present returns `400`.
- **Spatial search fails closed.** Invalid geometry yields zero results rather than an error, so a malformed AOI never widens the result set.
- **Pagination is page-based** via `limit` + `page` (zero-based). The `context` block reports `returned`, `matched`, `page`, and `pages`. See [Pagination](/api-reference/pagination).

A per-project, STAC-compatible variant (`items.search`) exists for project-member access; use `stac.search` for cross-collection, org-scoped queries. Errors follow [RFC 9457 problem+json](/api-reference/errors).

## WMTS tile access

The kernel exposes OGC WMTS 1.0.0 for any catalog item through two operations. A WMTS client points at the GetCapabilities document; the embedded `GetTile` template then drives tile requests.

### GetCapabilities

`items.tile.wmts_capabilities` returns the OGC WMTS 1.0.0 `Capabilities` XML for one item (`SupportedCRS: urn:ogc:def:crs:EPSG::3857`). `item_id` and `base_url` are required; `key` and `profile` are optional and, when present, are threaded into the advertised `GetTile` href so authenticated map clients keep working.

```bash
curl -X POST https://api.geopera.com/v1/op/items.tile.wmts_capabilities \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "item_id": "itm_8f3c0a2e",
    "base_url": "https://api.geopera.com",
    "profile": "true_color"
  }'
```

### GetTile

`items.tile.wmts_get_tile` is the KVP GetTile call. `item_id`, `z`, `x`, and `y` are required; `profile` selects the visualization. The response is image bytes. WMTS tiles are profile-based and revalidatable, and on a render error the operation returns a transparent tile rather than surfacing an error to the map.

```bash
curl -X POST https://api.geopera.com/v1/op/items.tile.wmts_get_tile \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "item_id": "itm_8f3c0a2e", "z": 12, "x": 2138, "y": 1453, "profile": "true_color" }'
```

For non-WMTS map clients the kernel also exposes `items.tile.tilejson` (TileJSON metadata) and `items.tile.render` (a single XYZ tile with `format`, `bands`, `expression`, `rescale`, and `colormap` controls). The provider facade mounts these at the conventional `/api/v1/items/{id}/tiles/...` paths so existing XYZ and WMTS map clients consume them with no code change.

## OGC API - Processing

Processing aligns with OGC API - Processes: an OGC-style process catalog plus an execution path that creates a job.

| Operation                     | Purpose                                        |
| ----------------------------- | ---------------------------------------------- |
| `processing.catalog.list`     | List available processes (OGC process catalog) |
| `processing.catalog.get`      | Describe one process by `process_id`           |
| `processing.catalog.validate` | Validate inputs against a process              |
| `processing.catalog.estimate` | Estimate the cost of a run                     |
| `processing.execute`          | Reserve credits and dispatch a job             |
| `processing.job.get`          | Fetch a job by id (org-scoped)                 |
| `processing.jobs.list`        | List jobs (org-scoped)                         |

`processing.catalog.list` and `processing.catalog.get` are the OGC-style public process catalog — mirroring an OGC `GET /processes` and `GET /processes/{id}`. Both are catalog reads requiring the `processing:read` scope:

```bash
curl -X POST https://api.geopera.com/v1/op/processing.catalog.get \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "process_id": "ndvi" }'
```

`processing.execute` is the OGC `/execution` equivalent: it reserves credits, dispatches the job, and emits a processing-job artifact. `process_id`, `workspaceId`, and an `inputs` object are the body:

```bash
curl -X POST https://api.geopera.com/v1/op/processing.execute \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "process_id": "ndvi",
    "workspaceId": "ws_4a1b",
    "inputs": { "item_id": "itm_8f3c0a2e" }
  }'
```

Because `processing.execute` spends credits, it is subject to budget and idempotency controls — always send an `Idempotency-Key` so a retried execution does not double-dispatch. See [Idempotency](/api-reference/idempotency). Poll the returned job with `processing.job.get`. The required scope is `processing:process` for execution and `processing:read` for catalog and job reads — see [Scopes](/api-reference/scopes).

## Provider-compatible REST facade

Legacy STAC, WMTS, and UP42-style clients that cannot speak the RPC envelope are served by a thin REST facade mounted at clean root paths. These routes do not contain business logic: each builds the caller principal from the `Bearer` token and forwards to the corresponding kernel operation, so the wire payloads are byte-identical to the canonical operations above.

| Facade path                                         | Forwards to                    |
| --------------------------------------------------- | ------------------------------ |
| `POST /assets/stac/search`                          | `stac.search`                  |
| `GET /assets/stac/collections`                      | `stac.collections.list`        |
| `GET /api/v1/items/{id}/tiles/WMTSCapabilities.xml` | `items.tile.wmts_capabilities` |
| `GET /api/v1/items/{id}/tiles/wmts`                 | `items.tile.wmts_get_tile`     |
| `GET /api/v1/items/{id}/tiles/tilejson.json`        | `items.tile.tilejson`          |
| `GET /api/v1/items/{id}/tiles/{z}/{x}/{y}.{format}` | `items.tile.render`            |
| `/processing` (OGC API - Processes)                 | `processing.*` operations      |

This facade exists purely for third-party tool compatibility. For everything you build yourself, call the operations directly: the typed envelope gives you consistent [authentication](/api-reference/authentication), [errors](/api-reference/errors), [idempotency](/api-reference/idempotency), and [rate limits](/api-reference/rate-limits) across all 227 operations.

## Worked example: discover, inspect, and render

A complete loop — search the catalog, fetch a STAC Feature, then pull a tile — using the kernel envelope end to end.

```python
import os
import httpx

BASE = "https://api.geopera.com/v1/op"
HEADERS = {"Authorization": f"Bearer {os.environ['GEOPERA_TOKEN']}"}


def op(name: str, body: dict) -> httpx.Response:
    return httpx.post(f"{BASE}/{name}", json=body, headers=HEADERS, timeout=30)


# 1. Cross-collection STAC search: low-cloud scenes over an AOI in June 2024.
search = op("stac.search", {
    "collections": ["col_sentinel2"],
    "datetime": "2024-06-01T00:00:00Z/2024-06-30T23:59:59Z",
    "bbox": [11.1, 46.6, 11.9, 47.1],
    "filter": {"op": "<=", "args": [{"property": "eo:cloud_cover"}, 10]},
    "sortby": [{"field": "datetime", "direction": "desc"}],
    "limit": 1,
}).json()

feature = search["features"][0]
item_id = feature["properties"]["geopera:item_id"]
print("matched", search["context"]["matched"], "scenes; using", item_id)

# 2. Fetch the full STAC 1.0.0 Feature (with embedded assets + links).
stac_item = op("items.get_stac", {
    "item_id": item_id,
    "base_url": "https://api.geopera.com",
}).json()
print("assets:", list(stac_item["assets"].keys()))

# 3. Render a single true-color XYZ tile for that item.
tile = op("items.tile.render", {
    "item_id": item_id,
    "z": 12, "x": 2138, "y": 1453,
    "format": "webp",
    "profile": "true_color",
})
with open("tile.webp", "wb") as fh:
    fh.write(tile.content)
print("wrote", len(tile.content), "bytes")
```

The same three calls are available through the [Python](/api-reference/sdks) (`geopera`) and TypeScript (`@geopera/sdk`) clients and the `geopera` CLI. For the authoritative request and response schema of every operation referenced here, see [Operations](/api-reference/operations).
