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

Geopera 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, 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, the conventional STAC, WMTS, and OGC API REST routes are served at clean root paths (e.g. /assets/stac/search, /api/v1/items/{id}/tiles/..., /processing), each mapping to the same operation, so off-the-shelf STAC, WMTS, and OGC API clients work unchanged. Every example below shows the canonical operation 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 controls the absolute hrefs written into the item’s links. When you supply it, the feature carries the standard self / root / parent / collection links; omit it and links comes back empty. 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/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:item_id": "itm_8f3c0a2e",
		"geopera:item_type": "satellite_capture",
		"geopera:name": "S2B_MSIL2A_20240611",
		"geopera:lifecycle_state": "active",
		"geopera:source": "TASKING"
	},
	"assets": {
		"cog": {
			"href": "https://api.geopera.com/...",
			"type": "image/tiff; application=geotiff",
			"roles": ["data"]
		}
	},
	"links": [
		{
			"rel": "self",
			"href": "https://api.geopera.com/items/itm_8f3c0a2e/stac",
			"type": "application/geo+json"
		},
		{ "rel": "root", "href": "https://api.geopera.com/", "type": "application/json" },
		{
			"rel": "parent",
			"href": "https://api.geopera.com/collections/col_sentinel2",
			"type": "application/json"
		},
		{
			"rel": "collection",
			"href": "https://api.geopera.com/collections/col_sentinel2",
			"type": "application/json"
		}
	]
}

The geopera: claims are projected on read and never stored on the item. Every Feature carries the same identity set — geopera:item_id, geopera:project_id, geopera:order_id, geopera:upload_id, geopera:job_id, geopera:item_type, geopera:name, geopera:lifecycle_state, and geopera:source (a few are elided above for brevity). 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.

stac_extensions can list more than one URI. The single geopera extension above is always present; an item also keeps any extensions it was created with, and richer products (notably mosaics) add the standard processing, raster, and classification extensions.

Lineage is standard STAC. When an item records the source collections it was derived from (in geopera:source_collections), each one is projected as a real STAC derived_from link, so downstream tools read provenance the standard way. Mosaics are the primary producer of these links — see below.

Collections

stac.collections.list returns the org-scoped collections envelope — { "collections": [...], "links": [...] } — where each entry is a minimal valid STAC Collection (license proprietary, a world spatial extent, an open temporal interval, and the standard self / root / parent / items links). The optional base_url builds those 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": { "limit": 10, "returned": 0, "matched": 0, "page": 0, "pages": 0 }
}

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; rejected only when a filter is sent
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:

  • Search results are lightweight. Each feature in the FeatureCollection comes back with assets: {} and a single self link. To get the embedded assets and full link set, fetch the item with items.get_stac (as the worked example does).
  • bbox and intersects are mutually exclusive. Sending both returns 400.
  • bbox must be exactly 4 numbers and intersects must be a GeoJSON Polygon of at most 999 vertices — otherwise 422.
  • filter-lang must be cql2-json (the default). Any other value is rejected with 400 only when a filter is also present.
  • Spatial search fails closed. A runtime spatial failure yields zero results rather than an error, so a malformed AOI never widens the result set. (Structurally invalid geometry is rejected up front with 422.)
  • Both reads are org-scoped. A principal with no organization gets 403; an organization with no projects gets an empty FeatureCollection.
  • 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.

STAC mosaics

A mosaic is a single seamless raster stitched from many scenes — Geopera mosaics any multi-scene order by default (see Mosaicking for the why). Geopera delivers a mosaic as a first-class STAC mosaic item: it searches, tiles, and renders exactly like any other Feature, and it carries a standards-shaped record of where every pixel came from.

The three-layer provenance model

