Customer API reference

The endpoints you actually use

Eighteen operations, grouped by what you are trying to do. Every request and response below was captured from a real call, not written by hand — where a figure moves (positions, counts, SLA), the live endpoint is the authority.

The complete machine-readable spec is at /openapi.json and rendered at /redoc. That spec deliberately describes the customer surface only: the platform's own operational controls are excluded from it, so what you see there is what you can build on.

1. Authentication and limits 2. Decide where to run 3. Managed jobs 4. Public grading 5. Satellites and ground segment 6. Federation (operators) 7. Errors 8. SDKs

1. Authentication and limits

Two header credentials, and most read endpoints need neither.

HeaderWhoWhere it comes from
X-API-KeyCompute buyers — routing, cost compare, managed jobs, usage.GET /api/v1/keys/demo for a shared evaluation key, or your own from /signup.
X-Operator-KeySatellite operators — federation node management.POST /api/v1/federation/operators. Shown exactly once.
(none)Every public read: SLA, satellites, ground stations, coverage, snapshot, pricing, status.Just curl it.
GET /api/v1/keys/demo no auth

A shared, rate-limited Free-tier key for evaluation. Not for production — it is shared with every other evaluator.

Response 200
{
  "api_key": "or_…",
  "note": "This is a shared demo key with Free tier limits (100 routing decisions/day). Use it in the X-API-Key header to evaluate; sign up at /signup for your own key."
}
GET /api/v1/usage X-API-Key

Your key's tier, daily limit and consumption. Pass the key in the header — never in a query string.

Response 200
{"owner": "demo-user", "tier": "free", "daily_limit": 100,
 "usage_today": 0, "usage_this_week": 0, "remaining_today": 100}

Daily limits are hard caps, not metered overage. Free 100, Starter 1,000, Pro 100,000, Enterprise 1,000,000 routing decisions per day. A call over the cap is refused with 429 — it is never billed as overage, and GET /api/v1/pricing reports overage_rate_cents: 0 for every tier. There is also a per-IP burst limit in front of everything (120 requests/minute unauthenticated, far higher with a key).

2. Decide where to run

POST /api/v1/route X-API-Key

The core call. Returns a decision plus the full reasoning behind it — no execution, no side effects.

Request
{"model_name": "llama-3-70b", "gpu_required": "H100",
 "input_size_mb": 512, "estimated_compute_seconds": 30,
 "job_type": "inference", "priority": "high", "max_latency_ms": 200}
Response 200 (abridged — confidence_basis is the long one)
{
  "job_id": "d26e8183-da6",
  "recommended_target": "terrestrial_aws",
  "target_name": "terrestrial_aws",
  "preference_score": 74.28,
  "confidence": null,
  "confidence_status": "uncalibrated",
  "confidence_basis": {
    "status": "uncalibrated",
    "measured_base_rate": null,
    "base_rate_source": "graded_jobs",
    "base_rate_method": "One-sided 95% Wilson lower bound on the deadline hit rate over real graded jobs (internal synthetic load excluded), gated at n >= 50.",
    "sample_size": 0,
    "min_sample_size": 50,
    "degradation_factors": { "element_age": {"factor": 1.0, "why": "…"}, "…": {} },
    "degradation_product": 0.9
  },
  "estimated_latency_ms": 261.0,
  "estimated_cost_usd": 0.0346,
  "orbital_option": {"asset": "OrbitCloud Alpha", "score": 61.03, "latency_ms": 3316.8,
                     "cost_usd": 0.0995, "capacity_class": "demo",
                     "reliability": null, "reliability_status": "unmeasured"},
  "terrestrial_option": {"provider": "terrestrial_aws", "score": 74.28,
                         "latency_ms": 261.0, "cost_usd": 0.0346},
  "reasoning": ["Terrestrial (terrestrial_aws) selected — score 74.28", "…"],
  "ground_station": "Mumbai, India",
  "next_orbital_window_utc": "2026-07-30T15:05:55Z",
  "timestamp": "2026-07-30T15:07:28Z"
}

Read preference_score and confidence as different things. preference_score is the engine's weighted ranking of the chosen option, 0–100. confidence is a probability that the prediction holds, and it is null until a measured base rate exists — the platform publishes no confidence it cannot measure. capacity_class tells you whether an orbital option is a demo asset, a modeled expander asset, or federated operator capacity. All of it is derived on /methodology.

POST /api/v1/cost-compare X-API-Key

Cost only, across three clouds and the modeled orbital rate, plus the price at which orbit wins for this workload. Same request schema as /route.

Response 200
{
  "orbital_cost_usd": 0.0995, "aws_cost_usd": 0.0346,
  "gcp_cost_usd": 0.0332, "azure_cost_usd": 0.0358,
  "savings_pct": -200.0, "orbital_premium_pct": 200.0,
  "basis": "modeled",
  "crossover": "Orbital pricing is modeled at 3.0x the cheapest terrestrial cloud rate ahead of the 2027 capacity wave — orbital reaches price parity for this workload when its $/GPU-hr crosses $3.98 (H100).",
  "recommendation": "terrestrial"
}

