OrbitAlert
Sign in →

OrbitAlert API

REST · JSON · HTTPS · Get started in 5 minutes

Satellite pass predictions and proactive webhook alerts — delivered to any HTTPS endpoint minutes before a satellite rises above your horizon. One API key, plain REST, official Python/JS SDKs and a CLI if you want them.

All systems operationalBase URL: https://api.orbitalert.net

Downloads & Playground

Prefer to explore in a tool instead of reading? Grab the machine-readable spec, import the collection into Postman, or try requests live in the browser.

Authentication

Every request must include your API key in the X-API-Key header. Manage your keys from the API Keys page.

bash
curl https://api.orbitalert.net/health \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Keep your key secret. Never commit it to version control or expose it in client-side code. Rotate it immediately from the dashboard if it is ever compromised.

GET /passes

Returns all predicted passes above the elevation threshold for the given observer and prediction window, sorted chronologically by AOS. Timing precision is ±10 seconds (10 s propagation steps, SGP4).

Query parameters

ParameterTypeDefaultDescription
satelliterequiredstringSatellite name from the Celestrak catalog, e.g. "NOAA-20", "ISS (ZARYA)", "METOP-B", "TERRA"
latrequiredfloatObserver geodetic latitude in degrees, −90 to 90
lonrequiredfloatObserver longitude in degrees, −180 to 180
hoursfloat24Prediction window in hours. Clamped to 0.1 – 72.
min_elevationfloat10Minimum peak elevation in degrees. Passes that never reach this angle are omitted.

Request

curl "https://api.orbitalert.net/passes?satellite=NOAA-20&lat=37.92&lon=23.73&hours=24" \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
[
  {
    "satellite": "NOAA-20",
    "aos_utc": "2026-06-01T06:14:32Z",
    "los_utc": "2026-06-01T06:23:17Z",
    "tca_utc": "2026-06-01T06:18:54Z",
    "max_elevation_deg": 67.3,
    "duration_seconds": 525,
    "azimuth_at_aos": 342.1,
    "azimuth_at_los": 158.7
  },
  {
    "satellite": "NOAA-20",
    "aos_utc": "2026-06-01T14:53:08Z",
    "los_utc": "2026-06-01T15:01:44Z",
    "tca_utc": "2026-06-01T14:57:26Z",
    "max_elevation_deg": 22.8,
    "duration_seconds": 516,
    "azimuth_at_aos": 315.4,
    "azimuth_at_los": 197.2
  }
]
FieldDescription
aos_utcAcquisition of Signal — satellite rises above horizon (ISO 8601 UTC)
los_utcLoss of Signal — satellite drops below horizon (ISO 8601 UTC)
tca_utcTime of Closest Approach — moment of maximum elevation (ISO 8601 UTC)
max_elevation_degPeak elevation angle in degrees (0–90). Higher = stronger signal.
duration_secondsTotal pass duration in whole seconds (LOS − AOS)
azimuth_at_aosSatellite compass bearing at rise — 0° = North, clockwise
azimuth_at_losSatellite compass bearing at set — 0° = North, clockwise

POST /passes/batch

Same prediction as GET /passes, for up to 20 satellite/ground-station combinations in one call — useful for a multi-satellite constellation or a multi-site ground-station network. Counts as a single request against your plan's rate limit no matter how many queries the batch contains. Each query fails independently: one unknown satellite doesn't drop the rest of the batch — check the error field on each result.

Request body

ParameterTypeDefaultDescription
queriesrequiredarray (1–20)Each item takes the same fields as GET /passes: satellite, lat, lon, and optional alt_m, hours, min_elevation.

Request

curl -X POST https://api.orbitalert.net/passes/batch \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ" \
  -H "Content-Type: application/json" \
  -d '{
    "queries": [
      { "satellite": "NOAA-20",    "lat": 37.92, "lon": 23.73, "hours": 24 },
      { "satellite": "ISS (ZARYA)", "lat": 37.92, "lon": 23.73, "hours": 24 }
    ]
  }'

Response 200 OK

json
[
  {
    "satellite": "NOAA-20",
    "lat": 37.92,
    "lon": 23.73,
    "passes": [ { "aos_utc": "2026-06-01T06:14:32Z", "...": "..." } ],
    "error": null
  },
  {
    "satellite": "ISS (ZARYA)",
    "lat": 37.92,
    "lon": 23.73,
    "passes": null,
    "error": "Unknown satellite: ISS (ZARYA)"
  }
]
FieldDescription
passesList of pass events (same shape as GET /passes), or null if this query failed
errorHuman-readable failure reason for this query, or null on success

