Managing data

Every piece of imagery the platform delivers lands as an item with one or more assets, optionally grouped into a collection, and all of it is reached through typed operations invoked as POST /v1/op/{operation_id} with a JSON body — there are no REST verbs or path parameters. This guide covers reading, organizing, editing, and removing that data; see Operations for the invocation model and Concepts for how items, collections, and assets relate.

The data model

NounWhat it isIdentified by
ItemA single catalog record (a STAC Feature) — one scene, mosaic, or derived layer.item_id
CollectionA named grouping of items within a project, with map display order and colour.collection_id
AssetA file attached to an item (the COG, a thumbnail, sidecar metadata).asset_id

An item always belongs to exactly one project. It may belong to zero or one collection (collection_id is nullable). Deleting a collection unlinks its items rather than deleting them — see Soft delete and expiry.

Every read operation gates on membership: you must be a member of the item’s or collection’s project (or its parent organization) to see it. Writes additionally require an editor or admin role; collection deletion requires admin/owner. See Scopes for the full matrix.

Operations at a glance

Items

OperationSide effectScopePurpose
items.listreaditems:readList a project’s items, optionally filtered by collection.
items.list_by_collectionreaditems:readList items in one collection.
items.getreaditems:readFetch a single item by id.
items.get_stacreaditems:readFetch an item as a STAC 1.0.0 Feature with embedded assets.
items.searchreaditems:readSearch a project’s items with STAC filters.
items.search_orgreaditems:readSearch every item across your organization.
items.list_assetsreaditems:readList the files attached to an item.
items.lineagereaditems:readGet an item’s parent/child lineage tree.
items.createcomputeitems:writeCreate an item in a project.
items.updatecomputeitems:writePatch an item’s mutable metadata.
items.duplicatecomputeitems:writeCopy an item with its assets and viz profiles.
items.deletedestructiveitems:writeSoft-delete an item.
items.asset.downloadshare_exportitems:readIssue a signed download URL for an asset.

Collections

OperationSide effectScopePurpose
collections.listreadcollections:readList a project’s collections with item counts.
collections.getreadcollections:readGet one collection by id.
collections.createcomputecollections:writeCreate a collection in a project.
collections.updatecomputecollections:writeUpdate a collection’s metadata.
collections.deletedestructivecollections:writeSoft-delete a collection (unlinks its items).

Assets

OperationSide effectScopePurpose
assets.deletedestructiveitems:writeDelete one file from an item.

Listing and fetching items

items.list returns a project’s active (non-deleted) items in an { items, total } envelope. Pass collection_id to scope to one collection, or uncollected: true to return only items not assigned to any collection. The limit/offset pair drives offset pagination — see Pagination for cursor conventions and the total field.

http
POST /v1/op/items.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{
  "project_id": "8f2c1e4a-...",
  "collection_id": "3a9b...",
  "limit": 50,
  "offset": 0
}
http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [
    {
      "id": "c71d...",
      "project_id": "8f2c1e4a-...",
      "collection_id": "3a9b...",
      "name": "Field 12 — 2026-05-18",
      "type": "image",
      "cloud_cover": 4.1,
      "gsd": 0.5,
      "lifecycle_state": "active"
    }
  ],
  "total": 137
}

To read a single item, call items.get with just item_id. It returns the item wrapped in an item object and raises a 404 if the item is missing or soft-deleted, 403 if you lack project access.

python
from geopera import Geopera

client = Geopera(api_key="gpra_your_api_key")

page = client.op("items.list", project_id=PROJECT_ID, limit=50)
print(page["total"], "items")

item = client.op("items.get", item_id=page["items"][0]["id"])
print(item["item"]["name"], item["item"]["lifecycle_state"])
typescript
import { Geopera } from '@geopera/sdk';

const client = new Geopera({ apiKey: 'gpra_your_api_key' });

const page = await client.op('items.list', { project_id: PROJECT_ID, limit: 50 });
console.log(page.total, 'items');

const { item } = await client.op('items.get', { item_id: page.items[0].id });
console.log(item.name, item.lifecycle_state);

STAC output

If your tooling speaks STAC, items.get_stac returns a full STAC 1.0.0 Feature (application/geo+json) with assets and links embedded. Pass an optional base_url to control the absolute link hrefs.

http
POST /v1/op/items.get_stac HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{ "item_id": "c71d...", "base_url": "https://catalog.example.com" }

Searching items

items.search filters a single project’s items with STAC-compatible parameters; items.search_org runs the same query across your whole organization. Both return a FeatureCollection with numberMatched. Supported filters:

