Research and public data

api.nasa.gov

NASA APIs - Scraping & Data Extraction

Notes an AI agent reads before it works on api.nasa.gov: what to open, what to call, what breaks. Written by browser-harness contributors after real runs, and kept on GitHub under the MIT license.

Give it to your agent

Any coding agent

Start the task with this line. The Markdown version is the same notes without the page around them.

Read https://browser-use.com/skills/nasa.md, then <your task>

Browser Harness

Save the files into your workspace and turn domain skills on. The code below calls its helpers, such as http_get and js. What Browser Harness is.

d="${BH_AGENT_WORKSPACE:-$HOME/.config/browser-harness/agent-workspace}/domain-skills/nasa"
mkdir -p "$d" && cd "$d"
curl -sLO https://raw.githubusercontent.com/browser-use/browser-harness/main/agent-workspace/domain-skills/nasa/scraping.md
export BH_DOMAIN_SKILLS=1

https://api.nasa.gov - open NASA data APIs. Never use the browser. All endpoints return JSON via http_get. DEMO_KEY works for low-volume use; register for a free personal key at https://api.nasa.gov/ to raise limits.

Do this first

All api.nasa.gov endpoints share the same rate-limit pool under DEMO_KEY. EPIC and Exoplanet Archive are on separate domains with no rate limit.

import json
from helpers import http_get
 
# Simplest call: today's Astronomy Picture of the Day
apod = json.loads(http_get("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"))
print(apod['date'], apod['title'], apod['media_type'])
# Confirmed output (2026-04-18): 2026-04-18 PanSTARRS and Planets image

Use DEMO_KEY for exploration. Switch to a personal key for any bulk work - DEMO_KEY hits its limit at ~10 req/hour/IP (daily budget around 50; retry-after header will show ~22 hours when exhausted).

Rate limits

Key typeLimitResets
DEMO_KEY10 req/hour, ~50/day per IPHourly window; daily hard stop with retry-after ~22h
Personal key (free)1,000 req/hourHourly window

Rate limit headers on every api.nasa.gov response:

  • X-Ratelimit-Limit - your current window limit (e.g. 10)
  • X-Ratelimit-Remaining - calls left this window
  • Retry-After - seconds until next window (only on 429)

EPIC (epic.gsfc.nasa.gov) and Exoplanet Archive (exoplanetarchive.ipac.caltech.edu) share no rate-limit pool with api.nasa.gov.

Common workflows

APOD - single day

import json
from helpers import http_get
 
apod = json.loads(http_get("https://api.nasa.gov/planetary/apod?api_key=DEMO_KEY"))
print(apod['date'])        # '2026-04-18'
print(apod['title'])       # 'PanSTARRS and Planets'
print(apod['media_type'])  # 'image' or 'video'
print(apod['url'])         # full-res or YouTube embed URL
print(apod['hdurl'])       # HD image URL (absent when media_type='video')
print(apod.get('copyright'))  # None if public domain
# Confirmed output (2026-04-18):
# url:   https://apod.nasa.gov/apod/image/2604/PanstarrsPlanetsPerrotLab1024.jpg
# hdurl: https://apod.nasa.gov/apod/image/2604/PanstarrsPlanetsPerrot.jpg
# copyright: Luc Perrot

APOD - date range (array response)

import json
from helpers import http_get
 
apods = json.loads(http_get(
    "https://api.nasa.gov/planetary/apod"
    "?start_date=2024-01-01&end_date=2024-01-07&api_key=DEMO_KEY"
))
# Returns a list of 7 dicts — same schema as single-day response
for a in apods:
    print(a['date'], a['media_type'], a['title'][:50])
# Confirmed output (7 items):
# 2024-01-01 image NGC 1232: A Grand Design Spiral Galaxy
# 2024-01-02 image Rocket Transits Rippling Moon
# 2024-01-03 image A SAR Arc from New Zealand
# 2024-01-04 image Zeta Oph: Runaway Star
# 2024-01-05 image Trapezium: At the Heart of Orion
# 2024-01-06 video The Snows of Churyumov-Gerasimenko
# 2024-01-07 image The Cat's Eye Nebula in Optical and X-ray

Optional params: date=YYYY-MM-DD (specific day), count=N (N random entries), thumbs=true (include thumbnail_url for video entries).

APOD - random sample

