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.
https://api.orbitalert.netDownloads & 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.
OpenAPI spec
openapi.json
Postman collection
postman_collection.json
Try it live
API Playground →
Versioning policy
Deprecation & support window →
Authentication
Every request must include your API key in the X-API-Key header. Manage your keys from the API Keys page.
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
| Parameter | Type | Default | Description |
|---|---|---|---|
satelliterequired | string | — | Satellite name from the Celestrak catalog, e.g. "NOAA-20", "ISS (ZARYA)", "METOP-B", "TERRA" |
latrequired | float | — | Observer geodetic latitude in degrees, −90 to 90 |
lonrequired | float | — | Observer longitude in degrees, −180 to 180 |
hours | float | 24 | Prediction window in hours. Clamped to 0.1 – 72. |
min_elevation | float | 10 | Minimum 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
[
{
"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
}
]| Field | Description |
|---|---|
aos_utc | Acquisition of Signal — satellite rises above horizon (ISO 8601 UTC) |
los_utc | Loss of Signal — satellite drops below horizon (ISO 8601 UTC) |
tca_utc | Time of Closest Approach — moment of maximum elevation (ISO 8601 UTC) |
max_elevation_deg | Peak elevation angle in degrees (0–90). Higher = stronger signal. |
duration_seconds | Total pass duration in whole seconds (LOS − AOS) |
azimuth_at_aos | Satellite compass bearing at rise — 0° = North, clockwise |
azimuth_at_los | Satellite 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
| Parameter | Type | Default | Description |
|---|---|---|---|
queriesrequired | array (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
[
{
"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)"
}
]| Field | Description |
|---|---|
passes | List of pass events (same shape as GET /passes), or null if this query failed |
error | Human-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
| Parameter | Type | Default | Description |
|---|---|---|---|
satellite_namerequired | string | — | Satellite name matching the Celestrak catalog, e.g. "NOAA-20", "METOP-C" |
observer_latrequired | float | — | Observer geodetic latitude, −90 to 90 |
observer_lonrequired | float | — | Observer longitude, −180 to 180 |
webhook_urlrequired | string (HTTPS) | — | Destination for webhook POSTs. Must use HTTPS — HTTP URLs are rejected. |
slack_webhook_url | string (HTTPS) | — | Pro plan and above. Mirrors delivery to a Slack Incoming Webhook — must start with https://hooks.slack.com/services/. |
discord_webhook_url | string (HTTPS) | — | Pro plan and above. Mirrors delivery to a Discord webhook — must start with https://discord.com/api/webhooks/. |
pagerduty_routing_key | string (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_before | integer | 10 | How many minutes before AOS to fire the webhook. Range: 1–60. |
min_elevation | float | 10.0 | Skip 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
{
"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.
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
{
"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"
}| severity | Kp range | What it means |
|---|---|---|
quiet | 0–2 | Nominal conditions. No impact on satellite operations or polar routes. |
unsettled | 3–4 | Minor disturbance. Enhanced auroral activity above 60°N/S. |
storm | 5–6 | Geomagnetic storm. HF radio degraded at polar latitudes. |
severe | 7–9 | Severe 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
| Parameter | Type | Default | Description |
|---|---|---|---|
fresh | boolean | false | Bypass 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
{
"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"
}| Field | Description |
|---|---|
tle_line1 / tle_line2 | The raw two-line element set, unmodified from the source |
last_updated | When 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
[
{
"ground_station_id": "gs_8f3a12bc",
"ground_station_name": "Athens HQ",
"norad_id": 25544,
"satellite_name": "ISS (ZARYA)",
"azimuth": 214.3,
"elevation": 38.6
}
]| Field | Description |
|---|---|
ground_station_id / ground_station_name | The station currently seeing this satellite |
norad_id / satellite_name | The satellite currently visible from that station |
azimuth / elevation | Current 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
[
{
"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
}
]| Field | Description |
|---|---|
primary_norad_id / primary_name | Your tracked satellite |
secondary_norad_id / secondary_name | The other catalog object it approaches |
tca_utc | Time of closest approach (ISO 8601 UTC) |
miss_distance_km | Predicted minimum separation at TCA |
relative_velocity_kms | Relative speed at TCA, if computable |
probability | Simplified 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
[
{
"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
}
]| Field | Description |
|---|---|
norad_id / name | The tracked satellite |
detected_at | When this jump was detected (ISO 8601 UTC) |
old_tle_epoch / new_tle_epoch | Epochs of the two element sets compared |
delta_inclination_deg | Absolute change in inclination (degrees) |
delta_eccentricity | Absolute change in eccentricity |
delta_mean_motion_rev_day | Absolute 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
[
{
"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"
}
]| Field | Description |
|---|---|
alert_id_a / satellite_a | The first tracked satellite in the conflict |
alert_id_b / satellite_b | The second tracked satellite in the conflict |
station_lat / station_lon | The shared ground station's coordinates |
overlap_start_utc / overlap_end_utc | The window during which both passes are simultaneously above the horizon |
POST /link-budget
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
| Parameter | Type | Default | Description |
|---|---|---|---|
frequency_mhzrequired | number | — | Carrier frequency (MHz) |
tx_power_wrequired | number | — | Transmitter output power (W) |
tx_antenna_gain_dbirequired | number | — | Transmit antenna gain (dBi) |
rx_antenna_gain_dbirequired | number | — | Receive antenna gain (dBi) |
slant_range_km | number | — | Manual mode: distance between transmitter and receiver (km) |
elevation_deg | number | — | Manual mode: used for the atmospheric-attenuation estimate |
satellite | string | — | Automatic mode: Celestrak name or NORAD ID |
lat / lon | number | — | Automatic mode: ground-station coordinates (°) |
alt_m | number | 0 | Automatic mode: ground-station altitude (m) |
weather | string | clear | One of "clear", "cloudy", "rain", "heavy_rain" |
rx_sensitivity_dbm | number | — | If 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
{
"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
}| Field | Description |
|---|---|
eirp_dbw | Effective isotropic radiated power |
fspl_db | Free-space path loss (standard km/MHz form) |
atmospheric_loss_db | Weather-dependent zenith loss scaled by a secant-law elevation factor |
total_path_loss_db | fspl_db + atmospheric_loss_db |
received_power_dbw / received_power_dbm | Power at the receiver input |
link_margin_db / link_closes | Only present if rx_sensitivity_dbm was supplied |
slant_range_km / elevation_deg | The actual geometry used — echoed back either way |
computed_from | "manual" or "next_pass" |
tca_utc | Set 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
| Parameter | Type | Default | Description |
|---|---|---|---|
candidatesrequired | array | — | 2-20 objects: { name, lat, lon, alt_m? } |
norad_id | integer | — | Track a single satellite by NORAD ID (exclusive with constellation) |
constellation | string | — | Track every satellite matching this name substring |
duration_hours | number | 48 | Prediction window in hours (1-72) |
min_elevation | number | 10 | Minimum 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
[
{
"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
}
]| Field | Description |
|---|---|
passes_per_day | Total passes over duration_hours, normalized to a 24h rate |
avg_max_elevation_deg | Mean of each pass's peak elevation |
total_visible_minutes | Summed duration of every pass |
recommended | true 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
| Parameter | Type | Default | Description |
|---|---|---|---|
latrequired | number | — | Observer latitude (°) |
lonrequired | number | — | Observer longitude (°) |
alt_m | number | 0 | Observer altitude above sea level (m) |
hours | number | 24 | Prediction window in hours (1-72) |
min_elevation | number | 10 | Minimum 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
{
"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
}| Field | Description |
|---|---|
satellites_matched | How many catalog entries matched the name substring |
hourly_coverage | One entry per hour in the window (last one clipped if hours isn't a whole number) |
coverage_pct | Fraction of that hour with at least one satellite above min_elevation |
satellites_visible_peak | Most satellites simultaneously visible during that hour |
overall_coverage_pct | Mean 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
| Parameter | Type | Default | Description |
|---|---|---|---|
satelliterequired | string | — | Celestrak satellite name or NORAD ID |
latrequired | number | — | Observer latitude (°) |
lonrequired | number | — | Observer longitude (°) |
alt_m | number | 0 | Observer altitude above sea level (m) |
frequency_mhzrequired | number | — | Carrier frequency (MHz) |
step_seconds | integer | 5 | Time step across the pass (1-60s) |
hours | number | 24 | Search window for the next qualifying pass (1-72) |
min_elevation | number | 10 | Minimum 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
{
"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
}
]
}| Field | Description |
|---|---|
aos_utc / los_utc / tca_utc | The matched pass's timing (same as GET /passes) |
points[].elevation_deg | Elevation above the horizon at that instant |
points[].azimuth_deg | Compass bearing at that instant — 0° = North, clockwise (also on GET /passes/rotator-track) |
points[].range_km | Slant range from the ground station at that instant |
points[].doppler_shift_hz | -f0 * v_radial / c — positive while approaching |
points[].shifted_frequency_mhz | frequency_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
| Parameter | Type | Default | Description |
|---|---|---|---|
satelliterequired | string | — | Celestrak satellite name or NORAD ID |
latrequired | number | — | Observer latitude (°) |
lonrequired | number | — | Observer longitude (°) |
alt_m | number | 0 | Observer altitude above sea level (m) |
step_seconds | integer | 5 | Time step across the pass (1-60s) |
hours | number | 24 | Search window for the next qualifying pass (1-72) |
min_elevation | number | 10 | Minimum 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
{
"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 }
]
}| Field | Description |
|---|---|
aos_utc / los_utc / tca_utc | The matched pass's timing (same as GET /passes) |
points[].azimuth_deg | Compass bearing at that instant — 0° = North, clockwise |
points[].elevation_deg | Elevation 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.
# 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.
{
"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"
}| Field | Description |
|---|---|
event | Always "pass.upcoming" in this version of the API |
alert_id | ID of the alert that triggered this delivery |
satellite | Satellite name from the Celestrak catalog |
satellite_name | Legacy alias of satellite — same value, kept for early integrations |
aos_utc / los_utc / tca_utc | Pass window timestamps in ISO 8601 UTC |
max_elevation_deg | Predicted peak elevation above the horizon (degrees) |
duration_seconds | Total pass duration in seconds (LOS − AOS) |
azimuth_at_aos / azimuth_at_los | Compass bearing at rise and set — 0° = North, clockwise |
kp_index | Current NOAA Kp at the moment of delivery (null if unavailable) |
weather_warning | true when Kp > 5 — expect ionospheric degradation on low-elevation links |
minutes_until_aos | Minutes remaining until the satellite clears the horizon |
observer | The lat/lon registered for this alert |
tle_epoch_utc | Epoch of the element set this prediction was computed from (ISO 8601 UTC) |
tle_age_hours | Hours 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_at | Server timestamp when this POST was dispatched (ISO 8601 UTC) |
test | Only present (true) on deliveries from POST /alerts/{id}/test-webhook |
account_domain | Only 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 "", 200Error codes
All errors return JSON with error (human-readable) and code (machine-readable) fields.
{
"error": "Invalid or missing API key.",
"code": "unauthorized"
}| Status | code | When it happens |
|---|---|---|
| 401 | unauthorized | Missing X-API-Key header, or the key has been revoked |
| 403 | forbidden | Your plan does not include this endpoint |
| 404 | not_found | Alert ID does not exist, or belongs to another account |
| 422 | validation_error | Request body failed schema validation — check field types and required fields |
| 422 | invalid_webhook_url | webhook_url must use HTTPS — http:// URLs are rejected |
| 429 | rate_limited | Too many requests this hour. Starter: 60/hr · Pro: 180/hr · Research: 600/hr · Enterprise: 3,000/hr |
| 500 | internal_error | Unexpected 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.
#!/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 →