NextPass

API · /v1

Satellite passes, as an API

One HTTP call returns every pass of a satellite over a point on Earth: rise, peak and set, the sampled track at 10 s, range rate and Doppler shift, sunlight and visibility - and the age of the orbital elements behind it, so your users know what the times are worth.

First call

The ISS over Lyon, the next 48 hours, passes above 10°
Shell
curl -H "X-API-Key: $NEXTPASS_API_KEY" \
  'https://sat-pass-predictor-api.onrender.com/v1/passes?noradId=25544&lat=45.7578&lon=4.832&alt=170'
Response, abridged
{
  "satellite": { "noradId": 25544, "name": "ISS (ZARYA)" },
  "tle": {
    "epoch": "2026-09-30T06:12:44.123Z",
    "ageSeconds": 21304,
    "source": "CelesTrak",
    "fetchedAt": "2026-09-30T09:40:02Z",
    "line1": "1 25544U 98067A ...",
    "line2": "2 25544  51.63 ..."
  },
  "observer": { "latitudeDeg": 45.7578, "longitudeDeg": 4.832, "altitudeM": 170 },
  "minElevationDeg": 10,
  "frequencyMhz": null,
  "computedAt": "2026-09-30T12:07:48Z",
  "passes": [
    {
      "aos":         { "instant": "2026-09-30T19:42:10.412Z", "azimuthDeg": 247.1, "elevationDeg": 10.0, ... },
      "culmination": { "instant": "2026-09-30T19:45:21.870Z", "azimuthDeg": 172.4, "elevationDeg": 48.6, ... },
      "los":         { "instant": "2026-09-30T19:48:33.095Z", "azimuthDeg": 97.9,  "elevationDeg": 10.0, ... },
      "durationSeconds": 382,
      "track": [ { "instant": "...", "azimuthDeg": 247.1, "elevationDeg": 10.0, "rangeKm": 1362.4,
                   "rangeRateKmS": -6.41, "dopplerHz": null, "subPoint": { ... },
                   "illuminated": true, "visible": true }, ... ]
    }
  ]
}

Every instant is ISO-8601 UTC: time zones are a display concern and the API refuses to have an opinion. aos, culmination and los are points of track itself, so a labelled instant always sits on the curve drawn from it. An empty passes list is a result, not an error.

tle.ageSeconds is computed by the server, once, from the epoch of the elements. Show it: SGP4's error grows with it, on the order of kilometres per day in low orbit.

Endpoints

https://sat-pass-predictor-api.onrender.com
RouteReturnsAccess
GET /v1/passesThe passes of one satellite over one site.X-API-Key, one request
GET /v1/passes/batchEvery satellite over every site: up to 10 satellites, 10 sites, 25 predictions.X-API-Key, one request per prediction
GET /api/satellites?q=Active satellites whose CelesTrak name or number matches, for a search box.Public

Parameters

/v1/passes
NameFormatDefaultMeaning
noradIdinteger, 1–99999requiredNORAD catalogue number of the satellite.
latdegrees, −90 to 90requiredLatitude of the observer (WGS84).
londegrees, −180 to 180requiredLongitude of the observer.
altmetres, −500 to 90000Altitude above the WGS84 ellipsoid, not above sea level.
hoursinteger, 1–24048Length of the window, starting at the request.
minElevationdegrees, 0–8910Elevation a pass must exceed to count; AOS and LOS sit on it.
frequencyMhzMHz, 1–300000noneDownlink carrier; adds dopplerHz to every point and phase.

Doppler for radio amateurs

Range rate on every point; the shift when you name a frequency
ISS voice downlink, 145.8 MHz
curl -H "X-API-Key: $NEXTPASS_API_KEY" \
  'https://sat-pass-predictor-api.onrender.com/v1/passes?noradId=25544&lat=45.7578&lon=4.832&frequencyMhz=145.8'

Every point and phase carries rangeRateKmS, from Orekit's velocity rather than differenced ranges - negative while the satellite approaches. With frequencyMhz, each one also carries dopplerHz, the first-order shift to apply to the receiver, −f · rangeRate / c. For an uplink, apply the opposite sign. Asking for several frequencies costs one propagation: the shift is scaled from the cached prediction.