import json
from helpers import http_get
 
apods = json.loads(http_get(
    "https://api.nasa.gov/planetary/apod?count=5&api_key=DEMO_KEY"
))
for a in apods:
    print(a['date'], a['title'][:40])
# Returns 5 random APOD entries — dates can be any day since 1995-06-16

NEO - Near Earth Objects feed

import json
from helpers import http_get
 
data = json.loads(http_get(
    "https://api.nasa.gov/neo/rest/v1/feed"
    "?start_date=2024-01-01&end_date=2024-01-02&api_key=DEMO_KEY"
))
print(data['element_count'])   # 32 (total NEOs across both days)
neos = data['near_earth_objects']  # dict keyed by date string
for date, objects in sorted(neos.items()):
    for neo in objects:
        ca = neo['close_approach_data'][0]
        print(
            neo['name'],
            'hazardous:', neo['is_potentially_hazardous_asteroid'],
            'miss km:', ca['miss_distance']['kilometers'][:12],
            'vel kph:', ca['relative_velocity']['kilometers_per_hour'][:10]
        )
# Confirmed output (2 days, 32 total NEOs):
# 415949 (2001 XY10) hazardous: False miss km: 50452409.34 vel kph: 57205.8951
# (22+ more objects per day)

NEO object fields:

  • id, name, nasa_jpl_url - identity
  • estimated_diameter - dict with kilometers, meters, miles, feet sub-dicts, each with min/max
  • is_potentially_hazardous_asteroid - bool
  • close_approach_data[0] - close_approach_date, miss_distance (au/lunar/km/mi), relative_velocity (km/s, km/h, mph), orbiting_body

Date range is capped at 7 days per request. For longer ranges, paginate with start_date / end_date in 7-day steps. links.next in the response gives the next 7-day window URL.

NEO - single asteroid lookup

import json
from helpers import http_get
 
# Asteroid ID comes from the feed's `id` field
neo = json.loads(http_get(
    "https://api.nasa.gov/neo/rest/v1/neo/2415949?api_key=DEMO_KEY"
))
print(neo['name'])
print(neo['orbital_data']['orbit_class']['orbit_class_description'])
# Full orbital history + all close approaches are in `close_approach_data` (long list)

Mars Rover photos - Curiosity by sol

import json
from helpers import http_get
 
# sol = Martian solar day since landing
data = json.loads(http_get(
    "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos"
    "?sol=1000&api_key=DEMO_KEY"
))
photos = data['photos']
print(f"Photos on sol 1000: {len(photos)}")
p = photos[0]
print(p['earth_date'])          # '2015-05-30'
print(p['img_src'])             # direct JPEG URL
print(p['camera']['name'])      # 'FHAZ'
print(p['camera']['full_name']) # 'Front Hazard Avoidance Camera'
print(p['rover']['name'])       # 'Curiosity'
print(p['rover']['status'])     # 'active'
print(p['rover']['max_sol'])    # highest sol with photos
 
# Filter by camera
data = json.loads(http_get(
    "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos"
    "?sol=1000&camera=navcam&api_key=DEMO_KEY"
))

Available cameras for Curiosity: fhaz, rhaz, mast, chemcam, mahli, mardi, navcam. Other rovers: opportunity, spirit, perseverance.

Use latest_photos to get the most recent available:

data = json.loads(http_get(
    "https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/latest_photos"
    "?api_key=DEMO_KEY"
))
photos = data['latest_photos']

Add &page=N for pagination (25 photos/page by default).

EPIC - Earth Polychromatic Imaging Camera

EPIC images are served from epic.gsfc.nasa.gov - no api_key required, no rate limit.

import json
from helpers import http_get
 
# Latest available images (natural color)
images = json.loads(http_get("https://epic.gsfc.nasa.gov/api/natural"))
print(f"Latest batch: {len(images)} images")  # Confirmed: 4 images on 2026-04-18
 
img = images[0]
print(img['identifier'])               # '20260416162050'
print(img['image'])                    # 'epic_1b_20260416162050'
print(img['date'])                     # '2026-04-16 16:16:01'
print(img['centroid_coordinates'])     # {'lat': 13.25, 'lon': -75.59}
 