FieldTypeNotes
bbox[west, south, east, north]Exactly four values.
datetimestringISO 8601 instant or range.
collectionsstring[]Restrict to these collection ids.
typestringItem type filter.
queryobjectSTAC property query.
sortbyobject[]Sort directives.
limit / offsetintlimit ≤ 1000.
formatstringSet to "stac" to enrich each feature with its assets.
http
POST /v1/op/items.search HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{
  "project_id": "8f2c1e4a-...",
  "bbox": [149.0, -35.4, 149.3, -35.1],
  "datetime": "2026-01-01T00:00:00Z/2026-06-01T00:00:00Z",
  "query": { "cloud_cover": { "lt": 10 } },
  "limit": 100
}
python
fc = client.op(
    "items.search",
    project_id=PROJECT_ID,
    bbox=[149.0, -35.4, 149.3, -35.1],
    datetime="2026-01-01T00:00:00Z/2026-06-01T00:00:00Z",
    query={"cloud_cover": {"lt": 10}},
    limit=100,
)
print(fc["numberMatched"], "matches")

By default each feature is the raw item record. Pass format="stac" to expand features into full STAC Features with their assets attached — useful when feeding a STAC-aware viewer, costlier because each feature triggers an asset lookup.

Updating items

items.update patches mutable metadata. Send item_id plus only the fields you want to change. The update is driven by which fields are present in the body, so sending an explicit null clears that column, while omitting a field leaves it untouched.

Mutable fields: collection_id, name, description, geometry, bbox, datetime, cloud_cover, gsd, properties, lifecycle_state.

http
POST /v1/op/items.update HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{
  "item_id": "c71d...",
  "name": "Field 12 — corrected",
  "description": "Re-georeferenced 2026-05-18 capture",
  "collection_id": "3a9b..."
}
python
updated = client.op(
    "items.update",
    item_id="c71d...",
    name="Field 12 — corrected",
    collection_id="3a9b...",
)
typescript
const updated = await client.op('items.update', {
	item_id: 'c71d...',
	name: 'Field 12 — corrected',
	collection_id: '3a9b...'
});

Setting collection_id on an item is exactly how you move it between collections — there is no separate add/remove operation. Set it to null to remove the item from its collection without deleting it. Updating requires editor/admin on the project.

Duplicating

items.duplicate copies an item, its assets (re-pointing at the same underlying files — no storage duplication), and its visualization profiles. The copy is recorded in lineage as derived from the source, so items.lineage will show the relationship. You can retarget the copy with new_name, target_collection_id, and target_project_id.

python
copy = client.op(
    "items.duplicate",
    item_id="c71d...",
    new_name="Field 12 — working copy",
    target_collection_id="3a9b...",
)

Working with collections

A collection is a lightweight grouping with display metadata: name, optional description, color, a show_on_map flag, and a layer_order integer that controls draw order on the map. Create one, then assign items to it via collection_id on items.create or items.update.

http
POST /v1/op/collections.create HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{
  "project_id": "8f2c1e4a-...",
  "name": "May 2026 survey",
  "color": "#2e7d32",
  "show_on_map": true,
  "layer_order": 10
}
python
collection = client.op(
    "collections.create",
    project_id=PROJECT_ID,
    name="May 2026 survey",
    color="#2e7d32",
)

# list with live item counts
for c in client.op("collections.list", project_id=PROJECT_ID):
    print(c["name"], c["item_count"])
typescript
const collection = await client.op('collections.create', {
	project_id: PROJECT_ID,
	name: 'May 2026 survey',
	color: '#2e7d32'
});

const collections = await client.op('collections.list', { project_id: PROJECT_ID });

collections.list returns each collection with a live item_count. collections.get additionally enforces a belongs-to-project guard: a collection_id from a different project returns 404, not someone else’s data. collections.update accepts the same fields as create (all optional) and requires editor/admin. To list a collection’s contents, use items.list_by_collection (or items.list with collection_id).

Accessing assets

An item’s files are its assets. List them with items.list_assets, which returns the bare asset array (each entry carries its key, media_type, href, and file_size_bytes). The primary file lives under the data key.

python
assets = client.op("items.list_assets", item_id="c71d...")
data_asset = next(a for a in assets if a["key"] == "data")

To actually download a file, call items.asset.download. It validates item access, checks your subscription is in good standing, runs egress rate limits, and returns a short-lived signed URL (valid 15 minutes) — it does not stream bytes through the API.

http
POST /v1/op/items.asset.download HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{ "item_id": "c71d...", "asset_id": "a55e..." }
http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "url": "https://storage.googleapis.com/geopera-...?X-Goog-Signature=...",
  "expires_in_seconds": 900,
  "asset_id": "a55e...",
  "size_bytes": 41857332,
  "media_type": "image/tiff; application=geotiff; profile=cloud-optimized"
}
python
dl = client.op("items.asset.download", item_id="c71d...", asset_id="a55e...")

