# NASA Earthdata Search: Satellite Datasets and Data Files (`yadroo/nasa-earthdata-search`) Actor

Search NASA's Earth-science metadata catalog without a key: datasets by keyword, mission, instrument, data center, processing level, bounding box and time range, or the single data files of a dataset with size, day/night flag, footprint and download, S3 and OPeNDAP links.

- **URL**: https://apify.com/yadroo/nasa-earthdata-search.md
- **Developed by:** [Samat Makatov](https://apify.com/yadroo) (community)
- **Categories:** Developer tools, Education, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 metadata row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

Search NASA's Earth-science metadata catalog without a key: datasets by keyword, mission, instrument, data center, processing level, bounding box and time range, or the single data files of a dataset with size, day/night flag, footprint and download, S3 and OPeNDAP links.

Every Earth observation dataset NASA distributes is registered in the Common Metadata Repository, the index behind the Earthdata Search website: 58 data centers (provider ids read on 2026-10-07), their datasets, and every single file of every dataset. This actor turns that index into dataset rows. In `collections` mode one row is one dataset — title, short name, concept id, publishing data center, satellites, processing level, period covered, footprint and how many files the archive holds. In `granules` mode one row is one data file — its acquisition window, size in megabytes, footprint, and its HTTPS download, `s3://`, OPeNDAP and browse-image addresses.

It reads **metadata only and never downloads a data file**: the file's address ends up in your row, so a file that needs a free Earthdata Login account still gives you a usable link. No API key, no account, no cookie, no browser, no proxy. All dates are UTC ISO-8601.

> **Need it tailored to your workflow?** Missing a field, a filter, another source or a different output format? [Open an issue](https://apify.com/yadroo/nasa-earthdata-search/issues) and describe what you need — we read every request and adapt the actor.

### Use cases

- **Find the dataset behind a question.** A fire, water or air-quality analyst types `wildfire`, `soil moisture` or `air quality` into `query`, switches `withGranulesOnly` on, and gets the datasets that really hold data, with their file counts.
- **Inventory one mission or one archive.** A remote-sensing team lists everything MODIS on Terra produced inside the land-processes archive (`platforms`, `instruments`, `providers`) ordered by how much people download it — an internal catalog in one run.
- **See what covers your area.** A regional GIS or environment agency sets `boundingBox` (or `point` for a single city) plus `processingLevels` `3` and `4` and gets the analysis-ready products whose footprint overlaps the area.
- **Get the files for an area and a week.** A data engineer switches to `granules`, names the dataset in `shortNames`, sets the box and the two dates, and gets one row per file with the download, S3 and OPeNDAP links to feed a pipeline.
- **Watch a dataset for new files.** A scheduled task in `granules` mode with one `shortNames` value, `sort` on newest first, `onlyNew` on and a small `maxItems` writes only the files that appeared since the previous run — and pays for nothing else.
- **Feed an agent or a pipeline.** Set `fields` to four or five columns and `maxSummaryChars` to `0`: short rows with the permanent `conceptId` as the key, cheap enough to call on every question.
- **Size a download before you pay for it.** `granuleCount` says how many files a dataset holds, so you know in advance whether a `granules` run on it is 40 rows or four million.

### Input

Every field is optional. The prefill is `query: "wildfire"` — run the actor as it comes and you get 25 wildfire datasets.

| Field | Type | Default | Allowed values / notes |
|---|---|---|---|
| `mode` | string (enum) | `collections` | `collections` (one row per dataset) or `granules` (one row per data file). A `granules` run needs `shortNames` or `conceptIds`. Strict: the platform refuses any other value before the run starts. |
| `query` | string | *(prefill `wildfire`)* | Free-text search over dataset title, summary, science keywords, mission and instrument names. Sent to the source. `collections` mode only — the catalog has no free-text search over files. |
| `shortNames` | string\[] | *(empty)* | The dataset's own short name: `MOD11A1`, `VNP46A2`, `MUR-JPL-L4-GLOB-v4.1`. At most 20, OR-combined. Checked against the catalog first; see [Dataset short names](#dataset-short-names). |
| `conceptIds` | string\[] | *(empty)* | The catalog's permanent dataset id, `C<digits>-<DATACENTER>`. At most 20, OR-combined. Checked against the catalog first. |
| `platforms` | string\[] | *(empty)* | Satellite or mission names, at most 10, OR-combined. Sent to the source. See [Satellites and platforms](#satellites-and-platforms). |
| `instruments` | string\[] | *(empty)* | Sensor names, at most 10, OR-combined. Sent to the source. See [Instruments](#instruments). |
| `providers` | string\[] | *(empty)* | Data-center ids, at most 10, OR-combined. Sent to the source as the `provider` filter. See [Data centers](#data-centers). |
| `processingLevels` | string\[] (enum) | *(empty = all)* | `0`, `1`, `1A`, `1B`, `1C`, `1T`, `2`, `2G`, `2P`, `3`, `4`, `NA`. Sent to the source. `collections` mode only. Strict: an off-list value is refused by the platform before the run. |
| `boundingBox` | string | *(empty)* | `west,south,east,north` in degrees, e.g. `46,40,88,56`. Sent to the source. Overlap, not containment. |
| `point` | string | *(empty)* | `longitude,latitude`, e.g. `76.889,43.238`. Sent to the source. Ignored when `boundingBox` is also set (the run says so). |
| `temporalFrom` | string | *(empty)* | `2026-09-01` or `2026-09-01T00:00:00Z`, read as UTC. Sent to the source. |
| `temporalTo` | string | *(empty)* | Same format, inclusive, UTC. Sent to the source. |
| `cloudHostedOnly` | boolean | `false` | Keep only datasets the archive moved into cloud storage (their files carry `s3://` links). Sent to the source. `collections` mode only. |
| `downloadableOnly` | boolean | `false` | Keep only records the catalog marks as having online access to the data. Sent to the source, both modes. |
| `withGranulesOnly` | boolean | `false` | Keep only datasets that already hold at least one file. Sent to the source. `collections` mode only. |
| `sort` | string (enum) | `relevance` | `relevance`, `newestData`, `oldestData`, `mostUsed`, `title`. See [Sort orders](#sort-orders). Strict enum. |
| `onlyNew` | boolean | `false` | Remember the concept ids this run wrote and, next run of the same actor or task, write only ids that were not written before. Applied by this actor. |
| `maxItems` | integer | `25` | 1–2000. Hard cap on the rows written, and therefore on what the run costs. |
| `includeGranuleCounts` | boolean | `true` | Fill the `granuleCount` column. Costs no extra request. `collections` mode only. |
| `maxSummaryChars` | integer | `1200` | 0–20000. The `summary` column is cut to this many characters at a word boundary; `0` leaves the column out of the row. `collections` mode only. |
| `fields` | string\[] | *(all)* | Keep only the named columns, **in this order**. `rowType`, `conceptId`, `title`, `url` and `fetchedAt` are always kept — the ones you do not name come first, and one you do name moves to the position you gave it. Letter case does not matter; a name this mode has no column for is reported in the status message, and a list with no real column at all is refused. |

**Free-text fields and strict enum fields are different.** `shortNames`, `conceptIds`, `platforms`, `instruments` and `providers` are free text: case, surrounding spaces, accents, `_`/`-`/space differences and the aliases listed below are all accepted, and any other spelling is refused with the closest known values in the message — never swapped for something else, because a near miss is another dataset as often as a typo. `mode`, `sort` and `processingLevels` are enum fields: the Apify platform refuses an off-list value with HTTP 400 **before the run starts**, so there is no typo correction for them and the allowed values must be written exactly as listed.

### Reference

#### Sort orders

| Value | `collections` | `granules` |
|---|---|---|
| `relevance` | the source's own ranking (only meaningful together with `query`) | the source's own order |
| `newestData` | datasets whose data starts latest first | newest file first — the order a watch task wants |
| `oldestData` | earliest data first | oldest file first |
| `mostUsed` | datasets people download most first | **no equivalent**: the run orders files newest first instead and says so in its status message |
| `title` | dataset title, A to Z | file name, A to Z |

#### Processing levels

| Value | What it is |
|---|---|
| `0` | Raw instrument data, unprocessed. |
| `1`, `1A`, `1B`, `1C`, `1T` | Instrument measurements — radiances and brightness values, with calibration and geolocation added at the later letters. |
| `2`, `2P` | Geophysical values (temperature, moisture, concentration) along the satellite's own track. |
| `2G` | The same, resampled onto a grid but still per orbit. |
| `3` | Gridded and averaged over space and time — the usual choice for a map. |
| `4` | Model or analysis output that blends several sources, e.g. a reanalysis. |
| `NA` | The publisher gave no level. |

An off-list value here is refused by the platform before the run starts, with the allowed values in the error.

#### Data centers

The `providers` input takes the catalog's own data-center id, which is exactly what the `dataCenter` column of every row carries. The ones people ask for most:

| Id | Archives |
|---|---|
| `LPCLOUD` | Land products: MODIS and VIIRS land, ECOSTRESS, EMIT, Landsat-derived surface reflectance. |
| `LAADS` | Atmosphere and calibrated radiances, and the VIIRS nighttime-lights products. |
| `POCLOUD` | Physical oceanography: sea surface temperature, sea level, salinity, ocean winds. |
| `NSIDC_ECS`, `NSIDC_CPRD` | Snow, ice, glaciers, frozen ground, ICESat-2 and SMAP. |
| `GES_DISC`, `GESDISCCLD` | Atmospheric composition, water and energy cycle, MERRA-2 and GPM. |
| `ORNL_CLOUD`, `ORNL_DAAC` | Biogeochemistry, vegetation, carbon and field campaigns. |
| `ASF` | Synthetic-aperture radar: Sentinel-1, ALOS, NISAR. |
| `LARC_CLOUD`, `LARC_ASDC` | Clouds, aerosols, radiation budget, tropospheric chemistry. |
| `OB_DAAC`, `OB_CLOUD`, `OBPG` | Ocean biology and ocean colour. |
| `SEDAC` | Human dimensions: population, settlements, health and climate. |
| `GHRC`, `GHRC_CLOUD` | Hydrology, lightning, severe weather field campaigns. |
| `USGS_EROS`, `USGS_LTA` | USGS-held Landsat and land-imaging collections. |
| `SCIOPS` | Older campaign and partner records registered without a cloud archive. |
| `LANCEMODIS`, `LANCEAMSR2`, `OMINRT` | Near-real-time streams, hours after acquisition. |
| `ESA`, `EUMETSAT`, `JAXA`, `ISRO`, `INPE`, `CCMEO` | Partner agency records registered in the same catalog. |

Fifty-eight ids exist in total; the run refuses an id the catalog does not have and names the closest ones. Spoken names are accepted as aliases: `PO.DAAC` → `POCLOUD`, `LP DAAC` / `LPDAAC` → `LPCLOUD`, `NSIDC` → `NSIDC_ECS`, `GES DISC` → `GES_DISC`, `ORNL` → `ORNL_CLOUD`, `ASDC` → `LARC_ASDC`, `FIRMS` → `LM_FIRMS`, `NCEI` → `NOAA_NCEI`, `Alaska Satellite Facility` → `ASF`. Two spellings that look like aliases are **ids of their own and stay as typed**: `PODAAC` and `ORNL_DAAC` are the older, non-cloud archives of the same institutions.

#### Satellites and platforms

Spelled as the catalog spells them (case and spaces do not matter):

`Terra`, `Aqua`, `Suomi-NPP`, `NOAA-20`, `NOAA-21`, `Landsat-5`, `Landsat-7`, `Landsat-8`, `Landsat-9`, `Sentinel-1A`, `Sentinel-1B`, `Sentinel-2A`, `Sentinel-2B`, `Sentinel-3A`, `Sentinel-3B`, `Sentinel-5P`, `Sentinel-6A`, `ICESat`, `ICESat-2`, `SMAP`, `GPM`, `TRMM`, `GRACE`, `GRACE-FO`, `CALIPSO`, `CloudSat`, `Aura`, `OCO-2`, `OCO-3`, `ISS`, `Nimbus-7`, `JASON-1`, `JASON-2`, `JASON-3`, `TOPEX/POSEIDON`, `SWOT`, `NISAR`, `PACE`, `GOES-16`, `GOES-17`, `GOES-18`, `METOP-A`, `METOP-B`, `METOP-C`, `ENVISAT`, `ERS-1`, `ERS-2`, `ALOS`, `RADARSAT-1`, `Aquarius_SAC-D`.

The catalog also uses non-satellite platform words, and they work the same way: `AIRCRAFT`, `BUOYS`, `FIELD SURVEYS`, `MODELS`, `COMPUTERS`, `NOT APPLICABLE`. Aliases include `snpp` / `npp` → `Suomi-NPP`, `Sentinel-6` and `Sentinel-6 Michael Freilich` → `Sentinel-6A`, `Aquarius` → `Aquarius_SAC-D`, `TOPEX` → `TOPEX/POSEIDON`, `International Space Station` → `ISS`.

The catalog's full platform vocabulary is far longer than this list. A name that is not listed here is **checked against the catalog itself** before the run searches: if some dataset really uses it, the run accepts it as typed; if nothing does, the run stops with the closest names from the list above. That check costs one extra request per unlisted name.

#### Instruments

`MODIS`, `VIIRS`, `ASTER`, `MISR`, `MOPITT`, `AIRS`, `AMSU-A`, `AMSR-E`, `AMSR2`, `ATLAS`, `OLI`, `OLI-2`, `TIRS`, `TIRS-2`, `ETM+`, `TM`, `MSS`, `SAR`, `PALSAR`, `SLSTR`, `OLCI`, `SRAL`, `TROPOMI`, `OMI`, `OMPS`, `MLS`, `TES`, `GMI`, `DPR`, `PR`, `TMI`, `ECOSTRESS`, `GEDI`, `AVHRR`, `SeaWiFS`, `OCI`, `ABI`, `POSEIDON-3`, `SMAP L-BAND RADAR`, `LIDAR`, `SPECTROMETERS`. Aliases: `ETM` → `ETM+`, `AMSR-2` → `AMSR2`. Unlisted names are checked against the catalog, exactly as for the platforms.

#### Dataset short names

Worth knowing, all checked against the catalog on 2026-10-07:

| Short name | Dataset | Data center |
|---|---|---|
| `MOD11A1` | MODIS/Terra daily land surface temperature, 1 km grid | `LPCLOUD` |
| `MOD09GA` | MODIS/Terra daily surface reflectance, 1 km grid | `LPCLOUD` |
| `MYD09GA` | The same from Aqua | `LPCLOUD` |
| `VNP46A2` | VIIRS Black Marble gap-filled nighttime lights, daily | `LAADS` |
| `MUR-JPL-L4-GLOB-v4.1` | MUR global sea surface temperature analysis, 1 km | `POCLOUD` |
| `MUR25-JPL-L4-GLOB-v04.2` | The same at 25 km, lighter files | `POCLOUD` |
| `OSTIA-UKMO-L4-GLOB-REP-v2.0` | OSTIA reprocessed sea surface temperature | `POCLOUD` |
| `SPL3SMP_E` | SMAP enhanced soil moisture, 9 km grid | `NSIDC_CPRD` |
| `SPL4SMGP` | SMAP level-4 surface and root-zone soil moisture | `NSIDC_CPRD` |
| `M2SMNXEDI` | MERRA-2 extreme precipitation and temperature indices | `GES_DISC` |

A short name the catalog does not know never becomes a wider search: it gets its own row with `found: false` and a note on what to do instead (and that row is billed like any other).

#### Bounding boxes to start from

| Area | `boundingBox` |
|---|---|
| Kazakhstan and Central Asia | `46,40,88,56` |
| Contiguous United States | `-125,24,-66,50` |
| Western Europe | `-10,35,30,60` |
| Amazon basin | `-79,-18,-44,6` |
| Sub-Saharan Africa | `-18,-35,52,16` |
| South and East Asia | `60,5,150,55` |
| Arctic Ocean | `-180,66,180,90` |

A dataset or file matches when its own footprint **overlaps** the box, so a global dataset matches every box — that is the source's rule, not ours.

### Examples

**Which NASA datasets exist about wildfire, and do they hold files?**

```json
{ "mode": "collections", "query": "wildfire", "withGranulesOnly": true, "includeGranuleCounts": true, "maxItems": 20 }
```

**Sea surface temperature datasets a cloud job can read directly**

```json
{ "mode": "collections", "query": "sea surface temperature", "cloudHostedOnly": true, "downloadableOnly": true, "maxItems": 20 }
```

**Everything MODIS/Terra in the land-processes archive, most used first**

```json
{ "mode": "collections", "providers": ["LPCLOUD"], "platforms": ["Terra"], "instruments": ["MODIS"], "withGranulesOnly": true, "sort": "mostUsed", "maxItems": 25 }
```

**Analysis-ready products covering Central Asia, with data since 2020**

```json
{ "mode": "collections", "boundingBox": "46,40,88,56", "temporalFrom": "2020-01-01", "processingLevels": ["3", "4"], "maxItems": 20 }
```

**The MODIS land-surface-temperature files over a box and three days**

```json
{ "mode": "granules", "shortNames": ["MOD11A1"], "boundingBox": "70,40,80,50", "temporalFrom": "2026-09-01", "temporalTo": "2026-09-03", "sort": "newestData", "maxItems": 20 }
```

**The files of one dataset by its permanent concept id, A to Z**

```json
{ "mode": "granules", "conceptIds": ["C1748058432-LPCLOUD"], "sort": "title", "maxItems": 10 }
```

**The newest nighttime-lights files, as a daily watch task**

```json
{ "mode": "granules", "shortNames": ["VNP46A2"], "sort": "newestData", "onlyNew": true, "maxItems": 20 }
```

**Short rows for an agent: ids, data center and size only**

```json
{ "mode": "collections", "query": "soil moisture", "withGranulesOnly": true, "maxSummaryChars": 0,
  "fields": ["title", "shortName", "dataCenter", "granuleCount", "timeStart"], "maxItems": 20 }
```

### Output

One real dataset row from the cloud run `4uT0wCs5LO6aOArAZ` (long arrays and the summary trimmed with `…` by the sampler):

```json
{
  "rowType": "collection",
  "conceptId": "C1748058432-LPCLOUD",
  "title": "MODIS/Terra Land Surface Temperature/Emissivity Daily L3 Global 1km SIN Grid V061",
  "url": "https://search.earthdata.nasa.gov/search/granules?p=C1748058432-LPCLOUD",
  "fetchedAt": "2026-10-07T22:50:53.654Z",
  "shortName": "MOD11A1",
  "versionId": "061",
  "entryId": "MOD11A1_061",
  "dataCenter": "LPCLOUD",
  "archiveCenter": "LP DAAC",
  "organizations": [
    "LP DAAC",
    "NASA/GSFC/SED/ESD/TISL/MODAPS"
  ],
  "platforms": [
    "Terra"
  ],
  "processingLevel": "3",
  "collectionDataType": "SCIENCE_QUALITY",
  "consortiums": [
    "GEOSS",
    "EOSDIS"
  ],
  "granuleCount": 3049436,
  "cloudHosted": true,
  "onlineAccess": true,
  "hasBrowse": true,
  "timeStart": "2000-02-24T00:00:00.000Z",
  "timeEnd": null,
  "ongoing": true,
  "boxes": [
    "-90 -180 90 180"
  ],
  "polygons": [],
  "points": [],
  "coordinateSystem": "CARTESIAN",
  "originalFormat": "UMM_JSON",
  "revisionDate": "2021-02-04T00:00:00.000Z",
  "doiUrl": "https://doi.org/10.5067/MODIS/MOD11A1.061",
  "documentationUrls": [
    "https://lpdaac.usgs.gov/documents/119/MOD11_ATBD.pdf",
    "https://lpdaac.usgs.gov/documents/715/MOD11_User_Guide_V61.pdf",
    "https://ladsweb.modaps.eosdis.nasa.gov/filespec/MODIS/61/MOD11A1",
    "https://modis-land.gsfc.nasa.gov/ValStatus.php?ProductID=MOD11",
    "https://modis-land.gsfc.nasa.gov/MODLAND_val.html",
    "…"
  ],
  "source": "cmr.earthdata.nasa.gov",
  "summary": "The Terra Moderate Resolution Imaging Spectroradiometer (MODIS) Land Surface Temperature/Emissivity Daily (MOD11A1) Version 6.1 product provides daily per-pixel Land Surface Temperature and Emissivity…"
}
```

And one real data-file row, from the cloud run `ec7jskhpiWkgK13vE`:

```json
{
  "rowType": "granule",
  "conceptId": "G4298541041-LPCLOUD",
  "title": "MOD11A1.A2026246.h22v04.061.2026247201930",
  "url": "https://search.earthdata.nasa.gov/search/granules?p=C1748058432-LPCLOUD",
  "fetchedAt": "2026-10-07T22:50:58.268Z",
  "producerGranuleId": "MOD11A1.A2026246.h22v04.061.2026247201930",
  "granuleUr": "MOD11A1.A2026246.h22v04.061.2026247201930",
  "collectionConceptId": "C1748058432-LPCLOUD",
  "datasetTitle": "MODIS/Terra Land Surface Temperature/Emissivity Daily L3 Global 1km SIN Grid V061",
  "dataCenter": "LPCLOUD",
  "dayNightFlag": "BOTH",
  "granuleSizeMb": 6.4578,
  "timeStart": "2026-09-03T00:00:00.000Z",
  "timeEnd": "2026-09-03T23:59:59.000Z",
  "boxes": [],
  "polygons": [
    "39.834 52.0142 39.7925 65.168 49.9983 77.8098 50.0583 62.1214 39.834 52.0142"
  ],
  "points": [],
  "coordinateSystem": "GEODETIC",
  "onlineAccess": true,
  "hasBrowse": true,
  "originalFormat": "ECHO10",
  "revisionDate": "2026-09-04T20:53:04.983Z",
  "downloadUrls": [
    "https://data.lpdaac.earthdatacloud.nasa.gov/lp-prod-protected/MOD11A1.061/MOD11A1.A2026246.h22v04.061.2026247201930/MOD11A1.A2026246.h22v04.061.2026247201930.hdf"
  ],
  "s3Urls": [
    "s3://lp-prod-protected/MOD11A1.061/MOD11A1.A2026246.h22v04.061.2026247201930/MOD11A1.A2026246.h22v04.061.2026247201930.hdf"
  ],
  "opendapUrl": "https://opendap.earthdata.nasa.gov/collections/C1748058432-LPCLOUD/granules/MOD11A1.A2026246.h22v04.061.2026247201930",
  "browseUrls": [
    "https://data.lpdaac.earthdatacloud.nasa.gov/lp-prod-public/MOD11A1.061/MOD11A1.A2026246.h22v04.061.2026247201930/BROWSE.MOD11A1.A2026246.h22v04.061.2026247201933.1.jpg",
    "https://data.lpdaac.earthdatacloud.nasa.gov/lp-prod-public/MOD11A1.061/MOD11A1.A2026246.h22v04.061.2026247201930/BROWSE.MOD11A1.A2026246.h22v04.061.2026247201933.2.jpg"
  ],
  "metadataUrls": [
    "https://data.lpdaac.earthdatacloud.nasa.gov/lp-prod-protected/MOD11A1.061/MOD11A1.A2026246.h22v04.061.2026247201930/MOD11A1.A2026246.h22v04.061.2026247201930.cmr.xml",
    "https://data.lpdaac.earthdatacloud.nasa.gov/s3credentials",
    "https://www.earthdata.nasa.gov/centers/lp-daac"
  ],
  "doiUrl": "https://doi.org/10.5067/MODIS/MOD11A1.061",
  "needsEarthdataLogin": true,
  "source": "cmr.earthdata.nasa.gov"
}
```

**Always present, in both modes**

| Field | Type | Meaning |
|---|---|---|
| `rowType` | string | `collection`, `granule`, or `notFound` for a marker row. |
| `conceptId` | string | The catalog's permanent id of the dataset (`C…`) or the file (`G…`). Empty only on a marker row. |
| `title` | string | Dataset title, or the producer's file name. |
| `url` | string | The catalog's own public page for the dataset and its files. |
| `fetchedAt` | string | When this run read the record, UTC. |

**Dataset rows (`collections`)**

| Field | Type | Meaning |
|---|---|---|
| `shortName`, `versionId`, `entryId` | string | The dataset's short name, version and the two joined. |
| `summary` | string | The publisher's description, cut to `maxSummaryChars`. Left out when that is `0`. |
| `dataCenter` | string | Publishing data center, the same id the `providers` input takes. |
| `archiveCenter`, `organizations` | string, string\[] | The archiving organisation in its own spelling, and every organisation listed. |
| `platforms` | string\[] | Satellites, aircraft, buoys or models behind the dataset. |
| `processingLevel` | string | `0` … `4` or `NA`. |
| `collectionDataType` | string | Science quality, near-real-time or expedited, where stated. |
| `consortiums` | string\[] | Programmes such as `EOSDIS` or `GEOSS`. |
| `granuleCount` | integer | Files the archive holds (with `includeGranuleCounts`). |
| `cloudHosted` | boolean | The dataset lives in cloud storage, so its files carry `s3://` links. |
| `ongoing` | boolean | True when the dataset has no end date — new files keep arriving. |
| `doiUrl`, `documentationUrls` | string, string\[] | Citation DOI, user guides and algorithm documents. |

**Data-file rows (`granules`)**

| Field | Type | Meaning |
|---|---|---|
| `producerGranuleId` | string | The file name the producing archive gave — what `title` carries. |
| `granuleUr` | string | The catalog's own title for the file; for some archives an internal id such as `LAADS:10066693097`. |
| `collectionConceptId`, `datasetTitle` | string | The dataset the file belongs to. |
| `dayNightFlag` | string | `DAY`, `NIGHT`, `BOTH` or `UNSPECIFIED`. |
| `granuleSizeMb` | number | File size in megabytes. |
| `downloadUrls` | string\[] | HTTPS addresses of this file. Links of the whole dataset are never mixed in. |
| `s3Urls` | string\[] | `s3://` addresses, for a job in the archive's own AWS region. |
| `opendapUrl` | string | OPeNDAP address, for reading a subset instead of the whole file. |
| `browseUrls`, `metadataUrls` | string\[] | Preview images, and the metadata documents the catalog links for the file and for its archive. |
| `needsEarthdataLogin` | boolean | A hint read from the URL path (`-protected` versus `-public`), not a guarantee. |

**Both modes:** `dataCenter`, `timeStart`, `timeEnd`, `boxes`, `polygons`, `points`, `coordinateSystem`, `onlineAccess`, `hasBrowse`, `originalFormat`, `revisionDate` (when the catalog record was last revised, not when the data changed), `source`.

**Marker rows.** A short name or concept id the catalog does not know gets one row with `rowType: "notFound"`, `found: false`, `requested`, `requestedField` and a `note` on what to do — instead of a silent empty result or a wider search. Such a row is written to the dataset, so it is billed like any other row.

With `fields` set, the row keeps only the columns you named, **in that order**, with the always-kept columns you did not name in front of them.

Dataset views: **Datasets and files found** (the overview table), **Data files and their links**, **Place and period covered**.

### Use it from code / agents

```bash
curl -X POST "https://api.apify.com/v2/acts/yadroo~nasa-earthdata-search/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode":"collections","query":"wildfire","withGranulesOnly":true,"maxItems":20}'
```

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
// Step 1: find the datasets and keep their concept ids.
const found = await client.actor('yadroo/nasa-earthdata-search').call({ query: 'nighttime lights', withGranulesOnly: true, maxItems: 10 });
const datasets = (await client.dataset(found.defaultDatasetId).listItems()).items;
// Step 2: list the files of one of them.
const files = await client.actor('yadroo/nasa-earthdata-search').call({
    mode: 'granules',
    conceptIds: [datasets[0].conceptId],
    temporalFrom: '2026-09-01',
    maxItems: 50,
});
const { items } = await client.dataset(files.defaultDatasetId).listItems();
```

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/nasa-earthdata-search").call(run_input={
    "mode": "granules",
    "shortNames": ["MOD11A1"],
    "boundingBox": "70,40,80,50",
    "temporalFrom": "2026-09-01",
    "temporalTo": "2026-09-03",
    "maxItems": 50,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

MCP: add `https://mcp.apify.com` to Claude / Cursor / any MCP client and call the `yadroo/nasa-earthdata-search` tool with the same JSON input.

**The two-step pattern** is the one worth knowing: a `collections` run answers "which dataset", a `granules` run answers "which files". Store the `conceptId`, not the short name — a concept id is permanent and never collides, while a short name can be re-versioned.

### Pricing

Pay per event: **$0.001 per run start + $0.002 per metadata row**. A row is any row written to the dataset — a dataset row, a data-file row, or the `found: false` marker row of a short name or concept id the catalog does not know. There is no free tier of rows: every run pays the start event even if it returns nothing.

| Run | Cost |
|---|---|
| The default run, 25 dataset rows | $0.001 + 25 × $0.002 = **$0.051** |
| A file listing, 50 rows | $0.001 + 50 × $0.002 = **$0.101** |
| A daily watch task that found 4 new files | $0.001 + 4 × $0.002 = **$0.009** |
| The largest run the schema allows, 2000 rows | $0.001 + 2000 × $0.002 = **$4.001** |

`maxItems` is the real cost control: paging stops as soon as the cap is reached. On top of it, the run respects your **Maximum cost per run**: when the budget runs out the run stops, keeps every row you already paid for, and ends with "Stopped at your spending limit: N rows delivered". Apify's paid plans apply their usual discount to these prices.

### Limits & FAQ

**How fast is it?** One request to the catalog returns up to 2000 records, so a normal run is one request. Measured in the Apify cloud on 2026-10-07: a 20–25-row run takes **3–10 seconds** end to end including the container start, and the largest run the schema allows — 2000 data files of a daily product — took **31 seconds** for all 2000 rows. That is about 4000 rows a minute, so nothing in this actor comes close to the default 900-second run timeout. The actor paces itself at **one request per second per host** regardless.

**Does the source rate-limit?** Yes, and it publishes no number: it throttles by request signature and answers HTTP 429 with a `Retry-After` header. The actor waits exactly that long and continues more slowly — a throttle is not a block. Because one request carries up to 2000 records, a normal run never comes close to the limit.

**What if a run cannot finish?** A run that nears its timeout stops starting new work, keeps every row already read, and ends successfully with "Stopped before the run timeout: N rows saved". Checked in the cloud on 2026-10-07: the same 2000-row input given only 30 seconds returned 1750 rows and that exact status instead of being killed. The spending limit works the same way — "Stopped at your spending limit: N rows delivered". You never lose rows you paid for.

**How deep can a file listing go?** An ungrouped file query in this catalog stops at its 10 000th match, and `maxItems` caps a single run at 2000 rows anyway. For more, split the job by time window or by area — the run says so in its status message when it hits the depth limit.

**Why can I not search files by keyword?** The catalog indexes free text over datasets only, not over single files. That is why `granules` mode needs `shortNames` or `conceptIds`: find the dataset first, then list its files.

**Why is there no instrument column on a dataset row?** The catalog's compact dataset record leaves the instrument list out. The `instruments` filter works (the source applies it); what you get back is the `platforms` column.

**Why is the input called "data center" but sent as a provider?** Because the catalog's own `data_center` *parameter* matches organisation names (`PO.DAAC`, `NASA/GSFC/…`), while the `dataCenter` *column* of a record holds the provider id (`POCLOUD`). Wiring the input to the parameter of the same name would return nothing — checked on 2026-10-07 — so the input sends `provider`, and filter and column always use the same words.

**Do the download links work without an account?** Many need a free Earthdata Login account; `needsEarthdataLogin` reads the archive's URL path and says `true` when they very likely do. It is a hint: the catalog publishes no per-file access flag. This actor never downloads a file, so no account is needed to run it.

**Is there a "changed in the last N hours" filter?** No, deliberately. The catalog's revision parameters are not something we verified, and filtering after the fetch would mean paying for rows that are then thrown away. Use the honest recipe instead: `granules` mode, one `shortNames` value, `sort: newestData`, `onlyNew: true` and a small `maxItems`, on a daily schedule. The memory is kept per actor or task, so two tasks watching two datasets never interfere.

**A global dataset matches my small bounding box. Why?** The spatial filter is overlap, not containment, and a global footprint overlaps everything. Combine it with `processingLevels` or `providers` to narrow the answer.

**What is not included?** The data files themselves, restricted metadata that needs an Earthdata Login token, and anything outside NASA's Common Metadata Repository. Individual international-partner datasets can carry their own use conditions on the files; those conditions are about the files, which this actor never touches.

**An empty result versus an error.** A filter the catalog simply has nothing for ends as a successful run with zero rows and a status message saying what was searched. A value the catalog does not know ends differently: a bad short name or concept id becomes a `found: false` row, and an unknown platform, instrument or data center stops the run with the closest known names — never a silently wider search.

Questions or problems: open an issue on the Issues tab of this actor.

***

Made by **Yadroo**. Sibling actors: [nasa-eonet-events](https://apify.com/yadroo/nasa-eonet-events), [nasa-asteroid-approaches](https://apify.com/yadroo/nasa-asteroid-approaches), [data-gov-datasets](https://apify.com/yadroo/data-gov-datasets), [open-meteo-weather](https://apify.com/yadroo/open-meteo-weather), [openalex-works](https://apify.com/yadroo/openalex-works).

# Actor input Schema

## `mode` (type: `string`):

What one row stands for, one question per run. `collections`: one row per dataset - id, mission, data center, processing level, time span, footprint and how many files it holds. `granules`: one row per data file - acquisition window, size in megabytes, footprint and the download, S3, OPeNDAP and browse links. The source has no free-text search over files, so a `granules` run needs at least one `shortNames` or `conceptIds` value; with neither it stops and names both fields.

## `query` (type: `string`):

Free-text search over dataset title, summary, science keywords, mission and instrument names: `wildfire`, `sea surface temperature`, `nighttime lights`. Several words are one phrase ranked by the source, not three filters. Leave empty and use the filters below to list a whole mission or data center instead of searching. `collections` mode only: the source indexes no free text per file, so a `granules` run ignores this and narrows files by dataset (`shortNames`, `conceptIds`), place and time.

## `shortNames` (type: `array`):

The source's own short name of a dataset, the identifier printed in papers and user guides: `MOD11A1`, `VNP46A2`, `MUR-JPL-L4-GLOB-v4.1`. Several values are OR-combined; case and surrounding spaces do not matter. Every value is checked against the catalog first, and a name it does not know is never sent as a filter - it gets its own row with `found: false` and what to do instead. In `granules` mode this is how you name the dataset whose files you want.

## `conceptIds` (type: `array`):

The catalog's permanent dataset id, shaped `C<digits>-<DATACENTER>`: `C1748058432-LPCLOUD`, `C1996881146-POCLOUD`. Several values are combined with OR. Unlike a short name it never changes and never collides, so it is the id an agent or a pipeline should store. In `collections` mode each id returns its own dataset row; in `granules` mode each id returns that dataset's files. An id the catalog does not know gets a row with `found: false` instead of a wider search.

## `platforms` (type: `array`):

Mission or platform names as the catalog writes them: `Terra`, `Aqua`, `Sentinel-3A`, `NOAA-20`, `Landsat-8`, `ICESat-2`, `SMAP`, `Suomi-NPP`, `BUOYS`. Several values are OR-combined. Case, spaces and the README's aliases are accepted (`terra`, `SENTINEL 3A`, `snpp`); a name outside the actor's list is checked against the catalog and refused with the closest names when nothing uses it. One dataset often lists many platforms, so this finds every dataset that used it.

## `instruments` (type: `array`):

Sensor names as the catalog writes them: `MODIS`, `VIIRS`, `SLSTR`, `OLI`, `ATLAS`, `AVHRR`, `AMSR2`, `GMI`, `ECOSTRESS`. Several values are OR-combined, and the filter pairs with `platforms` to separate one sensor on two satellites (`MODIS` on `Terra` versus on `Aqua`). Case and spaces do not matter; a name outside the actor's list is checked against the catalog and refused with the closest names when no dataset uses it. Dataset rows carry `platforms`, not instruments.

## `providers` (type: `array`):

The archive that publishes the dataset, by its catalog id: `LPCLOUD` and `LAADS` (land and atmosphere), `POCLOUD` (ocean), `NSIDC_ECS` (snow and ice), `GES_DISC` (water cycle), `ORNL_CLOUD`, `ASF` (radar), `OB_DAAC`. Several values are OR-combined. This is the value every row carries in its `dataCenter` column, so filter and column use the same words. Case and spaces do not matter and spoken names such as `PO.DAAC` are read as their id; any other unknown id is refused with the closest ones.

## `processingLevels` (type: `array`):

How far the data has been processed, OR-combined; empty means every level. `0` and `1*` are instrument radiances and brightness values, `2*` are geophysical values along the satellite track, `3` is gridded and averaged, `4` a model or analysis product, `NA` no level given. A map usually wants `3` or `4`. The platform refuses an off-list value before the run. `collections` mode only: the file endpoint knows no levels, so a `granules` run leaves this out and says so.

## `boundingBox` (type: `string`):

Four numbers in degrees, in this order: westernmost longitude, southernmost latitude, easternmost longitude, northernmost latitude. `46,40,88,56` is Kazakhstan and its neighbours. Longitude runs -180 to 180, latitude -90 to 90. A dataset or a file matches when its own footprint overlaps the box at all, not when it lies inside it, so a global dataset matches every box. The order is the one the source uses; a value in the wrong shape is refused by the platform before the run starts.

## `point` (type: `string`):

Longitude first, then latitude - `76.889,43.238` is Almaty, `-74.006,40.713` New York, `2.352,48.857` Paris. Keeps only datasets or files whose footprint covers that spot. Useful for the question people actually ask: "what does NASA have over this city". Longitude before latitude is the source's order and the opposite of what a map app shows, so check the numbers. Set either this or `boundingBox`, not both; if both are given the box wins and the run says so in its status message.

## `temporalFrom` (type: `string`):

Start of the period you care about, as `2026-09-01` or `2026-09-01T00:00:00Z`, read as UTC. A dataset matches when its own time span overlaps the period; a file matches when its acquisition window does. Leave empty for no lower bound. Together with `temporalTo` this is the cheapest filter in `granules` mode, because the source applies it and the run then pays only for files inside the window - a dataset such as MOD11A1 holds over three million files, so always set a window or a small `maxItems`.

## `temporalTo` (type: `string`):

End of the period, same format, inclusive, UTC. Leave empty for no upper bound - with only `temporalFrom` set you get everything from that date onwards, which is what a scheduled run wants. A dataset that is still being produced has no end date at all: its `timeEnd` is empty and its `ongoing` column is true, and it matches every period that starts after its `timeStart`.

## `cloudHostedOnly` (type: `boolean`):

Keep only datasets the archive has moved into cloud storage. Those are the ones whose files carry an `s3://` link, so a pipeline running in the same AWS region can read them directly instead of downloading them. Switch it on when the answer has to be usable from a cloud job; leave it off for a complete search, because many long archives are still only available over HTTPS. `collections` mode only: the file endpoint does not know this flag, so a `granules` run leaves it out and says so.

## `downloadableOnly` (type: `boolean`):

Keep only datasets the catalog marks as having online access to its data. Filters out catalog entries that describe data obtainable by order, on tape, or on request only - a common surprise with older campaigns and field surveys.

## `withGranulesOnly` (type: `boolean`):

Keep only datasets with at least one file in the archive. The catalog also holds registered datasets whose files live elsewhere or have not been ingested yet; they are real entries but a `granules` run on them returns nothing. Switch this on before you collect ids for a `granules` run. `collections` mode only.

## `sort` (type: `string`):

Which rows you get first when `maxItems` cuts the answer, so it decides what you pay for. `relevance` only means something together with `query`; with the filters alone the source falls back to its own internal order. `newestData` is the one for monitoring - the newest files of a dataset come first. `mostUsed` exists for datasets only; in `granules` mode the run uses newest-first instead and says so in its status message.

## `onlyNew` (type: `boolean`):

Remember the concept ids this run writes in the actor's key-value store and, on the next run of the same actor or task, write only ids that were not written before. This is the watch mode: a task in `granules` mode with one `shortNames` value, `sort` on newest first and a small `maxItems`, scheduled daily, then holds exactly the files that appeared since the last run - and pays for nothing else. The memory is kept per actor or task, so two tasks watching two datasets do not interfere.

## `maxItems` (type: `integer`):

Hard cap on the rows this run writes, and therefore on what it costs. Paging stops as soon as the cap is reached. For scale: a keyword search over the datasets usually has tens to a few thousand matches, while a single long-running dataset can hold millions of files - which is why a `granules` run should always carry a time window, a place, or a small cap here.

## `includeGranuleCounts` (type: `boolean`):

Fill the `granuleCount` column of a dataset row with how many files the archive holds for it. It is the fastest way to tell a continuously produced dataset from a one-off publication, and it tells you in advance how big a `granules` run on that dataset would be. Costs no extra request. `collections` mode only; a dataset the catalog keeps no count for keeps the column empty.

## `maxSummaryChars` (type: `integer`):

Dataset summaries run from one line to several pages. The `summary` column is cut to this many characters at a word boundary. `0` leaves the column out of the row altogether - the right setting for a wide export or for an agent with a small context window. `collections` mode only.

## `fields` (type: `array`):

Keep only the named columns, in the order given - for example `title, shortName, dataCenter, granuleCount`. Empty = every column. `rowType`, `conceptId`, `title`, `url` and `fetchedAt` are always present; name one of them here and it moves to the position you gave it. Letter case does not matter and unknown names are reported in the status message instead of being dropped in silence; a list with no real column at all is refused.

## Actor input object example

```json
{
  "mode": "collections",
  "query": "wildfire",
  "cloudHostedOnly": false,
  "downloadableOnly": false,
  "withGranulesOnly": false,
  "sort": "relevance",
  "onlyNew": false,
  "maxItems": 25,
  "includeGranuleCounts": true,
  "maxSummaryChars": 1200
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "query": "wildfire"
};

// Run the Actor and wait for it to finish
const run = await client.actor("yadroo/nasa-earthdata-search").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "query": "wildfire" }

# Run the Actor and wait for it to finish
run = client.actor("yadroo/nasa-earthdata-search").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "query": "wildfire"
}' |
apify call yadroo/nasa-earthdata-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,yadroo/nasa-earthdata-search"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/3nKhtO1bTAyBI4bW8/builds/a4IVX7QySfyJqIOVU/openapi.json