# Construct PNG URL from image name + date
date_str = img['date'].split(' ')[0]   # '2026-04-16'
year, month, day = date_str.split('-')
png_url = f"https://epic.gsfc.nasa.gov/archive/natural/{year}/{month}/{day}/png/{img['image']}.png"
jpg_thumb = f"https://epic.gsfc.nasa.gov/archive/natural/{year}/{month}/{day}/thumbs/{img['image']}.jpg"
print(png_url)
# Confirmed: https://epic.gsfc.nasa.gov/archive/natural/2026/04/16/png/epic_1b_20260416162050.png
# Images for a specific date
images = json.loads(http_get("https://epic.gsfc.nasa.gov/api/natural/date/2024-01-15"))
print(len(images))   # 14 images on 2024-01-15
 
# Enhanced (color-corrected) images — same API, different path
enhanced = json.loads(http_get("https://epic.gsfc.nasa.gov/api/enhanced/date/2024-01-15"))
# Enhanced image URL pattern uses 'enhanced' and 'epic_RGB_' prefix:
img = enhanced[0]
date_str = img['date'].split(' ')[0]
year, month, day = date_str.split('-')
url = f"https://epic.gsfc.nasa.gov/archive/enhanced/{year}/{month}/{day}/png/{img['image']}.png"
# e.g. .../archive/enhanced/2024/01/15/png/epic_RGB_20240115005515.png
 
# All available dates
all_dates = json.loads(http_get("https://epic.gsfc.nasa.gov/api/natural/all"))
print(f"Available dates: {len(all_dates)}")  # 3477 dates (2015-06-13 to present)
print(all_dates[0])   # {'date': '2026-04-16'}  (newest first)
print(all_dates[-1])  # {'date': '2015-06-13'}  (oldest)

Exoplanet Archive - TAP/ADQL queries

No API key or rate limit. SQL-like ADQL queries over the full archive.

import json
from helpers import http_get
 
# Short-period planets with known radii
planets = json.loads(http_get(
    "https://exoplanetarchive.ipac.caltech.edu/TAP/sync"
    "?query=select+pl_name,hostname,pl_orbper+from+ps+where+pl_orbper+%3C+10"
    "&format=json"
))
print(f"Rows: {len(planets)}")      # 17675 (table 'ps' includes duplicate measurements)
print(planets[0])
# {'pl_name': 'GJ 1214 b', 'hostname': 'GJ 1214', 'pl_orbper': 1.58040482}
# Use 'pscomppars' for one row per planet (composite best-estimate params)
planets = json.loads(http_get(
    "https://exoplanetarchive.ipac.caltech.edu/TAP/sync"
    "?query=select+pl_name,hostname,disc_year,discoverymethod,pl_orbper,pl_rade,pl_masse,pl_eqt,sy_dist"
    "+from+pscomppars+where+disc_year+%3E+2020+and+pl_rade+is+not+null"
    "+order+by+disc_year+desc"
    "&format=json&maxrec=5"
))
for p in planets:
    print(p['pl_name'], p['disc_year'], p['discoverymethod'], f"r={p['pl_rade']}Re")
# Confirmed output:
# KMT-2024-BLG-1870L b 2026 Microlensing r=13.8Re
# LHS 1903 b 2026 Transit r=1.382Re
# TOI-375 d 2026 Radial Velocity r=13.6Re

Key tables:

  • ps - all measurements per planet (multiple rows per planet, all sources)
  • pscomppars - one row per confirmed planet (best composite parameters)

Key columns: pl_name, hostname, disc_year, discoverymethod, pl_orbper (orbital period, days), pl_rade (radius in Earth radii), pl_masse (mass in Earth masses), pl_eqt (equilibrium temp K), sy_dist (distance in parsec).

URL-encode operators: < = %3C, > = %3E, spaces = +.

URL reference

api.nasa.gov endpoints

EndpointURL pattern
APOD todayhttps://api.nasa.gov/planetary/apod?api_key=KEY
APOD by date...&date=YYYY-MM-DD
APOD range...&start_date=YYYY-MM-DD&end_date=YYYY-MM-DD
APOD random N...&count=N
NEO feedhttps://api.nasa.gov/neo/rest/v1/feed?start_date=...&end_date=...&api_key=KEY
NEO by IDhttps://api.nasa.gov/neo/rest/v1/neo/{id}?api_key=KEY
Mars photos by solhttps://api.nasa.gov/mars-photos/api/v1/rovers/{rover}/photos?sol=N&api_key=KEY
Mars photos by date...?earth_date=YYYY-MM-DD&api_key=KEY
Mars latesthttps://api.nasa.gov/mars-photos/api/v1/rovers/{rover}/latest_photos?api_key=KEY

