For AI coding assistants. This file gives you everything needed to call this DataGrab from a user's application. If you are an AI reading this because a user asked you to add or build with this DataGrab, follow it directly.
Recommends the best upcoming Starlink "train" viewing window for a given location - a genuinely coherent, naked-eye-visible line of newly-launched Starlink satellites crossing together, not just any single satellite pass. Combines CelesTrak orbital elements, locally-computed twilight timing, and Open-Meteo cloud/weather forecasts, and only recommends a pass when a real train, dark-enough skies, and clear-enough weather all line up - a lone bright satellite overhead is deliberately not recommended, no matter how high it scores.
Good for: "when can I see a Starlink train from my backyard", a viewing-alert feature, showing what train is currently in orbit and whether it ever crosses a given location.
Not good for: guaranteed sightings (weather forecasts and orbital elements both change - see Caching below), real-time satellite tracking / live sky-map positions, non-Starlink satellites, moon-phase-aware recommendations (deliberately not modeled), locations inside the polar circles on a day when twilight never crosses the horizon (returns no window for that day - the honest answer, not an error).
Cost: $9/month, 1,000 calls/month.
Every call needs a Gimmee API key - issued per customer - sent one of two ways:
Header: x-api-key: {{GIMMEE_API_KEY}}
Querystring: ?api_key={{GIMMEE_API_KEY}}
Use the header form by default. Read the key from an environment variable or your app's secrets store - never hardcode it into committed source. No upstream (CelesTrak/Open-Meteo) key is needed from you - this DataGrab owns its own upstream credentials internally.
GET https://api.gimmee.io/datagrab/sky/starlink/viewer/{params}
{params} is one or more pipe (|)-delimited segments. The first segment picks the mode - a number means coordinates, anything else means a place name:
| Params | Uses |
|---|---|
{lat}|{lng} |
Coordinates, defaults for everything else |
{lat}|{lng}|{daysAhead}|{minElevationDeg}|{timezone} |
Coordinates with every optional overridden |
{"City, ST"} |
Place name, defaults for everything else |
{"City, ST"}|{daysAhead}|{minElevationDeg}|{timezone} |
Place name with every optional overridden |
lat / lng - decimal degrees. lat must be -90..90, lng must be -180..180.Geo.Location.Resolve DataGrab, so it accepts the same range of input that service does: "Bangor, ME", "Bangor Maine" with no comma, a bare city name ("Bangor"), or a non-US place ("Paris, France"). An unambiguous "City, ST"/"City, State" US form resolves directly; anything else falls through to an LLM-backed resolver on Geo.Location.Resolve's side. Ambiguous or unfindable input raises a 400 - see Error responses.daysAhead - integer, how many days out to search. Default 14. Internally capped to Open-Meteo's 16-day forecast ceiling regardless of what you ask for.minElevationDeg - decimal, the minimum elevation above the horizon a pass must reach to count as visible. Default 30.timezone - an IANA timezone id (e.g. America/New_York). Optional in both modes - if omitted, it's resolved automatically from the coordinates. Affects the local times in the response and an unsociable-hour penalty in scoring, so an explicit value only matters if you want a different timezone's clock time than the one the coordinates would naturally resolve to.URL-encode the whole {params} value as one unit (encodeURIComponent) - the | and , characters, and the space in a place name, are not safe unencoded in a URL path.
curl "https://api.gimmee.io/datagrab/sky/starlink/viewer/44.8016%7C-68.7712" \
-H "x-api-key: YOUR_GIMMEE_KEY"
const params = "Bangor, ME|14|30"; // "Bangor ME" (no comma) and "44.8016|-68.7712" also work
const response = await fetch(
`https://api.gimmee.io/datagrab/sky/starlink/viewer/${encodeURIComponent(params)}`,
{ headers: { "x-api-key": process.env.GIMMEE_API_KEY } }
);
const data = await response.json();
if (data.isVisibleWithinRange) {
console.log(`Best viewing: ${data.recommendedViewing.date} at ${data.recommendedViewing.peakTime} (score ${data.recommendedViewing.score})`);
} else {
console.log(data.summary, data.whyNot);
}
import os, requests
from urllib.parse import quote
params = "Bangor, ME|14|30" # "Bangor ME" (no comma) and "44.8016|-68.7712" also work
response = requests.get(
f"https://api.gimmee.io/datagrab/sky/starlink/viewer/{quote(params, safe='')}",
headers={"x-api-key": os.environ["GIMMEE_API_KEY"]}
)
data = response.json()
JSON field names are camelCase - verified by serializing a populated response and reading the
output, not assumed from the property names alone (see CLAUDE.md, "Fixed 2026-09-27": this DataGrab
goes live today, so there was no prior caller and no break to manage).
{
"query": { "lat": 44.8016, "lng": -68.7712, "timezone": "America/New_York", "daysAhead": 14 },
"isVisibleWithinRange": true,
"recommendedViewing": {
"date": "2026-08-12",
"startTime": "2026-08-12T20:44:12-04:00",
"peakTime": "2026-08-12T20:56:03-04:00",
"endTime": "2026-08-12T21:03:47-04:00",
"score": 8.3,
"conditions": "excellent",
"forecastConfidence": "high",
"trainId": "2026-177-1",
"launchDesignator": "2026-177",
"summary": "Pass reaches 85° elevation. 5 satellites visible at once, arriving as a tight train. Skies are clear."
},
"why": ["Pass reaches 85° elevation", "5 satellites visible at once, arriving as a tight train", "Skies are clear"],
"pass": {
"direction": "SW -> ENE",
"maxElevationDeg": 85,
"satellitesVisibleAtOnce": 5,
"clusterSize": 6,
"medianGapSeconds": 1.8,
"trainType": "tight",
"isDenseTrain": true,
"isTightTrain": true,
"estimatedMagnitude": 2.6,
"riseElevationDeg": 10,
"setElevationDeg": 10,
"riseAzimuthDeg": 228,
"peakAzimuthDeg": 270,
"setAzimuthDeg": 62,
"durationSeconds": 415
},
"weather": { "cloudCoverPercent": 5.0, "visibilityMeters": 24000, "weatherCode": 1 },
"upcomingCandidates": [
{
"date": "2026-08-19",
"startTime": "2026-08-19T20:10:02-04:00",
"peakTime": "2026-08-19T20:25:41-04:00",
"endTime": "2026-08-19T20:38:15-04:00",
"score": 4.9,
"conditions": "fair",
"isViewable": true,
"forecastConfidence": "low",
"trainId": "2026-181-1",
"launchDesignator": "2026-181",
"direction": "W -> NE",
"maxElevationDeg": 42,
"satellitesVisibleAtOnce": 24,
"trainType": "tight",
"isDenseTrain": true,
"isTightTrain": true,
"estimatedMagnitude": 3.9,
"cloudCoverPercent": 30.0,
"reason": "Pass reaches 42° elevation. 24 satellites visible at once, arriving as a tight train."
}
],
"metadata": { "generatedAtUtc": "2026-08-12T00:12:00Z", "sources": ["CelesTrak", "Open-Meteo"] },
"summary": null,
"whyNot": null,
"nextViewing": { "...": "same shape as an upcomingCandidates entry - see below" },
"denseTrainsInOrbit": 2,
"trainsInOrbit": [
{
"trainId": "2026-177-1",
"launchDesignator": "2026-177",
"satelliteCount": 6,
"medianGapSeconds": 1.8,
"spanSeconds": 285.0,
"altitudeKm": 350,
"inclinationDeg": 53.2,
"visibleFromHere": true,
"nextPassLocal": "2026-08-12T20:56:03-04:00",
"nextPassUtc": "2026-08-13T00:56:03Z",
"nextPassScore": 8.3,
"nextPassSatellitesVisible": 5
}
],
"launchCadence": {
"launchesPerWeek": 1.8,
"daysBetweenLaunches": 3.9,
"launchesInWindow": 12,
"windowDays": 30,
"windowEndsUtc": "2026-08-12T00:12:00Z",
"note": "12 Starlink launches in the last 30 days"
},
"dataProvidedBy": "Gimmee.info (CelesTrak, Open-Meteo)",
"termsOfServiceUrl": "",
"dataPullDateTime": "2026-08-12T00:12:00.1234567Z",
"gimmeeVersion": "v1.0.0"
}
| Field | Type | Meaning |
|---|---|---|
query |
object | Echoes back the resolved lat/lng/timezone/daysAhead actually used - useful to confirm what a place-name input resolved to. |
isVisibleWithinRange |
boolean | Read this first. true only when a real train, dark skies, and clear weather all cleared the bar together - see What this does. When false, recommendedViewing/pass/weather are all null; read summary and whyNot instead. |
recommendedViewing |
object | null | The single best recommended pass. null when isVisibleWithinRange is false. |
recommendedViewing.score |
number | 0-10. 8.0 is the bar a pass must clear to be recommended at all - see pass.trainType/isDenseTrain below for why a high-scoring lone satellite is never recommended even above that. |
recommendedViewing.conditions |
string | "excellent" (≥8), "good" (≥6), "fair" (≥4), "poor" (>0), "not viewable" (0 - either fully clouded out or scored zero from being outside sociable hours; not distinguishable from this field alone). |
recommendedViewing.forecastConfidence |
string | "high" (pass is ≤3 days out), "medium" (4-7 days), "low" (8+ days) - weather this far out is a rough guess, and Open-Meteo's own forecast will keep changing until the pass gets close. |
recommendedViewing.trainId / launchDesignator |
string | trainId (e.g. "2026-177-1") is the specific procession; launchDesignator (e.g. "2026-177") is the launch it came from. One launch commonly splits into several separately-crossing trains sharing a launchDesignator - use trainId to tell them apart. |
recommendedViewing.summary |
string | One-line prose summary of the recommended pass. This is where the summary lives on the success path - the top-level summary field is null here (see below). |
why |
string[] | null | The reasons the recommended pass was recommended, same content as recommendedViewing.summary joined by ". " but as a list. null when nothing was recommended. |
pass |
object | null | Rich detail on the recommended pass - direction, elevation, brightness, train shape. null when nothing was recommended. |
pass.satellitesVisibleAtOnce |
int | Measured, not assumed - the most satellites actually above the horizon at the same instant during the pass. |
pass.trainType |
string | "tight" - the classic single-file look, the whole string crossing within 90s. "loose" - 5+ satellites within 5 minutes, fanned across neighbouring ground tracks and culminating at different heights rather than in a line - still worth going outside for, just not single-file. "none" - neither. isDenseTrain/isTightTrain are the same two gates as booleans; every tight train is also a dense train, so these three fields can never disagree. |
pass.estimatedMagnitude |
number | Estimated naked-eye brightness at the brightest moment of the pass. Lower is brighter (~0 very bright, 6 about the limit of what's visible). Literature-calibrated, not measured against real observation logs yet - treat as accurate to roughly ±0.7 mag. |
weather |
object | null | The forecast that drove the recommended pass's score. null when nothing was recommended - weather for the best-available-but-not-recommended pass is on that entry inside upcomingCandidates instead, as cloudCoverPercent. |
upcomingCandidates[] |
object[] | Up to 5 other passes in score order, whether or not anything was recommended. Excludes whichever pass became recommendedViewing when there is one. Shown so a caller can see what came close and why - a low score/isViewable: false entry is not a bug, it's an explained rejection. |
upcomingCandidates[].isViewable |
boolean | Whether this candidate alone clears the "worth going outside for" score floor (4.0) - a coarser, single-pass version of the whole-response isVisibleWithinRange gate, which additionally requires a real train. |
upcomingCandidates[].reason |
string | Why this pass scored the way it did, prose form. |
summary |
string | null | One-line explanation of the overall result. Only populated when isVisibleWithinRange is false (e.g. "No Starlink train worth going outside for was found in the next 14 days."). On the success path this is null - use recommendedViewing.summary instead. |
whyNot |
string[] | null | Populated only when isVisibleWithinRange is false - specific reasons (too few satellites, too spread out, too cloudy, too low) drawn from the best actual pass found, not just the top-scoring one. null on the success path. |
nextViewing |
object | null | The soonest upcoming pass chronologically, regardless of quality - same shape as an upcomingCandidates entry. Unlike recommendedViewing, this is never score-gated, so you always know when the very next pass is even when it's a poor one. null only when there are no passes at all in the requested window. |
denseTrainsInOrbit |
int | How many dense trains currently exist in orbit, anywhere on Earth - independent of whether any of them cross your location. |
trainsInOrbit[] |
object[] | One entry per dense train currently in orbit, wherever it flies. Reported even when it never crosses your location - real trains are rare and short-lived, so "one exists, but not visible from here" is itself useful information. |
trainsInOrbit[].visibleFromHere |
boolean | true only if this train still has a pass ahead of your location (past passes don't count). When true, nextPassLocal/nextPassUtc/nextPassScore/nextPassSatellitesVisible describe it - a true here does not by itself mean the pass is good enough to appear in recommendedViewing or upcomingCandidates, since those are ranked by score and capped; these nextPass* fields are what tells the whole story for a given train. |
launchCadence |
object | null | How frequently new Starlink launches (and therefore new trains) are appearing recently, measured from actual launch dates. Occasionally null if the underlying catalog lookup failed - never blocks the rest of the response. |
dataProvidedBy |
string | Attribution for the upstream data sources. |
termsOfServiceUrl |
string | Empty for this DataGrab - no single upstream ToS applies. |
dataPullDateTime |
string (ISO 8601 UTC) | When this DataGrab pulled the data. |
gimmeeVersion |
string | This DataGrab's own contract version tag. |
| Status | Meaning | When it happens |
|---|---|---|
| 400 | Bad request | Missing lat|lng/place name, lat/lng out of range, or (in place-name mode) the location couldn't be resolved to coordinates at all - a typo, a fictional place, or something too ambiguous even for Geo.Location.Resolve's LLM fallback to settle. |
| 401 | Unauthorized | Missing or invalid x-api-key. |
| 429 | Quota exceeded | Monthly 1,000-call quota used up - resets on your billing cycle date. |
| 502 | Upstream failure | CelesTrak, Open-Meteo, or the internal Geo.Location.Resolve call failed or was unreachable. Weather failures are retried internally (3x, 1s/2s backoff) before this surfaces - a 502 means that already didn't help. Retry shortly. |
isVisibleWithinRange: false. This is deliberate: a bright lone satellite overhead scores well but is not a train, and is never substituted in as a fallback recommendation. Build your UI around "nothing right now, here's why, here's the next candidate anyway" rather than assuming a hit every call.Geo.Location.Resolve DataGrab internally to turn a place name into coordinates - that nested call doesn't consume a separate quota slot or bill you again; it's part of the one call you made here. Sending coordinates directly skips that extra hop entirely if you already have them.trainId vs launchDesignator: a single Starlink launch commonly splits into multiple separately-crossing trains. If you're deduplicating "have I already told the user about this launch tonight", key on launchDesignator; if you're deduplicating "is this the same train that crosses at 8pm and again at 9pm", key on trainId.estimatedMagnitude as a useful ranking signal, not a precise prediction.messageType: Sky.Starlink.Viewer
status: production
price: paid, $9/mo
quota: 1000/month
lastVerified: 2026-09-27
notes: place-name input resolved via the internal Geo.Location.Resolve DataGrab (nested call, not separately billed)
gimmeeVersion: v1.0.0 - first live release (2026-09-27); camelCase JSON verified, see "Response shape"