Alerts & notifications

Alert rules watch your catalog and fire an alert event when a band-math expression crosses a threshold on a new item; notifications are the per-user inbox that surfaces those events (and other platform activity) in your app. Both are plain operations — every call is a POST /v1/op/{operation_id} with a JSON body.

There are two distinct, org- vs user-scoped resources here:

ResourceOwned byScope namespace
Alert rules & eventsyour organizationalerts:read / alerts:write
Notificationsthe authenticated usernotifications:read / notifications:write

Notifications are keyed to a human user. An API key (gpra_…) or service principal has no human user behind it, so notification reads return an empty inbox and writes find nothing — see Notifications. Alert rules, by contrast, work with any principal that belongs to an organization. See Authentication and Scopes for the token model.

Operations at a glance

OperationSide effectScopePurpose
alerts.create_rulecomputealerts:writeCreate an alert rule
alerts.update_rulecomputealerts:writeUpdate a rule (partial)
alerts.delete_ruledestructivealerts:writeDelete a rule
alerts.test_rulereadalerts:readDry-run a rule against one item
alerts.acknowledge_eventcomputealerts:writeAcknowledge a fired event
alerts.rules.listreadalerts:readList rules
alerts.rule.getreadalerts:readGet one rule
alerts.events.listreadalerts:readList fired events
notifications.listreadnotifications:readList the caller’s notifications
notifications.unread_countreadnotifications:readCount unread notifications
notifications.mark_readcomputenotifications:writeMark one notification read
notifications.mark_all_readcomputenotifications:writeMark all read
notifications.dismisscomputenotifications:writeHide one notification

Alert rules

A rule pins an expression (raw band references like b1, b2) to a threshold, plus the item events it should react to. When a matching item appears, the platform reads the item’s asset COG, evaluates the expression, and — if the comparison passes — records an alert event and fans it out over the rule’s notification_channels.

Create a rule

alerts.create_rule is org-scoped: the organization comes from your authenticated principal, never from the body. Only name, expression, comparison, and threshold are required; everything else has a default.

FieldTypeDefaultNotes
namestringRequired. Human label for the rule.
expressionstringRequired. Band math over raw refs, e.g. (b4 - b3) / (b4 + b3).
comparisonstringRequired. One of lt, gt, eq, between.
thresholdnumberRequired. The bound to compare against.
threshold_uppernumbernullRequired only when comparison is between.
descriptionstringnullOptional free text.
collection_idstringnullRestrict the rule to one collection.
event_typesstring[]["item.created"]Item events that trigger evaluation.
asset_keystring"data"Which item asset to read the COG from.
band_mappingobject{}Map of band name → 1-based index.
notification_channelsstring[]["webhook"]How fired events are delivered.
cooldown_minutesinteger60Minimum gap between fires for this rule.

comparison must be one of lt, gt, eq, between; any other value is a 400. Choosing between requires threshold_upper — omitting it is also a 400.

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

{
  "name": "NDVI drop on north paddock",
  "description": "Flag scenes where mean NDVI falls below 0.3",
  "collection_id": "col_a1b2c3",
  "event_types": ["item.created"],
  "expression": "(b4 - b3) / (b4 + b3)",
  "comparison": "lt",
  "threshold": 0.3,
  "asset_key": "data",
  "band_mapping": { "red": 3, "nir": 4 },
  "notification_channels": ["webhook", "in_app"],
  "cooldown_minutes": 120
}

The response wraps the stored row under alert_rule:

json
{
	"alert_rule": {
		"id": "ar_7f3c91",
		"organization_id": "org_5d2e",
		"name": "NDVI drop on north paddock",
		"expression": "(b4 - b3) / (b4 + b3)",
		"comparison": "lt",
		"threshold": 0.3,
		"threshold_upper": null,
		"asset_key": "data",
		"notification_channels": ["webhook", "in_app"],
		"cooldown_minutes": 120,
		"active": true,
		"created_by": "9c1b...-uuid",
		"created_at": "2026-06-20T02:10:00Z"
	}
}

created_by is the human user’s UUID when a JWT principal creates the rule, and null when an API key or service principal creates it.

Test a rule before you trust it

alerts.test_rule evaluates a rule against a single item without persisting anything — no event, no notification. It reads the item’s asset_key COG, runs the expression, and reports the mean computed value and whether the comparison would trip. Use it to validate an expression and threshold against a known scene.

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

