Official Python SDK for the Off-Nadir Delta geospatial event-intelligence API and MCP server.
Project description
Off-Nadir Delta — Python SDK
Official Python client for the Off-Nadir Delta geospatial event-intelligence API and MCP server. Query geolocated, source-linked event signals, activity hotspots and statistics, search satellite imagery (STAC), and — on the Pro plan — run AI assessments and ask the analyst agent — all from Python.
- Sync (
Client) and async (AsyncClient) — one dependency (httpx). - Fully typed responses (Pydantic v2), forward-compatible with additive API fields.
- Automatic cursor pagination, retry with backoff, and a uniform error model.
- A thin MCP client (
McpClient/AsyncMcpClient) for the JSON-RPC MCP endpoint.
Requires Python 3.9+. Licensed under Apache-2.0.
Install
pip install offnadir-delta
Authentication
Create an API key from your account's Developer API page. Keys start with ond_ and are
shown once. Pass it explicitly or via the OFFNADIR_DELTA_API_KEY environment variable.
from offnadir_delta import Client
client = Client(api_key="ond_...") # or: Client() -> reads OFFNADIR_DELTA_API_KEY
The API meters usage in tokens against the same wallet as the web app. Every metered
response reports what it cost under meta.tokens. Check your balance for free first:
usage = client.usage()
print(usage.tokens.remaining, "tokens left, LLM access:", usage.plan.api_llm_access)
Quickstart
from offnadir_delta import Client
with Client() as client:
# One page of the highest-severity signals in an AOI (bbox = [min_lon, min_lat, max_lon, max_lat])
page = client.signals.list(
bbox=[22, 44, 40, 53],
days=7,
min_severity=5,
escalating=True,
sort="severity",
limit=100,
)
print(page.meta.count, "signals,", page.meta.tokens.charged, "tokens charged")
for s in page.signals:
print(s.event_date, s.country_code, s.title, s.severity_score)
Auto-pagination
signals.iterate() follows the cursor for you (each page is a separately-metered request):
for signal in client.signals.iterate(bbox=[22, 44, 40, 53], days=30, min_severity=6):
print(signal.title)
Aggregates & hotspots
stats = client.signals.stats(bbox=[22, 44, 40, 53], days=7)
for row in stats.stats.by_category:
print(row.category, row.count)
hotspots = client.signals.hotspots(bbox=[22, 44, 40, 53], precision=0.5)
for h in hotspots.hotspots:
print(h.lat, h.lng, h.count, h.max_severity)
Satellite imagery (STAC search)
Metadata only — no image bytes, no signed URLs. bbox is required.
scenes = client.imagery.search(
bbox=[22, 44, 40, 53],
collection="sentinel-2-l2a",
cloud_cover_max=20,
days=14,
)
for scene in scenes:
print(scene.id, scene.datetime, scene.cloud_cover)
Optical-observability weather
Cloud cover is what gates a Sentinel-2 optical pass. Get a per-day outlook (cloud
at the ~10:30 local overpass, best clear day) to decide optical vs SAR collection.
Requires a deployment with a commercial Open-Meteo licence. Weather data by
Open-Meteo.com (CC BY 4.0) — surface result.weather.attribution.
result = client.weather.observability(lat=48.85, lon=2.35, end_date="2026-07-22")
print("best optical day:", result.weather.best_optical_day)
for day in result:
print(day.date, day.optical_verdict, day.overpass_cloud_cover_pct)
AI assessment & analyst (Pro plan)
These require a plan with LLM access; a non-Pro key raises PermissionDeniedError.
assessment = client.intelligence.assess(event_id=123, kind="quick") # 5 quick / 15 deep tokens
print(assessment.content)
answer = client.intelligence.analyst(
"What is escalating in the Black Sea this week?",
bbox=[22, 44, 40, 53],
) # metered 5-45 tokens
print(answer.brief)
analyst is not idempotent and is never retried automatically. assess is cached per
(account, event, kind), so re-assessing the same event is free.
Daily World Brief (free)
brief = client.brief.get() # latest; or client.brief.get("2026-07-11")
Async
Every call has an await-able equivalent on AsyncClient:
import asyncio
from offnadir_delta import AsyncClient
async def main():
async with AsyncClient() as client:
async for signal in client.signals.iterate(bbox=[22, 44, 40, 53], days=7):
print(signal.title)
asyncio.run(main())
Error handling
All failures raise a subclass of OffnadirError carrying .status_code, .code, and
.request_id (quote it in support requests):
from offnadir_delta.errors import (
AuthenticationError, PermissionDeniedError, InvalidRequestError,
InsufficientTokensError, RateLimitError, APIError,
)
try:
client.signals.list(bbox=[22, 44, 40, 53])
except InsufficientTokensError as e:
print("Need", e.required, "have", e.available)
except RateLimitError as e:
print("Retry after", e.retry_after, "seconds")
Transient failures (429, 5xx, connection errors) are retried automatically with backoff
(honoring Retry-After); configure with Client(max_retries=...). The last response's
rate-limit headers are available on client.last_rate_limit.
MCP
The API also exposes a stateless MCP server at
/api/v1/mcp (JSON-RPC 2.0). MCP hosts such as Claude connect to it directly over
OAuth 2.1 — see the docs. For programmatic use with a
static API key, this SDK ships a thin client:
from offnadir_delta import McpClient
with McpClient() as mcp:
mcp.initialize()
print([t["name"] for t in mcp.list_tools()])
result = mcp.call_tool("query_signals", {"bbox": [22, 44, 40, 53], "days": 7})
brief = mcp.read_resource("brief://latest")
Tools: query_signals, query_stats, query_hotspots, search_imagery,
assess_weather_observability (optical outlook; commercial weather licence only),
get_world_brief, get_usage, assess_signal (Pro), ask_analyst (Pro).
Development
pip install -e ".[dev]"
ruff check src tests
mypy src
pytest
Tests run fully offline (HTTP mocked with respx) — they never call the live API or spend
tokens.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file offnadir_delta-0.1.0.tar.gz.
File metadata
- Download URL: offnadir_delta-0.1.0.tar.gz
- Upload date:
- Size: 26.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49dc76fb7d5449d6406a757070ad5feae770fd5a7c8274e4282c0e61884791da
|
|
| MD5 |
a0ffb4249c9a56b232929ebaeea4928f
|
|
| BLAKE2b-256 |
4c990e1a0ce23cb7677d898b5a7280cfd5f70dc195339bd0ca88996ebbf7e103
|
File details
Details for the file offnadir_delta-0.1.0-py3-none-any.whl.
File metadata
- Download URL: offnadir_delta-0.1.0-py3-none-any.whl
- Upload date:
- Size: 27.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b10db0f41496f66b56b7cc6429e5c9fd746922dc38a961dac19cba78c378eca2
|
|
| MD5 |
c792528a0ca08907253dee099ef52584
|
|
| BLAKE2b-256 |
f930b1f79930b1fb0062e6851b5a1fa4bed03b20aec385cf4e91380502c05094
|