import httpx
with httpx.stream("GET", dl["url"]) as r:
    with open("scene.tif", "wb") as f:
        for chunk in r.iter_bytes():
            f.write(chunk)

Because this operation has the share_export side effect, it is egress-tracked and subject to per-asset rate limits and a per-organization bandwidth circuit breaker. A 429 means you have hit a download limit; a 402 means the organization’s subscription is past due. See Rate limits and Errors.

Soft delete and expiry

Deletes on the platform are soft: the row is flagged, not erased, and every read operation filters out flagged rows automatically.

  • items.delete sets deleted_at, transitions the item’s lifecycle_state, and fires storage-billing decrements per asset. It requires editor/admin on the item’s project. Deleting an already-deleted item returns 404.
  • assets.delete removes one file from an item. Two guards apply: the asset must belong to the named item (403 otherwise), and you cannot delete the last remaining primary data asset (400).
  • collections.delete soft-deletes the collection and unlinks its items — the items survive with collection_id set back to null. This is a project admin/owner action.
http
POST /v1/op/items.delete HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{ "item_id": "c71d..." }
http
HTTP/1.1 200 OK
Content-Type: application/json

{ "ok": true, "id": "c71d..." }

Lifecycle states

Items carry a lifecycle_state that the platform advances over time. On free tiers an item moves active → expiring → expired → deleted as it approaches and passes its retention window; on paid tiers it can move active → archived instead. The expires_at timestamp tells you when the next transition is due. The retention machinery that advances items through these states runs internally on the platform — client tokens do not trigger it.

Once an item reaches expired (or deleted), attempting items.asset.download returns a retention error (410) — the imagery stops being served while the row and underlying file still exist. An expiring item is only a warning state and still downloads normally. Restore an expired item by upgrading the organization’s plan, which re-activates it.

Worked example: organize a delivery

Group a freshly delivered batch into a collection, tidy the metadata, and confirm the result.

python
from geopera import Geopera

client = Geopera(api_key="gpra_your_api_key")
PROJECT_ID = "8f2c1e4a-..."

# 1. Create a collection for the delivery.
collection = client.op(
    "collections.create",
    project_id=PROJECT_ID,
    name="May 2026 aerial survey",
    color="#2e7d32",
    show_on_map=True,
    layer_order=10,
)
cid = collection["id"]

# 2. Find the uncollected items from this project and assign them.
loose = client.op("items.list", project_id=PROJECT_ID, uncollected=True, limit=200)
for item in loose["items"]:
    client.op(
        "items.update",
        item_id=item["id"],
        collection_id=cid,
        name=item["name"].strip(),
    )

# 3. Verify the collection now reports the expected count.
got = client.op("collections.get", project_id=PROJECT_ID, collection_id=cid)
print(got["name"], "now holds", got["item_count"], "items")

# 4. Pull a signed download URL for the first item's primary asset.
contents = client.op("items.list_by_collection", collection_id=cid, limit=1)
first = contents["items"][0]
assets = client.op("items.list_assets", item_id=first["id"])
data = next(a for a in assets if a["key"] == "data")
dl = client.op("items.asset.download", item_id=first["id"], asset_id=data["id"])
print("download:", dl["url"], "expires in", dl["expires_in_seconds"], "s")

Gotchas

  • No REST verbs. Listing, getting, and deleting are encoded in the operation name; everything is POST /v1/op/{id} with a body. Do not look for GET /items/{id}.
  • Soft delete is the only delete. Deleted rows disappear from reads but are recoverable server-side; design idempotent flows accordingly. A second items.delete on the same id returns 404.
  • Clearing vs. omitting. On items.update, sending null clears a field; omitting it leaves the field as-is. To unassign an item from its collection, send "collection_id": null.
  • Download URLs are short-lived. Signed URLs expire after 15 minutes (expires_in_seconds: 900). Request one per download attempt rather than caching it.
  • Deleting a collection keeps its items. Items are unlinked (collection_idnull), never deleted, when their collection is removed.
  • Org search needs an org. items.search_org returns an empty FeatureCollection (not an error) for a principal with no organization.

Related

  • Operations — the POST /v1/op/{id} invocation model.
  • Pagination — the limit/offset/total envelope used by list and search.
  • Scopes — the read/write/admin scopes these operations require.
  • Errors — the problem+json shape behind every 4xx.
  • Rate limits — egress limits that gate items.asset.download.