POST /alerts

Creates a persistent alert. OrbitAlert checks for upcoming passes every 5 minutes and fires a POST to your webhook the specified number of minutes before each qualifying pass. Alerts remain active until deleted.

Request body

ParameterTypeDefaultDescription
satellite_namerequiredstringSatellite name matching the Celestrak catalog, e.g. "NOAA-20", "METOP-C"
observer_latrequiredfloatObserver geodetic latitude, −90 to 90
observer_lonrequiredfloatObserver longitude, −180 to 180
webhook_urlrequiredstring (HTTPS)Destination for webhook POSTs. Must use HTTPS — HTTP URLs are rejected.
slack_webhook_urlstring (HTTPS)Pro plan and above. Mirrors delivery to a Slack Incoming Webhook — must start with https://hooks.slack.com/services/.
discord_webhook_urlstring (HTTPS)Pro plan and above. Mirrors delivery to a Discord webhook — must start with https://discord.com/api/webhooks/.
pagerduty_routing_keystring (32 chars)Pro plan and above. Opens a PagerDuty Events API v2 incident per pass, deduplicated per pass so retries don't double-fire it.
minutes_beforeinteger10How many minutes before AOS to fire the webhook. Range: 1–60.
min_elevationfloat10.0Skip passes with a peak elevation below this threshold (degrees).

Request

curl -X POST https://api.orbitalert.net/alerts \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ" \
  -H "Content-Type: application/json" \
  -d '{
    "satellite_name": "NOAA-20",
    "observer_lat": 37.9195,
    "observer_lon": 23.7310,
    "webhook_url": "https://your-server.com/hook",
    "minutes_before": 10,
    "min_elevation": 15.0
  }'

Response 201 Created

json
{
  "id": "alert_8f3a12bc",
  "satellite_name": "NOAA-20",
  "observer_lat": 37.9195,
  "observer_lon": 23.7310,
  "webhook_url": "https://your-server.com/hook",
  "webhook_secret": "5f2c9a1e...(64 hex chars)",
  "minutes_before": 10,
  "min_elevation": 15.0,
  "created_at": "2026-05-31T14:22:07Z"
}

Delivery & retry policy

If your endpoint returns a non-2xx status, OrbitAlert makes up to 3 attempts in total, with back-off gaps of 30 s → 2 min between them. Your endpoint must respond within 10 seconds or the attempt is counted as failed. If every attempt fails, the pass details are emailed to the account owner as a fallback. All delivery attempts and their HTTP status codes are visible in the dashboard webhook log.

POST /push/subscribe

Registers a browser/PWA Web Push subscription — a real, standards-based notification channel alongside webhook/Slack/Discord/PagerDuty delivery, not tied to a plan. Normally driven by the "Enable browser notifications" control on /account, which registers a service worker, calls the browser's pushManager.subscribe(), and POSTs the resulting subscription object here as-is.

json
curl https://api.orbitalert.net/push/subscribe \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ" \
  -H "Content-Type: application/json" \
  -d '{
    "endpoint": "https://fcm.googleapis.com/fcm/send/...",
    "keys": {
      "p256dh": "BNc...",
      "auth": "8x...="
    }
  }'

Upserts on endpoint (a stable per-device id), so re-subscribing the same device is idempotent. Remove a subscription with POST /push/unsubscribe (same body shape, just { "endpoint": "..." }) — a dead subscription (browser-side unsubscribe, uninstalled PWA) is also pruned automatically the next time delivery to it returns HTTP 410.

GET /space-weather

Returns the current NOAA planetary K-index (Kp), a 0–9 scale measuring geomagnetic disturbance. Useful for scheduling high-priority passes or adjusting receive parameters. Data is refreshed every 5 minutes from NOAA SWPC.

Request

