API Reference
Base URL: https://api.eorouter.io/v1 — STAC-API 1.0.0 compliant. All endpoints require Authorization: Bearer {key}.
Authentication
All API endpoints require authentication via Bearer token. You can use either a JWT (from login/signup) or an API key.
# 1. Sign up
POST /v1/auth/signup
{ "email": "you@company.com", "password": "SecurePass10+", "name": "Your Name" }
// → { "access_token": "eyJ...", "user": { ... } }

# 2. Create an API key (recommended for production)
POST /v1/keys
{ "name": "CI/CD pipeline", "type": "live" }
// → { "full_key": "eor_live_sk_a1b2c3..." }  ← save this! shown ONCE

# 3. Use in all requests
Authorization: Bearer eor_live_sk_a1b2c3...

API keys are bcrypt-hashed — the full key is only returned at creation. Store it in a secrets manager immediately. Test keys (eor_test_sk_...) work identically but incur no real charges.

Provider Reference
EO Router connects to 14 satellite imagery providers. Commercial providers are billed per km²; free providers serve open data at no cost.
ProviderIDSensorGSDFreeTaskingBYOKCredential Type
Planet Labsplanetoptical3m / 0.5mNoYesYesapi_key
ICEYEiceyesar0.5m / 3mNoYesYesoauth2_client
Capella Spacecapellasar0.35mNoYesYesapi_key
Umbraumbrasar0.25mPartialYesYesapi_key
Airbusairbusoptical0.3m / 1.5mNoYesYesoauth2_client
Maxarmaxaroptical0.31mNoYesNoapi_key
BlackSkyblackskyoptical0.5mNoYesYesapi_key
Earth Searchearthsearchoptical10m / 30mYesNoNo
ESA / CDSEcdseoptical, sar10m / 5mYesNoNo
NASA CMRnasaoptical30mYesNoNo
USGS M2Musgsoptical30mYesNoNo
Sentinel Hubsentinelhuboptical10mYesNoYesoauth2_client
Google Earth Enginegeeoptical, sar500m / 10mYesNoYesservice_account
LGND.ailgndoptical, sarvariesYesNoYesbearer_token

Maxar WV-3 / WV-4: ITAR-controlled. Only available to users with country_code: "US" in their profile. See ITAR Compliance section.

GET /collections
Returns all available collections across all providers. Filter by provider, sensor type and access tier.
GET /v1/collections?provider=iceye&sensor_type=sar&limit=20
GET /v1/collections/iceye_spotlight_grd
Orders
Archive orders purchase existing scenes. The order amount is pre-authorized on confirm and captured on delivery against actual clipped area.
POST /v1/orders
{
  "type": "archive",
  "items": [{ "scene_id": "ps_20260315_..." }],
  "processing": { "format": "COG", "clip_to_aoi": true, "aoi": {...} },
  "delivery": { "type": "s3", "destination_id": "dest_prod" },
  "license_accepted": true,
  "confirm": true   // false = estimate only
}
// → 202 { "order_id": "ord_7e2a1b4d", "status": "queued", "amount_eur": 89.4 }
Status lifecycle: queued → running → processing → delivering → completed (or failed — amount released)
MethodPathDescription
POST/v1/ordersCreate order (confirm=false for estimate)
GET/v1/ordersList orders, paginated
GET/v1/orders/{id}Order status and event log
GET/v1/orders/{id}/manifestDelivery manifest with checksums
DELETE/v1/orders/{id}Cancel (pre-delivery only)
Tasking
Task a satellite for a future acquisition. Call feasibility first (free) to get opportunity windows and pricing from all 7 commercial providers: ICEYE, Capella, Planet, Airbus, Maxar, Umbra, BlackSky. Then confirm with orders.
// Step 1: feasibility (free, no charge)
POST /v1/tasking/feasibility
{
  "aoi": { ... },
  "time_window": { "from": "2026-03-22T00:00:00Z", "to": "2026-03-28T23:59:59Z" },
  "requirements": { "sensor_type": "sar", "max_gsd_m": 1.0, "orbit_direction": "ascending" },
  "providers": ["iceye", "capella", "umbra"],
  "priority": "standard",  // standard (1x) | priority (1.75x) | rush (3x)
  "budget_cap": 2000       // optional: max EUR per opportunity
}

