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
| Noun | What it is | Identified by |
|---|---|---|
| Item | A single catalog record (a STAC Feature) — one scene, mosaic, or derived layer. | item_id |
| Collection | A named grouping of items within a project, with map display order and colour. | collection_id |
| Asset | A 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
| Operation | Side effect | Scope | Purpose |
|---|---|---|---|
items.list | read | items:read | List a project’s items, optionally filtered by collection. |
items.list_by_collection | read | items:read | List items in one collection. |
items.get | read | items:read | Fetch a single item by id. |
items.get_stac | read | items:read | Fetch an item as a STAC 1.0.0 Feature with embedded assets. |
items.search | read | items:read | Search a project’s items with STAC filters. |
items.search_org | read | items:read | Search every item across your organization. |
items.list_assets | read | items:read | List the files attached to an item. |
items.lineage | read | items:read | Get an item’s parent/child lineage tree. |
items.create | compute | items:write | Create an item in a project. |
items.update | compute | items:write | Patch an item’s mutable metadata. |
items.duplicate | compute | items:write | Copy an item with its assets and viz profiles. |
items.delete | destructive | items:write | Soft-delete an item. |
items.asset.download | share_export | items:read | Issue a signed download URL for an asset. |
Collections
| Operation | Side effect | Scope | Purpose |
|---|---|---|---|
collections.list | read | collections:read | List a project’s collections with item counts. |
collections.get | read | collections:read | Get one collection by id. |
collections.create | compute | collections:write | Create a collection in a project. |
collections.update | compute | collections:write | Update a collection’s metadata. |
collections.delete | destructive | collections:write | Soft-delete a collection (unlinks its items). |
Assets
| Operation | Side effect | Scope | Purpose |
|---|---|---|---|
assets.delete | destructive | items:write | Delete 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.
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/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.
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"])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.
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:
| Field | Type | Notes |
|---|---|---|
bbox | [west, south, east, north] | Exactly four values. |
datetime | string | ISO 8601 instant or range. |
collections | string[] | Restrict to these collection ids. |
type | string | Item type filter. |
query | object | STAC property query. |
sortby | object[] | Sort directives. |
limit / offset | int | limit ≤ 1000. |
format | string | Set to "stac" to enrich each feature with its assets. |
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
}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.
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..."
}updated = client.op(
"items.update",
item_id="c71d...",
name="Field 12 — corrected",
collection_id="3a9b...",
)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.
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.
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
}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"])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.
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.
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/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"
}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.deletesetsdeleted_at, transitions the item’slifecycle_state, and fires storage-billing decrements per asset. It requireseditor/adminon the item’s project. Deleting an already-deleted item returns404.assets.deleteremoves one file from an item. Two guards apply: the asset must belong to the named item (403otherwise), and you cannot delete the last remaining primarydataasset (400).collections.deletesoft-deletes the collection and unlinks its items — the items survive withcollection_idset back tonull. This is a projectadmin/owneraction.
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/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.
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 forGET /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.deleteon the same id returns404. - Clearing vs. omitting. On
items.update, sendingnullclears 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_id→null), never deleted, when their collection is removed. - Org search needs an org.
items.search_orgreturns an emptyFeatureCollection(not an error) for a principal with no organization.
Related
- Operations — the
POST /v1/op/{id}invocation model. - Pagination — the
limit/offset/totalenvelope 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.