# geog.ai Spatial Intelligence API — cURL Cheatsheet

Copy-pasteable cURL for every endpoint, with example payloads filled in. Replace `geog_test_sk_replace_me` with your real API key (test or live).

**Base URL:** `https://api.geog.ai/v1`  
**Spec version:** `1.0`  
**Regenerate:** `python3 docs/postman/generate_clients.py` (re-runs after `openapi.yaml` changes).

---

## Quick auth check

```bash
curl -H 'Authorization: Bearer geog_test_sk_replace_me' https://api.geog.ai/v1/jobs/job_demo_0001
```

---

## Endpoints

- [Context API](#context-api) — 5 endpoint(s)
- [Simulate API](#simulate-api) — 7 endpoint(s)
- [RF API](#rf-api) — 5 endpoint(s)
- [Optimize API](#optimize-api) — 4 endpoint(s)
- [Trajectory API](#trajectory-api) — 2 endpoint(s)
- [Graph API](#graph-api) — 3 endpoint(s)
- [Calibration](#calibration) — 1 endpoint(s)
- [Jobs](#jobs) — 1 endpoint(s)
- [Webhooks](#webhooks) — 9 endpoint(s)
- [STAC Catalog](#stac-catalog) — 8 endpoint(s)

---

## Context API

Resolve spatial state for registered devices and arbitrary coordinates.

### Resolve SpatialState for a registered device

`GET /context`

Returns the full `SpatialState` for a registered device — terrain, jurisdiction, wind vector,
nearby entities, atmospheric stability class, and asset proximity. Enrichment runs at ingest
time; this call returns the pre-computed state.

**Pricing:** $0.001 / call

```bash
curl -X GET 'https://api.geog.ai/v1/context?device_id=sensor_h2s_023' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Batch context resolution (up to 500 devices)

`GET /context/batch`

Resolves SpatialState for up to 500 devices in a single request. Partial failures
are represented with `null` result entries and an error sub-object.

**Pricing:** $0.001 / device

```bash
curl -X GET 'https://api.geog.ai/v1/context/batch?device_ids=["sensor_h2s_023","sensor_h2s_024","drone_01"]' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Proximity search

`GET /nearby`

Returns all registered entities within a given radius of a coordinate, optionally
filtered by type. Results are ordered by distance.

**Pricing:** $0.001 / call

```bash
curl -X GET 'https://api.geog.ai/v1/nearby?lat=31.96&lon=-99.9&radius_m=500' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Polygon containment query

`GET /within`

Returns all registered entities whose location falls within a given GeoJSON polygon.
Used for zone-based queries — "which sensors are inside this permit boundary?"

**Pricing:** $0.001 / call (+$0.001/entity if include_context=true)

```bash
curl -X GET 'https://api.geog.ai/v1/within?geometry=' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Jurisdiction resolution for any coordinate

`GET /jurisdiction`

Returns all applicable regulatory jurisdictions and permit obligations for a given
coordinate — air quality authority, watershed governance, noise ordinance, and
cross-boundary relationships.

**Pricing:** $0.0005 / call (free when device_id is passed to /context)

```bash
curl -X GET 'https://api.geog.ai/v1/jurisdiction' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## Simulate API

Physics-based propagation simulations (async jobs).

### Atmospheric dispersion simulation

`POST /simulate/plume`

Runs an atmospheric dispersion model for a point or area source. Tier 1 uses Gaussian
plume (fast screening, <8 s). Tier 2 uses AERMOD-style regulatory-grade modeling
(15–60 s). Tier 3 uses Lagrangian particle tracking for non-steady-state scenarios
(minutes). Returns concentration isopleths, predicted readings at registered sensor
locations, and affected zone classifications.

**Pricing:** $0.05–$0.25 / run depending on tier.

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/plume' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "source": {
    "lat": 31.9642,
    "lon": -99.9035,
    "alt_m": 545,
    "emission_rate_gs": 2.4,
    "stack_height_m": 12
  },
  "wind": {
    "speed_ms": 8.2,
    "bearing_deg": 312
  },
  "stability_class": "D",
  "duration_min": 60,
  "tier": 2,
  "species": "H2S",
  "output_receptors": [
    "sensor_h2s_021",
    "sensor_h2s_023",
    "sensor_h2s_024"
  ]
}'
```

### Live plume tracking via Server-Sent Events

`POST /simulate/plume/stream`

Opens a Server-Sent Events (SSE) stream that delivers a running plume
simulation in real time. The client POSTs a `PlumeStreamRequest` with
`Accept: text/event-stream`; the server responds `200 OK` with
`Content-Type: text/event-stream` and emits a sequence of named events
until the simulation completes or the client disconnects.

**Event types** (the SSE `event:` field):

- `tick` — heartbeat with simulation clock and wind vector, emitted
  every `tick_interval_s` seconds
- `concentration` — updated isopleth polygons and per-receptor
  concentrations, emitted on every tick after the field stabilises
- `alert` — emitted when an `alert_thresholds` value is crossed at
  any receptor or isopleth
- `end` — terminal event with run summary; clients should stop
  reconnecting after receiving this

Every event carries a stable `id:` field. Clients that drop the
connection should reconnect with `Last-Event-ID` to resume from the
last event seen. The official SDKs (`streamPlume` in TypeScript,
`stream_plume` in Python) implement this reconnect-with-backoff
behaviour automatically.

**Availability:** Site Calibration plans only.

**Pricing:** $0.25 / run, billed once on `end`.

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/plume/stream' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: text/event-stream' \
  -H 'Content-Type: application/json' \
  -d '{
  "source": {
    "lat": 31.9642,
    "lon": -99.9035,
    "alt_m": 545,
    "emission_rate_gs": 2.4,
    "stack_height_m": 12
  },
  "wind": {
    "speed_ms": 8.2,
    "bearing_deg": 312
  },
  "stability_class": "D",
  "duration_min": 60,
  "species": "H2S",
  "output_receptors": [
    "sensor_h2s_021",
    "sensor_h2s_023",
    "sensor_h2s_024"
  ],
  "tick_interval_s": 5,
  "alert_thresholds": {
    "warn": 1.0,
    "action": 5.0,
    "evacuate": 10.0
  }
}'
```

### Flood and hydrological simulation

`POST /simulate/flood`

Runs DEM-based watershed delineation, SCS Curve Number rainfall-runoff modeling,
kinematic wave channel routing, and 2D inundation mapping. Returns flood extent
polygons at multiple return intervals, time-to-crest forecasts, and sensors predicted
to be inundated.

Either `watershed_id` or `area` is required.

**Pricing:** $0.10 / run

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/flood' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "watershed_id": "basin_colorado_mid",
  "precip_mm": 85,
  "duration_hr": 3,
  "return_periods": [
    10,
    100
  ],
  "antecedent_moisture": "wet"
}'
```

### Single-node RF coverage simulation

`POST /simulate/rf_coverage`

Runs the ITM/Longley-Rice terrain propagation model for a single RF node. Returns a
coverage probability polygon at the specified RSSI threshold, plus a raster heatmap
and link budget summary. Either `node_id` or `lat/lon/alt_m` is required.

**Pricing:** $0.08 / run

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/rf_coverage' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "node_id": "rf_node_A7",
  "frequency_mhz": 915,
  "tx_power_dbm": 20,
  "rssi_threshold_dbm": -95,
  "antenna_height_m": 6,
  "model": "itm"
}'
```

### Noise propagation simulation

`POST /simulate/noise`

Runs ISO 9613-2 octave-band outdoor sound propagation from a point or line source.
Accounts for geometric spreading, atmospheric absorption, ground effect, barrier
insertion loss, and vegetation attenuation. Returns noise contour polygons (isodB
lines) and exceedance flags at registered receptor locations.

Either `source_id` or a source geometry must be provided.

**Pricing:** $0.08 / run

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/noise' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "source_id": "compressor_C4",
  "octave_bands_db": [
    82,
    86,
    91,
    95,
    93,
    88,
    84,
    79
  ],
  "contour_levels_dba": [
    45,
    55,
    65
  ]
}'
```

### Thermal / heat diffusion simulation

`POST /simulate/thermal`

Models heat propagation from industrial sources, fire events, or equipment failures.
Returns time-to-temperature-threshold at monitored locations and hot zone polygons.

**Pricing:** $0.08 / run

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/thermal' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "source": {
    "lat": 31.9642,
    "lon": -99.9035,
    "power_kw": 850,
    "temperature_c": 240
  },
  "duration_min": 120,
  "threshold_c": [
    40,
    60,
    80
  ],
  "output_receptors": [
    "sensor_temp_07",
    "sensor_temp_12"
  ]
}'
```

### Fire spread simulation

`POST /simulate/fire`

Models wildfire spread using terrain, fuel model, and weather conditions. Returns fire
perimeter polygons at specified time intervals, rate-of-spread vectors, and smoke plume
trajectory (linked to `/simulate/plume` for full smoke transport).

**Pricing:** $0.15 / run

```bash
curl -X POST 'https://api.geog.ai/v1/simulate/fire' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "ignition_point": {
    "type": "Point",
    "coordinates": [
      -119.42,
      34.21
    ]
  },
  "fuel_model": "GR2",
  "weather": {
    "wind_speed_ms": 8.3,
    "wind_bearing_deg": 225,
    "rh_pct": 12,
    "temp_c": 38
  },
  "duration_hr": 24,
  "output_intervals_hr": [
    1,
    4,
    12,
    24
  ]
}'
```

---

## RF API

Radio-frequency coverage, link budget, and mesh analysis.

### Coverage polygon for a registered node

`GET /rf/coverage`

Returns the coverage probability polygon for a registered RF node at a given RSSI
threshold. Cached per node until terrain or node configuration changes.

**Pricing:** $0.08 / call

```bash
curl -X GET 'https://api.geog.ai/v1/rf/coverage?node=rf_node_A7' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Point-to-point RF link budget

`GET /rf/link`

Returns the link budget between two registered nodes — predicted RSSI, fade margin,
Fresnel zone clearance, and obstruction analysis. Returns a pass/fail recommendation
given the specified reliability threshold.

**Pricing:** $0.03 / call

```bash
curl -X GET 'https://api.geog.ai/v1/rf/link?from=rf_node_A7&to=rf_node_B3' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Coverage gap heatmap within an area

`GET /rf/gaps`

Identifies coverage gaps within a defined area given existing node topology. Returns a
GeoJSON heatmap of gap severity and a list of H3 cells with insufficient coverage.

**Pricing:** $0.05 / call

```bash
curl -X GET 'https://api.geog.ai/v1/rf/gaps?area=' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Full mesh connectivity matrix

`POST /rf/mesh/viability`

Computes the full link budget matrix for a mesh network — every node-to-node pair,
with failure probability based on calibrated path loss models. Returns a connectivity
matrix, mesh reliability score, single-points-of-failure, and failure scenario analysis.

Async job; typical completion 15–40 seconds for networks up to 200 nodes.

**Pricing:** $0.15 / run

```bash
curl -X POST 'https://api.geog.ai/v1/rf/mesh/viability' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "node_ids": [
    "rf_node_A7",
    "rf_node_B3",
    "rf_node_C2",
    "rf_node_D9"
  ],
  "min_path_reliability_pct": 99.5,
  "failure_scenarios": [
    {
      "node_id": "rf_node_A7",
      "failure_probability": 0.05
    }
  ]
}'
```

### Node placement optimizer for RF coverage

`POST /rf/optimize/placement`

Given existing mesh topology, terrain, and coverage requirements, returns the top-N
candidate locations that maximize marginal coverage gain.

**Pricing:** $1.00–$5.00 / run

```bash
curl -X POST 'https://api.geog.ai/v1/rf/optimize/placement' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "area": {
    "type": "Polygon",
    "coordinates": [
      [
        [
          "-99.92 - 31.95",
          [
            -99.88,
            31.95
          ],
          [
            -99.88,
            31.99
          ],
          [
            -99.92,
            31.99
          ],
          [
            -99.92,
            31.95
          ]
        ]
      ]
    ]
  },
  "coverage_threshold": 0.95,
  "existing_nodes": [
    "rf_node_A7",
    "rf_node_B3"
  ],
  "candidate_count": 5,
  "frequency_mhz": 915,
  "constraints": {
    "min_distance_from_edge_m": 50,
    "require_road_access": true
  }
}'
```

---

## Optimize API

Node and sensor placement optimization; coverage and gap analysis.

### General node placement optimizer

`POST /optimize/node_placement`

Generalizes RF placement to any monitoring objective — air quality sensor spacing
(accounting for plume physics), flood gauge placement (watershed coverage), or
acoustic monitor placement (source coverage). Returns ranked candidate locations with
objective-specific marginal gain scores.

**Pricing:** $1.00–$5.00 / run

```bash
curl -X POST 'https://api.geog.ai/v1/optimize/node_placement' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "area": {
    "type": "Polygon",
    "coordinates": [
      [
        [
          "-99.92 - 31.95",
          [
            -99.88,
            31.95
          ],
          [
            -99.88,
            31.99
          ],
          [
            -99.92,
            31.99
          ],
          [
            -99.92,
            31.95
          ]
        ]
      ]
    ]
  },
  "coverage_threshold": 0.9,
  "existing_nodes": [
    "sensor_h2s_021",
    "sensor_h2s_023"
  ],
  "objective": "plume_detection"
}'
```

### Vertical-aware sensor placement optimization

`POST /optimize/sensor_placement`

Recommends sensor placement for a given ZOOID vertical — accounts for the vertical's
specific measurement physics, regulatory requirements, and deployment constraints.

**Pricing:** $2.00–$5.00 / run

```bash
curl -X POST 'https://api.geog.ai/v1/optimize/sensor_placement' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "vertical": "V01",
  "area": {
    "type": "Polygon",
    "coordinates": [
      [
        [
          "-99.92 - 31.95",
          [
            -99.88,
            31.95
          ],
          [
            -99.88,
            31.99
          ],
          [
            -99.92,
            31.99
          ],
          [
            -99.92,
            31.95
          ]
        ]
      ]
    ]
  },
  "target_coverage_pct": 0.95,
  "budget_nodes": 8
}'
```

### Coverage analysis for an area

`GET /coverage`

Returns the current monitoring coverage fraction for a given area and sensor type.
Coverage is defined by the spatial footprint of registered sensors given their
detection physics.

**Pricing:** $0.01 / call

```bash
curl -X GET 'https://api.geog.ai/v1/coverage?area=&type=gas_sensor' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Monitoring gap analysis for an area

`GET /gaps`

Identifies monitoring gaps within a defined area — H3 cells where no sensor provides
adequate coverage given detection physics. Returns a GeoJSON gap heatmap and summary
statistics.

**Pricing:** $0.01 / call

```bash
curl -X GET 'https://api.geog.ai/v1/gaps?area=' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## Trajectory API

Spatiotemporal trajectory storage, retrieval, and prediction.

### Trajectory history for a moving entity

`GET /trajectory`

Returns the stored trajectory (sequence of positions with timestamps and uncertainty
bounds) for a registered moving entity over a specified time window.

**Pricing:** $0.002 / call

```bash
curl -X GET 'https://api.geog.ai/v1/trajectory?entity_id=drone_01&from=2026-04-21T02:00:00Z&to=2026-04-21T03:00:00Z' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Trajectory prediction / dead reckoning

`POST /trajectory/predict`

Predicts the future trajectory of a moving entity given current position, velocity,
and historical movement patterns. Returns a predicted path with expanding uncertainty
bounds. Used for drone flight path prediction, fleet routing optimization, and
personnel zone arrival forecasting.

**Pricing:** $0.005 / call

```bash
curl -X POST 'https://api.geog.ai/v1/trajectory/predict' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "entity_id": "drone_01",
  "lookahead_min": 30,
  "mission_plan": {
    "type": "LineString",
    "coordinates": [
      [
        -99.9024,
        31.9701
      ],
      [
        -99.898,
        31.9755
      ],
      [
        -99.8932,
        31.981
      ]
    ]
  }
}'
```

---

## Graph API

Spatial graph operations — influence spread, evacuation routing, upstream tracing.

### Downstream influence spread from a node

`GET /graph/influence`

Returns all nodes in the sensor graph that are "downstream" of a source event —
entities likely to be affected if the source produces an event. Edge weights reflect
propagation physics (plume transport, fluid flow, RF signal paths). Results are
ordered by predicted arrival time and estimated impact magnitude.

**Pricing:** $0.005 / call

```bash
curl -X GET 'https://api.geog.ai/v1/graph/influence?source_id=sensor_h2s_023' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Evacuation routing given current hazard state

`GET /graph/evacuate`

Returns optimal evacuation routes from specified locations to safe zones, routing around
active hazard polygons (from live plume, fire, or flood simulations). Uses pgRouting on
the facility graph with real-time hazard avoidance.

**Pricing:** $0.01 / call

```bash
curl -X GET 'https://api.geog.ai/v1/graph/evacuate?from_ids=personnel_zone_C,personnel_zone_D' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Upstream sensor identification for a gauge

`GET /graph/upstream`

For a given stream gauge or sensor, identifies all upstream contributing sensors
in the watershed or pipeline network, ordered by distance and travel time.

**Pricing:** $0.003 / call

```bash
curl -X GET 'https://api.geog.ai/v1/graph/upstream?entity_id=gauge_colorado_a7' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## Calibration

Site-specific model calibration state.

### Site-specific model calibration state

`GET /model/calibration`

Returns the current calibration state for a given propagation model and site —
including calibration age, parameter corrections applied, and prediction accuracy
metrics over the last 30/90 days.

Requires Site Calibration plan.

**Pricing:** $0.001 / call (Site Calibration plan only)

```bash
curl -X GET 'https://api.geog.ai/v1/model/calibration?model=plume&site_id=wellpad_07' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## Jobs

Async job status polling.

### Poll async job status and retrieve result

`GET /jobs/{job_id}`

Returns the current status of an async simulation job. When status is `complete`,
the full result is included in the `result` field of the response envelope.
Jobs expire 48 hours after completion.

```bash
curl -X GET 'https://api.geog.ai/v1/jobs/job_01hx2a3f9k8qmzn7p' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## Webhooks

Register HTTPS callbacks to receive async job results, signed with HMAC-SHA256.

### List all registered webhooks for the account

`GET /webhooks`

Returns every webhook registered on the calling account, ordered by `created_at` descending.
The signing `secret` is **never** returned by this endpoint — it is only available on the
initial `POST /webhooks` response. To rotate a secret, delete the webhook and register a new one.

```bash
curl -X GET 'https://api.geog.ai/v1/webhooks' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Register a webhook endpoint to receive async job results

`POST /webhooks`

Registers an HTTPS callback URL on your account. Once registered, every async job your
account creates (`/simulate/*`, `/rf/mesh/viability`, `/rf/optimize/placement`,
`/optimize/*`) will POST its full result envelope to this URL when it transitions to
`complete` or `failed`. Per-job overrides can still be supplied via the `webhook_url`
field on the job request body.

## Delivery payload

Each delivery is a `POST` with `Content-Type: application/json` containing the same
response envelope returned by `GET /jobs/{job_id}` when the job is complete.

The following headers are always set on the delivery:

| Header | Description |
| --- | --- |
| `X-Geog-Signature` | Hex-encoded HMAC-SHA256 of `{timestamp}.{raw_body}` using the webhook secret. Format: `t=<unix_ts>,v1=<hex_digest>` |
| `X-Geog-Event` | Event type (`job.complete`, `job.failed`, `calibration.updated`) |
| `X-Geog-Delivery` | Unique delivery ID — safe to use for idempotency on the receiver side |
| `X-Geog-Webhook-Id` | ID of the registered webhook that produced this delivery |
| `X-Geog-Test` | Present and set to `true` only on deliveries produced by `POST /webhooks/{id}/test`. Absent on real job-driven deliveries — receivers can use it to short-circuit production side-effects for synthetic traffic |

## Verifying the signature (HMAC-SHA256)

1. Read the `X-Geog-Signature` header and parse the comma-separated `t=` (unix timestamp,
   seconds) and `v1=` (hex digest) parts.
2. Reject the delivery if `|now - t| > 300` seconds to mitigate replay attacks.
3. Concatenate `t`, a literal `.`, and the raw request body bytes.
4. Compute `HMAC-SHA256(secret, payload).hexdigest()` and compare it to `v1` using a
   constant-time comparison (e.g. `hmac.compare_digest`). Reject on mismatch.

## Retry behaviour

Non-2xx responses (or no response within 10 seconds) trigger automatic retries with
exponential backoff at 30s, 2m, 10m, 30m, 2h, 6h, and 24h. After 7 failed attempts the
delivery is marked `undeliverable`. If 50% of deliveries in the last hour have failed
the webhook is automatically disabled (`active=false`, `disabled_reason=auto_failure_rate`)
and an email is sent to the account owner. To detect this auto-disable programmatically
rather than relying on the owner's inbox, either subscribe to the `webhook.disabled`
event (delivered on the final attempt before the webhook is paused — best subscribed on
a separately-monitored receiver) or poll `GET /webhooks/{id}/health`, which returns the
current rolling-hour failure rate, the auto-disable threshold, and the `disabled_at`
timestamp once tripped. All deliveries (including failures) are visible on the dashboard
for 30 days.

```bash
curl -X POST 'https://api.geog.ai/v1/webhooks' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://api.acme.com/hooks/geog",
  "events": [
    "job.complete",
    "job.failed",
    "webhook.disabled"
  ],
  "description": "Production plume results \u2192 SOC pipeline",
  "active": true
}'
```

### Retrieve a single registered webhook

`GET /webhooks/{webhook_id}`

Returns the current configuration of one webhook. The signing `secret` is **not**
returned — it is only available on the original `POST /webhooks` response. Rotate by
deleting and re-registering.

```bash
curl -X GET 'https://api.geog.ai/v1/webhooks/{webhook_id}' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Update a registered webhook

`PATCH /webhooks/{webhook_id}`

Partially updates a webhook. Send only the fields you want to change. Common uses:
toggling `active` to pause/resume deliveries, re-pointing `url` after a backend
migration, or narrowing the subscribed `events`.

The signing `secret` cannot be changed via this endpoint. To rotate it, `DELETE` the
webhook and register a new one with `POST /webhooks` — store the new secret from that
response, then cut over your verification code.

```bash
curl -X PATCH 'https://api.geog.ai/v1/webhooks/{webhook_id}' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "url": "https://api.acme.com/hooks/geog-v2",
  "events": [
    "job.complete",
    "job.failed",
    "webhook.disabled"
  ],
  "description": "Production plume results \u2192 SOC pipeline (v2)",
  "active": false
}'
```

### Delete a registered webhook

`DELETE /webhooks/{webhook_id}`

Permanently removes a webhook. Deliveries already in flight may still arrive briefly,
but no new deliveries will be queued. Use this to revoke a leaked endpoint or to rotate
the signing secret (delete, then `POST /webhooks` to register a fresh one).

```bash
curl -X DELETE 'https://api.geog.ai/v1/webhooks/{webhook_id}' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Send a synthetic event to a webhook to verify the receiver

`POST /webhooks/{id}/test`

Generates a synthetic delivery for the registered webhook and POSTs it to the URL
exactly as a real event would be sent — same envelope shape, same `X-Geog-Signature`
HMAC, same retry rules. Designed for day-one verification of a brand-new receiver
before any real job has run: confirm that the URL is reachable, the TLS certificate
is valid, and the receiver's signature-verification code accepts the payload.

## Distinguishing test deliveries

Test deliveries carry an additional `X-Geog-Test: true` header on the outbound POST,
and the resulting `WebhookDelivery` record reports `test: true`. The synthetic
`job_id` is prefixed `job_test_` so it cannot collide with a real job. Either signal
is safe to gate on; the header is recommended so the receiver can short-circuit
production side-effects (alerting, billing, downstream writes) before parsing the
body.

Test deliveries appear in `GET /webhooks/{id}/deliveries` alongside real ones, are
subject to the same retry schedule and auto-disable rules, and are throttled to 60
test deliveries per webhook per hour (`TEST_RATE_LIMITED`).

```bash
curl -X POST 'https://api.geog.ai/v1/webhooks/whk_01hx2f8q7n3tdvm5j/test' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "event": "job.complete"
}'
```

### List recent deliveries for a webhook

`GET /webhooks/{id}/deliveries`

Returns a paginated list of delivery attempts for the given webhook, newest first.
Mirrors the delivery log shown on the dashboard and is intended for debugging
receiver-side bugs without waiting for the next real job.

Deliveries are retained for 30 days; older records return an empty page rather than
an error. Use the `status` filter to narrow to actively-failing deliveries you may
want to replay after fixing your handler.

```bash
curl -X GET 'https://api.geog.ai/v1/webhooks/whk_01hx2f8q7n3tdvm5j/deliveries' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Re-send a previously attempted webhook delivery

`POST /webhooks/{id}/deliveries/{delivery_id}/replay`

Re-queues a previously attempted delivery for immediate re-send to the webhook's
currently registered URL. Useful after fixing a receiver-side bug — there is no need
to wait for the next real job.

**Replays use the original payload and signature timestamp.** The exact bytes that
were originally signed are re-sent, and the original `X-Geog-Signature` (including
its `t=` value) and `X-Geog-Delivery` ID are reused. This means:

- HMAC verification on the receiver behaves identically to the original delivery.
- The 5-minute replay-window check (`|now - t| > 300`) **will reject the request**
  if the original signature timestamp is older than 5 minutes. Receivers that need
  to accept replays of old deliveries should relax this check for requests carrying
  a known `X-Geog-Delivery` ID.
- Idempotent receivers keyed on `X-Geog-Delivery` will naturally deduplicate.

The replay attempt counts toward the webhook's retry budget and the delivery's
`attempt_count` is incremented. Calling replay on a delivery whose `status` is
already `pending` returns `409 DELIVERY_ALREADY_PENDING`.

```bash
curl -X POST 'https://api.geog.ai/v1/webhooks/whk_01hx2f8q7n3tdvm5j/deliveries/whd_01hx2g4n8p7rqzm3k/replay' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Read the rolling-hour delivery health for a webhook

`GET /webhooks/{id}/health`

Returns the same rolling-window failure-rate counters that drive the platform's
auto-disable rule, plus the current `active` / `disabled_reason` state. Use this
endpoint to detect — programmatically, without depending on the account owner's
inbox — when a webhook has been auto-disabled after sustained delivery failures
(≥50% failed deliveries in the last hour), or is trending toward it.

Typical patterns:

- **Polling guard.** Call once a minute from the same job that registered the
  webhook. Alert your own on-call channel as soon as `status == "degraded"`
  (failure rate has crossed 25%) so you can fix the receiver before the platform
  auto-disables it. Treat `status == "disabled"` with `disabled_reason ==
  "auto_failure_rate"` as a hard incident.
- **Post-mortem.** After receiving a `webhook.disabled` event delivery (or the
  owner-email alert), call this endpoint to read `failure_rate`, `failed_count`,
  `undeliverable_count`, and `last_success_at` to understand exactly what tripped
  the auto-disable rule.

This endpoint is read-only and cheap — it does not consume webhook delivery quota
and is rate-limited only by the standard account-level rate limit.

```bash
curl -X GET 'https://api.geog.ai/v1/webhooks/whk_01hx2f8q7n3tdvm5j/health' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

---

## STAC Catalog

### STAC catalog root

`GET /v1/stac/`

STAC-API §1.0.0 catalog root. Carries the conformance set and
navigation links. Tenant-scoped: collections listed are filtered
to the caller's `vertical_ids` and own `tenant_id`.

**Conformance set:** Core, Collections, OGC API Features, Item Search
(GET + POST). CQL2 filter is intentionally **not** advertised — see
the geog.ai roadmap entry `geog-ai-15` for the planned addition.

**Pricing:** $0.002 / call (read family, 2.0 cost units).

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### STAC conformance classes implemented by this server

`GET /v1/stac/conformance`

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/conformance' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### List STAC Collections visible to the caller

`GET /v1/stac/collections`

One Collection per `(engine, vertical_id)` tuple the tenant has
produced jobs for. Each Collection carries `extent.spatial.bbox`,
`extent.temporal.interval`, a per-tenant `license` (varies by plan),
and a `providers` chain naming geog.ai as licensor and the engine
as processor.

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/collections' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Fetch a single STAC Collection

`GET /v1/stac/collections/{collection_id}`

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/collections/<collection_id>' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### List STAC Items within a Collection

`GET /v1/stac/collections/{collection_id}/items`

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/collections/<collection_id>/items' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### Fetch a single STAC Item

`GET /v1/stac/collections/{collection_id}/items/{item_id}`

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/collections/<collection_id>/items/<item_id>' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### STAC Item Search (GET)

`GET /v1/stac/search`

STAC §15 Item Search. Supported predicates:
  - `bbox` — pushed into PostGIS via `ST_Intersects`.
  - `datetime` — ISO instant or `start/end` interval.
  - `collections` — comma-separated `{engine}__{vertical_id}` list.
  - `limit` / `cursor` — opaque signed pagination.

Tenant-scoped: every result row is intersected against the caller's
`vertical_ids` and `tenant_id` (per Phase 3 §3.11 acceptance
criterion 9). CQL2 filtering is **not** supported on this
endpoint — see roadmap entry `geog-ai-15`.

```bash
curl -X GET 'https://api.geog.ai/v1/v1/stac/search' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json'
```

### STAC Item Search (POST)

`POST /v1/stac/search`

Identical predicate set to `GET /v1/stac/search`, sent as a JSON
body. STAC clients use this surface when query strings would
exceed proxy URL-length limits, or when posting array-shaped
`bbox` / `collections` values.

```bash
curl -X POST 'https://api.geog.ai/v1/v1/stac/search' \
  -H 'Authorization: Bearer geog_test_sk_replace_me' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "bbox": [
    -100.0,
    31.0,
    -99.5,
    32.0
  ],
  "datetime": "2026-04-01T00:00:00Z/2026-04-30T23:59:59Z",
  "collections": [
    "plume_aermod__oilgas"
  ],
  "limit": 50
}'
```

---