// Step 2: order from opportunity_id
POST /v1/tasking/orders
{
  "opportunity_id": "topp_iceye_...",
  "estimated_eur": 1240.5,  // from feasibility response
  "confirm": true                 // false = estimate only
}
Priority cost multipliers: standard = 1x base cost, priority = 1.75x (faster scheduling), rush = 3x (next available pass).
Deviation alerts: Each opportunity includes a deviations array warning you when it differs from your request:
DeviationSeverityDescription
time_shiftwarningOpportunity is outside the requested time window
gsd_exceededwarningResolution is coarser than requested max_gsd_m
off_nadir_exceededwarningOff-nadir angle exceeds requested maximum
orbit_direction_mismatchinfoOrbit direction differs from preference
budget_exceededcriticalEstimated cost exceeds budget_cap
high_off_nadirinfoOff-nadir > 40° (geometric distortion risk)

The response also includes a top-level warnings array for systemic issues (e.g. all opportunities shifted, all exceed budget, provider API failures).

Subscriptions
Monitor an AOI for new matching imagery. Set auto_order: true with budget caps to purchase and deliver every match automatically.
POST /v1/subscriptions
{
  "name": "port-genoa-sar",
  "aoi": { ... },
  "filter": { "sensor_types": ["sar"], "max_gsd_m": 3.0 },
  "providers": ["iceye", "cdse"],
  "auto_order": true,
  "auto_order_config": {
    "delivery_destination_id": "dest_prod",
    "max_eur_per_match": 200,
    "daily_eur_cap": 500
  }
}

// Pause/resume
PATCH /v1/subscriptions/{id}
{ "status": "paused" }
BYOK Credentials
Bring Your Own Key (BYOK) lets you use your own provider API credentials. Orders route directly to the provider at wholesale cost with a 10% routing fee (vs 2.5% platform fee without BYOK).
POST /v1/credentials
{
  "provider": "planet",
  "type": "api_key",
  "name": "Production Planet key",
  "value": "PLAKxxxxxxxxxxxxxxxx"
}

// For OAuth2 providers (ICEYE, Airbus, SentinelHub):
POST /v1/credentials
{
  "provider": "iceye",
  "type": "oauth2_client",
  "name": "ICEYE production",
  "client_id": "your-client-id",
  "client_secret": "your-client-secret"
}
ProviderCredential TypeFields
Planetapi_keyvalue
Capellaapi_keyvalue
Maxarapi_keyvalue
BlackSkyapi_keyvalue
Umbraapi_keyvalue
ICEYEoauth2_clientclient_id + client_secret
Airbusoauth2_clientclient_id + client_secret
Sentinel Huboauth2_clientclient_id + client_secret

All credentials are encrypted at rest with AES-256-GCM. The plaintext is never stored or logged.

Destinations
Pre-configure cloud delivery destinations for orders and subscriptions. Credentials are encrypted at rest.
// Amazon S3
POST /v1/destinations
{
  "name": "Production S3",
  "type": "s3",
  "bucket": "my-imagery-bucket",
  "region": "eu-west-1",
  "access_key_id": "AKIA...",
  "secret_access_key": "wJa...",
  "path_template": "imagery/{date}/{scene_id}.tif"
}

// Google Cloud Storage
{ "type": "gcs", "bucket": "...", "service_account_json": "{...}" }

// Azure Blob Storage
{ "type": "azure_blob", "container": "...", "connection_string": "..." }

// Platform (download from dashboard — no config needed)
{ "type": "platform", "name": "Dashboard download" }
Path template placeholders: {order_id}, {scene_id}, {date}
Webhooks
Receive real-time notifications for order and tasking events. Payloads are signed with HMAC-SHA256.
POST /v1/webhooks
{
  "url": "https://your-server.com/hooks/eorouter",
  "event_types": ["order.completed", "order.failed", "tasking.acquired"]
}
// → { "signing_secret": "whsec_a1b2c3..." }  ← save this! shown ONCE
Event types:
EventWhen
order.queuedOrder accepted and amount pre-authorized
order.completedDelivery finished, amount captured
order.failedOrder failed, amount released
tasking.confirmedTasking order accepted by provider
tasking.acquiredSatellite has acquired the image
subscription.matchNew imagery matches subscription criteria
Signature verification (Python):
import hmac, hashlib