basis: "modeled" is the whole disclosure: the three cloud numbers are published list prices, the orbital number is a modeled multiple. The same math runs client-side, with the rate card visible, on /calculator.

3. Managed jobs

Hand OrbitRoute the job instead of just the question: it queues, binds to a real pass window, dispatches, and grades itself against its own prediction. Jobs are scoped to the key that created them.

Execution is simulated today. Orbital dispatch runs through a simulated adapter — no bytes reach orbital hardware, because no orbital compute hardware is contracted. Scheduling, pass binding, deadline handling, terrestrial offload and grading are all real. Build against the decision and the grading; do not build assuming the payload flew.
POST /api/v1/jobs X-API-Key

Same request as /route, plus an optional deadline_utc. If the deadline cannot be met in orbit, the job is re-homed to terrestrial cloud and remediation records why.

Request
{"model_name": "llama-3-70b", "gpu_required": "H100", "input_size_mb": 512,
 "estimated_compute_seconds": 30, "deadline_utc": "2026-08-01T18:00:00Z"}
Response 201
{
  "job_id": "22cbe03b-4a0", "status": "queued",
  "target": "terrestrial_aws", "target_name": "terrestrial_aws", "asset_id": "",
  "gpu_type": "H100", "model_name": "llama-3-70b", "priority": "medium",
  "station_name": "McMurdo, Antarctica",
  "scheduled_start": null, "scheduled_end": null,
  "deadline_utc": "2026-08-01T18:00:00+00:00",
  "predicted_latency_ms": 30261.0, "predicted_cost_usd": 0.0346,
  "actual_latency_ms": null, "actual_cost_usd": null,
  "submitted_at": "2026-07-30T15:08:48Z", "started_at": null, "completed_at": null,
  "failure_reason": "", "remediation": ""
}

predicted_latency_ms is end-to-end — submit to complete, including pass-window wait — not link RTT.

GET /api/v1/jobs X-API-Key

Your jobs, newest first. ?limit= defaults to 50, max 200. Returns [] when you have none.

GET /api/v1/jobs/{job_id} X-API-Key

One JobRecord, including predicted-vs-actual once graded. 404 if it does not exist or is not yours — the two are indistinguishable on purpose.

DELETE /api/v1/jobs/{job_id} X-API-Key

Cancel while queued or scheduled; returns the record with status: "cancelled". 409 once it is running or terminal.

Lifecycle
queued -> scheduled -> running -> completed
                              \-> failed
queued | scheduled -> cancelled

4. Public grading

GET /api/v1/jobs-stats/sla no auth

The scoreboard behind /sla: predicted vs actual across every graded job, misses included, plus scheduler counters. synthetic_jobs_graded is separated from jobs_measured so internal load can never inflate the customer-facing record.

Response 200
{"jobs_measured": 9, "avg_latency_error_pct": 115.24,
 "deadline_hit_rate_pct": 88.9, "synthetic_jobs_graded": 19838,
 "scheduler": {"ticks": 109, "jobs_scheduled": 0, "jobs_dispatched": 13,
               "jobs_completed": 15, "jobs_offloaded": 0, "jobs_failed": 0,
               "last_tick_utc": "2026-07-30T15:09:13Z"}}

Reading from 2026-07-30. Nine graded jobs is a small number and it is published as one — below 50 the platform calls itself uncalibrated everywhere it matters.

5. Satellites and ground segment

GET /api/v1/satellites/positions no auth

SGP4 positions for every tracked asset, each with its capacity_class and the age of the orbital elements it was propagated from. This is a large payload; for a compact form of the same state use /api/v1/viz/snapshot.

GET /api/v1/satellites/{asset_id}/passes no auth

Upcoming ground-station passes for one asset, gated by each station's own elevation mask.

Response 200 (one entry)
{"asset_id": "OC-ALPHA", "passes": [
  {"pass_start": "2026-07-30T15:22:54Z", "pass_end": "2026-07-30T15:29:37Z",
   "duration_s": 403, "max_elevation_deg": 54.4,
   "station_id": "gs-dongara", "station_name": "Dongara, Western Australia",
   "minutes_until": 5, "in_progress": false,
   "truncated_at_window": false, "start_clipped": false}]}

in_progress and start_clipped exist because a pass already underway is a different fact from one that has not begun — the API says which.

GET /api/v1/ground-stations no auth

All 37 modeled sites with position, link type, modeled bandwidth, elevation mask, and current weather-gated link recommendation.

Response 200 (one station)
{"count": 37, "stations": [
  {"station_id": "gs-svalbard", "name": "Svalbard (SvalSat)",
   "lat": 78.2307, "lon": 15.3896, "link_type": "both",
   "bandwidth_gbps": 4.0, "elevation_mask_deg": 5.0,
   "network": "KSAT", "source": "modeled",
   "weather": {"cloud_cover_pct": 62.0, "optical_viable": false,
               "rf_available": true, "recommended_link": "rf",
               "source": "simulated", "source_kind": "simulated"}}]}