curl https://api.orbitalert.net/space-weather \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
{
  "kp_index": 3.2,
  "severity": "unsettled",
  "description": "Minor geomagnetic disturbance. Enhanced auroral activity above 60°N/S. No impact on VHF/UHF satellite links.",
  "updated_at": "2026-05-31T14:00:00Z"
}
severityKp rangeWhat it means
quiet0–2Nominal conditions. No impact on satellite operations or polar routes.
unsettled3–4Minor disturbance. Enhanced auroral activity above 60°N/S.
storm5–6Geomagnetic storm. HF radio degraded at polar latitudes.
severe7–9Severe storm. Possible satellite drag increase and orientation effects.

GET /satellites/{norad_id}/tlePro plan+

Raw two-line-element data for feeding into your own propagation or analysis tooling, rather than OrbitAlert's processed pass predictions. By default this reads the cached catalog entry (refreshed every 24h from Celestrak). Enterprise plans can pass fresh=true to bypass the cache with a live lookup — useful right after a maneuver, when the cached orbit may be stale.

Query parameters

ParameterTypeDefaultDescription
freshbooleanfalseBypass the 24h cache with a live lookup instead. Enterprise plan only — 403 on Pro/Research.

Request

curl https://api.orbitalert.net/satellites/25544/tle \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
{
  "norad_id": 25544,
  "name": "ISS (ZARYA)",
  "tle_line1": "1 25544U 98067A   26189.15353387  .00005161  00000+0  10196-3 0  9993",
  "tle_line2": "2 25544  51.6304 196.3226 0006696 270.4034  89.6187 15.48940380575005",
  "last_updated": "2026-07-10T03:00:00Z",
  "source": "catalog"
}
FieldDescription
tle_line1 / tle_line2The raw two-line element set, unmodified from the source
last_updatedWhen this TLE was fetched (ISO 8601 UTC)
source"catalog" (24h cache) or "live" (fresh=true, Enterprise only)

GET /coverageEnterprise

Live cross-reference of your ground stations against your tracked satellites: which of your stations currently have line-of-sight to which of your satellites, right now. No parameters — it uses your saved ground stations (see POST /ground-stations in the dashboard) and the satellites referenced by your active alerts. Only entries currently above the horizon are returned.

Request

curl https://api.orbitalert.net/coverage \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
[
  {
    "ground_station_id": "gs_8f3a12bc",
    "ground_station_name": "Athens HQ",
    "norad_id": 25544,
    "satellite_name": "ISS (ZARYA)",
    "azimuth": 214.3,
    "elevation": 38.6
  }
]
FieldDescription
ground_station_id / ground_station_nameThe station currently seeing this satellite
norad_id / satellite_nameThe satellite currently visible from that station
azimuth / elevationCurrent look angle from the station, in degrees

GET /conjunctionsResearch plan+

Predicted close approaches ("conjunctions") between your tracked satellites (from active alerts) and any other object in the satellite catalog, refreshed in the background every 6 hours using an altitude-band pre-filter followed by coarse-to-fine SGP4 propagation. No parameters — results are scoped to your organization.

miss_distance_km is a real geometric result of the propagation. probability is a simplified heuristic — Celestrak TLEs carry no covariance data, so treat it as a relative severity signal, not an authoritative Space-Track conjunction data message (CDM).

Request

curl https://api.orbitalert.net/conjunctions \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
[
  {
    "id": "conj_9a1b2c3d",
    "primary_norad_id": 43013,
    "primary_name": "NOAA 20",
    "secondary_norad_id": 48274,
    "secondary_name": "COSMOS 2251 DEB",
    "tca_utc": "2026-07-12T04:18:22Z",
    "miss_distance_km": 1.84,
    "relative_velocity_kms": 8.21,
    "probability": 0.15
  }
]
FieldDescription
primary_norad_id / primary_nameYour tracked satellite
secondary_norad_id / secondary_nameThe other catalog object it approaches
tca_utcTime of closest approach (ISO 8601 UTC)
miss_distance_kmPredicted minimum separation at TCA
relative_velocity_kmsRelative speed at TCA, if computable
probabilitySimplified heuristic proxy, not an authoritative Pc — see note above

GET /maneuversResearch plan+

Orbital-element jumps (inclination, eccentricity, mean motion) detected for satellites you actively track (alerts with a norad_id), over the last 30 days. Detection runs as part of the same background cycle that refreshes the satellite catalog. No parameters — results are scoped to your organization.

This is a heuristic, not a confirmed maneuver report. Thresholds are set well above typical drag-only drift for a LEO object, but only the operator's own maneuver log can confirm a burn actually happened — treat a result here as "worth checking," not authoritative. See how we validate accuracy for the same honesty applied to prediction confidence generally.

