Webhooks

Webhooks let your systems react to what happens on Geopera without polling. Subscribe an HTTPS endpoint to one or more event types, and Geopera delivers a signed POST to it whenever a matching event occurs — an order is delivered, a processing job finishes, an upload completes, your credit balance runs low.

StepOperationScope
Subscribeevent_subscriptions.createevent_subscriptions:write
List / inspectevent_subscriptions.list / .getevent_subscriptions:read
Update / pauseevent_subscriptions.updateevent_subscriptions:write
Test (real delivery)event_subscriptions.testevent_subscriptions:write
Removeevent_subscriptions.deleteevent_subscriptions:write

Subscribe

bash
curl -s -X POST https://api.geopera.com/v1/op/event_subscriptions.create \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "order.fulfilled",
    "endpoint_url": "https://your-app.example.com/hooks/geopera",
    "filter_config": {},
    "description": "Notify fulfillment service"
  }'

The response includes a signing secret (whsec_…) — it is shown once, on creation. Store it; you need it to verify deliveries. The endpoint must be HTTPS and publicly resolvable (an SSRF guard rejects private/internal hosts). Use filter_config to narrow deliveries (e.g. {"entity_type": "item"} or {"data.project_id": "…"}).

The delivery request

Each delivery is an HTTP POST with a JSON body and these headers:

HeaderMeaning
X-Geopera-Signaturesha256=<hmac> — HMAC-SHA256 of the raw body, keyed by your secret
X-Geopera-Eventthe event type (e.g. order.fulfilled)
X-Geopera-Event-IDthe event’s unique id (use it to dedupe)
X-Geopera-Delivery-IDthe unique id of this delivery attempt

Body:

json
{
  "event_id": "b1c0...",
  "event_type": "order.fulfilled",
  "timestamp": "2026-06-13T04:21:00Z",
  "organization_id": "c4e2...",
  "entity_type": "order",
  "entity_id": "f2b1...",
  "data": { "...": "event-specific fields" }
}

Respond with any 2xx to acknowledge. A non-2xx (or a timeout) is treated as a failed delivery and retried (see below). Acknowledge quickly and do slow work asynchronously.

Verify the signature

Always verify the signature before trusting a webhook. Recompute the HMAC-SHA256 of the raw request body with your subscription secret and compare it, constant-time, to the X-Geopera-Signature header.

python
import hmac, hashlib

def verify(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")
ts
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, signatureHeader: string, secret: string): boolean {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(signatureHeader || "");
  return a.length === b.length && timingSafeEqual(a, b);
}

Sign over the exact bytes you received, before any JSON re-serialization — a re-encoded body will not match.

Delivery guarantees

Delivery is durable and at-least-once:

  • Every event→subscription delivery is recorded as a durable row the moment the event occurs, so a delivery is never lost if a server restarts mid-flight.
  • Failed deliveries are retried with exponential backoff (minutes → hours) until they succeed or are exhausted, then dead-lettered for inspection.
  • Because retries (and rare duplicates) are possible, make your handler idempotent — dedupe on X-Geopera-Event-ID.

Order is not guaranteed; use the timestamp and your own state if ordering matters.

Test a subscription

event_subscriptions.test sends a real signed POST to your endpoint and returns the actual outcome, so you can confirm connectivity before relying on it (it works even while the subscription is paused):

bash
curl -s -X POST https://api.geopera.com/v1/op/event_subscriptions.test \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "subscription_id": "sub_…" }'
json
{ "status": "delivered", "delivered": true, "http_status": 200,
  "event_id": "…", "event_type": "order.fulfilled" }

A status of failed (with the HTTP status / error) means your endpoint didn’t accept the POST — fix it and test again.

Event catalogue

Discover the live list programmatically via the event-types endpoint (or the SDK’s webhooks.event_types()). The events that fire today include:

DomainEvents
Ordersorder.created, order.placed, order.being_fulfilled, order.fulfilled, order.failed, order.cancelled
Taskingfeasibility.requested, feasibility.answered, quotation.requested, quotation.accepted, quotation.rejected
Processing jobsjob.created, job.started, job.captured, job.released, job.completed, job.failed
Items & dataitem.created, item.updated, item.deleted, asset.deleted, upload.completed
Collections & projectscollection.created, collection.updated, project.created
Analyticsanalytics.executed
Billingtopup.succeeded, credits.purchased, credits.low_balance, credits.depleted, topup.failed, payment_method.attached, payment_method.detached

Subscribe to one event type per subscription; create several subscriptions (or use the SDK’s subscribe(events=[…])) to cover multiple.

In the SDKs

The Python SDK wraps this surface — see Python SDK → Webhooks. Everything here is also reachable as plain operations from the TypeScript client and the MCP agent tools.