Batch predictions

A fleet of satellites over a network of ground stations, in one call
Shell
curl -H "X-API-Key: $NEXTPASS_API_KEY" \
  'https://sat-pass-predictor-api.onrender.com/v1/passes/batch?noradId=25544,20580&site=45.7578,4.8320,170&site=-33.92,18.42&track=false'
  • site is lat,lon or lat,lon,alt; repeat it for each site. Duplicates are dropped before counting.
  • results is satellite-major. Each entry holds either prediction, exactly the /v1/passes body, or error, exactly its Problem Details. One failed satellite does not fail the batch.
  • A batch is admitted whole or refused whole, and counts one request per prediction. Ask for track=false unless you need the polylines: 25 cached predictions return in milliseconds and half a megabyte instead of 13.
  • There is no frequencyMhz in a batch; compute −rangeRateKmS / 299792.458 × f yourself.

From your code

No SDK to install: one GET and one header
JavaScript (Node 18+ or a server runtime)
const url = new URL('https://sat-pass-predictor-api.onrender.com/v1/passes');
url.search = new URLSearchParams({ noradId: '25544', lat: '45.7578', lon: '4.832' }).toString();

const response = await fetch(url, { headers: { 'X-API-Key': process.env.NEXTPASS_API_KEY } });
if (!response.ok) {
  const problem = await response.json();          // RFC 9457: branch on problem.type
  throw new Error(`${problem.title}: ${problem.detail}`);
}
const { passes } = await response.json();
for (const pass of passes) console.log(pass.aos.instant, pass.culmination.elevationDeg);
Python
import os, requests

response = requests.get(
    "https://sat-pass-predictor-api.onrender.com/v1/passes",
    params={"noradId": 25544, "lat": 45.7578, "lon": 4.832},
    headers={"X-API-Key": os.environ["NEXTPASS_API_KEY"]},
    timeout=60,
)
if not response.ok:
    problem = response.json()                      # RFC 9457: branch on problem["type"]
    raise RuntimeError(f"{problem['title']}: {problem.get('detail')}")
for p in response.json()["passes"]:
    print(p["aos"]["instant"], p["culmination"]["elevationDeg"])

Keep the key on a server. A key shipped in a web page is a key anyone can read and spend. Cross-origin browser calls are refused unless the operator lists your origin, and even then CORS is not authentication.

Errors

RFC 9457 Problem Details, one format everywhere

Branch on type, never on the sentence: two different failures share status 503. Every type is https://github.com/warlaxx/sat-pass-predictor/errors/ followed by the slug below.

StatusTypeWhat to do
400invalid-requestA parameter is missing, malformed or out of bounds. Not retryable as is.
400batch-exceeds-rate-limitA batch larger than the key’s per-minute limit: it could never be admitted.
401invalid-api-keyMissing, unknown or revoked X-API-Key.
404unknown-satelliteThe number is absent from CelesTrak: most likely a re-entered object.
429rate-limit-exceeded · daily-quota-exceeded · monthly-quota-exceededLimit reached. Retry-After and resetsAt say when; limit says which.
503tle-unavailableNo source of elements answered and none is held. Retry in about 15 s.
503tle-staleThe only elements held are older than 7 days: refused rather than shown.
503api-access-unavailableQuota accounting is unavailable. The API fails closed; retry later.

Limits worth knowing

All plans →
  • The free preview allows 100 requests per UTC day and 10 per minute, with one key.
  • Minute limits are fixed UTC minute windows, not a sliding window. A cached answer counts like a computed one: you pay for the call, not the CPU.
  • Every admitted request counts, including one that later fails validation. Invalid keys, quota refusals and CORS preflights do not.
  • Responses carry Cache-Control: no-store. Do not put them behind a shared cache.
  • The service currently runs on a free instance that sleeps: the first call after a quiet period can take up to half a minute. Use a 60 s timeout. Check the status page.