Request

curl https://api.orbitalert.net/maneuvers \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
[
  {
    "id": "man_7f2e9d1a",
    "norad_id": 43013,
    "name": "NOAA 20",
    "detected_at": "2026-08-30T02:11:04Z",
    "old_tle_epoch": "2026-08-28T14:02:11Z",
    "new_tle_epoch": "2026-08-29T13:55:47Z",
    "delta_inclination_deg": 0.0243,
    "delta_eccentricity": 0.000412,
    "delta_mean_motion_rev_day": 0.001187
  }
]
FieldDescription
norad_id / nameThe tracked satellite
detected_atWhen this jump was detected (ISO 8601 UTC)
old_tle_epoch / new_tle_epochEpochs of the two element sets compared
delta_inclination_degAbsolute change in inclination (degrees)
delta_eccentricityAbsolute change in eccentricity
delta_mean_motion_rev_dayAbsolute change in mean motion (rev/day)

GET /schedule-conflictsResearch plan+

"Single-Station Conflict" detection: flags when two of your tracked satellites (alerts with a norad_id) are both above the horizon at the same physical ground station (within ~110 m) at overlapping times — a single antenna can only track one of them. Refreshed every 30 minutes. No parameters — results are scoped to your organization.

This is about ground-hardware contention, not orbital collision risk — see GET /conjunctions for that. Without this check, most tracking setups default to whichever satellite reaches 10° elevation first and silently never switch to the other for the rest of its pass — this surfaces that collision instead of hiding it.

Request

curl https://api.orbitalert.net/schedule-conflicts \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
[
  {
    "id": "conf_3c8e1a2b",
    "alert_id_a": "alert_8f3a12bc",
    "satellite_a": "NOAA-20",
    "alert_id_b": "alert_a91fe2d0",
    "satellite_b": "METOP-C",
    "station_lat": 37.9838,
    "station_lon": 23.7275,
    "overlap_start_utc": "2026-08-31T14:22:10Z",
    "overlap_end_utc": "2026-08-31T14:26:45Z"
  }
]
FieldDescription
alert_id_a / satellite_aThe first tracked satellite in the conflict
alert_id_b / satellite_bThe second tracked satellite in the conflict
station_lat / station_lonThe shared ground station's coordinates
overlap_start_utc / overlap_end_utcThe window during which both passes are simultaneously above the horizon

Stateless RF link budget calculator: EIRP, free-space path loss, a weather/elevation-based atmospheric attenuation estimate, and received power. Available on every plan.

Give it geometry one of two ways — exactly one, not both: manual (slant_range_km + elevation_deg, e.g. from a GET /passes result, or a hypothetical scenario), or automatic (satellite + lat + lon — range and elevation are computed for you at the moment of peak elevation (TCA) of the next qualifying pass, the representative best-case link opportunity for that pair).

Request body

ParameterTypeDefaultDescription
frequency_mhzrequirednumberCarrier frequency (MHz)
tx_power_wrequirednumberTransmitter output power (W)
tx_antenna_gain_dbirequirednumberTransmit antenna gain (dBi)
rx_antenna_gain_dbirequirednumberReceive antenna gain (dBi)
slant_range_kmnumberManual mode: distance between transmitter and receiver (km)
elevation_degnumberManual mode: used for the atmospheric-attenuation estimate
satellitestringAutomatic mode: Celestrak name or NORAD ID
lat / lonnumberAutomatic mode: ground-station coordinates (°)
alt_mnumber0Automatic mode: ground-station altitude (m)
weatherstringclearOne of "clear", "cloudy", "rain", "heavy_rain"
rx_sensitivity_dbmnumberIf given, also returns link_margin_db and link_closes

Request

curl https://api.orbitalert.net/link-budget \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ" \
  -H "Content-Type: application/json" \
  -d '{
    "frequency_mhz": 437.5,
    "tx_power_w": 5,
    "tx_antenna_gain_dbi": 3,
    "rx_antenna_gain_dbi": 12,
    "slant_range_km": 1200,
    "elevation_deg": 25,
    "weather": "rain",
    "rx_sensitivity_dbm": -110
  }'

Response 200 OK