A mosaic can draw on anything from a handful of scenes to thousands, so its lineage is delivered in three layers of increasing fidelity. A mosaic includes whichever layers apply to it:

  1. Summary lineage — the source collections a mosaic was built from are stored in geopera:source_collections and projected as standard STAC derived_from links. This is a handful of links, not thousands of inline scene links, so it stays cheap to read. The compositing method is reported in geopera:mosaic_method (brightest, median, first, …) and the human label in geopera:mosaic_name.
  2. Per-pixel provenance — an optional provenance band asset (tag it with roles ["data", "provenance"] so clients can pick it out) encodes which source scene each pixel came from as an integer index. The integer→scene lookup is delivered inline as classification:classes when the mosaic draws on 256 or fewer scenes; above that it moves to a source_lookup sidecar asset (registered via source_lookup_asset) to keep the Item small. geopera:source_scene_count reports the number of contributing scenes.
  3. Full scene manifest — an optional source_manifest STAC-GeoParquet sidecar asset (roles ["metadata", "source-manifest"]) holds one row per contributing scene, for the complete, auditable scene list. It is referenced, never inlined, so the Item stays small at thousands of rows.

A mosaic declares the processing and geopera extensions, and adds raster and classification when the corresponding provenance layers are present.

A mosaic as a STAC Feature

Fetched with items.get_stac, a three-band mosaic with inline per-pixel provenance and a scene manifest looks like this:

json
{
	"type": "Feature",
	"stac_version": "1.0.0",
	"stac_extensions": [
		"https://stac-extensions.github.io/processing/v1.2.0/schema.json",
		"https://api.geopera.com/stac-extensions/geopera/v1.0.0/schema.json",
		"https://stac-extensions.github.io/raster/v1.1.0/schema.json",
		"https://stac-extensions.github.io/classification/v1.1.0/schema.json"
	],
	"id": "mosaic_brisbane_q2_2025",
	"collection": "col_mosaics",
	"geometry": { "type": "Polygon", "coordinates": [] },
	"bbox": [152.6, -27.7, 153.4, -27.0],
	"properties": {
		"geopera:item_type": "mosaic",
		"geopera:mosaic_name": "Q2 2025 cloud-free composite — Brisbane",
		"geopera:mosaic_method": "brightest",
		"geopera:source_collections": ["col_sentinel2"],
		"geopera:source_scene_count": 2,
		"geopera:data_type": "raster",
		"band_roles": { "RED": 1, "GREEN": 2, "BLUE": 3 },
		"start_datetime": "2025-04-01T00:00:00Z",
		"end_datetime": "2025-06-30T23:59:59Z",
		"classification:classes": [
			{ "value": 1, "name": "S2B_MSIL2A_20250412", "description": "col_sentinel2" },
			{ "value": 2, "name": "S2A_MSIL2A_20250517", "description": "col_sentinel2" }
		]
	},
	"assets": {
		"data": {
			"href": "https://api.geopera.com/...",
			"type": "image/tiff; application=geotiff; profile=cloud-optimized",
			"roles": ["data"]
		},
		"provenance": {
			"href": "https://api.geopera.com/...",
			"type": "image/tiff; application=geotiff; profile=cloud-optimized",
			"roles": ["data", "provenance"]
		},
		"source_manifest": {
			"href": "https://api.geopera.com/...",
			"type": "application/x-parquet",
			"title": "Contributing scene manifest (STAC-GeoParquet)",
			"roles": ["metadata", "source-manifest"]
		}
	},
	"links": [{ "rel": "derived_from", "href": "col_sentinel2", "type": "application/json" }]
}

Notes on the shape:

  • band_roles is only emitted for an exactly-three-band mosaic (and only when not supplied explicitly), so a natural-colour default is offered without ever mislabelling a four-band (e.g. NIR-first) composite as RGB.
  • The lookup is inline or a sidecar, never both. With 256 or fewer scenes you get classification:classes in properties — one entry per contributing scene, so its length equals geopera:source_scene_count; above 256 you get the source_lookup sidecar asset instead and no classification:classes.
  • Like every catalog item, a mosaic also carries the projected geopera: identity keys shown under Items as STAC Features; they are elided here to keep the focus on the mosaic-specific fields.

Discovering and rendering mosaics

Mosaics live in an auto-created Mosaics collection (unless you register them into a collection of your own), so they surface in stac.collections.list and collections.list alongside your scene collections. Find them by collection or by filtering on geopera:item_type:

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_mosaics"],
    "bbox": [152.6, -27.7, 153.4, -27.0],
    "sortby": [ { "field": "datetime", "direction": "desc" } ]
  }'

The mosaic’s data asset renders through the same tile operations as any item — items.tile.render, items.tile.tilejson, and the WMTS pair below. The provenance band has its own visualization: render it with items.tile.render and viz_id=provenance to get a categorical “which scene is this pixel from” map, coloured by the same integer→scene lookup carried in classification:classes.