{ "rule_id": "ar_7f3c91", "item_id": "item_abc123" }
json
{
	"computed_value": 0.214,
	"threshold": 0.3,
	"comparison": "lt",
	"would_trigger": true,
	"expression": "(b4 - b3) / (b4 + b3)",
	"item_id": "item_abc123"
}

A missing rule is a 404; a missing asset, an unreadable COG, an expression that fails to evaluate, or a result with no finite pixels each surface as a 400 with a descriptive detail.

List, get, and update

alerts.rules.list returns every rule in your organization, newest first, under an { alert_rules, count } envelope. Narrow with the optional active and collection_id filters:

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

{ "active": true, "collection_id": "col_a1b2c3" }
json
{
	"alert_rules": [{ "id": "ar_7f3c91", "name": "NDVI drop on north paddock", "active": true }],
	"count": 1
}

alerts.rule.get takes a single rule_id and returns the row under alert_rule. Because rules are org-scoped, a rule_id that exists under a different organization returns 404 — the row is invisible to you, never leaked.

alerts.update_rule is a partial update: send rule_id plus only the fields you want to change. An empty patch is a 400, and if you include comparison it is re-validated against the same four values. The response echoes the updated row under alert_rule.

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

{ "rule_id": "ar_7f3c91", "active": false, "cooldown_minutes": 240 }

alerts.delete_rule (side effect destructive) takes a single rule_id and returns { "deleted": true }. Deleting a rule you don’t own — or one already gone — is a 404.

Alert events

When a rule fires, it records an alert event. List them with alerts.events.list, which returns { alert_events, count } ordered by triggered_at descending. Filter by rule_id and acknowledged, and page with limit (clamped to 1200, default 50) and offset (≥ 0).

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

{ "rule_id": "ar_7f3c91", "acknowledged": false, "limit": 25, "offset": 0 }
json
{
	"alert_events": [
		{
			"id": "ae_4488",
			"rule_id": "ar_7f3c91",
			"computed_value": 0.214,
			"triggered_at": "2026-06-20T03:02:11Z",
			"acknowledged": false
		}
	],
	"count": 1
}

Clear an event from the unacknowledged queue with alerts.acknowledge_event; it stamps acknowledged / acknowledged_at and returns the row under alert_event. An unknown or cross-org event_id is a 404.

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

{ "event_id": "ae_4488" }

Notifications

Notifications are the authenticated user’s in-app inbox. They are per-user UI state, not lineage artifacts, so they are keyed to a human user UUID. A gpra_ API key or service principal has no human user, so:

  • notifications.list returns [],
  • notifications.unread_count returns { "count": 0 },
  • the write ops find nothing and return 404 (or marked_count: 0).

To work with an inbox, authenticate with a user JWT, not an API key.

Read the inbox

notifications.list returns a bare JSON array (not an envelope), newest first. Pass unread_only to skip read items, and page with limit (default 50) and offset (default 0).

http
POST /v1/op/notifications.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer eyJhbGci...session-token
Content-Type: application/json

{ "unread_only": true, "limit": 20, "offset": 0 }
json
[
	{
		"id": "nt_aa10",
		"title": "Alert: NDVI drop on north paddock",
		"body": "Mean NDVI 0.214 fell below 0.3 on item_abc123",
		"read": false,
		"dismissed": false,
		"created_at": "2026-06-20T03:02:12Z"
	}
]

notifications.unread_count is a lightweight poll for a badge — it takes an empty body and returns { "count": N } of unread, undismissed notifications.

http
POST /v1/op/notifications.unread_count HTTP/1.1
Host: api.geopera.com
Authorization: Bearer eyJhbGci...session-token
Content-Type: application/json

{}

Mark read & dismiss

notifications.mark_read and notifications.dismiss each take notification_id and return the updated row (its id plus any other fields). mark_read flips it to read; dismiss hides it from the list entirely. A notification that isn’t yours, doesn’t exist, or is already in the target state is a 404.

http
POST /v1/op/notifications.mark_read HTTP/1.1
Host: api.geopera.com
Authorization: Bearer eyJhbGci...session-token
Content-Type: application/json

{ "notification_id": "nt_aa10" }

notifications.mark_all_read takes an empty body and clears the whole unread queue, returning the count it touched:

json
{ "marked_count": 7 }

How alerts relate to webhooks

