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 payloads — items.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.
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:
{
"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.
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:
{
"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:
| 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; rejected only when a filter is sent |
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:
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
FeatureCollectioncomes back withassets: {}and a singleselflink. To get the embedded assets and full link set, fetch the item withitems.get_stac(as the worked example does). bboxandintersectsare mutually exclusive. Sending both returns400.bboxmust be exactly 4 numbers andintersectsmust be a GeoJSONPolygonof at most 999 vertices — otherwise422.filter-langmust becql2-json(the default). Any other value is rejected with400only when afilteris 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 emptyFeatureCollection. - Pagination is page-based via
limit+page(zero-based). Thecontextblock reportsreturned,matched,page, andpages. 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:
- Summary lineage — the source collections a mosaic was built from are stored in
geopera:source_collectionsand projected as standard STACderived_fromlinks. This is a handful of links, not thousands of inline scene links, so it stays cheap to read. The compositing method is reported ingeopera:mosaic_method(brightest,median,first, …) and the human label ingeopera:mosaic_name. - Per-pixel provenance — an optional
provenanceband 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 asclassification:classeswhen the mosaic draws on 256 or fewer scenes; above that it moves to asource_lookupsidecar asset (registered viasource_lookup_asset) to keep the Item small.geopera:source_scene_countreports the number of contributing scenes. - Full scene manifest — an optional
source_manifestSTAC-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:
{
"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_rolesis 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:classesinproperties— one entry per contributing scene, so its length equalsgeopera:source_scene_count; above 256 you get thesource_lookupsidecar asset instead and noclassification: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:
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:
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" }
}'| Field | Type | Required | Notes |
|---|---|---|---|
project_id | string | yes | Target project (Editor/admin) |
name | string | yes | 1–500 chars; becomes geopera:mosaic_name |
stac_id | string | — | Sets the STAC Feature id; otherwise one is generated |
data_asset | asset | yes | The mosaic COG (href, optional media_type / roles / title / band_count) |
mosaic_method | string | — | Compositing method → geopera:mosaic_method |
source_collections | string[] | — | Summary lineage → derived_from links |
provenance_asset | asset | — | Per-pixel source-index band |
source_lookup | object[] | — | Integer→scene list; inlined as classification:classes when ≤256 scenes |
source_lookup_asset | asset | — | Sidecar lookup, used instead of inline when the list is large |
manifest_asset | asset | — | STAC-GeoParquet scene manifest sidecar |
geometry / bbox | GeoJSON / array | — | Mosaic footprint |
datetime / start_datetime / end_datetime | string | — | Acquisition window |
collection_id | string | — | Register into this collection instead of the auto Mosaics collection |
properties | object | — | Extra 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.
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.
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.
| 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:
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:
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.
| Endpoint | Maps to |
|---|---|
GET /assets/stac/ | STAC landing Catalog |
GET /assets/stac/conformance | OGC API conformance classes |
GET /assets/stac/queryables | CQL2 queryable-fields schema |
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 |
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.
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.