Registering a mosaic

mosaic.register turns a finished composite into a searchable STAC mosaic item. It requires the items:write scope and Editor or admin access on the target project, and it references the mosaic COG by href (it does not copy bytes). Supply the data asset plus whichever provenance layers you have:

bash
curl -X POST https://api.geopera.com/v1/op/mosaic.register \
  -H "Authorization: Bearer $GEOPERA_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "project_id": "prj_4a1b",
    "name": "Q2 2025 cloud-free composite — Brisbane",
    "mosaic_method": "brightest",
    "bbox": [152.6, -27.7, 153.4, -27.0],
    "start_datetime": "2025-04-01T00:00:00Z",
    "end_datetime": "2025-06-30T23:59:59Z",
    "source_collections": ["col_sentinel2"],
    "data_asset": { "href": "https://.../mosaic.tif", "band_count": 3 },
    "provenance_asset": { "href": "https://.../provenance.tif", "roles": ["data", "provenance"] },
    "source_lookup": [
      { "value": 1, "scene_id": "S2B_MSIL2A_20250412", "collection": "col_sentinel2" },
      { "value": 2, "scene_id": "S2A_MSIL2A_20250517", "collection": "col_sentinel2" }
    ],
    "manifest_asset": { "href": "https://.../manifest.parquet" }
  }'
FieldTypeRequiredNotes
project_idstringyesTarget project (Editor/admin)
namestringyes1–500 chars; becomes geopera:mosaic_name
stac_idstringSets the STAC Feature id; otherwise one is generated
data_assetassetyesThe mosaic COG (href, optional media_type / roles / title / band_count)
mosaic_methodstringCompositing method → geopera:mosaic_method
source_collectionsstring[]Summary lineage → derived_from links
provenance_assetassetPer-pixel source-index band
source_lookupobject[]Integer→scene list; inlined as classification:classes when ≤256 scenes
source_lookup_assetassetSidecar lookup, used instead of inline when the list is large
manifest_assetassetSTAC-GeoParquet scene manifest sidecar
geometry / bboxGeoJSON / arrayMosaic footprint
datetime / start_datetime / end_datetimestringAcquisition window
collection_idstringRegister into this collection instead of the auto Mosaics collection
propertiesobjectExtra properties merged into the item; reserved geopera: keys (mosaic_name / mosaic_method / source_collections) are set by the operation and take precedence

mosaic.register spends no credits but does create an item, so send an Idempotency-Key to make retries safe — see Idempotency. It emits an item.created event and returns the created item; from then on the mosaic is another STAC item you can search, fetch, and render.

WMTS tile access

Geopera 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 Geopera also exposes items.tile.tilejson (TileJSON metadata) and items.tile.render (a single XYZ tile). items.tile.render takes format, bands, expression, rescale, and colormap controls, plus a profile or viz_id selector to pick a saved or live visualization (the two are mutually exclusive — supplying both returns 400). items.tile.statistics returns histogram/quantile stats for an asset, and items.tile.preview returns a rendered tile inline as base64 for AI/MCP clients. These tile operations are also served 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.

Standard STAC, WMTS & OGC endpoints

Geopera also serves the conventional STAC, WMTS, and OGC API REST routes at clean root paths, for off-the-shelf tools that expect them. Each route authenticates with the same Bearer token and maps to the matching operation, so its wire payload is byte-identical to the canonical operation above.

EndpointMaps to
GET /assets/stac/STAC landing Catalog
GET /assets/stac/conformanceOGC API conformance classes
GET /assets/stac/queryablesCQL2 queryable-fields schema
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

The landing, conformance, and queryables endpoints make the catalog self-describing for STAC API clients: the landing Catalog advertises its conformsTo classes, and queryables returns the JSON Schema of fields you can filter on with CQL2-JSON. These routes are there for off-the-shelf tooling; for everything you build yourself, call the operations directly — the typed envelope gives you consistent authentication, errors, idempotency, and rate limits across every operation.

Worked example: discover, inspect, and render

A complete loop — search the catalog, fetch a STAC Feature, then pull a tile — using the operation 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 (search results carry no assets — get_stac does).
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.