json
{
  "eirp_dbw": 11.99,
  "fspl_db": 148.16,
  "atmospheric_loss_db": 9.46,
  "total_path_loss_db": 157.62,
  "received_power_dbw": -133.63,
  "received_power_dbm": -103.63,
  "link_margin_db": 6.37,
  "link_closes": true,
  "slant_range_km": 1200.0,
  "elevation_deg": 25.0,
  "computed_from": "manual",
  "tca_utc": null
}
FieldDescription
eirp_dbwEffective isotropic radiated power
fspl_dbFree-space path loss (standard km/MHz form)
atmospheric_loss_dbWeather-dependent zenith loss scaled by a secant-law elevation factor
total_path_loss_dbfspl_db + atmospheric_loss_db
received_power_dbw / received_power_dbmPower at the receiver input
link_margin_db / link_closesOnly present if rx_sensitivity_dbm was supplied
slant_range_km / elevation_degThe actual geometry used — echoed back either way
computed_from"manual" or "next_pass"
tca_utcSet only when computed_from is "next_pass"

POST /optimize-location

Ranks 2-20 candidate ground-station locations by predicted visibility of a target satellite (norad_id) or constellation (constellation name substring, case-insensitive, capped at 50 matched satellites) — passes/day, average peak elevation, and total visible minutes over duration_hours. The top-ranked candidate is flagged recommended: true.

Request body

ParameterTypeDefaultDescription
candidatesrequiredarray2-20 objects: { name, lat, lon, alt_m? }
norad_idintegerTrack a single satellite by NORAD ID (exclusive with constellation)
constellationstringTrack every satellite matching this name substring
duration_hoursnumber48Prediction window in hours (1-72)
min_elevationnumber10Minimum peak elevation (°) to count as a pass

Request

curl https://api.orbitalert.net/optimize-location \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ" \
  -H "Content-Type: application/json" \
  -d '{
    "candidates": [
      { "name": "Athens",    "lat": 37.98, "lon": 23.73 },
      { "name": "Svalbard",  "lat": 78.22, "lon": 15.65 },
      { "name": "Singapore", "lat": 1.35,  "lon": 103.82 }
    ],
    "norad_id": 43013,
    "duration_hours": 48
  }'

Response 200 OK

json
[
  {
    "name": "Svalbard",
    "lat": 78.22,
    "lon": 15.65,
    "alt_m": 0.0,
    "passes_per_day": 12.5,
    "avg_max_elevation_deg": 38.4,
    "total_visible_minutes": 94.2,
    "recommended": true
  },
  {
    "name": "Athens",
    "lat": 37.98,
    "lon": 23.73,
    "alt_m": 0.0,
    "passes_per_day": 5.0,
    "avg_max_elevation_deg": 41.2,
    "total_visible_minutes": 41.8,
    "recommended": false
  },
  {
    "name": "Singapore",
    "lat": 1.35,
    "lon": 103.82,
    "alt_m": 0.0,
    "passes_per_day": 4.5,
    "avg_max_elevation_deg": 29.7,
    "total_visible_minutes": 33.1,
    "recommended": false
  }
]
FieldDescription
passes_per_dayTotal passes over duration_hours, normalized to a 24h rate
avg_max_elevation_degMean of each pass's peak elevation
total_visible_minutesSummed duration of every pass
recommendedtrue on exactly one candidate — the highest passes_per_day

GET /constellation/{name}/coverage

Matches {name} as a case-insensitive substring against the satellite catalog (e.g. starlink, oneweb), capped at 200 matches, then aggregates every matched satellite's predicted passes into hourly coverage buckets — what fraction of each hour has at least one satellite above min_elevation, and the peak number simultaneously visible. Returns 404 if the name matches nothing.

Query parameters

ParameterTypeDefaultDescription
latrequirednumberObserver latitude (°)
lonrequirednumberObserver longitude (°)
alt_mnumber0Observer altitude above sea level (m)
hoursnumber24Prediction window in hours (1-72)
min_elevationnumber10Minimum peak elevation (°) to count as visible

Request

