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.
| Step | Operation | Scope |
|---|---|---|
| Subscribe | event_subscriptions.create | event_subscriptions:write |
| List / inspect | event_subscriptions.list / .get | event_subscriptions:read |
| Update / pause | event_subscriptions.update | event_subscriptions:write |
| Test (real delivery) | event_subscriptions.test | event_subscriptions:write |
| Remove | event_subscriptions.delete | event_subscriptions:write |
Subscribe
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:
| Header | Meaning |
|---|---|
X-Geopera-Signature | sha256=<hmac> — HMAC-SHA256 of the raw body, keyed by your secret |
X-Geopera-Event | the event type (e.g. order.fulfilled) |
X-Geopera-Event-ID | the event’s unique id (use it to dedupe) |
X-Geopera-Delivery-ID | the unique id of this delivery attempt |
Body:
{
"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.
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 "")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):
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_…" }'{ "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:
| Domain | Events |
|---|---|
| Orders | order.created, order.placed, order.being_fulfilled, order.fulfilled, order.failed, order.cancelled |
| Tasking | feasibility.requested, feasibility.answered, quotation.requested, quotation.accepted, quotation.rejected |
| Processing jobs | job.created, job.started, job.captured, job.released, job.completed, job.failed |
| Items & data | item.created, item.updated, item.deleted, asset.deleted, upload.completed |
| Collections & projects | collection.created, collection.updated, project.created |
| Analytics | analytics.executed |
| Billing | topup.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.