API & MCP
Query geolocated world event signals and the Daily World Brief programmatically — over a REST API and an MCP server. Usage is metered on your token balance.
Overview
The API and MCP server are available on every plan, including Free. Every endpoint lives under https://offnadir-delta.com/api/v1 and authenticates with an API key. Requests are billed to your own token balance — the same wallet used across the app.
Issue and manage keys from Account → Developer API. No plan yet? See pricing.
Quickstart
Fetch the free Daily World Brief to confirm your key works, then query signals:
# 1) Free — verify your key
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/brief"
# 2) Query signals (3 token/page)
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals?days=1&categories=armed_conflict&limit=50"Authentication
Pass your key as a bearer token (or the X-API-Key header). Keys are shown once at creation — store them securely and never expose them client-side.
Authorization: Bearer ond_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxErrors
Errors return a JSON body with a stable machine-readable code, a human message, and a requestId for support.
{
"error": "date must be YYYY-MM-DD",
"code": "INVALID_PARAMETERS",
"requestId": "req_1783..."
}| 401 | Missing or invalid key |
| 403 | Your plan does not include this endpoint |
| 402 | Insufficient tokens for a metered call |
| 400 | Invalid parameters (see the message) |
| 404 | Resource not found |
| 429 | Rate limit exceeded |
Rate limits
Successful responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. When exceeded, the API returns 429 with a Retry-After header — back off and retry after the indicated delay.
Pagination
/signals is cursor-paginated. Each response carries meta.next_cursor and meta.has_more; pass the cursor back to fetch the next page. Each page is billed as one query.
# follow the cursor until has_more is false
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals?limit=100&cursor=MTAw"Endpoints
Grouped by resource. Every endpoint lives under /api/v1 and is billed to your token balance.
Signals
Query geolocated event signals, aggregate trends, and geographic hotspots.
/api/v1/signals3 tok / pageGeolocated world event signals — geopolitical, security, disaster, and infrastructure events distilled from global news media, enriched with AI-verified geolocation, severity/GEOINT scores, and satellite-collection recommendations. Returns up to 500 rows per page (cursor-paginated).
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | Bounding box "minLon,minLat,maxLon,maxLat" (WGS84). Omit for worldwide. |
| date | string (date) | Window end date YYYY-MM-DD (UTC). Defaults to today. |
| days | integer 1–30 | Window length in days ending on `date`. |
| categories | string | Comma-separated category filter. Omit for all. |
| markets | string | Comma-separated market-impact filter (oil, natural_gas, grain, shipping, defense, metals, semiconductors, fx, equities): only signals AI-tagged as exposing these markets via a direct physical/supply channel. Informational only — not investment advice. Omit for all. |
| limit | integer 1–500 | Max rows per page. |
| cursor | string | Opaque pagination cursor from a previous response's meta.next_cursor. |
| min_severity | number 0–10 | Keep only signals with severity_score >= this (1-10). |
| escalating | boolean | Set true to keep only signals whose escalation_trend is "escalating". |
| sort | string | Result ordering. Omit for the feed default ranking. "geoint" ranks by GEOINT spatial-collection value so imageable, high-value events rise and non-observable news noise sinks. |
| updated_since | string (ISO 8601) | Differential fetch: only signals (re)enriched at/after this ISO 8601 timestamp. Ignores the date window; response signals carry last_updated_at. Use for incremental sync. |
| created_since | string (ISO 8601) | Differential fetch: only signals first enriched at/after this ISO 8601 timestamp. |
| observability | string | Keep only signals with this satellite observability — is a physical mark imageable at all (intelligence.satellite_observability). |
| observability_status | string | 3-state observability filter (observable / not_observable / insufficient_detail). Exposes the insufficient_detail bucket that the binary observability filter excludes from both sides. |
| open_data | string | Keep only signals with this open-data sufficiency — free imagery enough vs commercial tasking recommended (intelligence.open_data_sufficiency). |
| min_information_gain | number 0–1 | Keep only signals whose intelligence.expected_information_gain >= this (0-1). |
| taskable_only | boolean | Keep only signals whose coordinate is search_ready — GEO-READY ONLY: drops country centroids, ADM1 mismatches, reporting-dateline fallbacks and unresolved fixes (intelligence.geo_validation.search_ready). Does NOT imply observable or quality-passed; for automated imagery tasking use collection_ready_only (or combine with observability=observable). |
| collection_ready_only | boolean | STRICT tasking-candidate filter: search_ready AND observability=observable AND quality.status!=failed AND a concrete collection plan (rs_target + rs_reason present) AND an event coordinate. The safe input set for automated imagery tasking. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals?days=1&categories=armed_conflict&limit=2"{
"meta": {
"start_date": "2026-07-08",
"end_date": "2026-07-08",
"bbox": [-180, -90, 180, 90],
"categories": ["armed_conflict"],
"count": 2,
"coverage": {
"bbox_area_share": 1,
"worldwide": true,
"global_available": null,
"global_capped": false,
"global_basis": "not_probed",
"hints": []
},
"next_cursor": "Mg",
"has_more": true,
"tokens": { "charged": 3, "remaining": 9994 }
},
"signals": [
{
"id": 1312473707,
"event_date": "2026-07-08",
"category": "armed_conflict",
"event_type": "…",
"title": "…",
"summary": "…",
"location": "Kyiv, Ukraine",
"country_code": "UA",
"lat": 50.45, "lng": 30.52,
"geo_verified": true,
"severity_score": 7.8,
"geoint_score": 6.4,
"escalation_trend": "escalating",
"cross_border_risk": true,
"infrastructure_target": "energy",
"goldstein_scale": -8,
"avg_tone": -4.2,
"actors": { "actor1": "…", "actor2": "…" },
"tags": ["strike", "energy"],
"num_mentions": 42,
"num_sources": 11,
"source_url": "https://…",
"market_impact": ["oil"],
"market_note": "…",
"last_updated_at": "2026-07-08T06:12:00Z",
"collection": {
"rs_level": "GSD<1m", "rs_sensor": "sentinel-1-sar",
"rs_target": "…", "rs_reason": "…",
"rs_niirs": "…", "rs_analysis": "…"
},
"intelligence": {
"satellite_observability": "observable",
"open_data_sufficiency": "commercial-recommended",
"expected_information_gain": 0.72
}
}
]
}/api/v1/signals/{id}1 tokenRe-read one signal you already hold an id for — cheaper than re-running the query that found it, and the way to check whether its verification or observability has moved since. A cluster member id resolves to its cluster.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | integer (required) | The signal id (global_event_id) from a /api/v1/signals result. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/signals/1312473707"{
"signal": { "id": 1312473707, "category": "armed_conflict", "severity": 7.4,
"verification_status": "corroborated", "independent_source_count": 4 },
"meta": { "tokens": { "charged": 1 } }
}/api/v1/signals/stats1 tok / queryAggregate statistics over the signal corpus — total event count plus a per-category and per-day (trend) breakdown for a bounding box and date window. Roll-ups, not rows: cheaper than /signals.
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | Bounding box "minLon,minLat,maxLon,maxLat" (WGS84). Omit for worldwide. |
| date | string (date) | Window end date YYYY-MM-DD (UTC). Defaults to today. |
| days | integer 1–30 | Window length in days ending on `date`. |
| categories | string | Comma-separated category filter. Omit for all. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals/stats?days=7&categories=armed_conflict"{
"meta": {
"start_date": "2026-07-05", "end_date": "2026-07-11",
"bbox": [-180, -90, 180, 90], "categories": ["armed_conflict"],
"total": 8241,
"tokens": { "charged": 1, "remaining": 9993 }
},
"stats": {
"by_category": [{ "category": "armed_conflict", "count": 8241 }],
"by_day": [{ "date": "2026-07-05", "count": 1180 }]
}
}/api/v1/signals/hotspots1 tok / queryGeographic hotspots — signal density grid-binned into cells and ranked by event count, each with peak severity and the categories present. Answers “where is activity concentrating right now?”
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | Bounding box "minLon,minLat,maxLon,maxLat" (WGS84). Omit for worldwide. |
| date | string (date) | Window end date YYYY-MM-DD (UTC). Defaults to today. |
| days | integer 1–30 | Window length in days ending on `date`. |
| categories | string | Comma-separated category filter. Omit for all. |
| precision | number 0.1–5 | Grid cell size in decimal degrees. |
| min_severity | number 0–10 | Keep only points with severity_score >= this (1-10). |
| limit | integer 1–500 | Max source points sampled before grid-binning — not the number of cells returned. Lower values fragment clusters; leave at the default for a representative density map. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals/hotspots?days=3&precision=1&min_severity=6"{
"meta": {
"start_date": "2026-07-09", "end_date": "2026-07-11",
"bbox": [-180, -90, 180, 90], "precision": 1,
"point_count": 214, "min_severity": 6,
"tokens": { "charged": 1, "remaining": 9992 }
},
"hotspots": [
{ "lat": 50, "lng": 30, "count": 37, "max_severity": 9, "categories": ["armed_conflict"] }
]
}/api/v1/developmentsmeteredThe difference between "new to you" and "new in the world". Ask this instead of diffing two /signals responses: it separates events that just happened from updates to events you already knew about, and tells you which half of the change came from the reporting and which from our own observation.
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | minLon,minLat,maxLon,maxLat. Omit for worldwide. |
| date | string | End of the window (YYYY-MM-DD). Defaults to today. |
| days | integer 1–30 | Window length ending at date (1..30, default 7). |
| categories | string | Comma-separated signal categories. |
| development_types | string | Comma-separated kinds of change. An unknown value is a 400, never an empty result. |
| notable_only | boolean | Default true. False returns every recorded change, including minor ones. |
| limit | integer 1–200 | How many developments to return (1..200, default 50). |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/developments?bbox=22,44,40,53&days=7"{
"developments": [
{ "canonical_event_id": "3f0c…", "event_id": 1319087300,
"development_type": "casualty_count_raised", "source": "world",
"previous_state": "5", "current_state": "25",
"change_summary": "The reported number killed rose from 5 to 25.",
"is_new": false, "detected_at": "2026-08-22T09:15:00Z", "event_occurred_at": "2026-08-19T03:40:00Z" },
{ "canonical_event_id": "91ab…", "event_id": 1319104412,
"development_type": "imagery_available", "source": "measurement",
"previous_state": "none", "current_state": "confirmed_post",
"change_summary": "Post-event imagery now covers this event.", "is_new": false }
],
"meta": { "events_examined": 214, "count": 2, "by_type": { "casualty_count_raised": 1, "imagery_available": 1 } }
}/api/v1/events/{eventId}/threadfreeA news feed cannot tell you whether today’s article is about today’s event. This can: the timeline separates the event from the reporting about it, so an attribution published in 2026 about a 2025 attack reads as an update, not as a new attack.
Parameters
| Parameter | Type | Description |
|---|---|---|
| eventId | integer (required) | Any signal id belonging to the event. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/events/1319087300/thread"{
"canonical_event": {
"canonical_event_id": "3f0c…", "title": "Red Fort explosion",
"occurred_at": "2025-11-10T18:00:00Z", "occurred_at_precision": "hour",
"occurred_at_basis": "stated_in_article",
"first_reported_at": "2025-11-10T19:15:00Z", "last_reported_at": "2026-08-21T06:00:00Z",
"casualties_killed": 13, "attributed_actor": "AQIS",
"attribution_basis": "attributed_by_official", "history_incomplete": false
},
"timeline": [
{ "at": "2026-08-21T06:10:00Z", "development_type": "attribution_stated", "source": "world",
"change_summary": "This event is now attributed to AQIS." }
],
"sources": [{ "event_id": 1319087300, "url": "https://…", "reported_at": "2026-08-21T06:00:00Z" }]
}Imagery
Search the satellite imagery catalog for scenes over an area, and check the optical-weather outlook before tasking — the natural follow-ups to a signal.
/api/v1/imagery2 tok / searchSearch the satellite imagery catalog (Sentinel-1, Sentinel-2, NISAR L-band) for scenes over an area and date window — the natural follow-up to a signal. Returns minimal scene metadata (id, datetime, footprint, cloud cover, platform, preview); no imagery bytes and no signed URLs. Pass the event_* parameters to get pre/post timing, SAR pair status, and per-scene target_relation for the event geometry.
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string (required) | Bounding box "minLon,minLat,maxLon,maxLat" (WGS84). Required. |
| collection | string | Catalog collection. Defaults to sentinel-2-l2a. |
| date | string (date) | Window end date YYYY-MM-DD (UTC). Defaults to today. |
| days | integer 1–30 | Window length in days ending on `date`. |
| event_date | string (date) | Event date YYYY-MM-DD (UTC). When set, each scene is tagged timing=pre/post/same_day_unknown, the window is widened to the canonical pre/post span, and meta reports has_pre_baseline / has_post / bracketing_available / window_status; for sentinel-1-grd it also reports sar_pair_status + orbit_note. |
| event_point | string | Event point "lon,lat" (WGS84). When set, each scene reports target_relation (covers_event_geometry = geometry gate; usable_for_analysis additionally requires acceptable cloud for optical) so a scene that only clips the wide bbox — or a cloud-obscured optical scene — is not presented as usable for the event. |
| event_aoi | string | Event AOI bbox "minLon,minLat,maxLon,maxLat" (WGS84) for target_relation.intersects_event_aoi / event_aoi_coverage_ratio. |
| event_timestamp | string (ISO 8601) | Full event timestamp (ISO 8601) when known — promotes same-day scenes from same_day_unknown to pre/post by time. |
| cloud_cover_max | number 0–100 | Sentinel-2 only: max cloud cover percentage. |
| limit | integer 1–100 | Max scenes to return. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/imagery?bbox=30.3,50.3,30.7,50.6&collection=sentinel-2-l2a&days=14&cloud_cover_max=30"{
"meta": {
"collection": "sentinel-2-l2a",
"start_date": "2026-06-28", "end_date": "2026-07-11",
"bbox": [30.3, 50.3, 30.7, 50.6], "count": 6,
"tokens": { "charged": 2, "remaining": 9990 }
},
"scenes": [
{
"id": "S2A_36UUA_20260710_0_L2A",
"collection": "sentinel-2-l2a",
"datetime": "2026-07-10T08:56:21Z",
"bbox": [30.2, 50.2, 30.8, 50.7],
"cloud_cover": 12.4,
"platform": "sentinel-2a",
"preview": "https://…/preview.png"
}
]
}/api/v1/collection/plan4 tokensOne call plans the imagery for one event: SAR and optical each exactly once, against the event footprint, with per-step counts of what actually covers it. Removes the two ways ad-hoc searching goes wrong — skipping the all-weather look, and repeating the same query.
Parameters
| Parameter | Type | Description |
|---|---|---|
| event_id | integer (required) | The signal id from /api/v1/signals. |
| analysis_goal | string (required) | damage_assessment | flood_mapping | wildfire_assessment. |
| event_date | string (date) | Event date YYYY-MM-DD. Optional — the event row supplies it when known. |
curl -s "https://offnadir-delta.com/api/v1/collection/plan?event_id=1315064079&analysis_goal=damage_assessment" \
-H "Authorization: Bearer ond_..."{
"meta": { "event_id": 1315064079, "analysis_goal": "damage_assessment", "event_date": "2026-07-24",
"tokens": { "charged": 4, "remaining": 9984 } },
"plan": {
"steps": [
{ "collection": "sentinel-1-grd", "query_reason": "All-weather baseline", "returned_count": 12,
"usable_count": 0, "sar_pair_status": "catalog_pair_exists_but_geometry_unverified" },
{ "collection": "sentinel-2-l2a", "query_reason": "Human-legible confirmation", "returned_count": 12, "usable_count": 0 }
],
"skipped_duplicates": 0,
"capability": { "screening": "suitable", "detection": "suitable", "identification": "not_suitable" },
"vhr_recommendation": { "open_data_still_valid": true }
}
}/api/v1/collection/priority1 tokenWhere observation is most worthwhile, and what class of satellite it needs. Separates targets a free Sentinel pass can answer from those that genuinely require commercial sub-metre tasking — the decision that precedes any imagery spend.
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | string | Area as "west,south,east,north" (WGS84). Omit for a global survey. |
| start_date | string (date) | Inclusive start date YYYY-MM-DD. Defaults to today. Not clamped to a plan history floor on this surface — the corpus epoch is the only floor. |
| end_date | string (date) | Inclusive end date YYYY-MM-DD. Defaults to today. Window capped at 30 days. |
| categories | string | Comma-separated Delta categories to restrict to. |
| min_geoint_score | number | Drop events below this GEOINT score before ranking. |
| top_n | integer 1–50 | How many top targets to return (1..50, default 12). |
curl -s "https://offnadir-delta.com/api/v1/collection/priority?bbox=30.2,50.2,30.8,50.7&top_n=10" \
-H "Authorization: Bearer ond_..."{
"meta": { "bbox": [30.2, 50.2, 30.8, 50.7], "start_date": "2026-07-25", "end_date": "2026-07-25",
"tokens": { "charged": 1, "remaining": 9989 } },
"priority": {
"by_spec_class": { "vhr": { "count": 4, "mean_importance": 0.81 }, "hr": { "count": 11, "mean_importance": 0.44 } },
"top": [ { "event_id": 1315064079, "importance": 0.86, "required_class": "vhr", "rs_aoi": [56.2, 27.1, 56.4, 27.3] } ]
}
}/api/v1/collection/observability1 tokenWhich events a sensor can actually resolve — exhaustively, not just the top few. The honest answer to "what can we see" before committing collection effort, and cheap enough to run over a whole window.
Parameters
| Parameter | Type | Description |
|---|---|---|
| sensor | string | sentinel-2 (~10 m optical) or sentinel-1 (SAR). Default sentinel-2. |
| bbox | string | Area as "west,south,east,north" (WGS84). Omit for a global survey. |
| start_date | string (date) | Inclusive start date YYYY-MM-DD. Defaults to today. Not clamped to a plan history floor on this surface — the corpus epoch is the only floor. |
| end_date | string (date) | Inclusive end date YYYY-MM-DD. Defaults to today. Window capped at 30 days. |
| categories | string | Comma-separated Delta categories to restrict to. |
| min_geoint_score | number | Drop events below this GEOINT score before surveying. |
| top_n | integer 1–50 | How many observable events to return (1..50, default 20). |
curl -s "https://offnadir-delta.com/api/v1/collection/observability?sensor=sentinel-1&bbox=30.2,50.2,30.8,50.7" \
-H "Authorization: Bearer ond_..."{
"meta": { "bbox": [30.2, 50.2, 30.8, 50.7], "start_date": "2026-07-25", "end_date": "2026-07-25",
"tokens": { "charged": 1, "remaining": 9988 } },
"survey": {
"sensor": "sentinel-1",
"observable_count": 9,
"not_observable_count": 23,
"by_rs_level": { "GSD<10m": 6, "GSD<100m": 3 },
"top": [ { "event_id": 1315107514, "rs_target": "damaged structures ~10m at Bucha" } ]
}
}/api/v1/passes2 tokensWhen can this place next be imaged, and by what. Free systematic passes (Sentinel/Landsat) are separated from taskable commercial access windows (WorldView / ICEYE / Capella / SkySat), so you can decide whether to wait or to pay — and a failed element fetch says timing is unavailable rather than reporting "no passes".
Parameters
| Parameter | Type | Description |
|---|---|---|
| lat | number -90–90 | Target latitude (-90..90; positive = North). Required unless bbox is given. |
| lon | number -180–180 | Target longitude (-180..180; positive = East). Required unless bbox is given. |
| bbox | string | Area as "west,south,east,north" (WGS84). The CENTRE is used as the target when lat/lon are omitted. |
| start_date | string (date) | Inclusive start date YYYY-MM-DD (UTC). Defaults to today. |
| end_date | string (date) | Inclusive end date YYYY-MM-DD (UTC). Defaults to start+2 days; capped to a 7-day horizon. |
| satellites | string | Comma-separated family keys to consider: sentinel-1, sentinel-2, landsat, nisar, worldview, iceye, capella, skysat, umbra, synspective, iqps, radarsat-2, cosmo-skymed. Omit for all thirteen. |
| max_passes | integer 1–100 | Maximum passes to return, soonest first (1..100, default 40). |
curl -s "https://offnadir-delta.com/api/v1/passes?lat=27.1842&lon=56.2893&satellites=sentinel-1,sentinel-2" \
-H "Authorization: Bearer ond_..."{
"meta": {
"target": { "lat": 27.1842, "lon": 56.2893 },
"window": { "start": "2026-07-25T00:00:00Z", "end": "2026-07-27T23:59:59Z" },
"tokens": { "charged": 2, "remaining": 9988 }
},
"passes": [
{
"satellite": "SENTINEL-1C",
"family_key": "sentinel-1",
"sensor": "sar",
"collection_mode": "systematic",
"peak": "2026-07-26T02:14:08Z",
"off_nadir_deg": 21.4,
"direction": "descending",
"sunlit": false,
"tle_age_days": 1.2, "is_past": false
}
],
"freshness": { "max_tle_age_days": 1.2, "stale": false },
"retrieval_ok": true,
"note": "Systematic (Sentinel/Landsat) passes are scheduled acquisitions.",
"attribution": "Orbital data by CelesTrak"
}Measure
Put a number on the ground itself: how an index moved across the archive, how many vessels are in a scene, what the terrain does to a radar look. These measure imagery on demand — Monitoring, further down, is the same measurements repeated as new acquisitions arrive.
/api/v1/elevationfreeHeight, and the numbers that depend on it. Relief (max − min) is the one that matters operationally: it governs how much of an area radar loses to layover and shadow, so a fabricated relief produces fabricated collection advice. Land only — no coverage over water is reported as absence, never as 0 m.
Parameters
| Parameter | Type | Description |
|---|---|---|
| lat | number -90–90 | Latitude of a single point (use with lon). |
| lon | number -180–180 | Longitude of a single point (use with lat). |
| bbox | string | Area as "lon_min,lat_min,lon_max,lat_max" (WGS84). |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/elevation?bbox=138.70,35.34,138.76,35.38"{
"mode": "bbox",
"elevation": { "min_m": 852.4, "max_m": 3768.4, "mean_m": 1892.7, "median_m": 1740.2,
"p10_m": 1004.1, "p90_m": 2988.6, "relief_m": 2916.0,
"sample_spacing_m": 31, "downsampled": true, "measured_over": "bbox" },
"attribution": "Produced using Copernicus WorldDEM™-30 …"
}/api/v1/elevationfreeThe same measurement clipped to the ring you drew rather than the box around it. For an L-shaped or diagonal AOI those are different areas, and reporting the box as "the polygon" is the overstatement this endpoint exists to avoid — the response names the clip in `measured_over`.
Parameters
| Parameter | Type | Description |
|---|---|---|
| polygon | array[] | WGS84 ring [[lon, lat], …] with at least 3 vertices. A GeoJSON Polygon or Feature is also accepted. |
| bbox | number[] | Alternative to polygon: [lon_min, lat_min, lon_max, lat_max]. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"polygon": [[138.72,35.34],[138.76,35.36],[138.73,35.38],[138.70,35.36]]}' \
"https://offnadir-delta.com/api/v1/elevation"{
"mode": "polygon",
"elevation": { "min_m": 1204.8, "max_m": 3768.4, "mean_m": 2311.5, "relief_m": 2563.6,
"measured_over": "polygon_interior", "polygon_vertices": 4 },
"attribution": "Produced using Copernicus WorldDEM™-30 …"
}/api/v1/terrainfreeThe arithmetic behind "can radar actually use this ground", and behind "can this position see that one". Both change with inputs that must not be assumed: layover measured on one volcanic flank was 2.6% looking west against 0.7% looking east at the same 35°, which is why the geometry is required rather than defaulted. Both answers state the sample spacing they were computed at — a coarser grid reads flatter, and therefore more observable, than the ground is.
Parameters
| Parameter | Type | Description |
|---|---|---|
| operation | string (required) | Which computation to run. |
| bbox | string | For sar_geometry: area as "lon_min,lat_min,lon_max,lat_max" (WGS84). |
| incidence_deg | number 10–80 | For sar_geometry: sensor incidence angle. Sentinel-1 IW spans roughly 29-46 degrees. |
| look_azimuth_deg | number 0–360 | For sar_geometry: compass bearing the sensor looks along the ground range. A right-looking descending pass looks roughly west (270). |
| lat | number -90–90 | For profile: latitude of the observer end. |
| lon | number -180–180 | For profile: longitude of the observer end. |
| to_lat | number -90–90 | For profile: latitude of the far end. |
| to_lon | number -180–180 | For profile: longitude of the far end. |
| observer_height_m | number | For profile: eye height above the ground, default 2 m. Use the real mast or tower height when that is the question. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/terrain?operation=sar_geometry&bbox=138.70,35.34,138.76,35.38&incidence_deg=35&look_azimuth_deg=270"{
"operation": "sar_geometry",
"sar_geometry": { "layover_fraction": 0.026, "shadow_fraction": 0.0,
"foreshortening_fraction": 0.517, "usable_fraction": 0.457,
"mean_local_incidence_deg": 32.7, "sample_spacing_m": 31 },
"interpretation": "3% of the area is in layover at this geometry …"
}/api/v1/signals/{id}/refine-location3–29 tokens, 0 if it does not improveMost signals are geolocated once, for free, from one article’s place string — about two thirds sit at ±5 km or have no bounded radius at all. This buys a second, deeper read of the sources for one signal you care about.
Parameters
| Parameter | Type | Description |
|---|---|---|
| id | integer (required) | The signal (global_event_id) whose location should be researched further. |
curl -X POST -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/signals/1318224420/refine-location"{
"summary": "Refined from locality precision (±5000 m) to site precision (±500 m) on a source that names "Narita International Airport, Chiba, Japan".",
"result": {
"improved": true,
"before": { "granularity": "locality", "uncertainty_m": 5000, "footprint_kind": "area" },
"after": { "granularity": "site", "uncertainty_m": 500, "footprint_kind": "point" },
"evidence": [{ "url": "https://…", "publisher": "…", "place_named": "Narita International Airport, Chiba, Japan", "origin": "live_search" }],
"tokens": { "charged": 14, "maximum_possible": 29 }
}
}/api/v1/index-series0.5 tokens / scene measuredThe retrospective half of Monitoring: the same measurement, run backwards over the archive instead of forwards over new acquisitions. Estimate first — it is free, and it is the only way to know the cost before spending it.
Parameters
| Parameter | Type | Description |
|---|---|---|
| polygon | array[] | WGS84 ring [[lon, lat], …] with at least 3 vertices. Statistics cover the samples INSIDE it. A GeoJSON Polygon or Feature is also accepted. |
| bbox | number[4] | Alternative to polygon: [lon_min, lat_min, lon_max, lat_max] (WGS84). |
| index | string (required) | Which optical index to measure: ndvi, evi, savi, ndmi, ndwi, mndwi, ndbi, ndsi, nbr, iron-oxide, clay, ferrous. |
| start | string (date) (required) | Start date YYYY-MM-DD (UTC). Clamped forward to 2015-06-27 if earlier. |
| end | string (date) (required) | End date YYYY-MM-DD (UTC). |
| max_scenes | integer 1–24 | Cap on scenes measured in this call. Server maximum 24. |
| max_cloud_cover | number 1–100 | Scene cloud-cover ceiling in percent. Default 30. |
| estimate_only | boolean | True = free: return the scene count, date span and token cost WITHOUT measuring or charging. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"bbox": [67.64, 33.31, 67.74, 33.84], "index": "ndvi", "start": "2016-01-01", "end": "2026-08-01", "estimate_only": true}' \
"https://offnadir-delta.com/api/v1/index-series"{
"summary": "322 Sentinel-2 scene(s) match. The 100 examined span 2023-02-06 to 2026-07-25. Measuring 24 of them would cost 12 token(s) (0.5 per scene).",
"estimate": { "index": "ndvi", "scenes_found": 322, "scenes_examined": 100, "scenes_measurable": 24,
"truncated": true, "archive_start": "2015-06-27", "estimated_tokens": 12,
"candidate_date_span": { "first": "2023-02-06", "last": "2026-07-25" },
"notes": ["Only the newest 100 of those 322 were examined, so the earliest date shown here is the edge of that page — not the start of the archive …"] },
"charged": false
}/api/v1/ships5 tokens / detectionRadar sees through cloud and at night, so a vessel count works when optical would not. The number alone is not the answer — what the land mask did, and how much of your area the scene actually covers, decide what it means.
Parameters
| Parameter | Type | Description |
|---|---|---|
| collection | string (required) | Catalog collection of the scene. |
| item_id | string (required) | STAC item id of the scene, as returned by GET /api/v1/imagery. |
| bbox | number[4] | Area to search within the scene, [lon_min, lat_min, lon_max, lat_max] (WGS84). Must be under 120,000 km². |
| geometry | object | Alternative to bbox: a GeoJSON Polygon or MultiPolygon. |
| algorithm_version | string | Detector version. 'auto' picks the recommended version for the sensor. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"collection": "sentinel-1-grd", "item_id": "S1A_IW_GRDH_1SDV_20260801T031500", "bbox": [37.79, 44.69, 37.83, 44.72]}' \
"https://offnadir-delta.com/api/v1/ships"{
"summary": "14 vessel-like target(s) detected over the requested area (sentinel-1-grd, algorithm v3). Detections within 300 m of the coast were excluded, so vessels alongside a quay are not in this count.",
"count": 14,
"caveats": ["Detections within 300 m of the coast were excluded, …"],
"processing": { "resolved_algorithm_version": "v3", "sea_ratio": 0.62, "land_mask_source": "osm:land_polygons" }
}Brief
The AI-synthesized Daily World Brief (free).
/api/v1/brieffreeThe Daily World Brief — an AI-synthesized digest of the previous day’s worldwide event signals (headline, executive summary, top developments, ranked signals). Free of token charges.
Parameters
| Parameter | Type | Description |
|---|---|---|
| date | string (date) | Brief date YYYY-MM-DD (UTC). Defaults to the latest available. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/brief"{
"brief": {
"date": "2026-07-07",
"headline": "…",
"summary": "…",
"developments": [ { "title": "…", "why_it_matters": "…" } ],
"key_judgments": [ … ]
}
}Intelligence (AI)
AI assessment and the agentic analyst — available on every plan, metered on your token balance.
/api/v1/assess5/15 tok · AIAI remote-sensing deep-dive for a single signal: what to observe, recommended sensors, and a collection window. `eventId` is the `id` from a signals result. A prior (event, kind) assessment is cached and not re-charged. Available on every plan, including Free; metered on your token balance. The response also carries a deterministic `context` block whose `imagery_handoff.parameters` are the exact `/api/v1/imagery` inputs for real pre/post scene candidates — pass them through unchanged.
Parameters
| Parameter | Type | Description |
|---|---|---|
| eventId | integer (required) | Signal id (global_event_id) from /api/v1/signals. |
| kind | string | Assessment depth. quick=5 tok, deep=15 tok. Defaults to quick. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"eventId": 1312473707, "kind": "quick"}' \
"https://offnadir-delta.com/api/v1/assess"{
"kind": "quick",
"cached": false,
"model": "…",
"content": {
"summary": "…",
"observability": "…",
"recommendedSensors": ["SAR satellites", "high-resolution optical"]
},
"context": {
"event_id": 1312473707,
"event_date": "2026-07-24",
"target": "port terminal",
"aoi_bbox": [30.72, 46.46, 30.79, 46.52],
"satellite_observability": "observable",
"quality_status": "passed",
"imagery_handoff": {
"tool": "search_imagery",
"handoff_mode": "targeted_collection",
"parameters": {
"bbox": [30.72, 46.46, 30.79, 46.52],
"eventDate": "2026-07-24",
"eventPoint": [30.75, 46.49],
"eventAoi": [30.72, 46.46, 30.79, 46.52],
"eventTimestamp": null
},
"missing_parameters": [
{ "field": "eventTimestamp", "reason": "Only the event date is available; same-day scenes cannot be ordered pre/post without a wall-clock time." }
]
}
},
"meta": { "tokens": { "charged": 5, "remaining": 195 } }
}/api/v1/analyst5–123 tok · AIAsk the Delta Analyst an OSINT/GEOINT question. Runs an agentic multi-step analysis and returns a structured brief (summary, findings with collection recommendations, assessment, citations). Usage-based pricing: 5–123 tokens metered by the compute the question actually uses (the exact charge is returned in meta.tokens.charged). Hybrid async: the call waits up to ~95s and returns the brief (200); a longer run returns 202 with a job_id to poll at GET /api/v1/analyst/{jobId}. Send an Idempotency-Key header to make a timeout or a retry safe — the same key resolves to the same run with no second charge. Available on every plan, including Free; metered on your token balance.
Parameters
| Parameter | Type | Description |
|---|---|---|
| Idempotency-Key | string | At-most-once key. A re-send with the same key resolves to the SAME run: done → the brief (no re-charge); still running → 202 with the job_id. |
| question | string (required) | The analytic question (≤ 500 chars). |
| bbox | number[4] | Optional focus bounding box [minLon, minLat, maxLon, maxLat] (WGS84). |
| mode | string | fast (default) or deep. Deep enables extended reasoning and wider evidence-gathering budgets for forecasting, collection trade-offs and market-implication questions; it is slower and the ceiling rises from 123 to 415 tokens. Charging stays metered by what the run actually consumes. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: my-question-2026-07-25-1" \
-d '{"question": "Key escalation signals in the Black Sea this week?"}' \
"https://offnadir-delta.com/api/v1/analyst"{
"brief": {
"summary": "…",
"findings": [ { "global_event_id": 131…, "recommended_sensor": "SAR", "rationale": "…" } ],
"assessment": "…",
"citations": [ "https://…" ]
},
"meta": { "tokens": { "charged": 12, "remaining": 150 } }
}/api/v1/analyst/{jobId}freePoll an analyst run started by POST /api/v1/analyst. Free — the run itself is charged once, on completion. While the job is processing the response carries a progress block (stage plus completed_steps / total_steps); completed_steps reaches total_steps only when status is "done".
Parameters
| Parameter | Type | Description |
|---|---|---|
| jobId | string (uuid) (required) | The job_id returned by POST /api/v1/analyst. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/analyst/c27a5d66-64a6-4785-aa9a-ea182e23efa6"{
"job_id": "c27a5d66-64a6-4785-aa9a-ea182e23efa6",
"status": "processing",
"progress": { "stage": "synthesis", "completed_steps": 3, "total_steps": 6 },
"created_at": "2026-07-25T09:12:00Z",
"updated_at": "2026-07-25T09:12:41Z"
}Evidence
The claim ledger: what you were told, how well established it was, and whether a later answer restated it — the audit trail behind the answers.
/api/v1/claimsfreeWhat you were told, and whether it held. Each claim keeps its evidence class and independent source count, and — the part no other feed gives you — a link to the later answer that restated it, saying whether the evidence got stronger or weaker. Audit before you act on it.
Parameters
| Parameter | Type | Description |
|---|---|---|
| since | string | Only claims asserted on or after this date (YYYY-MM-DD or ISO 8601). |
| evidence_class | string | Restrict to one evidence class. |
| restated_only | boolean | Only claims that sit in a restatement chain. |
| downgraded_only | boolean | Only claims a later answer restated with WEAKER evidence — read these first. |
| event_id | integer | Only claims about this event (global event id). Two anchors are tried, both deterministic: the claim’s own recorded event, and the registry facilities the event is linked to. Neither is a guess — when neither resolves, nothing is returned and `event_link.linked` says so. |
| limit | integer 1–200 | How many claims to return (1..200, default 50). |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/claims?downgraded_only=true"{
"claims": [
{ "claim_id": "8f3c…", "text": "US fired on a ship attempting to break the blockade…",
"evidence_class": "REPORTED", "independent_source_count": 2,
"sources": ["reuters.com", "apnews.com"],
"restated_by": { "claim_id": "b21a…", "evidence_class": "CONFIRMED",
"what_changed": "A later answer restated this with STRONGER evidence." } }
],
"counts": { "total": 1, "by_evidence_class": { "REPORTED": 1 }, "restated": 1, "downgraded": 0 },
"summary": "1 recorded claim(s): 1 REPORTED. 1 sit in a restatement chain."
}/api/v1/discriminators3 tokensBefore you task a satellite, find out whether the picture could change your mind. This returns the observation that would refute the most of what you currently believe — and, in the same answer, the observations that every one of your statements predicts, which therefore cannot separate them however convincing they look.
Parameters
| Parameter | Type | Description |
|---|---|---|
| hypotheses | string | Two to eight competing statements, separated by `|` (not a comma — statements contain commas). Each up to 500 characters: one assertion per hypothesis, not a paragraph. |
| mode | string | `competing` (default) when the statements are mutually exclusive — the goal is to separate them, so an observable required by all of them has no diagnostic value. `joint` when they are held together — the goal is to falsify the most at once, so an observable required by all of them is the best one. Ignored with `event_id`, which is always joint. |
| event_id | integer | Use the standing claims you were given about this event (global event id) as the statements. Restated claims are excluded — only what currently stands is tested. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/discriminators?hypotheses=Naval%20forces%20rerouted%20shipping%20away%20from%20the%20strait.%7CShipping%20halted%20because%20the%20port%20was%20closed."{
"meta": { "mode": "competing", "source": "supplied", "hypothesis_count": 2 },
"best": { "label": "How full a port is", "verdict": "actionable",
"refutes_if_absent": ["h2"], "max_elimination": 1,
"data_sources": ["sentinel-1-sar", "sentinel-2", "vhr-optical"] },
"discriminators": [
{ "label": "A vessel changed course", "verdict": "procurable",
"availability": "open_external_source_required",
"how_to_obtain": "Coastal transponder traffic is published by open aggregators; open-ocean coverage comes from satellite AIS, which is a commercial product." },
{ "label": "When it happened", "verdict": "not_observable",
"cannot_establish": "Imagery brackets an event between two passes; it does not time it." }
],
"no_diagnostic_value": [ { "label": "Traffic moved off its usual lane", "max_elimination": 0 } ],
"note": "An observation can only eliminate here. A required observable that is ABSENT refutes every statement requiring it; one that is PRESENT refutes nothing…"
}Standing Orders
Put an area under continuous watch. A deterministic pass decides whether anything crossed the reporting bar, so quiet periods cost nothing — only a firing check runs the Analyst and is metered.
/api/v1/standing-ordersfreeWhat you are currently watching, and what your plan still allows. quiet_checks is the honest metric here — a watch that has found nothing for many cycles is one you should retire.
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/standing-orders"{
"orders": [
{ "id": "8f3c…", "name": "Kyiv oblast", "cadence": "daily", "bbox": [30.3, 50.3, 30.7, 50.6],
"last_checked_at": "2026-07-26T09:15:00Z", "last_run_at": "2026-07-24T09:15:00Z",
"quiet_checks": 2, "total_runs": 6 }
],
"limits": { "max_orders": 5, "used": 1, "remaining": 4, "min_interval_hours": 24 }
}/api/v1/standing-ordersfree to create · metered per fireTurn a question into a watch. The cost is per CHANGE, not per check: a deterministic pass over the corpus decides whether anything crossed the reporting bar, and quiet periods never invoke the model. Use this when the question is "tell me when this changes" rather than "what is happening now".
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | number[4] (required) | Area to watch, [minLon, minLat, maxLon, maxLat] (WGS84). Required — a global watch would fire on everything. |
| question | string | The question to re-answer when something changes (≤ 500 chars). Defaults to "what changed in this area, and what does it mean?". |
| name | string | Label for the order. |
| cadence | string | How often to CHECK — checking is free, only a firing check costs tokens. Default weekly. |
| categories | string[] | Restrict the watch to these Delta categories. |
| min_geoint_score | number 0–10 | Reporting bar 1–10 (default 6). Raise it to hear only about major developments. |
| min_new_events | number 1–50 | How many new qualifying events must appear before a run is triggered (default 1). |
| notify_email | boolean | Email the result when it fires (default true). Results are readable from the list either way. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"name": "Kyiv oblast", "bbox": [30.3, 50.3, 30.7, 50.6], "cadence": "daily",
"question": "What changed in the strike pattern, and what does it imply?"}' \
"https://offnadir-delta.com/api/v1/standing-orders"{
"order": { "id": "8f3c…", "name": "Kyiv oblast", "cadence": "daily", "next_due_at": "2026-07-26T09:20:00Z" },
"projected_monthly_tokens_typical": 1370,
"projected_monthly_tokens_max": 3744
}/api/v1/standing-orders/{orderId}freeStop watching. Deleting frees the plan slot; pausing (POST with active:false) keeps the order and its history but not the slot.
Parameters
| Parameter | Type | Description |
|---|---|---|
| orderId | string (uuid) (required) | The id returned when the order was created. |
curl -X DELETE -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/standing-orders/8f3c1a2b-…"{ "deleted": "8f3c1a2b-…" }/api/v1/standing-orders/{orderId}freePause a watch without losing it. Resuming starts from now rather than replaying the gap — a queue of stale alerts is worse than no alert.
Parameters
| Parameter | Type | Description |
|---|---|---|
| orderId | string (uuid) (required) | The id returned when the order was created. |
| active | boolean (required) | false = pause, true = resume. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" -d '{"active": false}' \
"https://offnadir-delta.com/api/v1/standing-orders/8f3c1a2b-…"{ "order": { "id": "8f3c1a2b-…", "active": false } }Monitoring
Watchlist measurements: fix a place, pick a quantity, and every new acquisition over it is measured automatically. Reading is free; a check is metered only when it finds new imagery to measure.
/api/v1/monitoringfreeWhat you are currently measuring, and the latest number for each. coverage is the honest field here: measured counts what we actually computed, and window_total is null rather than 0 when we have not asked the catalog how many acquisitions exist.
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/monitoring"{
"areas": [
{ "area_id": "2b5f…", "name": "Strait of Hormuz", "status": "anomaly",
"bbox": [56.0, 26.4, 56.8, 27.0],
"metrics": [
{ "polygon_id": "2b5f…", "metric": "ship_detection", "metric_label": "ships",
"satellite": "sentinel-1-grd", "latest": { "date": "2026-07-30", "value": 31, "is_anomaly": true },
"delta": 18, "coverage": { "measured": 12, "acquired": 18, "window_total": null } }
] }
],
"limits": { "max_areas": 1, "used": 1, "remaining": 0 }
}/api/v1/monitoringfree to create · metered per sceneTurn a place into a time series. The cost follows the imagery, not the calendar: a check that finds no new acquisition measures nothing and charges nothing. Use this when the question is about a quantity at a fixed place over time, rather than about events — that is a standing order.
Parameters
| Parameter | Type | Description |
|---|---|---|
| bbox | number[4] (required) | Area to measure, [minLon, minLat, maxLon, maxLat] (WGS84). Must be under 5,000 km². |
| metric | string (required) | What to count. Plain words work (ships, fires, vegetation, water, burn, snow, built_up, moisture, night_lights) as do index names (ship_detection, fire_count, ndvi, evi, savi, ndmi, ndwi, mndwi, ndbi, ndsi, nbr, dnb, vv, vh, rvi, rfdi, cr). |
| name | string | Label for the area. |
| start_date | string (date) | YYYY-MM-DD to begin the history from (default 30 days ago). A longer backfill measures more scenes and costs more on the first check. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"name": "Sheskharis berth", "bbox": [37.79, 44.69, 37.83, 44.72], "metric": "ships"}' \
"https://offnadir-delta.com/api/v1/monitoring"{
"area": { "area_id": "2b5f…", "name": "Sheskharis berth", "status": "ok",
"metrics": [{ "metric": "ship_detection", "satellite": "sentinel-1-grd", "latest": null }] },
"metering": { "create": "free", "per_check_tokens": 2, "per_scene_tokens": 0.5 }
}/api/v1/monitoring/{areaId}freeThe series behind the number — what to read when the question is "is it going up", "when did it change", or "how unusual is today". Every point carries the STAC item it was measured from, so any value can be traced back to a specific acquisition.
Parameters
| Parameter | Type | Description |
|---|---|---|
| areaId | string (uuid) (required) | The area_id from the list endpoint (a metric polygon_id also resolves). |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/monitoring/2b5f1a2b-…"{
"area": { "area_id": "2b5f…", "name": "Strait of Hormuz", "status": "anomaly",
"metrics": [{ "metric": "ship_detection", "metric_label": "ships",
"series": [ { "date": "2026-07-18", "value": 13, "is_anomaly": false, "stac_item_id": "S1A_IW_…" },
{ "date": "2026-07-30", "value": 31, "is_anomaly": true, "stac_item_id": "S1A_IW_…" } ] }] }
}Locations
The location registry — ports, bases, plants, chokepoints and named seas — and what has happened at each. Every event link carries how it was established (the report named the place, or it was geolocated within range) so an association is never mistaken for a location claim. Reading is free; there is no bulk export.
/api/v1/entities1 token / lookupTurn a place name into something the rest of the API can anchor on. A facility whose only recorded name is in the local script is findable by that name — the registry does not invent English names it does not have.
Parameters
| Parameter | Type | Description |
|---|---|---|
| q | string (required) | Name or partial name (min 2 characters). |
| subtypes | string | Comma-separated: port_facility, military_base, airport, refinery_energy, chokepoint, water_body. |
| limit | integer 1–50 | Max rows (1-50, default 20). |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/entities?q=Umm%20Qasr&subtypes=port_facility"{
"entities": [
{ "id": "3f1b…", "canonical": "Northern Umm Qasr Port", "subtype": "port_facility",
"lat": 30.03, "lng": 47.93, "source": "osm",
"attribution": "© OpenStreetMap contributors (ODbL)" }
],
"limit": 20
}/api/v1/entities/{entityId}1 token / lookupThe history of a place, with the provenance of every link attached. This is what makes a facility page auditable rather than asserted.
Parameters
| Parameter | Type | Description |
|---|---|---|
| entityId | string (required) | The registry id. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/entities/3f1b2a00-…"{
"entity": { "canonical": "Strait of Hormuz", "subtype": "chokepoint" },
"events": [
{ "global_event_id": 1320168847, "title": "Tanker Hit by Unknown Projectile…",
"link_basis": "alias_exact_location", "confidence": 0.95, "distance_m": null }
],
"link_basis_counts": { "alias_exact_location": 200 },
"event_count": 200, "claim_count": 1,
"observability": {
"events_analysed": 20, "served_marks": 2,
"marks": [
{ "label": "How much shipping is in an area", "events": 9, "delta_serves": true,
"test_strength": "indirect",
"cannot_establish": "The cause of a change. Traffic falls for weather, holidays, prices and insurance too." },
{ "label": "Which vessel this is (name / IMO / flag / cargo)", "events": 6, "delta_serves": false,
"how_to_obtain": "Coastal transponder traffic is published by open aggregators; open-ocean coverage comes from satellite AIS, which is a commercial product." }
],
"summary": "Across the 20 events linked to this place, the observables that would have tested them were …"
}
}Watchlist
The aggregation layer over everything under watch. A watch is a target (an area or a real-world event) plus its capabilities — measurements, standing orders, event tracking — grouped as one object with a state bucket. Monitoring and standing orders created anywhere join the Watchlist automatically.
/api/v1/watchesfreeWhat changed in what you care about — the one call that answers it. Buckets are meaningful-change based: a new measurement point alone is not a change; an anomaly, a notable development, or a fired standing order is.
Parameters
| Parameter | Type | Description |
|---|---|---|
| updated_since | string (ISO 8601) | ISO 8601 instant. Return only watches whose content changed after it. This tracks every table that contributes to a watch (measurements, notes, notifications, bindings), not just edits to the watch row, so a quiet result really means nothing changed rather than nothing was edited. |
| cursor | string | Opaque pagination cursor from a previous response's meta.next_cursor. Bound to the query that produced it — a cursor from a different updated_since is rejected rather than silently returning overlapping rows. |
| limit | integer 1–500 | Watches per page when paging (1..500, default 100). |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/watches"{
"watches": [
{ "id": "3f1b…", "name": "Strait of Hormuz", "target_type": "area", "status": "active",
"bucket": "needs_attention", "area": { "bbox": [55.8, 25.8, 57.2, 27.2], "area_status": "anomaly" },
"measurements": [{ "metric": "ship_detection", "latest": { "value": 31, "is_anomaly": true } }],
"questions": [{ "question": "Is shipping being disrupted?", "cadence": "daily" }],
"latest_change": { "at": "2026-08-27T06:10:00Z", "summary": "Anomaly in Ship count" } }
],
"buckets": { "needs_attention": 1, "changed_today": 0, "awaiting_data": 0, "stable": 2 },
"limits": { "max_active": 25, "used_active": 3, "remaining": 22 }
}/api/v1/watchesfreeWatch a real-world event, not its coverage: the binding is the canonical event, so the watch survives new reporting and cluster merges. Event watches track state already computed for everyone — they never charge.
Parameters
| Parameter | Type | Description |
|---|---|---|
| target_type | string (required) | "event" or "area". |
| event_id | number | The signal’s numeric event id (required for "event"). |
| bbox | number[4] | [minLon, minLat, maxLon, maxLat] WGS84 (required for "area"). |
| name | string | Label for the watch. |
| notify_email | boolean | Email on meaningful changes (default false). |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"target_type": "event", "event_id": 1320038641, "notify_email": true}' \
"https://offnadir-delta.com/api/v1/watches"{
"watch": { "id": "3f1b…", "name": "Odesa Port strike", "target_type": "event", "status": "active",
"bucket": "awaiting_data", "event": { "subject_key": "cluster:1864…", "stage": "reported" } },
"already_existed": false
}/api/v1/watches/{watchId}freeThe single-call state readout: built so an agent does not need a chain of follow-ups to answer "what is the state of what I watch".
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
| include_passes | boolean | Add the collection outlook: which operators can image this target, when, at what off-nadir geometry, whether a commercial tasking order is needed and where to place it. Costs the same as GET /api/v1/passes because it is the same answer; omit it and the call stays free. Off-Nadir Delta does not broker tasking. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/watches/3f1b2a00-…"{
"watch": { "id": "3f1b…", "target_type": "event", "bucket": "changed_today",
"event": { "stage": "corroborated", "developments_24h": 2,
"imagery_post_status": "post_available" } },
"thread": { "canonical_event": { "title": "Odesa Port strike" }, "timeline": [ "…" ], "sources": [ "…" ] }
}/api/v1/watches/{watchId}/decision-package5 tokensThe whole state of one watch as one object, with a revision you can diff against. Built for systems that hold a copy: absence of evidence is typed explicitly, so "we could not look" never arrives looking like "it was not there".
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/watches/3f1b2a00-…/decision-package"{
"schema_version": "1.0",
"package_id": "dpkg_3f1b2a00-…",
"external_key": "watch:3f1b2a00-…",
"revision": "9c1f4a7b02de",
"watch": { "bucket": "awaiting_data", "target_type": "event" },
"discriminators": [
{ "observable": "vessel_damage", "verdict": "actionable", "max_elimination": 2,
"refutes_if_absent": ["claim-1", "claim-2"] }
],
"negative_evidence": [
{ "observable": "burn_extent", "axis": "post_event_imagery", "status": "inconclusive",
"reason": "cloud", "source_status": "analysis_unusable",
"detail": "Imagery existed but could not answer the question. This is not evidence of absence." }
],
"limitations": ["3 of 5 observation records establish nothing either way."]
}/api/v1/watches/{watchId}freePause propagates: a paused watch is not a display state over live billing — the bound measurements and checks actually stop.
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
| name | string | New label. |
| status | string | "active", "paused" or "saved". |
| notify_email | boolean | Email on meaningful changes. |
| question | string | The one question this watch is meant to settle. Empty string clears it. |
curl -X PATCH -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"status": "paused"}' \
"https://offnadir-delta.com/api/v1/watches/3f1b2a00-…"{ "watch": { "id": "3f1b…", "status": "paused" } }/api/v1/watches/{watchId}freeThe destructive one, and it says so: bound monitoring and standing orders go with the watch. Pause instead to keep the history.
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
curl -X DELETE -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/watches/3f1b2a00-…"{ "deleted": "3f1b…", "removed": { "monitoring_areas": 1, "standing_orders": 1 } }/api/v1/watches/{watchId}/notesfreeA note is just text. Add a confidence and it becomes a judgment — confidence and likelihood are separate axes on purpose: how sure you are, and how probable the thing is, are different statements.
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
| note | string (required) | What to keep with this watch. |
| title | string | A heading makes it a thread others can reply to. A reply cannot have one. |
| parent_id | string | Reply to this note id. One level deep. |
| confidence | string | How sure the judgment is — about the evidence, not the event. Omit it and the entry is a plain note. |
| likelihood | string | How probable the thing itself is (ICD 203). Omit rather than guess. |
| gaps | string[] | What would change this judgment. |
| next_check | string | When or what to look at next. |
curl -X POST -H "Authorization: Bearer ond_..." \
-H "Content-Type: application/json" \
-d '{"note": "Port authority notice gives a different figure — watch whether the reporting converges."}' \
"https://offnadir-delta.com/api/v1/watches/3f1b2a00-…/notes"{
"note": { "id": "b1c2…", "note": "Port authority notice gives …", "confidence": null,
"supersedes_id": null, "created_by": "user" }
}/api/v1/watches/{watchId}/notes/{noteId}freeThe note goes; the deletion is still recorded. A ledger you cannot correct is a ledger nobody writes in.
Parameters
| Parameter | Type | Description |
|---|---|---|
| watchId | string (required) | The watch id. |
| noteId | string (required) | The note id. |
curl -X DELETE -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/watches/3f1b2a00-…/notes/b1c2d3e4-…"{ "deleted": "b1c2d3e4-…" }Workspace
The map assets your account owns — saved layer sets and your own uploaded data. Reading is free. Creating them is map work (arranging layers, transferring a file), so it stays in the app.
/api/v1/layer-setsfreeThe named map configurations saved on this account, most recently changed first. Includes how many layers each holds, its tags and when it was last opened — enough to tell a working configuration from an abandoned one. The serialized layer tree is internal and not returned.
Parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer 1–100 | Max layer sets to return. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/layer-sets?limit=2"{
"layer_sets": [
{ "id": "…", "name": "Hormuz watch", "layer_count": 4, "tags": ["maritime"], "last_loaded_at": "2026-08-20T09:12:00Z" }
],
"total": 7
}/api/v1/layer-sets/{layerSetId}freeOne saved configuration by id. Returns 404 when no layer set with that id belongs to this account. `contents` is always "not_exposed".
Parameters
| Parameter | Type | Description |
|---|---|---|
| layerSetId | string (required) | The layer set id. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/layer-sets/00000000-0000-0000-0000-000000000000"{
"layer_set": { "id": "…", "name": "Hormuz watch", "layer_count": 4, "contents": "not_exposed" }
}/api/v1/uploadsfreeThe data uploaded to the map on this account, newest first, plus the formats the uploader accepts. Uploading itself is a multipart transfer in the app, so it has no endpoint here — this is how a script or an agent sees what is already there.
Parameters
| Parameter | Type | Description |
|---|---|---|
| limit | integer 1–100 | Max uploads to return. |
curl -H "Authorization: Bearer ond_..." \
"https://offnadir-delta.com/api/v1/uploads?limit=2"{
"uploads": [
{ "id": "…", "name": "aoi.geojson", "file_type": "geojson", "size_bytes": 21455 }
],
"total": 3,
"accepted_formats": ["geojson", "geotiff"]
}Usage
Your token balance and plan capabilities (free) — pre-flight a metered call.
/api/v1/usagefreeYour key’s current token balance and plan capabilities — call it to pre-flight a metered request without spending anything.
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/usage"{
"tokens": { "allocation": 10000, "used": 42, "remaining": 9958 },
"plan": { "api_llm_access": true }
}/api/v1/statusfreePre-flight the data, not just the balance. If the pipeline is behind, a metered query still succeeds and still charges — it just answers from an older corpus than you assumed. This is how you find that out first.
curl -H "Authorization: Bearer ond_..." "https://offnadir-delta.com/api/v1/status"{
"pipeline_status": "ok",
"data_current_through": "2026-08-14T06:00:00Z",
"server": { "version": "1.11.0", "tool_count": 24, "environment": "production" }
}/api/v1/versionfree · no key neededThe only unauthenticated endpoint. Its purpose is drift detection: MCP has no handshake for a changed tool set, so a connector can cache a stale roster indefinitely. Compare the three fingerprints against what your client cached — if they differ, reconnect.
curl "https://offnadir-delta.com/api/v1/version"{
"name": "off-nadir-delta", "version": "1.11.0", "tool_count": 24, "toolset": "standard",
"schema_hash": "7d71fd6d9f4e", "description_hash": "256e28bd5a61", "descriptor_hash": "96beb5b8b825",
"environment": "production"
}MCP server
An MCP (Model Context Protocol) server at /api/v1/mcp exposes the same surface as tools, plus resources and prompts, so any MCP-capable agent can consume the intelligence directly.
query_signals·query_stats·query_hotspots·get_world_brief·get_usage·search_imagery·plan_event_imagery·rank_imaging_priority·survey_observable_events·predict_satellite_passes·get_analyst_job·query_claims·test_hypotheses·query_developments·get_event_thread·create_standing_order·list_layer_sets·get_layer_set·list_uploaded_layers·list_standing_orders·delete_standing_order·list_monitored_areas·get_monitored_area·create_monitored_area·list_watches·get_watch·get_decision_package·create_watch·update_watch·delete_watch·add_note·delete_note·search_entities·get_entity·get_related_events·lookup_elevation·analyze_terrain·measure_index_series·detect_ships— tools (metered / free)assess_signal·ask_analyst·refine_location— AI tools- Resources:
brief://latest,signals://schema,usage://current,imagery://collections,status://current,brief://{date},watch://{watch_id} - Prompts:
daily-situation-briefing,assess-top-signal,aoi-watch,market-exposure-check
Claude & other web apps (OAuth)
Web apps such as Claude (claude.ai) connect over OAuth — no API key to paste. Add a custom connector (Settings → Connectors → Add custom connector) and enter the MCP URL below. The endpoint is an OAuth 2.1 resource server with discovery, dynamic client registration, and PKCE, so the app completes the flow for you — just sign in to Off-Nadir Delta and approve. Metering and plan gating are billed to that account.
https://offnadir-delta.com/api/v1/mcpClaude Code
claude mcp add --transport http off-nadir-delta \
https://offnadir-delta.com/api/v1/mcp \
--header "Authorization: Bearer ond_..."Config file (API key)
{
"mcpServers": {
"off-nadir-delta": {
"type": "http",
"url": "https://offnadir-delta.com/api/v1/mcp",
"headers": { "Authorization": "Bearer ond_..." }
}
}
}OpenAPI spec
A machine-readable OpenAPI 3.1 document is available for codegen and API explorers:
https://offnadir-delta.com/api/v1/openapi.jsonFrom headline to satellite evidence
One connected intelligence workflow across four surfaces — free to start, no GIS software or remote-sensing background required.