curl "https://api.orbitalert.net/constellation/starlink/coverage?lat=37.98&lon=23.73&hours=6" \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
{
  "constellation": "starlink",
  "satellites_matched": 200,
  "hourly_coverage": [
    { "hour_start_utc": "2026-07-11T12:00:00Z", "coverage_pct": 97.4, "satellites_visible_peak": 6 },
    { "hour_start_utc": "2026-07-11T13:00:00Z", "coverage_pct": 100.0, "satellites_visible_peak": 8 }
  ],
  "overall_coverage_pct": 98.7
}
FieldDescription
satellites_matchedHow many catalog entries matched the name substring
hourly_coverageOne entry per hour in the window (last one clipped if hours isn't a whole number)
coverage_pctFraction of that hour with at least one satellite above min_elevation
satellites_visible_peakMost satellites simultaneously visible during that hour
overall_coverage_pctMean coverage_pct across all hourly buckets

GET /passes/doppler

Finds the next pass of a satellite over a ground station and returns the Doppler shift curve across that pass's AOS→LOS window for a given carrier frequency — slant range, range-rate, and the resulting shift at each time step (positive = approaching / blue-shifted). Same satellite/location parameters as GET /passes, plus frequency_mhz and step_seconds. Returns 404 if no qualifying pass is found in the search window.

Query parameters

ParameterTypeDefaultDescription
satelliterequiredstringCelestrak satellite name or NORAD ID
latrequirednumberObserver latitude (°)
lonrequirednumberObserver longitude (°)
alt_mnumber0Observer altitude above sea level (m)
frequency_mhzrequirednumberCarrier frequency (MHz)
step_secondsinteger5Time step across the pass (1-60s)
hoursnumber24Search window for the next qualifying pass (1-72)
min_elevationnumber10Minimum peak elevation (°) to qualify

Request

curl "https://api.orbitalert.net/passes/doppler?satellite=NOAA-20&lat=37.98&lon=23.73&frequency_mhz=137.1&step_seconds=5" \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
{
  "satellite": "NOAA 20",
  "aos_utc": "2026-07-11T06:14:32Z",
  "los_utc": "2026-07-11T06:23:17Z",
  "tca_utc": "2026-07-11T06:18:54Z",
  "frequency_mhz": 137.1,
  "points": [
    {
      "t_utc": "2026-07-11T06:14:32Z",
      "elevation_deg": 10.1,
      "azimuth_deg": 342.6,
      "range_km": 1980.4,
      "doppler_shift_hz": 3421.6,
      "shifted_frequency_mhz": 137.103422
    },
    {
      "t_utc": "2026-07-11T06:18:54Z",
      "elevation_deg": 62.3,
      "azimuth_deg": 58.9,
      "range_km": 825.1,
      "doppler_shift_hz": 12.4,
      "shifted_frequency_mhz": 137.100012
    }
  ]
}
FieldDescription
aos_utc / los_utc / tca_utcThe matched pass's timing (same as GET /passes)
points[].elevation_degElevation above the horizon at that instant
points[].azimuth_degCompass bearing at that instant — 0° = North, clockwise (also on GET /passes/rotator-track)
points[].range_kmSlant range from the ground station at that instant
points[].doppler_shift_hz-f0 * v_radial / c — positive while approaching
points[].shifted_frequency_mhzfrequency_mhz adjusted by the Doppler shift

GET /passes/rotator-track

Finds the next pass of a satellite over a ground station and returns azimuth/elevation waypoints across that pass's AOS→LOS window — the trajectory an antenna rotator needs. Same parameters as GET /passes/doppler, minus frequency_mhz. Returns 404 if no qualifying pass is found in the search window.

This API has no way to hold a live session with hardware on your network — it returns the trajectory, not a live control channel. Feed it to your own rotctld (hamlib) instance, e.g. by replaying each waypoint as a P <az> <el> command over its local TCP socket — see the reference snippet below.

Query parameters

ParameterTypeDefaultDescription
satelliterequiredstringCelestrak satellite name or NORAD ID
latrequirednumberObserver latitude (°)
lonrequirednumberObserver longitude (°)
alt_mnumber0Observer altitude above sea level (m)
step_secondsinteger5Time step across the pass (1-60s)
hoursnumber24Search window for the next qualifying pass (1-72)
min_elevationnumber10Minimum peak elevation (°) to qualify

Request

curl "https://api.orbitalert.net/passes/rotator-track?satellite=NOAA-20&lat=37.98&lon=23.73&step_seconds=5" \
  -H "X-API-Key: sat_xk29mQpLvN3rT8wZ"

Response 200 OK

json
{
  "satellite": "NOAA 20",
  "aos_utc": "2026-07-11T06:14:32Z",
  "los_utc": "2026-07-11T06:23:17Z",
  "tca_utc": "2026-07-11T06:18:54Z",
  "points": [
    { "t_utc": "2026-07-11T06:14:32Z", "azimuth_deg": 342.6, "elevation_deg": 10.1 },
    { "t_utc": "2026-07-11T06:18:54Z", "azimuth_deg": 58.9,  "elevation_deg": 62.3 }
  ]
}
FieldDescription
aos_utc / los_utc / tca_utcThe matched pass's timing (same as GET /passes)
points[].azimuth_degCompass bearing at that instant — 0° = North, clockwise
points[].elevation_degElevation above the horizon at that instant

Replaying against rotctld

import socket
import time
import requests

track = requests.get(
    "https://api.orbitalert.net/passes/rotator-track",
    params={"satellite": "NOAA-20", "lat": 37.98, "lon": 23.73, "step_seconds": 5},
    headers={"X-API-Key": "sat_xk29mQpLvN3rT8wZ"},
).json()

# Replay against a local rotctld (hamlib) — must be reachable from wherever
# this script runs, e.g. on the same machine as the rotator hardware.
sock = socket.create_connection(("localhost", 4533))
for point in track["points"]:
    az, el = point["azimuth_deg"], point["elevation_deg"]
    sock.sendall(f"P {az:.1f} {el:.1f}\n".encode())
    sock.recv(1024)
    time.sleep(5)  # matches step_seconds above
sock.close()

GraphQL API

A read-only GraphQL surface at POST /graphql over the two things people actually want one round-trip for: pass predictions and your own alerts. Authenticated the same way as every REST endpoint (X-API-Key header). This is a deliberate subset, not REST parity — open /graphql in a browser for the interactive GraphiQL explorer, or request more fields if what you need isn't here yet.

graphql
# POST /graphql, body: { "query": "..." }
query {
  passes(satellite: "NOAA-20", lat: 37.98, lon: 23.73, hours: 24) {
    satellite
    aosUtc
    losUtc
    maxElevationDeg
    sunlit
  }
  alerts {
    id
    satelliteName
    active
  }
}

Webhook payload

When a pass is imminent, OrbitAlert POSTs the following JSON to your registered webhook_url. Respond with any 2xx status within 10 seconds to acknowledge delivery. Every delivery carries the source TLE's freshness (tle_age_hours / tle_quality) — see how we validate accuracy for what that does and doesn't promise.

json
{
  "event": "pass.upcoming",
  "alert_id": "alert_8f3a12bc",
  "satellite": "NOAA-20",
  "satellite_name": "NOAA-20",
  "aos_utc": "2026-06-01T06:14:32Z",
  "los_utc": "2026-06-01T06:23:17Z",
  "tca_utc": "2026-06-01T06:18:54Z",
  "max_elevation_deg": 67.3,
  "duration_seconds": 525,
  "azimuth_at_aos": 342.1,
  "azimuth_at_los": 158.7,
  "kp_index": 3.2,
  "weather_warning": false,
  "minutes_until_aos": 9,
  "observer": {
    "lat": 37.9195,
    "lon": 23.7310
  },
  "tle_epoch_utc": "2026-05-31T14:02:11Z",
  "tle_age_hours": 16.1,
  "tle_quality": "fresh",
  "delivered_at": "2026-06-01T06:05:33Z"
}
FieldDescription
eventAlways "pass.upcoming" in this version of the API
alert_idID of the alert that triggered this delivery
satelliteSatellite name from the Celestrak catalog
satellite_nameLegacy alias of satellite — same value, kept for early integrations
aos_utc / los_utc / tca_utcPass window timestamps in ISO 8601 UTC
max_elevation_degPredicted peak elevation above the horizon (degrees)
duration_secondsTotal pass duration in seconds (LOS − AOS)
azimuth_at_aos / azimuth_at_losCompass bearing at rise and set — 0° = North, clockwise
kp_indexCurrent NOAA Kp at the moment of delivery (null if unavailable)
weather_warningtrue when Kp > 5 — expect ionospheric degradation on low-elevation links
minutes_until_aosMinutes remaining until the satellite clears the horizon
observerThe lat/lon registered for this alert
tle_epoch_utcEpoch of the element set this prediction was computed from (ISO 8601 UTC)
tle_age_hoursHours elapsed since tle_epoch_utc at delivery time
tle_quality"fresh" (<24h), "aging" (<72h), or "stale" — see the accuracy page for what this does and doesn't promise
delivered_atServer timestamp when this POST was dispatched (ISO 8601 UTC)
testOnly present (true) on deliveries from POST /alerts/{id}/test-webhook
account_domainOnly present for orgs with a verified white-label domain (Enterprise)

Verifying webhook signatures

Every alert has its own signing key — the webhook_secret field returned when you create it (see the response above). Each delivery carries an X-OrbitAlert-Signature header — sha256=<hex>, an HMAC-SHA256 of the exact request body — so your endpoint can confirm a request genuinely came from OrbitAlert before acting on it.

Compute the HMAC over the raw request body — not a re-serialized version of the parsed JSON, which can differ in key order or whitespace — and compare with a constant-time function (hmac.compare_digest / crypto.timingSafeEqual) to avoid leaking the expected value through response-timing.

import hashlib, hmac
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = "5f2c9a1e..."  # the webhook_secret from your alert's API response

@app.post("/hook")
def hook():
    signature = request.headers.get("X-OrbitAlert-Signature", "")
    expected = "sha256=" + hmac.new(
        WEBHOOK_SECRET.encode(), request.get_data(), hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(signature, expected):
        abort(401, "Invalid signature")

    payload = request.get_json()
    print(f"Verified pass alert: {payload['satellite_name']}")
    return "", 200

Error codes

All errors return JSON with error (human-readable) and code (machine-readable) fields.

json
{
  "error": "Invalid or missing API key.",
  "code": "unauthorized"
}
StatuscodeWhen it happens
401unauthorizedMissing X-API-Key header, or the key has been revoked
403forbiddenYour plan does not include this endpoint
404not_foundAlert ID does not exist, or belongs to another account
422validation_errorRequest body failed schema validation — check field types and required fields
422invalid_webhook_urlwebhook_url must use HTTPS — http:// URLs are rejected
429rate_limitedToo many requests this hour. Starter: 60/hr · Pro: 180/hr · Research: 600/hr · Enterprise: 3,000/hr
500internal_errorUnexpected server error. Persistent occurrences should be reported.

Complete quickstart

A self-contained Python script that starts a local webhook receiver, registers a NOAA-20 alert over Athens, and prints the full payload when the satellite is 10 minutes out. Zero dependencies beyond requests.

python
#!/usr/bin/env python3
"""
OrbitAlert quickstart — register an alert and print the webhook payload.
Run:  pip install requests && python quickstart.py
"""
import time, threading, requests
from http.server import HTTPServer, BaseHTTPRequestHandler

API_KEY = "sat_xk29mQpLvN3rT8wZ"          # replace with your key
WEBHOOK = "https://YOUR_NGROK_ID.ngrok.io/hook"  # expose :9000 via ngrok
BASE    = "https://api.orbitalert.net"

# 1. Local receiver — prints every incoming POST body
class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        n    = int(self.headers["Content-Length"])
        body = self.rfile.read(n).decode()
        print(f"\n>>> Webhook received:\n{body}\n")
        self.send_response(200)
        self.end_headers()
    def log_message(self, *_): pass

threading.Thread(
    target=HTTPServer(("", 9000), Hook).serve_forever,
    daemon=True,
).start()
print("Webhook receiver listening on :9000")

# 2. Register an alert — NOAA-20 over Athens, 10 min notice, ≥ 15° elevation
r = requests.post(f"{BASE}/alerts",
    headers={"X-API-Key": API_KEY},
    json={"satellite_name": "NOAA-20",
          "observer_lat": 37.9195, "observer_lon": 23.7310,
          "webhook_url": WEBHOOK,
          "minutes_before": 10, "min_elevation": 15.0})
r.raise_for_status()
print(f"Alert registered: {r.json()['id']}")
print("Waiting for the next NOAA-20 pass…  (Ctrl-C to quit)")
time.sleep(86400)

For local development, expose port 9000 with ngrok (ngrok http 9000) and replace YOUR_NGROK_ID with the subdomain it assigns. The first qualifying pass typically fires within a few hours, depending on your location.

OrbitAlert API · Dashboard

Get an API key →