A rule’s notification_channels decide where a fired event goes. "in_app" populates the notification inbox described above; "webhook" delivers the event to your subscribed HTTPS endpoints. The webhook fan-out runs through the same durable, signed, at-least-once delivery pipeline as every other platform event — alerts do not ship their own delivery mechanism.

So a full “alert me in my own systems” loop is two independent setups:

  1. Create the alert rule with "webhook" in notification_channels (this guide).
  2. Subscribe an endpoint to the relevant event type and verify the HMAC signature — see the Webhooks guide.

Treat webhook deliveries as at-least-once: dedupe on the delivery’s event id, exactly as the webhooks guide describes. For polling-based UIs, prefer the "in_app" channel plus notifications.unread_count.

Worked example: monitor, then surface

This walks the whole loop — create a rule, dry-run it, then react to fired events and acknowledge them.

HTTP

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

{
  "name": "Burn-scar NBR alert",
  "expression": "(b4 - b6) / (b4 + b6)",
  "comparison": "lt",
  "threshold": -0.1,
  "collection_id": "col_a1b2c3",
  "notification_channels": ["webhook", "in_app"]
}
http
POST /v1/op/alerts.events.list HTTP/1.1
Host: api.geopera.com
Authorization: Bearer gpra_your_api_key
Content-Type: application/json

{ "acknowledged": false, "limit": 50 }

Python

The Python package is geopera (install with pip install geopera). Every operation is invoked through client.op(operation_id, body).

python
from geopera import Geopera

client = Geopera(token="gpra_your_api_key")

# 1. Create the rule.
created = client.op(
    "alerts.create_rule",
    {
        "name": "Burn-scar NBR alert",
        "expression": "(b4 - b6) / (b4 + b6)",
        "comparison": "lt",
        "threshold": -0.1,
        "collection_id": "col_a1b2c3",
        "notification_channels": ["webhook", "in_app"],
    },
)
rule_id = created["alert_rule"]["id"]

# 2. Dry-run it against a known item before trusting it.
dry = client.op("alerts.test_rule", {"rule_id": rule_id, "item_id": "item_abc123"})
print(dry["computed_value"], "would_trigger:", dry["would_trigger"])

# 3. Drain the unacknowledged event queue.
events = client.op(
    "alerts.events.list",
    {"rule_id": rule_id, "acknowledged": False, "limit": 50},
)
for event in events["alert_events"]:
    client.op("alerts.acknowledge_event", {"event_id": event["id"]})

TypeScript

The TypeScript SDK is @geopera/sdk (install with npm install @geopera/sdk). It mirrors the Python idiom: client.op(operationId, body) returns a promise.

typescript
import { Geopera } from '@geopera/sdk';

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

// 1. Create the rule.
const created = await client.op('alerts.create_rule', {
	name: 'Burn-scar NBR alert',
	expression: '(b4 - b6) / (b4 + b6)',
	comparison: 'lt',
	threshold: -0.1,
	collectionId: 'col_a1b2c3',
	notificationChannels: ['webhook', 'in_app']
});
const ruleId = created.alert_rule.id;

// 2. Dry-run it.
const dry = await client.op('alerts.test_rule', {
	ruleId,
	itemId: 'item_abc123'
});
console.log(dry.computed_value, 'would_trigger:', dry.would_trigger);

// 3. Acknowledge everything outstanding.
const events = await client.op('alerts.events.list', {
	ruleId,
	acknowledged: false,
	limit: 50
});
for (const event of events.alert_events) {
	await client.op('alerts.acknowledge_event', { eventId: event.id });
}

Errors & edge cases

All failures are RFC 9457 problem+json — see Errors for the envelope. The ones specific to these operations:

StatusWhen
400Invalid comparison; between without threshold_upper; empty update patch; alerts.test_rule against a missing/unreadable asset or an expression that yields no finite pixels.
403The principal belongs to no organization (User not in an organization) for any alert operation.
404A rule, event, or notification that doesn’t exist — or exists under another org/user. Cross-tenant ids are never distinguishable from missing ones.

Notification writes from a non-human principal silently no-op: 404 for single-item ops and marked_count: 0 for notifications.mark_all_read. If you need machine-driven alerting, route through the "webhook" channel rather than the inbox.

Related

  • Webhooks — subscribe to and verify event deliveries.
  • Operations — authoritative request/response schema for every operation.
  • Scopes — the alerts:* and notifications:* permissions.
  • Paginationlimit/offset conventions for alerts.events.list and notifications.list.
  • Errors — the problem+json error envelope.