Sky.Starlink.Viewer - AI Integration Guide

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.


What this does

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.


Authentication

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.


Endpoint

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

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.


Code samples

cURL

curl "https://api.gimmee.io/datagrab/sky/starlink/viewer/44.8016%7C-68.7712" \
  -H "x-api-key: YOUR_GIMMEE_KEY"

JavaScript (fetch)

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);
}

Python (requests)

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()

Response shape

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.

Error responses

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.

Integration notes

Metadata (for the AI's own reference, not for display)

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"