source: "modeled" is on every station for a reason. There are no commercial ground-station agreements. These are real geographic locations with modeled bandwidth and masks — geometry, not booked capacity. The full table is published on /methodology. The weather block carries its own source: live_openmeteo when a provider answered, simulated when it did not.
GET /api/v1/ground-stations/coverage no auth

What the network can actually do: contact duty cycle and worst gap for reference orbits, simulated against those exact 37 stations and their masks.

Response 200 (abridged)
{"stations": 37, "stations_modeled": 37, "aggregate_bandwidth_gbps": 148.5,
 "contact_duty_cycle": [
   {"orbit_class": "leo_550_53", "label": "550 km / 53.0 deg (Starlink-like)",
    "duty_cycle_pct": 44.7, "worst_gap_min": 40.5, "contact_intervals": 79,
    "stations_considered": 37, "window_hours": 24, "step_seconds": 30},
   {"orbit_class": "sso_550_97", "duty_cycle_pct": 58.2, "worst_gap_min": 30.5},
   {"orbit_class": "leo_420_51", "duty_cycle_pct": 32.7, "worst_gap_min": 47.0}]}

A duty cycle in the forties with a worst gap around 40 minutes — the reading above, taken 2026-07-30 — is the honest shape of a 37-site network. A latitude-band count would have read better and meant less. Curl it for the current values.

GET /api/v1/viz/snapshot no auth

The compact whole-system state behind the globe: satellites as positional arrays [lat, lon, alt_km, class], stations, modeled uplinks, and the subsolar point. Roughly an order of magnitude smaller than /satellites/positions — prefer it for bulk reads.

Response 200 (abridged)
{"sats": [[-7.41, 93.16, 432.3, "t"], …],
 "legend": {"s": "starlink", "o": "oneweb", "t": "crewed", "x": "other"},
 "stations": [{"id": "gs-svalbard", "name": "Svalbard (SvalSat)", "lat": 78.2307,
               "lon": 15.3896, "bandwidth_gbps": 4.0, "source": "modeled", …}],
 "links": [{"name": "STARLINK-1807", "sat": [35.61, 139.44, 463.6], "si": 8,
            "elev": 87.1, "type": "optical", "gbps": 10.0}, …],
 "links_note": "Modeled line-of-sight uplinks, LEO only (<= 2000 km), ranked by bandwidth x sin(elevation) — usable geometry, not elevation angle alone.",
 "sun": [18.397, -48.545],
 "stats": {"tracking": 11690, "active_links": 7113, "pass_windows": 118499,
           "fleet_size": 11692, "last_tle_refresh": "2026-07-30T14:48:59Z"}}

6. Federation (operators)

If you fly spacecraft, this is how your capacity becomes routable supply. Listing is free. Full walkthrough on /federation.

POST /api/v1/federation/operators no auth

Register once. The returned or_op_ key is shown exactly once and is not recoverable.

Request
{"name": "Acme Orbital", "contact_email": "ops@acme.example", "jurisdiction": "US"}
POST /api/v1/federation/nodes X-Operator-Key

Advertise a node. It is directory-listed immediately; it becomes routable only when OrbitRoute can propagate its orbit (a resolvable norad_id, or TLE lines under 14 days old) and has attested that you operate that spacecraft. The 201 tells you which condition is outstanding.

Request
{"node_id": "acme-tile-1", "norad_id": 25544, "gpu_type": "H100",
 "gpu_count": 8, "power_budget_w": 12000, "price_usd_per_gpu_hr": 1.99}

Additional advertised physics — battery_fraction, thermal_margin_c, storage_gb, isl_neighbors, jurisdiction, attestation — is defined in the full reference under CapabilityAdvertisement. Fields the scorer does not yet read are stored and served back, not silently dropped.

GET /api/v1/federation/nodes no auth

The public directory: every advertised node with its live position, illumination and verification state. Returns [] today — no operator has registered yet, and an empty directory is published as empty.

GET /api/v1/federation/nodes/mine X-Operator-Key

Your own nodes, including anything not yet routable and the reason.

7. Errors

CodeMeansDo
401Missing or unknown API key on an endpoint that requires one.Send X-API-Key. Check you did not paste the key into a query string.
404Unknown job — or a job that belongs to another key.Jobs are owner-scoped; use the key that created it.
409Cancelling a job that is already running or terminal.Re-read the record; the lifecycle is one-way.
422Request body failed schema validation.The response names the field and why.
429Over the per-IP burst limit or over your tier's daily cap.Back off. There is no overage billing — over the cap is refused, not charged.
503A dependency is degraded (e.g. disk pressure, or an upstream data source down).Retry. /status and /api/v1/health/deep say what is unhealthy.

8. SDKs

# Python — sync + async, one httpx dependency
pip install orbitroute

# TypeScript / JavaScript — zero dependencies, fetch-based
npm install orbitroute

Both wrap routing, cost-compare, managed jobs, SLA and federation, and raise a typed error carrying .status_code and .detail. Prefer plain HTTP? The surface above is the whole surface, and /openapi.json feeds any generator.

Where the numbers come from: /methodology. What a paid audit looks like: /sample-audit. Whether it is up right now: /status.