EPIC (epic.gsfc.nasa.gov - no key, no rate limit)

EndpointURL
Latest natural imageshttps://epic.gsfc.nasa.gov/api/natural
Natural by datehttps://epic.gsfc.nasa.gov/api/natural/date/YYYY-MM-DD
Enhanced latesthttps://epic.gsfc.nasa.gov/api/enhanced
Enhanced by datehttps://epic.gsfc.nasa.gov/api/enhanced/date/YYYY-MM-DD
All available dateshttps://epic.gsfc.nasa.gov/api/natural/all
PNG imagehttps://epic.gsfc.nasa.gov/archive/natural/YYYY/MM/DD/png/{image}.png
Thumbnail (JPEG)https://epic.gsfc.nasa.gov/archive/natural/YYYY/MM/DD/thumbs/{image}.jpg
Enhanced PNGhttps://epic.gsfc.nasa.gov/archive/enhanced/YYYY/MM/DD/png/{image}.png

Exoplanet Archive (no key, no rate limit)

https://exoplanetarchive.ipac.caltech.edu/TAP/sync?query=<ADQL>&format=json&maxrec=<N>

Gotchas

  • DEMO_KEY limit is effectively 10/hour per IP, not 30 - The X-Ratelimit-Limit header shows 10 in practice. When the daily budget (~50 req) is exhausted, the retry-after header is set to ~80,000 seconds (about 22 hours). Register a free personal key at https://api.nasa.gov/ to get 1,000/hour.

  • All api.nasa.gov paths share one rate-limit pool - APOD, NEO, Mars Rover, and all other api.nasa.gov paths draw from the same DEMO_KEY bucket. Calling any one of them depletes the limit for all others.

  • EPIC and Exoplanet Archive are fully free - epic.gsfc.nasa.gov returns no rate-limit headers and is not throttled. exoplanetarchive.ipac.caltech.edu/TAP/sync is similarly unrestricted. Use these freely without fear of exhausting DEMO_KEY.

  • NEO date range max is 7 days - Requests spanning more than 7 days return HTTP 400. Paginate with 7-day windows and use links.next from the response to get the next URL.

  • APOD earliest date is 1995-06-16 - Requesting date before 1995-06-16 returns HTTP 400 with an error message. No upper date bound other than today.

  • APOD hdurl is absent for video entries - When media_type is video, the response has url (a YouTube embed URL) but no hdurl. Always check media_type before accessing hdurl.

  • Mars Rover sol vs earth_date - Both are valid filter params. sol is the Martian solar day since rover landing. earth_date uses YYYY-MM-DD. You cannot mix them in one request.

  • Mars Rover pagination defaults to 25 photos/page - Large sols (Curiosity sol 1000 has many photos) require &page=2, &page=3, etc. There is no total count in the response; keep paginating until you get an empty photos list.

  • EPIC image name encodes type in the prefix - Natural images use epic_1b_ prefix; enhanced color-corrected images use epic_RGB_ prefix. The API returns the correct filename in img['image']; don't guess the prefix.

  • EPIC /api/natural/all returns newest-first - The list of 3,477+ available dates starts from today and goes back to 2015-06-13. Not all days have images (gaps during spacecraft maintenance).

  • Exoplanet ps table has multiple rows per planet - Different publications report different measurements for the same planet. Use pscomppars for one-row-per-planet composite parameters. ps is useful when you need all reported values or want to filter by specific reference.

  • Exoplanet null values come back as None in JSON - Many fields like pl_masse are null for planets without mass measurements. Always guard with if row['pl_masse'] is not None.

  • http_get in helpers.py uses stdlib urllib - On some macOS Python 3.11 installs, SSL certificate verification fails (CERTIFICATE_VERIFY_FAILED). If you hit this, run curl via subprocess as a fallback, or install certifi and patch the default SSL context. The harness's browser CDP connection is not affected; only http_get is.

scraping.md · updated Apr 28, 2026 · 1,727 words

Sites change. These notes describe what worked when they were last updated, not a guarantee they still do. If something broke, the fix is a pull request to browser-harness.