Off-Nadir Delta

A collection manager’s loop — rank targets, plan imagery, check the next pass

You can only task or review so much imagery; the question is which events deserve it, what to collect, and when the next opportunity is.

Steps

  1. Rank where observation is most worthwhile (collection priority).
  2. For the top event, get the deterministic SAR + optical plan — the all-weather look can never be silently skipped.
  3. Check when the place can next be imaged, and by which satellite.

In the web app

See imaging priority on the Watchfloor

REST

Authenticate with Authorization: Bearer ond_… (create a key at /account/api). Try any of these live in the Playground.

Rank imaging priority

bash
curl "https://offnadir-delta.com/api/v1/collection/priority?top_n=10" \
  -H "Authorization: Bearer ond_YOUR_API_KEY"

Plan imagery for one event

bash
curl "https://offnadir-delta.com/api/v1/collection/plan?event_id=1315064079&analysis_goal=damage_assessment" \
  -H "Authorization: Bearer ond_YOUR_API_KEY"

Next passes over the target

bash
curl "https://offnadir-delta.com/api/v1/passes?lat=48.85&lon=2.35" \
  -H "Authorization: Bearer ond_YOUR_API_KEY"

Python

This is the runnable example shipped with the SDK (pip install offnadir-delta, then set OFFNADIR_DELTA_API_KEY).

python
"""A collection manager's loop: rank targets, plan the imagery, check the next pass.

This is the workflow the SDK could not express before 0.4.0 — you could read events but
not act on them. Nothing here invents a capability: every step reports what the data can
and cannot support, which is the part a tasking decision actually rests on.

Run with:  OFFNADIR_DELTA_API_KEY=ond_... python examples/collection_tasking.py
"""

from offnadir_delta import Client

# bbox = [min_lon, min_lat, max_lon, max_lat].
AOI = [-180.0, -85.0, 180.0, 85.0]   # worldwide; narrow this to your own area of interest.


def main() -> None:
    with Client() as client:
        # 1. WHAT IS WORTH IMAGING. `total_available` vs `returned` matters: a short list
        #    can mean "few candidates" or "few that survived the readiness gates", and
        #    those are different situations for a collection manager.
        ranked = client.collection.priority(bbox=AOI, top_n=5)
        body = ranked.priority
        if body is None:
            print("No priority body returned.")
            return
        print(f"{body.returned} target(s) returned of {body.total_available} available")

        # An empty list is a real answer, and the exclusion breakdown is the useful part
        # of it: it says WHICH gate emptied the funnel (measured worldwide on 2026-07-28,
        # 28 of 34 candidates were dropped for `geo_not_ready` alone). A collection
        # manager needs that, not a silent zero.
        if not ranked.targets:
            print("\nNothing is collection-ready. Excluded by:")
            for gate, count in sorted((body.excluded or {}).items(), key=lambda kv: -kv[1]):
                if count:
                    print(f"  {gate:32s} {count}")
            print("\nResolve the blocking gate (usually an event-specific coordinate) and re-run.")
            return
        print()

        for target in ranked.targets:
            blockers = target.readiness_blockers or []
            state = "ready" if target.collection_ready else f"blocked ({', '.join(blockers) or 'n/a'})"
            print(f"  {target.global_event_id}  {(target.headline or '')[:52]:52s} {target.rs_level or '-':10s} {state}")

        # 2. PLAN THE FIRST READY TARGET. The plan searches each collection exactly once
        #    against the event footprint, so the all-weather SAR look is never skipped in
        #    favour of whichever optical scene happened to be cloud-free.
        target = next((t for t in ranked.targets if t.collection_ready), ranked.targets[0])
        print(f"\nPlanning imagery for {target.global_event_id}…")
        plan = client.collection.plan(event_id=target.global_event_id, analysis_goal="damage_assessment")
        for step in (plan.plan.steps if plan.plan else []):
            pair = f" sar_pair={step.sar_pair_status}" if step.sar_pair_status else ""
            print(f"  {step.collection:18s} returned={step.returned_count} usable={step.usable_count}{pair}")

        # 3. WHEN CAN IT NEXT BE SEEN. `collection_mode` is the field to read: a
        #    systematic satellite will acquire on its own plan (the data will exist), an
        #    agile one only images if somebody orders it. A pass is an OPPORTUNITY.
        aoi = target.rs_aoi or []
        if len(aoi) != 4:
            print("\n(no AOI on the target — pass prediction needs a point or bbox)")
        else:
            lon, lat = (aoi[0] + aoi[2]) / 2, (aoi[1] + aoi[3]) / 2
            passes = client.collection.passes(lat=lat, lon=lon, max_passes=5)
            if passes.retrieval_ok is False:
                print("\n! orbital elements could not be refreshed — treat these windows as indicative")
            print()
            for p in passes.passes:
                print(f"  {p.start}  {p.satellite:22s} {p.collection_mode or '-':10s} peak={p.peak_elevation_deg}°")

        # 4. WATCH IT. Creating the order is free; only a check that finds something new
        #    runs the Analyst and is metered, and the response states the monthly ceiling.
        order = client.standing_orders.create(bbox=AOI, name="aoi-watch-example", cadence="weekly")
        print(f"\n{order.summary}")
        if order.order and order.order.id:
            client.standing_orders.delete(order.order.id)   # example only — don't leave it behind
            print("(example order deleted)")


if __name__ == "__main__":
    main()

MCP

Connect once, from any MCP client:

bash
claude mcp add --transport http off-nadir-delta \
  https://offnadir-delta.com/api/v1/mcp \
  --header "Authorization: Bearer ond_..."

The tools this workflow uses: rank_imaging_priority, plan_event_imagery, predict_satellite_passes. See the full roster at /docs/mcp.

Go deeper

← All recipes

From headline to satellite evidence

One connected intelligence workflow across four surfaces — free to start, no GIS software or remote-sensing background required.