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 payloadsitems.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 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:

FieldTypeNotes
collectionsstring[]Restrict to these collection ids
idsstring[]Restrict to these STAC item ids
datetimestringSingle instant, or an open/closed interval start/end
bboxnumber[]Exactly 4 elements [west, south, east, north]
intersectsGeoJSONA GeoJSON Polygon; max 999 vertices
filterobjectCQL2-JSON filter expression
filter-langstringDefaults to cql2-json; other values are rejected
sortbyobject[]{ "field": "...", "direction": "asc" \| "desc" }
limitinteger1–10000, default 10
pageintegerZero-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.

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.

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.

OperationPurpose
processing.catalog.listList available processes (OGC process catalog)
processing.catalog.getDescribe one process by process_id
processing.catalog.validateValidate inputs against a process
processing.catalog.estimateEstimate the cost of a run
processing.executeReserve credits and dispatch a job
processing.job.getFetch a job by id (org-scoped)
processing.jobs.listList 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. 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.

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 pathForwards to
POST /assets/stac/searchstac.search
GET /assets/stac/collectionsstac.collections.list
GET /api/v1/items/{id}/tiles/WMTSCapabilities.xmlitems.tile.wmts_capabilities
GET /api/v1/items/{id}/tiles/wmtsitems.tile.wmts_get_tile
GET /api/v1/items/{id}/tiles/tilejson.jsonitems.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, errors, idempotency, and 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 (geopera) and TypeScript (@geopera/sdk) clients and the geopera CLI. For the authoritative request and response schema of every operation referenced here, see Operations.