Purpose
Search NASA's Image and Video Library for media (images, videos, audio) matching a free-text query, filters (center, keywords, photographer, year range, location, album), or a known NASA asset ID, and return each match's title, description, NASA ID, date, capture center, media type, and direct URLs to every rendition (thumb/small/medium/large/original for images; mp4/mov/srt for videos). Read-only — never uploads, edits, or comments.
When to Use
- Find photos / videos for a topic (e.g. "Apollo 11", "Mars Perseverance", "Hubble nebula") for use in articles, slide decks, or downstream image-analysis pipelines.
- Resolve a known
nasa_id(e.g.PIA23591,as11-40-5874) to its full set of file renditions and EXIF metadata. - Enumerate an entire NASA album (e.g.
Mars_2020_Perseverance) page by page. - Bulk-build image datasets filtered by capture center (JSC, JPL, KSC, GSFC, HQ, …), photographer, or year range.
- Anywhere you'd otherwise scrape
images.nasa.gov— the public JSON API is faster, cheaper, fully unauthenticated, and structurally more reliable.
Workflow
The NASA Image and Video Library web UI at https://images.nasa.gov is a thin React SPA over a fully public JSON API at https://images-api.nasa.gov. No API key, no auth, no cookies, no Referer header, no User-Agent gating, no stealth session, and no residential proxy. A plain HTTPS GET from any HTTP client works. Lead with the API; only fall back to the browser UI if you specifically need to capture an on-page screenshot of the search-results page itself. The browser path is ~100× more expensive for the same data because the SPA hydrates the same JSON endpoint behind a fully JS-rendered page (a browserless_agent snapshot of /search?q=... returns 0 listing refs).
Build the search URL. Base path:
https://images-api.nasa.gov/search. Compose any combination of these query params (at least one filter is required — see gotcha below):Param Meaning Notes qFree-text query Searches title + description + keywords. Space → %20or+.media_typeimage,video,audio, or comma-separated liste.g. media_type=image,video. Omit to search all types.centerNASA capture center Three-letter code: JSC,JPL,KSC,GSFC,HQ,MSFC,LRC,ARC,AFRC,SSC,GRC. Case-sensitive uppercase.keywordsComma-separated keyword list Matches against data[0].keywords.locationCapture location Free text, e.g. Kennedy Space Center. Matches againstdata[0].location.photographerPrimary photographer e.g. NASA/Bill Ingalls. URL-encode the/.secondary_creatorCredit / agency e.g. NASA/JPL-Caltech.titleTitle-only substring Narrower than q.descriptionDescription substring description_508508-compliant alt-text substring nasa_idExact asset ID lookup Single result. Equivalent to /asset/{nasa_id}for the metadata block only.year_start,year_endCapture-year bounds Inclusive, four-digit years (e.g. year_start=2020&year_end=2023). Filters againstdate_created.page1-based page number Default 1.page_sizeResults per page Default 100. Server accepts up to several hundred; when the fetch is routed throughbrowserless_functionthe practical ceiling is ~600 because a 1 MB response cap kicks in aroundpage_size=700(502 "response body exceeded 1MB"), andbrowserless_function's own text return is capped (~200k chars) — project/summarize the items inside the eval, never return the raw payload. Use 100 by default; raise only when you need fewer round-trips and have confirmed the response stays under 1 MB.Issue the GET — a plain HTTPS GET from any client:
GET https://images-api.nasa.gov/search?q=apollo%2011&media_type=image&page_size=100Under restricted egress, route it through
browserless_function(it runs in a browser page context, so a barefetchhas no egress until the page navigates):page.goto('https://images-api.nasa.gov/')first, thenpage.evaluate(async () => fetch('/search?q=apollo%2011&media_type=image&page_size=100').then(r => r.json()))(same-origin), and project the fields you need before returning (the ~200k-char return cap). Response is JSON. The top-level shape is the NASA Collection+JSON envelope:{ "collection": { "version": "1.1", "href": "<echoed request URL>", "items": [/* see step 3 */], "metadata": { "total_hits": 5881 }, "links": [{ "rel": "next", "prompt": "Next", "href": "<next page URL>" }] } }Read
collection.metadata.total_hitsfor the result count andcollection.links[?rel==next].hrefto drive pagination (see step 5).Decode each item.
collection.items[]is the result array. Each element has exactly three keys:href— URL to the per-asset manifest (https://images-assets.nasa.gov/{type}/{nasa_id}/collection.json). This is the same payload/asset/{nasa_id}returns and lists every file rendition (originals + thumbnails + metadata.json + captions.srt).data— single-element array;data[0]is the metadata object:Only{ "nasa_id": "PIA23591", "title": "Seeing the Mars 2020 Rover Off", "description": "On Feb. 11, 2020, ...", "description_508": "On Feb. 11, 2020, ...", // alt-text variant, may be missing "media_type": "image", // or "video" / "audio" "center": "JPL", "date_created": "2020-02-12T00:00:00Z", "keywords": ["Mars 2020 Rover"], // may be missing "album": ["Mars_2020_Perseverance"], // may be missing "location": "Kennedy Space Center", // may be missing "photographer": "NASA/Aubrey Gemignani", // may be missing "secondary_creator": "NASA/JPL-Caltech" // may be missing }nasa_id,title,media_type,center,date_createdare guaranteed present. Everything else is optional — guard withdata[0].get(key, default).links— array of file-rendition URLs previewable on the search-results card. Formedia_type=imageitems this is the full image set (thumb,small,medium,large,orig); formedia_type=videoitems this contains only thumbnail JPEGs + a.srtcaptions link — to enumerate the actual.mp4/.movvideo files you must call/asset/{nasa_id}(step 4). Each link looks like:The rendition suffix follows the pattern{ "href": "https://images-assets.nasa.gov/image/PIA23591/PIA23591~medium.jpg", "rel": "alternate", // or "preview" (thumb), "canonical" (orig), "captions" (.srt) "render": "image", // null for captions "width": 1280, "height": 916, "size": 112000 }{nasa_id}~{thumb|small|medium|large|orig}.{jpg|tif|png}. The~origfile may be.jpg,.png,.tif, or.tiffdepending on what was uploaded — read thehref, don't assume.jpg.
(Optional) Resolve full asset manifest for video files or EXIF metadata — another plain GET (or the same
browserless_functionsame-originfetchunder restricted egress):GET https://images-api.nasa.gov/asset/PIA23591Returns the same envelope but
items[]is now one entry per file (all renditions + ametadata.jsonentry). Use this to:- Get the original-resolution video file URL (
.mp4/.mov) formedia_type=videoitems. - Get the path to the full EXIF + AVAIL metadata JSON (the entry whose
hrefends in/metadata.json). - Get the captions
.srtURL.
Companion lookup endpoints:
https://images-api.nasa.gov/metadata/{nasa_id}— returns{"location": "<S3 URL>"}redirector. Fetch thelocationURL to get the full EXIF block (camera make/model, dimensions, allAVAIL:*fields).https://images-api.nasa.gov/captions/{nasa_id}— same redirector pattern, points to the.srtfile for videos.https://images-api.nasa.gov/album/{album_name}— returns the samecollectionenvelope filtered to one album. Album names are internal collection IDs likeMars_2020_PerseveranceorKSC_50th_Anniversary, not human-readable slugs — discover them by first running a search and readingitems[].data[0].album[].
- Get the original-resolution video file URL (
Paginate when
total_hits > page_size. Two equivalent paths:- Follow the server-provided next URL:
collection.links[?rel==next].href(already includes the incrementedpage). - Or increment
pageyourself:&page=2&page_size=100. Pages are 1-indexed.
When the response no longer contains a
links[].rel == "next"entry you've reached the last page. Stop atpage * page_size >= total_hitsas a safety bound.- Follow the server-provided next URL:
Build the public web URL for a result (for citing back to a user-facing page):
https://images.nasa.gov/details/{nasa_id}This is the SPA detail page. The API returns no direct field for it — just construct from
nasa_id. The URL renders for any valid NASA ID.
Browser fallback
Use only when the JSON API is unreachable (extremely rare — no documented downtime in our trace, no rate-limit at 10 req/s) or when you specifically need a screenshot of the rendered search page. Drive it with a single browserless_agent call — no stealth, no proxy required (images.nasa.gov serves anonymous traffic without any anti-bot challenge). There is no session-release step, and nothing to release. Batching the whole flow (nav → hydrate → extract) in one call's commands array saves round-trips. (The session persists across calls, keyed by the session config; it is not torn down on return.) Steps for the commands array:
{ "method": "goto", "params": { "url": "https://images.nasa.gov/search?q={query}&media_type=image", "waitUntil": "load", "timeout": 45000 } }(the SPA's search URL accepts the same param shape). Never usenetworkidle.{ "method": "waitForTimeout", "params": { "time": 3000 } }to let it hydrate, then{ "method": "html", "params": { "selector": "body" } }or{ "method": "snapshot" }.- The page calls
https://images-api.nasa.gov/search?...under the hood — so prefer just calling that JSON endpoint directly (a plain GET, or viabrowserless_functionsame-originfetchunder restricted egress) rather than scraping the rendered page. If you must scrape, regex the rendered cards from anevaluate(data-asset-id="<nasa_id>"plus.image-asset__image img[src]for the thumbnail) — but the cards render only the visible page, so the direct JSON call is strictly better.
Site-Specific Gotchas
- At least one search parameter is required. Bare
GET /searchreturns400 {"reason": "Expected 'q' text search parameter or other keywords."}.media_type=imagealone counts ("228,163 image hits");q=alone counts; any filter combination counts. Do not emit a bare/searchrequest even as a probe. page_sizeserver limit vs. transport limit. The API server itself accepts at leastpage_size=600. When routed throughbrowserless_function, requests withpage_size >= 700(≈ 1 MB JSON) return502 "response body exceeded the maximum allowed size of 1MB"— that's the response cap, not NASA (andbrowserless_function's text return is separately capped at ~200k chars). Stick topage_size=100for general use; if you raise it, cap at 500 to stay safely under the limit, or capture the response inside abrowserless_agentevaluate(whose XHR isn't response-capped the same way) and project it there.- Most metadata fields are optional. Only
nasa_id,title,media_type,center,date_createdare guaranteed.keywords,album,location,photographer,secondary_creator,description,description_508are all sometimes-absent. Always.get(key)with a default. - Video search results don't include the actual video file URLs. For
media_type=videoitems,items[].links[]contains only thumbnail JPEGs (~thumb,~small,~medium,~large) plus a.srtcaptions link. To get the.mp4/.movoriginal, you must call/asset/{nasa_id}in a follow-up request. Images, by contrast, ship all five renditions directly in the search response. - Original-image file extensions vary.
{nasa_id}~origmay be.jpg,.png,.tif, or.tiff(verified:PIA23591~orig.jpg,NHQ201907190146~orig.tif). Read thehreffromlinks[].rel == "canonical"; don't construct the URL from the nasa_id + assume.jpg. - Some
nasa_idvalues contain literal spaces. Video asset IDs likeNDTV000908_Apollo_Digest_Series_Spacecraft for Apollohave unencoded spaces in theirhrefURLs as returned by the API. When following these URLs, URL-encode the space (%20) or your HTTP client will reject the request. centeris case-sensitive uppercase.center=jplreturns 0 hits;center=JPLreturns thousands. The canonical codes are the three- or four-letter NASA center abbreviations (JPL,JSC,KSC,GSFC,HQ,MSFC,LRC,ARC,AFRC,SSC,GRC).media_typeaccepts comma-separated lists.media_type=image,videoreturns mixed results in the same response — branch downstream ondata[0].media_typeper item. There is nomedia_type=alltoggle; omitting the param entirely is the "all types" search.year_start/year_endfilter againstdate_created, not upload date. Some assets have adate_createddecades before they were uploaded (e.g. Apollo-era photos uploaded in 2007). Filteringyear_start=2020excludes them even if NASA published them in 2020.- Album names are internal collection IDs, not slugs.
/album/apollo11returns404 {"reason": "No assets found for album=\"apollo11\" page=1"}. The right name is something likeApollo_11_50th_AnniversaryorKSC_50th_Anniversary. Discover the exact string by running a/search?q=apollo+11&page_size=5first and readingitems[].data[0].album[]. /metadata/{nasa_id}is a two-hop endpoint. It returns{"location": "<S3 URL>"}— the actual EXIF block lives at that S3 URL (animages-assets.nasa.gov/.../metadata.json). Same pattern for/captions/{nasa_id}(returns.srtURL). Fetch thelocationto get the actual payload. There is no direct-content variant.- 404 error bodies are JSON, not HTML.
/asset/<bad-id>→{"reason": "No AssetDB records for nasaid=..."}./album/<bad-name>→{"reason": "No assets found for album=\"...\" page=1"}. Parsereasonfor human-readable failure messages. - No auth, no rate-limit observed, no anti-bot. 10-request bursts from the same IP all returned 200 in our trace. NASA does not publish a documented rate-limit for this API; stay under ~10 req/s as a self-imposed politeness ceiling. No
Referer,User-Agent, or cookies are checked. hrefURLs may usehttp://(nothttps://). Several response fields — notably the/asset/{nasa_id}item hrefs — returnhttp://images-assets.nasa.gov/.... The same URLs work over HTTPS; upgrade the scheme client-side if you care about TLS.- Search SPA at
images.nasa.gov/search?...returns 0 snapshot refs. It's a JS-hydrated React app — abrowserless_agentsnapshotafter thegotoload shows the chrome but no per-result refs. Don't try to click cards; use the underlying JSON API (callimages-api.nasa.govdirectly) instead.
Expected Output
{
"query": "apollo 11",
"filters": {
"media_type": "image",
"year_start": null,
"year_end": null,
"center": null,
"page": 1,
"page_size": 100
},
"total_hits": 5881,
"page": 1,
"page_size": 100,
"next_page_url": "https://images-api.nasa.gov/search?q=apollo+11&media_type=image&page_size=100&page=2",
"results": [
{
"nasa_id": "jsc2007e034221",
"title": "Apollo 11 spacecraft pre-launch",
"description": "Personnel atop the 402-ft. Mobile Service Structure look back at the Apollo 11 spacecraft as the tower is moved away during a Countdown Demonstration Test. Photo filed 11 July 1969.",
"media_type": "image",
"center": "JSC",
"date_created": "1969-07-11T00:00:00Z",
"keywords": ["Apollo", "Apollo 11", "Launch"],
"album": ["KSC_50th_Anniversary"],
"location": null,
"photographer": null,
"secondary_creator": null,
"asset_manifest_url": "https://images-assets.nasa.gov/image/jsc2007e034221/collection.json",
"details_url": "https://images.nasa.gov/details/jsc2007e034221",
"renditions": {
"thumb": {
"url": "https://images-assets.nasa.gov/image/jsc2007e034221/jsc2007e034221~thumb.jpg",
"width": 487,
"height": 640,
"size_bytes": 60000
},
"small": {
"url": "https://images-assets.nasa.gov/image/jsc2007e034221/jsc2007e034221~small.jpg",
"width": 487,
"height": 640,
"size_bytes": 60000
},
"medium": {
"url": "https://images-assets.nasa.gov/image/jsc2007e034221/jsc2007e034221~medium.jpg",
"width": 975,
"height": 1280,
"size_bytes": 176000
},
"large": {
"url": "https://images-assets.nasa.gov/image/jsc2007e034221/jsc2007e034221~large.jpg",
"width": 1463,
"height": 1920,
"size_bytes": 332000
},
"orig": {
"url": "https://images-assets.nasa.gov/image/jsc2007e034221/jsc2007e034221~orig.jpg",
"width": 2341,
"height": 3072,
"size_bytes": 1402000
}
}
}
]
}Video result variant (media_type=video) — note renditions contains only thumbnails + captions; fetch asset_manifest_url to enumerate the .mp4/.mov files:
{
"nasa_id": "NDTV000908_Apollo_Digest_Series_Spacecraft for Apollo",
"title": "Apollo Digest Series — Spacecraft for Apollo",
"media_type": "video",
"center": "HQ",
"date_created": "1967-01-01T00:00:00Z",
"asset_manifest_url": "https://images-assets.nasa.gov/video/NDTV000908_Apollo_Digest_Series_Spacecraft%20for%20Apollo/collection.json",
"renditions": {
"thumb": {
"url": "https://images-assets.nasa.gov/video/.../...~thumb.jpg"
},
"captions": {
"url": "https://images-assets.nasa.gov/video/.../....srt",
"rel": "captions"
}
},
"note": "Video file URLs (.mp4/.mov) require a follow-up GET to asset_manifest_url."
}Not-found variant (when resolving a specific nasa_id via /asset/{nasa_id} or /album/{album_name}):
{
"success": false,
"reason": "asset_not_found",
"nasa_id": "THIS_DOES_NOT_EXIST_zzz",
"api_message": "No AssetDB records for nasaid=THIS_DOES_NOT_EXIST_zzz"
}