A witty, high-performance API that provides "Cities Skylines" style narratives for recent SpaceX launches. It uses the xAI Grok API to transform technical launch data into dry, humorous descriptions suitable for scrolling tickers, dashboards, and monitoring tools.
The API is live and can be accessed at: https://launch-narrative-api-dafccc521fb8.herokuapp.com/
Returns a chronological list (newest first) of witty descriptions for recent SpaceX launches.
- Endpoint:
GET /recent_launches_narratives - Parameters:
force=true(optional, bypasses cache) - Response Format: JSON
- Fields:
descriptions: (array) List of witty strings in "month/day HHMM: description" format.last_updated: (string) ISO8601 timestamp of when the narratives were last generated.
- Sample Response:
{
"descriptions": [
"01/04 1200: Falcon 9 launches Starlink 12-1 from SLC-40; booster B1080 nails the landing on A Shortfall of Gravitas, mission nominal.",
"12/30 1530: Falcon 9 lofted O3b mPOWER 7 & 8 from SLC-40; B1078 completes 12th flight, orbital insertion confirmed."
],
"last_updated": "2026-01-04T15:00:00.000000+00:00"
}- Caching: Results are cached for 1 hour. The API uses incremental generation to append new launches without changing existing witty descriptions.
Returns structured upcoming and previous SpaceX launches. Default payload is slim (no all_data): the same convenience fields spacex-dashboard already keeps after pop('all_data') — mission, net, pad, video_url, next-launch trajectory_data, etc.
This is the production cutover: default /launches drops the ~19MB raw LL blobs. LaunchBuddy fetchLaunchesFull should call GET /launches?full=true (or use /launch_details/{id} / /launch_raw/{id} for one launch).
- Endpoint:
GET /launches - Parameters:
force=true(optional) — refresh the list cachefull=true/include_raw=true(optional) — restore the legacy giant payload withall_dataon every launchslim=true(optional) — explicit alias for the default slim shape
- Response Format: JSON
- Fields:
upcoming: (array) List of upcoming launch objects.previous: (array) List of historical launch objects.last_updated: (string) ISO8601 timestamp of when the launch data was last fetched.
- Fields (Launch Object - Top Level):
id: (string) Unique UUID for the launch.name: (string) Full name of the mission.mission: (string) Same asname(dashboard fast-path field).net,date,time,status,rocket,orbit,padvideo_url,x_video_urltrajectory_data: (object, optional) Modeled ground track for the next launch. Ascent follows the pad and an inclination (51.6° for ISS crew/cargo, otherwise the orbit family, or a figure stated in the mission text). It is not a Flight Club or webcast telemetry path.trajectory: (array) Ascent ground track. Same azimuth as the orbit, shortened to the ground range of a linear speed ramp from rest to circular velocity (about half an orbital-rate coast).orbit_path: (array) List of{lat, lon, r}points for the rest of one modeled orbit.booster_trajectory: (array) A piece of that same ground track. RTLS runs from staging back to the published landing coordinate. A droneship runs from staging out to the published downrange. Ocean splashdowns with no downrange are omitted. This is not a pad-to-pad marker and not a telemetry replay.booster_ground_track: (string or null)along_track_return,along_track_downrange, or null.landing_site: (object or null) Published landing latitude/longitude when Launch Library has them.inclination_deg: (number) Inclination used for the ground track.sep_idx: (int or null)0whenbooster_trajectoryis a standalone leg.launch_site: (object) Coordinates and name of the launch site.landing_location: (string) Name of the landing zone or droneship.landing_type: (string) Type of landing (ASDS, RTLS, Ocean, etc.).landing_latitude,landing_longitude,landing_downrange_km: published landing geometry from Launch Library, when present.orbit: (string) Normalized orbit type (LEO-Equatorial, LEO-Polar, GTO, etc.).mission: (string) Mission name.pad: (string) Full name of the launch pad.
all_datais omitted by default. Use?full=trueorGET /launch_details/{id}.
- Caching: 10 minutes. List cache is slim;
all_datais hydrated only for?full=truefromlaunch_raw_v2:{id}.
Same slim launch list as default GET /launches. Field names and Swift-safe values are unchanged.
- Endpoint:
GET /launches_slim - Parameters:
force=true(optional) - Response Format: JSON
- Fields: Same as default
/launches(noall_data; trajectory kept on the next launch). - Caching: 10 minutes.
Returns the full, unpruned Launch Library record for a specific launch from the side store, leftover cache, or a single-launch LL fetch.
- Endpoint:
GET /launch_raw/{launch_id} - Parameters:
hot=1(optional) — for the current/next launch only, serve a short-lived (~20s) stale-while-revalidate cache or one single-flight Launch Library GET. Other launch ids ignorehotand use the normal side store. Withouthot, behavior is unchanged (side-store leftover, which can be as stale as the 10-minute list refresh).
- Response Format: JSON
- Sample Response: (Large nested JSON object)
- Also:
GET /launch_details/{launch_id}always fetches the current LL record (authenticated with the same LL token as the list fetch).
Returns parsed METAR weather data for SpaceX launch and development sites (Starbase, Vandy, Cape, Hawthorne, Bastrop), enhanced with high-frequency live wind data from the National Weather Service (NWS) API.
- Endpoint:
GET /weather/{location}orGET /weather_all - Parameters:
force=true(optional) - Response Format: JSON
- Fields:
- For
/weather/{location}: A single weather object (see below). - For
/weather_all: A dictionary mapping location names to weather objects, plus a globallast_updatedfield. - Weather Object Fields:
temperature_c: (int) Temperature in Celsius.temperature_f: (float) Temperature in Fahrenheit.dewpoint_c: (int) Dewpoint in Celsius.dewpoint_f: (float) Dewpoint in Fahrenheit.humidity: (int) Relative humidity percentage.wind_speed_kts: (int) Wind speed in knots (from METAR).wind_gust_kts: (int) Wind gust speed in knots (0 if none).wind_direction: (int) Wind direction in degrees.live_wind: (object, optional) High-frequency data from NWS observations.speed_kts: (float) Real-time wind speed.gust_kts: (float) Real-time wind gust.direction: (int) Real-time wind direction.timestamp: (string) Observation time.source: (string) "NWS Real-time".
visibility_sm: (float) Visibility in statute miles.altimeter_inhg: (float) Altimeter setting in inches of mercury.cloud_cover: (int) Percentage of cloud cover estimation.flight_category: (string) Estimated flight category (VFR, MVFR, IFR, LIFR).raw: (string) Raw METAR string from the weather service.forecast: (object) 7-day forecast data from Open-Meteo.daily: (object) Daily forecast includingtime,temperature_2m_max,temperature_2m_min, andweathercode.hourly: (object) Hourly data includingtime,temperature_2m,windspeed_10m, andwinddirection_10m.
last_updated: (string) ISO8601 timestamp of the weather fetch.
- For
- Sample Response (
/weather_all):
{
"weather": {
"Starbase": {
"temperature_c": 18,
"temperature_f": 64.4,
"dewpoint_c": 14,
"dewpoint_f": 57.2,
"humidity": 77,
"wind_speed_kts": 8,
"wind_gust_kts": 0,
"wind_direction": 160,
"visibility_sm": 10.0,
"altimeter_inhg": 30.12,
"cloud_cover": 25,
"flight_category": "VFR",
"raw": "KBRO 041453Z 16008KT 10SM FEW025 18/14 A3012 RMK AO2 SLP198 T01830139",
"forecast": {
"daily": {
"time": ["2026-01-04", "2026-01-05", "..."],
"temperature_2m_max": [22.5, 23.1, "..."],
"temperature_2m_min": [15.2, 14.8, "..."],
"weathercode": [0, 1, "..."]
},
"hourly": {
"time": ["2026-01-04T00:00", "2026-01-04T01:00", "..."],
"temperature_2m": [18.5, 18.2, "..."],
"windspeed_10m": [12.5, 11.8, "..."],
"winddirection_10m": [160, 155, "..."]
}
},
"last_updated": "2026-01-04T14:55:00Z"
},
"Vandy": { "temperature_c": 12, "last_updated": "2026-01-04T14:55:00Z" },
"Cape": { "temperature_c": 22, "last_updated": "2026-01-04T14:55:00Z" },
"Hawthorne": { "temperature_c": 19, "last_updated": "2026-01-04T14:55:00Z" },
"Bastrop": { "temperature_c": 28, "last_updated": "2026-01-04T14:55:00Z" }
},
"last_updated": "2026-01-04T14:55:00Z"
}- Caching: 5 minutes. Concurrent refreshes are single-flight + debounced (~20s) so
/weather_all,/dashboard, and the background worker cannot stampede Open-Meteo/METAR.
Returns METAR and 7-day forecast data for any user-provided location.
- Endpoint:
GET /user_weather - Parameters:
lat: (float, required) Latitude of the location.lon: (float, required) Longitude of the location.station_id: (string, optional) ICAO METAR station ID (e.g., "KBRO").
- Response Format: JSON
- Fields: Combined METAR (if
station_idprovided) and Forecast data. - Caching: None. Always fetches fresh data.
Provides real-time and historical performance data, including request counts, cache efficiency, and interactive history.
- Endpoint:
GET /metrics - Parameters:
range=1h(default),24h,7d, or30d - Response Format: JSON
- Fields:
current: (object) Current counters for:total_requests: Total HTTP requests received.cache_hits: Number of requests served from cache.cache_misses: Number of requests that required a backend fetch.api_calls: Number of calls made to external APIs (Grok, Launch Library v2.3.0, and Aviation Weather).
history: (array) List of snapshots containingtimestampanddata(current metrics at that time).hits_per_day: (float) Rolling average of requests projected to a 24-hour period based on the selected range.
- Sample Response:
{
"current": {
"total_requests": 1520,
"cache_hits": 1450,
"cache_misses": 70,
"api_calls": 125
},
"history": [
{
"timestamp": "2026-01-04T14:59:00Z",
"data": {
"total_requests": 1518,
"cache_hits": 1448,
"cache_misses": 70,
"api_calls": 124
}
}
],
"hits_per_day": 36480.0
}Manually triggers the API to poll for new launches and generate new narratives using Grok.
- Endpoint:
POST /refresh - Response Format: JSON
- Fields:
status: (string) Confirmation message.count: (int) Total number of narratives currently in the cache.timestamp: (string) ISO8601 update time.
- Sample Response:
{
"status": "Cache refreshed",
"count": 42,
"timestamp": "2026-01-04T15:00:00Z"
}- Behavior: Incremental. It only generates narratives for launches not already in the cache.
One response with every cloud-side payload the dashboard needs. Device hardware, Wi-Fi, brightness, hostname, and local settings stay on the Pi.
- Endpoint:
GET /dashboard - Parameters:
tz(optional IANA timezone, e.g.America/Chicago)location(optional site name:Starbase,Vandy,Cape,Hawthorne,Bastrop— used to pick timezone iftzis omitted)include_calendar=true(optional, default true)force=true(optional)
- Response Fields:
locations: site coords, timezones, METAR stations, Windy embed URLslaunches: slim upcoming/previous launches (all_datastripped; trajectory kept on the next launch)weather: all sites including Bastropnarratives.descriptions/narratives.prepared: raw ticker strings plus timezone-adjusted rows with launch metadatanext_launch: next T- launch, or in-window T+ launch if nothing is upcomingupcoming: next 10 dated launches withlocal_date/local_timetrends: current-year cumulative + rolling 12-month series for Starship / Falcon 9 / Falcon Heavycalendar:YYYY-MM-DD→ slim launches (omitted ifinclude_calendar=false)trajectory: next-launch globe pathclosest_x_video_url: nearest-in-time X/Twitter webcastlast_updated: per-source timestamps
These are the same fields as /dashboard, split out for clients that already have launch data cached.
- Site metadata:
GET /locations - Next launch:
GET /next_launch?tz=America/Chicago(orlocation=Starbase) - Upcoming list:
GET /upcoming_launches?tz=...&limit=10 - Calendar:
GET /calendar?tz=... - Trends:
GET /launch_trendsorGET /launch_trends?mode=cumulative/mode=rolling - Trajectory:
GET /trajectory(next launch) orGET /trajectory/{launch_id}
- Get Single Launch Details:
GET /launch_details/{launch_id}- Returns the full raw Launch Library record for one launch (on-demand LL fetch). Default
GET /launchesis slim; use?full=truefor the old list-wideall_datapayload. - Sample Response: (Large JSON object containing technical mission/rocket/pad details)
- Returns the full raw Launch Library record for one launch (on-demand LL fetch). Default
- Get External Narratives:
GET /external_narratives- Returns witty descriptions from a secondary narrative source.
- Fields:
descriptions: (array) List of narrative strings.
- Sample Response:
{
"descriptions": [
"01/01 0000: Sample external narrative entry.",
"12/31 2359: Another external sample entry."
]
}Cached CelesTrak General Perturbations (GP / OMM) for the SpaceX dashboard Three.js globe. Pi clients should poll these endpoints so they never hit CelesTrak directly (avoids CORS and CelesTrak rate limits).
These are SGP4 predictions from GP/TLE element sets, not live telemetry. Propagate on the client with satellite.js (twoline2satrec) or an equivalent SGP4 library. CelesTrak asks not to hammer their GP API; this service caches for ~1 hour and keeps a longer stale copy if upstream fails.
- List:
GET /satellites/gp?group=starlink- Parameters:
group(optional, defaultstarlink) — allowlisted CelesTrakGROUP:starlink,stations(ISS / CSS),visual,oneweb,gps-ops,weatherforce=true(optional) — bypass the fresh cache and refetch
- Caching: Fresh for 1 hour (Redis via
set_cached_data/get_cached_data, in-memory fallback). Stale copies are kept ~48 hours and returned immediately; CelesTrak is refreshed in the background and is not on the request path while a copy exists. A cold cache fetches with an ~18s budget so the handler returns JSON before Heroku's ~30s router timeout. Starlink + stations are warmed on startup and refreshed hourly by the background worker. Responses are pre-rendered; clients that sendAccept-Encoding: gzipgetContent-Encoding: gzip(same JSON document).X-Satellite-Cacheishit,stale, ormiss. - Response fields:
group,fetched_at,ttl_seconds,count,stale,source,notesatellites: compact{name, norad_id, tle_line1, tle_line2}(unused OMM keywords omitted so ~6–10k Starlinks stay small)
- Parameters:
- Starlink alias:
GET /satellites/starlink— same payload as/satellites/gp?group=starlink - Metadata only:
GET /satellites/meta?group=starlink—fetched_at,count,group,ttl_seconds,age_seconds,stale,allowed_groups(does not call CelesTrak) - Deployed Starlink (Starship V3 / Group 31):
GET /satellites/deployed- Used by spacex-dashboard
globe.html(ingestDeployedPayload) to recolor this flight's sats inside the existing constellation Points shell. Pi clients proxy this path; they do not call CelesTrak. - Parameters:
launch_date(optional,YYYY-MM-DD) — UTC SATCAT launch date. Omit to use the NET date of the Starship launch whose mission text containsv3orgroup 31-(Flight 14 isStarship | Starlink Group 31-1).force=true(optional) — bypass the fresh cache and refetch SATCAT.
- Source: CelesTrak SATCAT
GROUP=starlink, filtered to thatLAUNCH_DATE, is preferred once it lists the flight. Decayed objects, rocket bodies, and debris are dropped. Remaining payloads are joined to GP TLEs by NORAD (the cached Starlink GP feed, thengp.php?INTDES=, then supplemental GP only for catalog numbers the main set does not have). Same-day Starlink rows are included. No positions are invented. - Before SATCAT catalogs the flight: SpaceX public MEME ephemerides (
https://api.starlink.com/public-files/ephemerides/MANIFEST.txt, files only — not the operator portal). Candidates areSTARLINK-ids in the post-Falcon band (id ≥ 40000) whose ephemeris window covers the launch date, or whose window has rolled forward (starts on or after the launch day) and still covers the current time. Each file's inertial state is interpolated at request time intolat/lon/alt_kmand an osculating TLE so the existing globe propagator can draw it.sourceisspacex-manifest-ephemeris.norad_idis omitted until CelesTrak assigns one;id/nameare the SpaceXSTARLINK-name. When SATCAT later has TLEs for that date, the same endpoint switches back tocelestrak-satcat. - Caching: SATCAT-backed copies are fresh for 1 hour (Redis key
satellites_deployed_v2:{date}, in-memory fallback). Manifest-backed copies and honest empties are fresh for 10 minutes so a newly published ephemeris is not stuck behind an empty SATCAT cache. Stale copies are kept ~48 hours. Raw multi-megabyte files are not stored; only the parsed satellite records are. - Response fields the globe reads:
satellites:{name, norad_id, tle_line1, tle_line2}for SATCAT, or{name, id, lat, lon, alt_km, epoch, tle_line1, tle_line2}for the manifest bridge. Both TLE lines are required;generationisv3on each row when the launch text says so. Rows without both TLE lines are ignored by the globe.catalog_count: SATCAT matches before the TLE join, or the ephemeris count on the manifest path.catalog_count > 0with no TLE lines → dashboardcatalog-no-tle.note: containsunavailableonly when SATCAT could not be fetched and nothing is cached → dashboardsatcat-unavailable. An honest empty catalog does not say unavailable → dashboardawaiting-catalog.source:celestrak-satcatorspacex-manifest-ephemeris.
- Also returned:
launch_date,generation,mission,fetched_at,ttl_seconds,count,stale,empty. - Sample (SATCAT has no rows for the flight date yet):
- Used by spacex-dashboard
{
"launch_date": "2026-09-28",
"generation": "v3",
"mission": "Starship | Starlink Group 31-1 (Starship Flight 14)",
"fetched_at": "2026-09-28T14:00:00Z",
"ttl_seconds": 600,
"count": 0,
"catalog_count": 0,
"stale": false,
"empty": true,
"source": "celestrak-satcat",
"note": "No Starlink SATCAT objects with LAUNCH_DATE 2026-09-28 yet. Positions are SGP4 predictions from GP/TLE element sets, not live telemetry.",
"satellites": []
}- Sample Response (
GET /satellites/gp?group=stations):
{
"group": "stations",
"fetched_at": "2026-09-19T23:00:00Z",
"ttl_seconds": 3600,
"count": 1,
"stale": false,
"source": "celestrak",
"note": "Positions are SGP4 predictions from GP/TLE element sets, not live telemetry.",
"satellites": [
{
"name": "ISS (ZARYA)",
"norad_id": 25544,
"tle_line1": "1 25544U 98067A 26262.30395647 .00006211 00000+0 12007-3 0 9997",
"tle_line2": "2 25544 51.6308 194.2901 0004815 157.3949 202.7252 15.49175317586346"
}
]
}Access the root URL (/) in any web browser to view the API Status Dashboard.
- Tabbed Interface: Switch between Narratives, Launches, and Weather views (Starbase, Vandy, Cape, Hawthorne, Bastrop).
- Real-time Monitoring: Interactive sparkline charts for traffic and efficiency with selectable time ranges (1h, 24h, 7d).
- Live Metrics: View live performance indicators like "Live Hits / Day" and system uptime.
- Detailed Launch Cards: Click any launch to see comprehensive technical data, images, mission descriptions, and raw API responses.
- Refresh Schedule: Visual countdown timers show exactly when each data category is scheduled to refresh.
- Manual Control: Per-tab refresh buttons to bypass cache and fetch fresh data instantly.
curl https://launch-narrative-api-dafccc521fb8.herokuapp.com/recent_launches_narrativesfetch('https://launch-narrative-api-dafccc521fb8.herokuapp.com/recent_launches_narratives')
.then(response => response.json())
.then(data => console.log(data.descriptions));import requests
url = "https://launch-narrative-api-dafccc521fb8.herokuapp.com/recent_launches_narratives"
response = requests.get(url)
launches = response.json().get("descriptions", [])
for launch in launches:
print(launch)The API utilizes a Timer-Based Refresh Strategy to ensure stability and speed:
- Background Refresh: A dedicated worker thread in the backend automatically refreshes the cache for Narratives (15m), Launches (10m), Weather (2m for high-frequency wind), and Starlink / stations GP (1h).
/launchesand/launches_slimstay on this 10-minute list cadence. - Hot raw (opt-in):
GET /launch_raw/{id}?hot=1refreshes only the current/next launch from Launch Library on a ~20s stale-while-revalidate TTL (single-flight per id). This is not a full-list refresh. Other ids, and/launch_raw/{id}withouthot, keep the long-lived side store. - Manual Force: Users can trigger an immediate refresh via the dashboard buttons or by appending
?force=trueto API requests. - Incremental History: Previous launch data is never fully replaced; new launches are appended to the existing historical cache to preserve a continuous record.
- Satellite GP:
/satellites/gp,/satellites/starlink, and/satellites/metaare a CelesTrak proxy with a 1-hour freshness window. A stale Starlink catalog is returned immediately (gzip when the client accepts it) and refreshed in the background, so a cold dyno does not rebuild the catalog on the request./satellites/deployedprefers the Starship V3 SATCAT join, then SpaceX public ephemerides. Element sets are GP/TLE; globe dots are SGP4 predictions, not live positions. An empty deployed list means neither SATCAT nor the manifest had plottable sats for that launch date.
MIT