def verify_webhook(payload: bytes, header: str, secret: str) -> bool:
    # header format: "t=1711234567,v1=a1b2c3d4..."
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(
        secret.encode(), f'{parts["t"]}.'.encode() + payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Failed deliveries are retried up to 5 times with exponential backoff (1min, 5min, 30min, 2h, 12h).

Jobs
Every async operation produces an immutable job with an append-only event log.
GET /v1/jobs/{job_id}

{
  "job_id": "job_8f3a2b1c",
  "status": "completed",
  "events": [
    { "type": "job_created",   "time": "10:31:00Z", "message": "89.4 Cr pre-authorized" },
    { "type": "provider_queued", "time": "10:31:08Z", "message": "ICEYE order queued" },
    { "type": "job_completed",  "time": "11:04:32Z", "message": "87.3 Cr captured for 10.9 km²" }
  ]
}
GET /providers
GET /v1/providers

{ "providers": [{ "id": "iceye", "status": "operational", "uptime_30d": 99.2, "tasking": true, "byok": true }] }
POST /embeddings
Retrieve 64-dimensional satellite embeddings from Google DeepMind's AlphaEarth Foundations. Embeddings capture land cover, vegetation, and built environment features at 10m resolution. Coverage: global, 2017-2025.
POST /v1/embeddings
{
  "bbox": [11.34, 44.49, 11.36, 44.50],  // max ~100 km²
  "year": 2024,                              // 2017-2025
  "resolution": 10                           // meters (10m native)
}

// Response
{
  "bbox": [11.34, 44.49, 11.36, 44.50],
  "year": 2024,
  "resolution": 10,
  "dimensions": 64,
  "pixel_count": 22,
  "pixels": [
    { "lon": 11.3405, "lat": 44.4995, "embedding": [0.12, -0.34, ...] },
    // ... 64 floats per pixel
  ]
}
Use cases: similarity search, land cover classification, change detection, feature extraction for ML pipelines.

AlphaEarth embeddings are served by the platform — no credentials required from your side. Max bbox area: ~100 km². Rate limit: 10 requests/minute.

LGND.ai — Similarity Search & Change Detection
LGND.ai provides embedding-based similarity search, chip-level search, and AI change detection on satellite and aerial imagery. Free tier covers California NAIP (2020-2022) and France Sentinel-2 (2024-2025). All endpoints are fully integrated into the EO Router API.
POST /lgnd/search
Find similar image chips by location.
POST /v1/lgnd/search
{
  "latitude": 37.77,
  "longitude": -122.42,
  "top_k": 5,
  "start_date": "2022-01-01",
  "end_date": "2022-12-31"
}

// Response
{
  "query": { "latitude": 37.77, "longitude": -122.42, "top_k": 5 },
  "results": [
    { "chip_id": "naip_ca_2022_abc123", "score": 0.94, "metadata": {...} }
  ],
  "distance_metric": "cosine"
}
POST /lgnd/search-by-chip
Find visually similar chips by chip ID from previous results.
POST /v1/lgnd/search-by-chip
{
  "chip_id": "naip_ca_2022_abc123",
  "top_k": 10
}
POST /lgnd/changes
Detect changes using text descriptions of past/current states. Returns chip pairs (before + after) with scores.
POST /v1/lgnd/changes
{
  "geometry": { "type": "Polygon", "coordinates": [[...]] },
  "positive_texts_past": ["empty field"],
  "positive_texts_current": ["new construction"],
  "negative_texts_past": ["forest"],
  "negative_texts_current": ["forest"],
  "positive_chip_ids_past": ["chip_id_example"],
  "positive_chip_ids_current": ["chip_id_example"],
  "negative_chip_ids_past": [],
  "negative_chip_ids_current": [],
  "top_k": 10
}

// Response — changed chip pairs
{
  "results": [
    { "chip_past": "s2_2024_a1", "chip_current": "s2_2025_a1", "cell_id": "cell_42", "score": 0.87 }
  ],
  "distance_metric": "cosine"
}
POST /lgnd/filter
Filter chips within a GeoJSON geometry. Returns chip IDs ordered by capture date.
POST /v1/lgnd/filter
{
  "geometry": { "type": "Polygon", "coordinates": [[...]] },
  "start_date": "2022-01-01",
  "limit": 100
}

// Response — chip IDs ordered by capture date
{
  "chips": ["naip_ca_2022_abc123", "naip_ca_2022_abc124"],
  "total": 2
}
GET /lgnd/collections
List available LGND collections (imagery sources, date ranges, coverage areas).

LGND is fully integrated — authenticate with your EO Router API key, no separate LGND token needed. Rate limit: 10 requests/minute. Free tier: read-only collections only.

Balance
Your account balance is in EUR. Amounts are pre-authorized on order confirm and captured on delivery against actual clipped area.
GET /v1/credits/balance
// → { "balance_eur": 348.60, "reserved_eur": 89.40, "available_eur": 259.20 }

GET /v1/credits/transactions?limit=20

// Cost formula:
cost_eur = price_eur_per_km2 * clipped_area_km2 * 1.025  // 2.5% platform fee

// BYOK routing fee:
routing_fee = max(area_km2 * 0.10, 0.50)  // EUR
Routing modes
ModeDescription
best_availableComposite: 40% spatial, 20% freshness, 20% resolution, 10% cloud, 10% price. Default.
cheapest_matchingLowest EUR cost. All constraints still applied.
freshest_matchingMost recent acquisition first.
highest_resolutionFinest GSD first. Price ignored.
sar_fallback_if_cloudOptical if cloud ≤ threshold; SAR otherwise.
open_data_firstFree/open providers ranked above commercial.
multi_provider_compareTop 3 per provider for side-by-side comparison.
SAR metadata
All SAR scenes expose normalized fields regardless of provider. Usable in CQL2-JSON filters.
PropertyTypeDescription
sar:frequency_bandstringX (ICEYE, Capella, Umbra) · C (Sentinel-1)
sar:product_typestringGRD · SLC · SICD · SIDD · CPHD
sar:polarizationsarray["HH"], ["VV"], ["VV","VH"]
sar:looks_rangeintegerNumber of looks in range direction
eorouter:sar.orbit_directionstringascending or descending
eorouter:sar.look_directionstringleft or right
eorouter:sar.mode_namestringSpotlight, Stripmap, ScanSAR, Dwell…
eorouter:sar.nesz_dbnumberNoise Equivalent Sigma Zero (lower = better)
eorouter:sar.coherence_availablebooleanInterferometric product available
STAC compliance
The /v1/search endpoint passes the STAC API validator. Any STAC-aware client works with no custom code.
from pystac_client import Client

catalog = Client.open(
    "https://api.eorouter.io/v1",
    headers={"Authorization": f"Bearer {os.environ['EOR_API_KEY']}"}
)
items = catalog.search(intersects=aoi, datetime="2026-03-01/2026-03-21").get_items()
Extensions: EO (eo:cloud_cover), SAR (sar:frequency_band), View (view:off_nadir), Processing (processing:level), EO Router (eorouter: namespace).
Processing options
ParameterDefaultDescription
formatCOGCOG · GeoTIFF · NITF · SLC · SICD · SIDD
clip_to_aoitrueClip to AOI polygon — reduces cost
reproject_epsgnullReproject to EPSG; null = provider default
harmonize_srfalsePlanet: normalize to surface reflectance
radiometric_correctionsigma0SAR: beta0 · sigma0 · gamma0
compressdeflateCOG: deflate · lzw · lerc · none

COG deliveries include auto-generated overview levels (256×256 tiles, levels 2–5×). GDAL, QGIS and rio-cogeo read them natively.

ITAR Compliance
Maxar WorldView-3 (WV-3) and WorldView-4 (WV-4) sub-50cm imagery is classified as defense articles under 15 USC §2778 (Arms Export Control Act / ITAR).
RestrictionDetails
Affected collectionsmaxar_wv03, maxar_wv04
RequirementUser must have country_code: "US" in their profile
Error403 itar_restriction for non-US users
ScopeOrdering only — search and browse are unrestricted
// Set your country code
PATCH /v1/auth/me
{ "country_code": "US" }

// Non-US users ordering Maxar WV-3 will get:
// 403 { "code": "itar_restriction", "message": "...ITAR-controlled..." }
Error Codes
All errors return a JSON body with code and message fields.
CodeHTTPWhen
itar_restriction403Non-US user ordering Maxar WV-3 / WV-4
insufficient_balance402Available EUR balance is less than order cost
email_taken409Signup with an email that already exists
invalid_credentials401Login with wrong email or password
wrong_password401Password change with incorrect current password
not_found404Resource does not exist or belongs to another user
invalid_geometry422Non-Polygon geometry or missing AOI
license_not_accepted400Order placed without license_accepted: true
order_not_cancellable409Cancel attempt on completed/delivering order
stripe_not_configured501Stripe payment not configured on server
oauth_not_configured501OAuth provider not configured on server
Tutorial: Change detection pipeline
from eorouter import EoRouterClient
client = EoRouterClient(api_key="...")

before = client.search(aoi=aoi, date_from="2026-01-01", date_to="2026-01-31",
                     max_cloud_cover=15, limit=1)[0]
after  = client.search(aoi=aoi, date_from="2026-03-01", date_to="2026-03-21",
                     max_cloud_cover=15, limit=1)[0]

order = client.orders.create(
    scene_ids=[before.scene_id, after.scene_id],
    processing={"format":"COG", "clip_to_aoi":True, "aoi":aoi},
    delivery={"type":"s3", "destination_id":"dest_prod"},
    confirm=True)

order.wait(timeout_minutes=90)
print([i["s3_key"] for i in order.manifest()["items"]])
Tutorial: SAR tasking workflow
opps = client.tasking.feasibility(
    aoi=aoi,
    time_window={"from":"2026-03-22T00:00:00Z", "to":"2026-03-27T23:59:59Z"},
    requirements={"sensor_type":"sar", "min_gsd_m":3.0, "orbit_direction":"ascending"},
    providers=["iceye", "capella"], priority="standard"
)
best = min(opps, key=lambda o: o["estimated_eur"])
task = client.tasking.order(opportunity_id=best["opportunity_id"], confirm=True)
# Webhooks: tasking.confirmed → tasking.acquired → order.completed
Tutorial: AOI monitoring with auto-order
sub = client.subscriptions.create(
    name="genoa-port-sar", aoi=port_aoi,
    filter={"sensor_types":["sar"], "max_gsd_m":3.0},
    providers=["iceye", "cdse"],  # cdse = Sentinel-1, free
    auto_order=True,
    auto_order_config={
        "delivery_destination_id": "dest_prod",
        "processing": {"format":"COG", "clip_to_aoi":True},
        "max_eur_per_match": 300, "daily_eur_cap": 600,
    })
Tutorial: Agent workflow via MCP
// Claude Desktop (MCP connected) conversation:

User: "Find best SAR over Suez Canal last 72h, ascending orbit,
       cheapest, order it."

Claude calls:
  eo_search(aoi=suez, sensor_types=["sar"], days=3, routing="cheapest_matching")
  eo_get_balance()
  eo_order_archive(scene_ids=["iceye_..."], confirm=False)  // estimate first

Claude: "Found ICEYE Stripmap (3m, ascending, 8h ago) — €312.00.
         Your balance is €1,240. Shall I place the order?"

User: "Yes, confirm it."

Claude calls:  // MCP clients can set allow_orders in their implementation
  eo_order_archive(scene_ids=["iceye_..."], confirm=True)
  eo_get_order_status(order_id="ord_...")

Claude: "Order placed. ICEYE scene queued for delivery to
         s3://your-bucket/... — €312.00 charged."

The full MCP tool schema is at GET https://api.eo-router.com/mcp/schema.

AI Chat (Beta)
BETA
EO Router provides an optional hosted AI assistant powered by Claude. Send natural language queries and the AI will search imagery, check feasibility, query balances, and more using the same tools available via MCP.
Disabled by default. Enable in Dashboard → Settings → AI Chat (Beta). No additional API keys required — the AI service is provided by EO Router.

Rate limit: 5 requests per minute. AI calls use Claude Haiku to minimize latency. This is a beta feature — responses may be imperfect.

POST /chat
Send a natural language query to EO Router's AI assistant.
POST /v1/chat
{
  "message": "Find Sentinel-2 images over Paris from last week with less than 20% cloud cover",
  "conversation_id": null,       // optional, for multi-turn
  "allow_orders": false          // see Order Safety below
}

// Response
{
  "response": "I found 8 Sentinel-2 scenes over Paris from the last 7 days...",
  "tool_calls": [
    { "tool": "eo_search", "result_summary": "8 scenes found" }
  ],
  "conversation_id": "conv_abc123",
  "beta": true
}
Order Safety
By default, the AI can only provide cost estimates — it cannot place real orders or spend credits. When the AI estimates an order, the response includes _requires_approval: true.
To let the AI place a confirmed order, set allow_orders: true in your request. Use this only after your app has obtained explicit user approval (e.g. a confirmation dialog).
// Step 1: User asks to order — AI returns estimate (allow_orders=false, default)
POST /v1/chat
{ "message": "Order that Sentinel-2 scene over Paris" }
// → AI calls eo_order_archive(confirm=false), returns estimate + _requires_approval

// Step 2: Your app shows the estimate, user clicks "Confirm"

// Step 3: Re-send with allow_orders=true
POST /v1/chat
{ "message": "Yes, confirm the order", "allow_orders": true }
// → AI places the order, charges EUR balance
Error Codes
CodeHTTPWhen
chat_disabled403AI Chat not enabled for your account
chat_unavailable503AI Chat service temporarily unavailable
chat_error500Unexpected AI processing error