# Nonprofit Grants.gov Database, Status Eligibility Award Details (`fetchsmith/grants-gov-scraper`) Actor

Search and enrich US federal grant opportunities from the official Grants.gov API: keyword, agency, status, eligibility and funding-category filters, plus award ceiling/floor, eligibility text and full synopsis via detail lookup. $0.0015/enriched result, $0.0007 for thin rows, no start fee.

- **URL**: https://apify.com/fetchsmith/grants-gov-scraper.md
- **Developed by:** [Fetch Smith](https://apify.com/fetchsmith) (community)
- **Categories:** Lead generation, Business
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 opportunity (enriched)s

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Nonprofit Grants.gov Scraper – Status Eligibility Award Details

Search US federal grant opportunities from Grants.gov's official public API — no API key, no login, no proxy. Filter by keyword, agency, status, eligibility, funding category and instrument, and optionally enrich each result with award ceiling/floor, eligibility text, funding instrument/category and the full synopsis.

### What it does

- Calls Grants.gov's own `search2`/`fetchOpportunity` endpoints (the same API that powers grants.gov/search-grants), not HTML scraping.
- Search results alone carry only 10 thin fields (id, number, title, agency, dates, status). Turn on `enrich` (default) to join each row with a second call for the money fields a grant seeker actually decides on: `awardCeiling`, `awardFloor`, `applicantEligibilityDesc`, `applicantTypes`, `fundingInstruments`, `fundingActivityCategories`, and the full synopsis text.
- **Forecasted opportunities (`docType: "forecast"`) are enriched too**, not just posted ones — roughly half of the default `oppStatuses` result set. Grants.gov gives a forecast its own estimated award ceiling/floor, applicant types and funding instruments/categories under the same field names as a posted synopsis, plus forecast-only fields: `numberOfAwards`, `estimatedFunding`, `estSynopsisPostingDate`, `estApplicationResponseDate`, `estAwardDate`, `estProjectStartDate`, `fiscalYear`.
- **The announcement documents come with the row.** Every enriched opportunity carries an `attachments` array of the files the agency attached — the full NOFO PDF/DOCX, special notices, Q\&A and amendment documents — each with a direct `downloadUrl`, plus `fileName`, `description`, `mimeType`, `sizeBytes`, `folderName`, `folderType` and `postedDate`. Measured on a live 20-row sample: 8 rows carried 45 files between them, while Grants.gov's separate "related documents" list (`synopsisDocumentURLs`) was populated on only 1 of the 20 — so the attachments are where the actual announcement lives. The download links are plain public URLs (no login, no session) and are not fetched during the run, so they cost you nothing extra.
- **Agency codes are resolved and expanded**, not passed through blind: Grants.gov's parent agency codes (e.g. `"USDA"`, `"DOD"`) do **not** automatically include their sub-agencies in a search — unlike some other government APIs. This Actor expands a parent code you supply into all of its real sub-agency codes (e.g. `"USDA"` → `USDA-NIFA`, `USDA-FS`, `USDA-APHIS`, …) so filtering by department actually works. An unrecognised code is dropped with a named warning instead of silently returning zero rows.
- **Opportunity-number lookup ignores your other filters.** Grants.gov ANDs `oppNum` with every other filter, including its own default status filter — looking up a *closed* or *archived* opportunity by its exact number normally returns nothing. Set `oppNum` and this Actor searches all statuses and ignores keyword/agency/eligibility filters, so an exact-number lookup always finds the opportunity if it exists.
- **`oppNums` — batch-lookup a whole list of opportunity numbers in one run.** Grants.gov's API has no batch or joined form for this (`"num1|num2"` and `"num1,num2"` both return zero results, verified live) so this Actor makes one exact-match lookup per number instead — same all-statuses behaviour as a single `oppNum`. A number that doesn't match anything is named in a warning rather than silently dropped, so a partial miss on a long list is never invisible.
- **`postedWithinDays` for cheap incremental pulls** — Grants.gov's own "Posted Date" filter accepts any positive number of days, not just its site's 3/7/14/21-day preset buttons (verified live). Use it instead of re-scanning the whole index on a daily/weekly cron.
- **`postedFrom`/`postedTo` for a fixed calendar window** — Grants.gov's API has no absolute-date filter server-side, so this Actor applies the range client-side against each row's own open date (already present on every result, no extra detail lookups needed). Use this for historical reporting ("everything posted in Q1") where `postedWithinDays`' relative-to-today window doesn't fit. If both are set, `postedFrom`/`postedTo` wins and `postedWithinDays` is ignored (with a warning).
- **`closeDateFrom`/`closeDateTo` filter on the application deadline** — the question a grant seeker actually asks ("what closes in the next 30 days?") is about the deadline, not the posting date. Grants.gov's API can *sort* by close date but cannot *filter* on it, so this is applied client-side against each row's own close date (already on every thin result row, no extra lookups, no extra cost). Be aware what has no deadline: a forecast never has one (Grants.gov returns an empty close date on 100% of `docType: "forecast"` rows), and neither do rolling/continuous announcements and RFIs (~18% of posted rows on an unfiltered sample). Those are dropped by this filter and reported under their own count in the run summary, and the Actor warns up front if you leave `forecasted` in `oppStatuses` while filtering on a deadline.
- **`closesWithinDays` — the same deadline filter, phrased relatively.** Resolves to `[today, today+N]` for you (same client-side mechanism and no-deadline exclusions as `closeDateFrom`/`closeDateTo`), so a cron doesn't have to compute calendar dates each run. If `closeDateFrom`/`closeDateTo` is also set, the absolute range wins and `closesWithinDays` is ignored (with a warning) — same precedence rule as `postedWithinDays` vs `postedFrom`/`postedTo`.
- **`minAwardAmount`/`maxAwardAmount` filter on award ceiling** — forces `enrich` on since the amount only exists in the per-opportunity detail record. Grants.gov returns award amounts as strings, and roughly a third to half of posted opportunities have no ceiling set at all (the API spells this as the literal string `"none"`, not null or absent) — this Actor normalizes both into real numbers or `null`, and the amount filter correctly drops the `"none"` rows rather than treating them as zero.
- **`watchLabel` — only what's new since your last run.** Name a saved search and every run after the first returns just the opportunities not already delivered under that label and filter combination, instead of the whole match set every time. The first run for a label is a free baseline (0 results, 0 charged); it records what already matches in a key-value store on your own Apify account, keyed by the label plus a fingerprint of your other filters, so editing a filter starts a fresh baseline instead of dumping every previously-excluded opportunity as "new". Built for a daily/weekly scheduled run.
- **`watchChanges` — also catch a deadline extension, a status change, a funding-range revision, an eligibility rewrite, or a forecast turning real.** Add this to `watchLabel` and an opportunity you already have gets re-delivered (at the normal per-row price, tagged `_watchChangeType`/`_watchPrevious`) if its closing date, `docType` (forecast → posted), `oppStatus` (posted → closed/archived), `awardCeiling`/`awardFloor`, `lastUpdatedDate` (Grants.gov's own "this synopsis/forecast was edited" timestamp) or `applicantEligibilityDesc` changes since you last saw it — not just brand-new opportunities. The last 3 only get watched when `enrich` is on (the default), since they only exist on the enriched detail record. Off by default so existing watches keep their current behaviour.
- Pay per result: charged only for rows actually returned.

### Use cases

- **Grant-seeking pipelines** — pull every open opportunity a nonprofit, university or small business is eligible for (`eligibilities` + `fundingCategories`), already joined with award ceiling/floor so you can triage by money without a second lookup.
- **Daily/weekly funding alerts** — run `postedWithinDays: 1` on a cron and only pay for the handful of opportunities posted since yesterday, instead of re-scanning the whole index.
- **Deadline triage** — `closeDateFrom`/`closeDateTo` (or `closesWithinDays: 30` for the same window without computing dates) to list only what a team can still realistically apply for, instead of paging through opportunities whose deadline has already passed or is a year away.
- **Award-size screening** — `minAwardAmount: 500000` to surface only large awards, or `maxAwardAmount` to find the small ones a single PI can realistically manage.
- **Grants-landscape research** — filter by `agencies` (parent codes expand to every sub-agency) and `postedFrom`/`postedTo` to reconstruct a fixed historical window, e.g. everything a department posted last quarter.
- **Enriching an existing list** — set `oppNum` for one opportunity, or `oppNums` for a whole list of numbers you already have, and get the full record back for each, even if it is closed or archived.
- **Deadline-amendment / forecast-to-posted alerts** — `watchLabel` + `watchChanges` on a saved search flags an agency extending a deadline or a forecast finally posting, without re-fetching and diffing the whole result set yourself.
- **Funding-range and eligibility-rewrite alerts** — the same `watchChanges` flag also catches an agency raising or lowering an award ceiling/floor, or rewriting the eligibility text, on an opportunity you already have — one Actor covers what some competitors ship as several separate single-purpose "watch" listings.

#### Example input

```json
{
  "keyword": "cancer research",
  "oppStatuses": ["posted"],
  "minAwardAmount": 500000,
  "maxResults": 3
}
```

### Input

| Field | Type | Description |
|---|---|---|
| `keyword` | string | Full-text search across title and synopsis |
| `oppStatuses` | array | `forecasted`, `posted`, `closed`, `archived` (default: forecasted + posted) |
| `agencies` | array | Agency codes, e.g. `NSF`, `USDA-NIFA`, `DOD-AMC`; parent codes are expanded to sub-agencies |
| `eligibilities` | array | Restrict to applicant types (state govt, nonprofit, small business, individuals, …) |
| `fundingCategories` | array | Restrict to funding activity categories (Health, Education, Environment, …) |
| `fundingInstruments` | array | Grant / Cooperative Agreement / Procurement Contract / Other |
| `cfda` | string | Restrict to one Assistance Listing (CFDA) number, e.g. `93.859` (the dot is optional — `93859` filters identically) |
| `oppNum` | string | Look up one opportunity by exact number — ignores all other filters |
| `oppNums` | array | Batch form of `oppNum` — look up a whole list of exact numbers in one run (one lookup call each; combined with `oppNum` if both set) |
| `sortBy` | string | `openDate\|desc`, `openDate\|asc`, `closeDate\|desc`, `closeDate\|asc` |
| `enrich` | boolean | Join each row with award/eligibility/synopsis detail (default `true`) |
| `postedWithinDays` | integer | Only opportunities posted in the last N days — cheap incremental pull |
| `postedFrom` | string | Only opportunities opened on/after this date (strict `YYYY-MM-DD`, a bad date stops the run); overrides `postedWithinDays` |
| `postedTo` | string | Only opportunities opened on/before this date (`YYYY-MM-DD`); overrides `postedWithinDays` |
| `closeDateFrom` | string | Only opportunities whose deadline falls on/after this date (`YYYY-MM-DD`); excludes rows with no deadline |
| `closeDateTo` | string | Only opportunities whose deadline falls on/before this date (`YYYY-MM-DD`); same exclusions |
| `closesWithinDays` | integer | Deadline falls within the next N days from today; ignored if `closeDateFrom`/`closeDateTo` is set |
| `minAwardAmount` | integer | Minimum award ceiling (USD); forces `enrich` on, excludes opportunities with no ceiling set |
| `maxAwardAmount` | integer | Maximum award ceiling (USD); same exclusions as `minAwardAmount` |
| `maxResults` | integer | Stop after this many opportunities (default 100) |
| `watchLabel` | string | Optional. Name a saved search to get only opportunities new since your last run under that label — see FAQ |
| `watchChanges` | boolean | Optional, requires `watchLabel`. Also re-deliver an already-seen opportunity if its closing date, `docType`, `oppStatus`, award ceiling/floor, last-updated date or eligibility text changed (default `false`) — see FAQ |
| `webhookUrl` | string | Optional. POST a small JSON completion summary (pushed/scanned counts, dataset ID, watch new/changed counts) here when the run finishes — see FAQ |

### Output (thin fields, always present)

`id`, `opportunityNumber`, `title`, `agencyCode`, `agency`, `openDate`, `closeDate`, `oppStatus`, `docType`, `cfdaList`, `url`, `enrichment`

### Output (enriched fields, when `enrich: true`)

`agencyName`, `agencyCode`, `topAgencyName`, `topAgencyCode`, `opportunityCategory`, `postingDate`, `responseDate`, `archiveDate`, `costSharing`, `awardCeiling`, `awardFloor`, `applicantEligibilityDesc`, `applicantTypes`, `fundingInstruments`, `fundingActivityCategories`, `synopsisText`, `cfdas`, `fundingDescLinkUrl`, `synopsisDocumentURLs`, `attachments`, `assistURL`, `lastUpdatedDate`, `modComments`

### Output (watch-mode change fields, only on a `watchChanges` re-delivery)

`_watchChangeType` (array, one or more of `closeDate`/`docType`/`oppStatus`/`awardCeiling`/`awardFloor`/`lastUpdatedDate`/`applicantEligibilityDesc`), `_watchPrevious` (object with the previous value(s) for each changed field — `applicantEligibilityDesc`'s previous value is a fixed note, not the old text, since only a fingerprint of it is stored, not the full text)

`assistURL` is Grants.gov's link to an agency's ASSIST application workspace. It is carried through verbatim from the API and is almost always empty: on a 48-opportunity live sample spanning five keyword searches and both forecast and posted rows, Grants.gov returned an empty `assistURL` and `assistCompatible: false` on every single row. The field is still emitted (as `null`) so the row shape stays stable, but do not build on it — use `url` for the public opportunity page and `attachments[].downloadUrl` for the announcement files.

On a `docType: "forecast"` row, `responseDate`/`archiveDate`/`applicantEligibilityDesc`/`fundingDescLinkUrl` are `null` (a forecast has no firm deadline or eligibility writeup yet) and seven forecast-only fields are added instead: `numberOfAwards`, `estimatedFunding`, `estSynopsisPostingDate` (Grants.gov's own estimate of when the real NOFO posts), `estApplicationResponseDate`, `estAwardDate`, `estProjectStartDate`, `fiscalYear`. These are `null` on synopsis-based (posted/closed/archived) rows.

#### Sample output (one real row from the example input above)

```json
{
  "id": "357002",
  "opportunityNumber": "PAR-24-311",
  "title": "Molecular Imaging of Inflammation in Cancer (R01 Clinical Trial Not Allowed)",
  "agencyCode": "HHS-NIH11",
  "agency": "National Institutes of Health",
  "openDate": "11/06/2024",
  "closeDate": "01/07/2028",
  "oppStatus": "posted",
  "docType": "synopsis",
  "cfdaList": ["93.394", "93.395", "93.396"],
  "url": "https://www.grants.gov/search-results-detail/357002",
  "topAgencyName": "Department of Health and Human Services",
  "topAgencyCode": "HHS",
  "opportunityCategory": "Discretionary",
  "postingDate": "Nov 06, 2024 12:00:00 AM EST",
  "responseDate": "Jan 07, 2028 12:00:00 AM EST",
  "archiveDate": "Feb 12, 2028 12:00:00 AM EST",
  "costSharing": false,
  "awardCeiling": 500000,
  "awardFloor": null,
  "applicantTypes": ["State governments", "Small businesses", "Independent school districts", "..."],
  "fundingInstruments": ["Grant"],
  "fundingActivityCategories": ["Education", "Health"],
  "synopsisText": "The purpose of this Notice of Funding Opportunity (NOFO) is to invite research grant applications (R01) for the development and use of ...",
  "cfdas": [{ "number": "93.394", "title": "Cancer Detection and Diagnosis Research" }],
  "fundingDescLinkUrl": "http://grants.nih.gov/grants/guide/pa-files/PAR-24-311.html",
  "lastUpdatedDate": "Nov 06, 2024 10:10:59 AM EST"
}
```

Note `awardFloor: null` alongside a real `awardCeiling` — agencies often set only one of the two. Dates come back in Grants.gov's own two formats: `MM/DD/YYYY` on the thin search fields, and a long `MMM DD, YYYY hh:mm:ss AM/PM TZ` string on the enriched detail fields. Both are passed through as the API returns them.

#### Sample output (the `attachments` array, one real row)

```json
{
  "id": "332894",
  "opportunityNumber": "W911NF21S0009",
  "title": "LPS Qubit Collaboratory (LQC)",
  "agency": "Dept of the Army -- Materiel Command",
  "synopsisDocumentURLs": [
    { "url": "https://www.arl.army.mil/business/broad-agency-announcements/", "description": "ARO & ARL BAA SITE" }
  ],
  "attachments": [
    {
      "fileName": "LQC BAA Final W911NF21S0009.pdf",
      "description": "LPS LQC BAA",
      "mimeType": "application/pdf",
      "sizeBytes": 887949,
      "folderName": "LPS BAA",
      "folderType": "Full Announcement",
      "postedDate": "Apr 16, 2021 12:37:01 PM EDT",
      "downloadUrl": "https://www.grants.gov/grantsws/rest/opportunity/att/download/306813"
    },
    {
      "fileName": "LQC BAA W911NF-21-S-0009-3.pdf",
      "description": "LQC BAA W911NF-21-S-0009-3",
      "mimeType": "application/pdf",
      "sizeBytes": 832097,
      "folderName": "LQC BAA W911NF-21-S-0009-3",
      "folderType": "Revised Full Announcement",
      "postedDate": "Mar 18, 2026 03:45:10 PM EDT",
      "downloadUrl": "https://www.grants.gov/grantsws/rest/opportunity/att/download/350603"
    }
  ]
}
```

This row has five attachments in total (the original BAA, a special notice, a revised announcement and two amendments) and exactly one entry in `synopsisDocumentURLs` — a link to the agency's own BAA page, not the announcement itself. `folderType` is how Grants.gov distinguishes the original from a revision (`Full Announcement` vs `Revised Full Announcement`), and `postedDate` tells you which revision is current. `sizeBytes` is the real byte size of the file behind `downloadUrl`. Rows with no attached files get `attachments: []`, never `null`.

#### Sample output (a forecast, `oppStatuses: ["forecasted"]`)

```json
{
  "id": "355824",
  "opportunityNumber": "MP-CPI-25-001",
  "title": "Making America Healthy Again by Addressing Dementia Disparities",
  "agencyCode": "HHS-OPHS",
  "agency": "Office of the Assistant Secretary for Health",
  "openDate": "08/01/2024",
  "closeDate": null,
  "oppStatus": "forecasted",
  "docType": "forecast",
  "cfdaList": ["93.137"],
  "url": "https://www.grants.gov/search-results-detail/355824",
  "opportunityCategory": "Discretionary",
  "costSharing": false,
  "awardCeiling": 600000,
  "awardFloor": 450000,
  "applicantTypes": ["State governments", "Nonprofits having a 501(c)(3) status with the IRS, other than institutions of higher education", "..."],
  "fundingInstruments": ["Grant"],
  "fundingActivityCategories": ["Health"],
  "synopsisText": "The Office of Minority Health announces the anticipated availability of funds for Fiscal Year (FY) 2025 ...",
  "cfdas": [{ "number": "93.137", "title": "Community Programs to Improve Minority Health" }],
  "numberOfAwards": 9,
  "estimatedFunding": 5000000,
  "estSynopsisPostingDate": "Apr 14, 2025 12:00:00 AM EDT",
  "estApplicationResponseDate": "Jun 23, 2025 12:00:00 AM EDT",
  "estAwardDate": "Sep 15, 2025 12:00:00 AM EDT",
  "estProjectStartDate": "Sep 30, 2025 12:00:00 AM EDT",
  "fiscalYear": 2025
}
```

`responseDate`, `archiveDate`, `applicantEligibilityDesc` and `fundingDescLinkUrl` are omitted above because Grants.gov has no forecast equivalent — they read `null`, not missing.

**Privacy note:** Grants.gov's detail API also carries an `agencyContactName`/`agencyContactEmail`/`agencyContactPhone` block and a `synopsis.agencyName`/`agencyPhone`/`agencyAddressDesc` block that are agency-entered free text — sometimes a department name, sometimes a named individual program officer with a direct phone and email. Because the two cases can't be told apart per row, none of those fields are ever emitted. Organisational contact info (`agencyName`/`agencyCode` from the structured agency lookup) is included instead.

### Pricing

Two events, no start fee. **The price follows the data, per row** — you are never charged the enriched rate for a row that arrived thin.

| Event | Price | Charged when |
| --- | --- | --- |
| `result` (enriched) | $0.0015 per item | The row carries its full detail record: award ceiling/floor, eligibility text, funding instrument/category, synopsis |
| `opportunity-thin` | $0.0007 per item | `enrich: false`, **or** Grants.gov has no detail record for that opportunity (some archived ones don't) |

**Pricing verified live 2026-10-02** against the whole niche, not just the leader: a 15-term Store sweep finds 84 listings mention Grants.gov, and every one of them was price-checked. (An earlier version of this paragraph said 44. That was a one-search-term count; a 15-term sweep of the same Store returns 84, so the niche is roughly twice the size we previously published. The prices below are unchanged — only the denominator was wrong.) `solidcode/grants-gov-scraper` (8 users) prices $0.0096/result on FREE down to $0.008 on DIAMOND plus a $0.005 Actor-start fee — we undercut even its cheapest (DIAMOND) tier at our more expensive enriched rate, and charge no start fee at all. `thoob/grants-gov-feed` (2 users) has no enrich/thin split and bills every row at a flat $0.01, 6.7x our enriched rate and 14x our thin rate.

**What we do not claim:** this is a crowded niche and we are not the cheapest listing in it. 60 of the 82 listings with a comparable per-event price charge an Actor-start fee ($0.00005–$0.10) and 22 charge none, so a no-start-fee listing is a minority but not rare. Per-row prices run from $0.00001 to $15.00, and **13 of those 82 match or beat our $0.0015 enriched rate** — among them `hridayrungta/grants-gov-scraper` and `andrew_avina/grants-mcp` at $0.0015 with no start fee, `ayush.naa/grants-fit-deadline` at $0.001 with no start fee (cheaper than our enriched rate at every volume), and `shahidirfan/Grants-gov-Scraper`, `springlike_meadowland/us-grant-opportunities-scraper`, `chorelet/government-tenders-scraper` and `jungle_synthesizer/grants-gov-crawler` at $0.001/row behind a start fee.

**Two listings are also cheaper than our $0.0007 thin rate**, which an earlier version of this paragraph wrongly said none were. `fiery_dream/scholarship-intel` (39 users — the niche's biggest listing by lifetime users) charges $0.00005 Actor-start plus **$0.00001/result** and has a `search_type: "grants"` ("Federal Grants Only") mode reading Grants.gov, so it is cheaper than our thin rate from the first row: a 100-row pull costs about $0.0011 there against $0.07 at our thin rate. It is a student-facing matcher, not a grants feed — its inputs are GPA, degree level, field of study and first-generation status, with no agency, status, posted-date or Assistance Listing (CFDA) filter, no enrich/thin split and no watch mode. `alizarin_refrigerator-owner/grants-gov-api---federal-grant-opportunities` (7 users) is a genuine Grants.gov API wrapper with agency/category/eligibility/award-range filters and a webhook, and its per-row rate is also $0.00001 — but it bills a **$0.10 Actor-start fee plus $0.01 per operation**, so a 100-row search costs about $0.111 there: it beats our enriched rate above roughly **74 rows per run** and our thin rate only above roughly **160 rows**, while we are cheaper below that. Neither ships the enrich/thin split itself (you are never charged the enriched rate for a row that arrived thin) or the watch/change-detection and CFDA-validation behaviour documented above. Price-shop on the feature list and on your rows-per-run, not on the headline rate. Pricing verified live 2026-10-02.

The run log prints the split (`Charged N as enriched "result" and M at the cheaper "opportunity-thin" rate`) so the invoice is checkable against the dataset.

### FAQ

**Do I need a Grants.gov account or API key?**
No. This uses Grants.gov's own public `search2`/`fetchOpportunity` endpoints — no key, no login, no proxy.

**Does `maxResults` count rows before or after the filters?**
After. It caps the number of opportunities actually returned to you, which is also the number you are charged for. Verified live: `keyword: "cancer research"`, `oppStatuses: ["posted"]`, `minAwardAmount: 500000`, `maxResults: 3` returned exactly 3 rows, all with an award ceiling of $500,000 or more — not 3 scanned rows of which some survived.

**Why does an opportunity have `awardCeiling: null`?**
Because the agency never set one. Grants.gov spells this as the literal string `"none"` in its detail record; this Actor normalizes it to `null` rather than passing through an inconsistently-typed string or pretending it is `0`. Measured live at roughly a third of posted opportunities, so it is a common case. Note that `minAwardAmount`/`maxAwardAmount` therefore *exclude* these rows — there is no ceiling to compare against. This applies equally to forecasts — a forecast can have `awardCeiling: null` too if the agency hasn't estimated one yet — but a forecast is never excluded just for *being* a forecast; its detail record is fetched and its `awardCeiling` compared the same as any posted opportunity's.

**Does the award-amount filter work on forecasted opportunities, or only posted ones?**
Both. Every `docType:"forecast"` row gets the same detail lookup as a posted one, and Grants.gov gives forecasts their own `awardCeiling`/`awardFloor` estimate under the same field names — so `minAwardAmount`/`maxAwardAmount` compare against it identically. (Fixed cycle 325: earlier builds silently treated every forecast as having no detail record at all, so `minAwardAmount`/`maxAwardAmount` dropped 100% of forecasts regardless of their real award ceiling. If you were filtering by amount before and never saw a forecast in your results, that's why — re-run now.)

**I filtered by `"USDA"` — do I get the sub-agencies too?**
Yes. Grants.gov's own API does not do this: a parent code matches nothing but itself, so a plain `"USDA"` search on the raw API returns almost nothing. This Actor expands the parent into its real sub-agency codes first. Verified live: `agencies: ["USDA"]` returns rows with `agencyCode` values like `USDA-NIFA` and `USDA-APHIS`. An unrecognised code is dropped with a named warning in the log instead of silently returning zero rows.

**Can I look up a closed or archived opportunity by its number?**
Yes, and you do not need to change `oppStatuses` to do it. When `oppNum` is set, this Actor searches all four statuses and ignores every other filter. Verified live: `oppNum: "USDA-NIFA-BFR-002918"` with the default statuses (forecasted + posted) and a deliberately unrelated `keyword: "quantum physics"` still returned that one archived opportunity.

**Can I look up more than one opportunity number at once?**
Yes — set `oppNums` (array) instead of, or alongside, `oppNum`. Grants.gov's API has no batch or joined form for this param (verified live: a pipe- or comma-joined value like `"num1|num2"` returns zero results, not two), so each number gets its own exact-match lookup call, same all-statuses behaviour as a single `oppNum`. If a number in the list has no match, it is named in a warning rather than silently missing from the output.

**What's the difference between `closesWithinDays` and `closeDateFrom`/`closeDateTo`?**
Same filter, two ways to express it. `closesWithinDays: 30` resolves to today through 30 days from now at run time, so a scheduled cron doesn't need to compute a fresh calendar date every time it runs — same tradeoff as `postedWithinDays` vs `postedFrom`/`postedTo`. Set `closeDateFrom`/`closeDateTo` instead for a fixed window (e.g. a specific fiscal quarter) that shouldn't shift with the run date. If both are set, the absolute range wins and `closesWithinDays` is ignored, with a warning in the run log.

**What happens if I typo one of the date filters?**
The run stops immediately with an error naming the bad value, before anything is fetched or charged. Only strict `YYYY-MM-DD` is accepted and it has to be a real calendar date, so `2024-02-30`, `2024-13-01`, `06/15/2024` and `2024-6-5` are all rejected rather than guessed at. This applies to `postedFrom`, `postedTo`, `closeDateFrom` and `closeDateTo`, and an unusable `postedWithinDays`/`closesWithinDays` day count (`"seven"`, `0`, `-5`) stops the run the same way. It is deliberate: an unparseable bound used to be dropped with a warning, which turned "posted in Q1" into "posted at any time" — a larger, wrong, fully billable result set with a completely normal-looking run log. Precedence between a valid relative window and a valid absolute range is unchanged: the absolute range still wins, with a warning, because both were things you asked for.

**Why did my run return zero results?**
Every filter is ANDed, and Grants.gov's API never reports a bad value — a typo'd code returns "success" with zero hits. Most common causes, in order: `oppStatuses` defaults to forecasted + posted, so history needs `closed`/`archived` added; a narrow keyword plus agency plus eligibility often genuinely has no matches; a small `postedWithinDays`/`postedFrom` window is a hard filter; and the award-amount filters drop every row with no ceiling set. The run log names which one applied.

**Should I turn `enrich` off?**
Only for fast sweeps where the thin fields (id, number, title, agency, dates, status, CFDA list, plus a URL this Actor builds for you) are enough — those rows are billed at $0.0007 instead of $0.0015, because they cost no detail lookup to serve. Everything a funding decision actually turns on — award amounts, eligibility text, funding instrument/category, the full synopsis — exists only in the detail record, which is why `enrich` defaults to on. It is forced on when you set an award-amount filter.

**I left `enrich` on but some rows came back without award amounts — was I charged full price for them?**
No. Grants.gov has no detail record at all for a small number of opportunities (mostly archived ones with no synopsis or forecast record). When the detail lookup comes back empty, the row is still returned with its thin fields and billed as `opportunity-thin` ($0.0007), not `result` ($0.0015). The split is printed in the run log at the end of every run. Every row also carries an `enrichment` field saying *which* of these happened — see the next question, because "Grants.gov has no detail record" and "Grants.gov did not answer us" are not the same thing and used to look identical.

**How do I tell "this opportunity has no attachments / no award ceiling" from "you failed to fetch them"?**
Read the row's `enrichment` field. It is one of four values:

| `enrichment` | What it means for the enriched fields on that row |
|---|---|
| `ok` | The detail record was fetched and merged. An empty `attachments` really is no attachments; a `null` `awardCeiling` really is an agency that set none. |
| `not-requested` | You ran with `enrich: false`. No detail lookup was made, so none of the enriched fields are present. |
| `no-detail-record` | Grants.gov answered and has no synopsis or forecast record for this opportunity (mostly archived ones). The enriched fields genuinely do not exist upstream. |
| `fetch-failed` | Grants.gov did **not** answer the detail lookup (retries exhausted, 5xx, or a non-JSON body). The enriched fields may well exist — we could not ask. Re-run to get them. |

Only `ok` licenses you to treat a missing value as a fact about the grant. This matters most with `minAwardAmount`/`maxAwardAmount`: a row whose detail lookup failed has no ceiling to compare, so it is dropped — but it is counted and reported separately from rows the agency genuinely left open-ended, and the run log names the count.

**Was my result set complete? (`RUN_SUMMARY`)**
Every run writes a `RUN_SUMMARY` record to its own key-value store — no webhook needed:

```
GET https://api.apify.com/v2/actor-runs/<runId>/key-value-store/records/RUN_SUMMARY
```

```json
{
  "declaredMatches": 2113,
  "scanned": 1000,
  "delivered": 100,
  "complete": false,
  "incompleteReason": "max-results",
  "incompleteDetail": "Stopped at maxResults=100; matching opportunities past this point were not returned.",
  "mode": "search",
  "enrichedCharged": 98,
  "thinCharged": 2,
  "detailFetchFailures": 0,
  "detailNoRecord": 2,
  "droppedNoAward": 0,
  "droppedUnknownAward": 0,
  "republishedRowsDropped": 0,
  "notFoundOppNums": [],
  "failedOppNums": [],
  "baselineSize": null,
  "baselineTruncated": null,
  "baselineTruncatedTotal": null,
  "watchChangeBlindFilters": null
}
```

`declaredMatches` is Grants.gov's own count of everything matching your filters, so `delivered` is checkable against it from code rather than by reading English in a log. `complete` is deliberately **separate** from any status string: a run can succeed and still be truncated, and that is exactly the case this record exists to make machine-readable. `incompleteReason` is one of `max-results` (your own cap — benign), `charge-limit` (the run's maximum-cost limit stopped it), `seed-cap` (a watch baseline hit the 20,000-opportunity cap), or `search-request-failed` (Grants.gov stopped answering mid-walk — the result set is short through no choice of yours, and before this existed that failure ended the paging walk looking exactly like a finished run). When the run is incomplete the Actor also sets a run status message saying so. `baselineSize`/`baselineTruncated`/`baselineTruncatedTotal` (watch mode only) report the current baseline size and the "Baseline size cap" defect above — see that FAQ entry. `watchChangeBlindFilters` (`null` unless `watchChanges` is on) lists any filter in this run that narrows on a field `watchChanges` tracks and therefore hides those changes — empty array means nothing is blinding change detection; see "don't filter on the field you're watching" below.

**I set a `cfda` number and got zero rows — is the number wrong, or is there really nothing?**
Grants.gov can't tell you: it answers an unusable Assistance Listing number exactly the way it answers a genuinely empty search — HTTP 200, `errorcode: 0`, `"Webservice Succeeds"`, zero results. So this Actor answers it for you. Whenever a `cfda`-filtered search declares zero matches, it re-asks Grants.gov for that same number across **all four** statuses with no other filter, and the log then says outright which case you're in: either the cfda matches nothing at all on Grants.gov (so the number — not your other filters — emptied the run), or it matches *n* opportunities and your other filters ANDed them away. The count lands in `RUN_SUMMARY.cfdaMatchesAnyStatus` for pipelines; `null` there means "not checked" (no `cfda` set, rows were returned, or the check itself failed) and should never be read as "the number is fine".

One honest caveat: a *real* Assistance Listing that has simply never been attached to a Grants.gov opportunity looks the same as a typo (`10.001` is a live example). The check reports "matches nothing on Grants.gov", which is the fact that affects your result set — not "this is not a real CFDA number". Look numbers up at [sam.gov/content/assistance-listings](https://sam.gov/content/assistance-listings). The value you pass is always sent to Grants.gov exactly as given; nothing is dropped, normalised or guessed.

**Can the same opportunity come back twice (and be charged twice)?**
No — Grants.gov sometimes serves the exact same opportunity under two (or more) brand-new `id`s within one result set, which an id-keyed check can never catch, since the id is exactly what differs. Measured live 2026-09-24 on a 400-row unfiltered sample (`oppStatuses: forecasted|posted|closed|archived`, no keyword): 1% of rows were byte-identical republications (same title/agencyCode/openDate/closeDate/opportunityNumber/docType) — one Fish & Wildlife Service opportunity ("Evaluation and Improvement in Desert Bighorn Sheep Population Estimates") was posted 3 times under ids 51589/51581/51611. `opportunityNumber` alone is **not** a safe dedup key: the same sample also had 2 cases where Grants.gov reused a number for a genuinely revised posting (different title and/or open date — a real correction, not a duplicate). Every run now dedupes on a same-source content hash (`opportunityNumber` + `title` + `agencyCode` + `openDate` + `closeDate` + `docType`, requiring all six to match) before any charge, and reports how many it dropped in `RUN_SUMMARY.republishedRowsDropped`.

**How does `watchLabel` know what's already new, and where is that baseline stored?**
The first run for a label walks the whole match set (every page, not just `maxResults` of it), records every opportunity's `id`, and returns nothing — you are charged $0. Every later run with the same label and the same other filters returns only opportunities whose `id` isn't in that recorded set, then adds them to it. The baseline lives in a key-value store named `fetchsmith-grants-watch` in *your own* Apify account (Storage tab in the console), not ours — you can inspect or delete it any time. Deleting the record for a label resets it to a fresh baseline on the next run. Verified live on build 0.1.9: a seed run over `keyword: "water"` recorded 18,458 opportunity ids and returned 0 rows; an identical rerun returned 0 new; removing 3 ids from the baseline directly and rerunning returned exactly those 3.

**Baseline size cap.** A baseline holds up to **60,000** opportunity ids in one saved record. If a label's baseline grows past that, the oldest ids are dropped — and a dropped id is no longer recognised, so it comes back as "new" on a later run **and is charged again**. The run that drops them says so explicitly: a warning in the log, a note on the run's status message, and `baselineTruncated` / `baselineTruncatedTotal` (this run / the whole life of the label) in the saved record, on the `webhookUrl` payload, and in `RUN_SUMMARY`. If you see it, narrow the watch query (`keyword`, `agencies`, `postedFrom`/`postedTo`, `eligibilities`) or split it across several labels so each baseline stays under the cap.

**If I change a filter, does `watchLabel` dump a pile of "new" results I've actually seen before?**
No. The baseline key includes a fingerprint of every other filter you set, so changing `keyword`, `agencies`, `postedFrom`/`postedTo`, `minAwardAmount`, etc. starts an entirely fresh baseline (another free, zero-result seed run) under that label instead of comparing against the old filter's baseline. `oppNum` lookups ignore `watchLabel` entirely — an exact single-opportunity lookup has no "new since last time" to track.

**What does `watchChanges` add, and does it cost extra to turn on?**
No extra fee — a changed opportunity is billed at the same per-row price as a new one ($0.0015 enriched / $0.0007 thin), so you only pay when there is actually something to see. Plain `watchLabel` only ever tells you about opportunities it has never delivered before; it stays silent forever about one it already sent you, even if that agency later extends the deadline, closes it early, revises the award range, rewrites eligibility, or turns a `forecast` into a real posted `synopsis`. Set `watchChanges: true` and each run also compares every already-delivered opportunity's `closeDate`/`docType`/`oppStatus`/`awardCeiling`/`awardFloor`/`lastUpdatedDate`/`applicantEligibilityDesc` against what it looked like last time; if any moved, the row is re-delivered tagged with `_watchChangeType` (which field(s) changed) and `_watchPrevious` (what they used to be, except `applicantEligibilityDesc` — only an 8-character fingerprint of that text is stored, never the full text, so its "previous" value is a fixed note rather than the old wording). The award/eligibility/last-updated fields only exist on the enriched detail record, so they're only watched when `enrich` is on (the default) — with `enrich: false`, `watchChanges` still catches `closeDate`/`docType`/`oppStatus`. Verified live: seeding a baseline, editing 2 opportunities' recorded closing date and doc type directly, then rerunning returned exactly those 2 rows with the correct change tags and nothing else — and a plain unchanged rerun after that returned 0 rows again; the award-ceiling/floor, last-updated-date and eligibility-fingerprint detection was verified the same way in a follow-up test (2 more opportunities mutated on those fields, correctly and only those 2 re-delivered with the right `_watchChangeType`). Existing watch labels created before this feature shipped work immediately; the first run under a newly-tracked field just starts detecting drift from that point forward rather than reporting an artificial backlog.

**Important: don't filter on the field you're watching for changes (`oppStatuses` catches most people).**
Change detection can only compare an opportunity that is still **in this run's match set** — so a filter on a field `watchChanges` tracks is self-defeating: the very change you're watching for is what removes the row from view, and you never hear about it. The default `oppStatuses` (`forecasted, posted`) hits this: an opportunity that goes **posted → closed** drops straight out of the default match set, so the closure alert never fires. Measured against the live API on 2026-09-26 — keyword `wildfire` returned 21 hits under the default statuses, **none** of them closed, while `oppStatuses: closed` returned 380 completely different hits, including two that closed within the previous month (ids `363103`, `363336`) and would have been sitting in a month-old watch baseline. The same trap applies to `closeDateFrom`/`closeDateTo`/`closesWithinDays` (a deadline moved outside your window vanishes — the deadline amendments this feature exists to catch), `minAwardAmount`/`maxAwardAmount` (a revised ceiling leaves the range) and `eligibilities` (a rewrite drops the category).

**What to do:** for each field you want alerts on, widen or drop the filter on *that field* in the watch query and filter your own copy of the rows instead — e.g. set `oppStatuses` to all four (`forecasted`, `posted`, `closed`, `archived`) to catch closures, and leave the deadline/award-amount filters off the watch label. **Widening costs nothing to backfill:** a label's first run on a new filter set is a free baseline (0 rows charged), so every historical opportunity the wider query newly matches lands in that baseline for free and is never charged; only genuinely new and genuinely changed opportunities are billed from then on. Each run that has a change-blind filter set says so in a log warning and lists them in `RUN_SUMMARY.watchChangeBlindFilters` (an empty array means nothing is blinding change detection), so a scheduled caller can assert on that field before trusting a quiet "no changes this run".

**How is `webhookUrl` different from Apify's own platform webhooks?**
Apify's platform webhooks are configured separately per Task/Actor via the Console or the Webhooks API — useful if you're already living in the Apify Console, but extra setup if you're calling this Actor's API directly and just want a completion ping. `webhookUrl` is a plain input field: set it on the run itself and it POSTs a JSON body (`actorRunId`, `defaultDatasetId`, `pushed`, `scanned`, `enrichedCharged`, `thinCharged`, a `summary` object identical to the `RUN_SUMMARY` record described above, and — if `watchLabel` is set — `watchNewCount`/`watchChangedCount`) once the run finishes and every row is already pushed and charged. It's best-effort: a slow or failing webhook only logs a warning, it never fails the run, changes the result set, or affects billing.

### Notes

Only public data from Grants.gov's official API is collected. Issues or feature requests: support@fetchsmith.com. Also available as a hosted API at https://fetchsmith.com

### Related guides

- [Grants.gov's search API has two opposite silent failures — and only one of them is safe](https://fetchsmith.com/blog/grants-gov-api-fails-open-and-closed) — a typo'd filter *value* returns zero rows; a typo'd filter *name* returns the whole unfiltered catalog at the same `errorcode: 0`. Measured both, and the guard this Actor now runs on every search page.
- https://fetchsmith.com/blog/grants-gov-federal-grant-opportunities-json-api
- https://fetchsmith.com/blog/nih-reporter-grants-json-api
- [Eight ways an "only new since last run" watch mode silently stops working](https://fetchsmith.com/blog/incremental-api-watch-mode-four-traps) — how `watchLabel` is built, and why a cheap id-only baseline still has to apply the award-amount filter.
- [We nearly charged our own buyers twice for rows they'd already paid for](https://fetchsmith.com/blog/watch-baseline-eviction-rebilling) — a capped watch-mode baseline can silently evict old-but-current ids on a high-volume run, re-delivering (and re-billing) rows already paid for. Reproduced on this Actor, closed with truncation tracking.
- [Eight government JSON APIs that need no key — and the specific way each one lies to you](https://fetchsmith.com/blog/free-government-data-json-apis-no-key) — how this API's silent-failure shape compares across all eight free government JSON APIs we scrape.

### Source code

https://github.com/Fetchsmith/fetchsmith/tree/main/actors/grants-gov-scraper

More tools: [fetchsmith.com/tools](https://fetchsmith.com/tools) — 19 HTTP-only Actors for public data sources, no browser required.

# Actor input Schema

## `keyword` (type: `string`):

Full-text search across the opportunity title and synopsis. Leave empty to return everything matching the other filters.

## `oppStatuses` (type: `array`):

Which opportunity statuses to include. Defaults to forecasted + posted (open for applications). "Closed" and "archived" are historical.

## `agencies` (type: `array`):

Grants.gov agency codes, e.g. "NSF", "DOD-AMC", "USDA-NIFA". Leave empty for all agencies. Codes are validated against the live agency list; an unrecognised code is dropped with a warning rather than silently returning zero results.

## `eligibilities` (type: `array`):

Restrict to opportunities open to these applicant types.

## `fundingCategories` (type: `array`):

Restrict to opportunities in these funding activity categories, e.g. Health, Education, Environment.

## `fundingInstruments` (type: `array`):

Restrict to these award types.

## `cfda` (type: `string`):

Restrict to one Assistance Listing (CFDA) number, e.g. "93.859" (the dot is optional — "93859" filters identically). Leave empty for all. Grants.gov does not reject an unusable number, it silently returns nothing — so if a cfda-filtered search comes back empty, this Actor re-checks the number against all four statuses and says in the log whether the cfda matched nothing at all or your other filters emptied the result (also in RUN\_SUMMARY as cfdaMatchesAnyStatus). Find numbers at https://sam.gov/content/assistance-listings.

## `oppNum` (type: `string`):

Look up a single opportunity by its exact funding opportunity number, e.g. "PD-25-275Y". When this or Opportunity numbers (batch) is set, all other filters are ignored.

## `oppNums` (type: `array`):

Look up multiple opportunities by their exact funding opportunity numbers in one run -- e.g. to enrich a list of numbers you already have. Combined with Opportunity number above if both are set (duplicates removed). Grants.gov's API has no batch lookup, so this makes one lookup call per number; each is still an exact match across all four statuses, same as a single Opportunity number. When set, all other filters are ignored.

## `sortBy` (type: `string`):

Sort by open date, close date, or opportunity number. An unsupported value silently returns zero rows on Grants.gov's own API (no error), so only these live-validated options are offered. Opportunity number is the only sort that is stable across runs -- use it if you page a large result set over several runs and need the order not to shift as grants open and close.

## `enrich` (type: `boolean`):

Grants.gov's search results carry only 11 thin fields (no award amounts, no eligibility text, no description). When on (default), each row is joined with a second call to fetch award ceiling/floor, eligibility text, funding instrument/category and the full synopsis, and is charged at the enriched rate ($0.0015/result). Turn off for fast sweeps of just the thin fields, charged at the cheaper thin rate ($0.0007/result) — the price follows the data, per row. Forced on automatically if minAwardAmount/maxAwardAmount is set, since the amount only exists in the detail record.

## `postedWithinDays` (type: `integer`):

Only return opportunities posted in the last N days (Grants.gov's own "Posted Date" facet, verified live to accept any positive day count, not just its 3/7/14/21-day preset buttons). Cheap way to do an incremental daily pull instead of re-scanning the whole index. Leave empty for no date restriction. Ignored when Opportunity number is set, and ignored if Posted from/to below is set (they'd otherwise double-filter in confusing ways).

## `postedFrom` (type: `string`):

Only return opportunities with an open (posted) date on or after this date. Unlike "Posted within the last N days" (which counts back from today), this is a fixed calendar date -- for pulling a specific historical window, e.g. everything posted in Q1. Grants.gov's own API has no server-side absolute-date filter, so this is applied client-side against each row's own open date (already present on every thin result row, no extra detail lookups needed). Leave empty for no lower bound. A value that is not a real calendar date in strict YYYY-MM-DD form stops the run with an error naming it, rather than being ignored -- dropping a date bound would widen the result set to every matching opportunity and charge you for the difference.

## `postedTo` (type: `string`):

Only return opportunities with an open (posted) date on or before this date. Same client-side filter as Posted from, using the same already-present open date field. Leave empty for no upper bound. A value that is not a real calendar date in strict YYYY-MM-DD form stops the run with an error naming it, rather than being ignored -- dropping a date bound would widen the result set to every matching opportunity and charge you for the difference.

## `closeDateFrom` (type: `string`):

Only return opportunities whose application deadline (close date) falls on or after this date. Use with "Deadline to" to answer the question grant seekers actually ask -- "what closes in the next 30 days?" -- instead of filtering on when something was posted. Applied client-side against each row's own close date (already present on every thin result row, no extra detail lookups). NOTE: forecasted opportunities have no firm deadline yet and Grants.gov returns an empty close date for them, as it also does for rolling/continuous announcements and RFIs -- those rows are dropped by this filter and counted separately in the run summary. Leave empty for no lower bound. A value that is not a real calendar date in strict YYYY-MM-DD form stops the run with an error naming it, rather than being ignored -- dropping a date bound would widen the result set to every matching opportunity and charge you for the difference.

## `closeDateTo` (type: `string`):

Only return opportunities whose application deadline (close date) falls on or before this date. Same client-side filter and same no-deadline exclusions as "Deadline from". Leave empty for no upper bound. A value that is not a real calendar date in strict YYYY-MM-DD form stops the run with an error naming it, rather than being ignored -- dropping a date bound would widen the result set to every matching opportunity and charge you for the difference.

## `closesWithinDays` (type: `integer`):

Only return opportunities whose application deadline falls within the next N days from today (resolves to today through today+N, same client-side filter and same no-deadline exclusions as "Deadline from"/"Deadline to"). Convenience for the most common cron use case -- "what's closing soon" -- without computing calendar dates yourself. Leave empty for no restriction. Ignored if Deadline from/to above is set (they'd otherwise double-filter in confusing ways).

## `minAwardAmount` (type: `integer`):

Only return opportunities whose award ceiling (the maximum a single award can pay) is at least this amount. Forces "Enrich" on, since the amount lives only in the per-opportunity detail record. Excludes: unposted "forecast" listings with no detail record at all (~3% of the index), AND opportunities whose detail record has no ceiling set at all -- Grants.gov spells this as the literal string "none", measured live at roughly a third to half of posted opportunities. This is common, not a rare edge case.

## `maxAwardAmount` (type: `integer`):

Only return opportunities whose award ceiling is at most this amount. Same exclusions as Minimum award ceiling: forces "Enrich" on and drops any opportunity with no usable ceiling (no detail record, or the detail record's ceiling is literally unset).

## `maxResults` (type: `integer`):

Stop after this many opportunities.

## `watchLabel` (type: `string`):

Optional. Name a saved search (e.g. "my-nsf-watch") and this run returns ONLY opportunities not delivered under that same label and filter set before, instead of the full match set every time. The first run for a label is a free baseline: it records what already matches and returns zero rows. Run it again later -- on a schedule, typically -- to get only what's new. The baseline is kept in your own Apify account (a named key-value store), keyed by label plus a fingerprint of your other filters, so changing a filter starts a fresh baseline instead of dumping previously-excluded opportunities as "new". Ignored when Opportunity number is set.

## `watchChanges` (type: `boolean`):

Only used together with Watch label. When on, a run also re-delivers an opportunity you already have if its closing date, forecast-vs-posted status, opportunity status (posted/closed/archived), award ceiling/floor, last-updated date or eligibility text has changed since you last saw it -- e.g. a deadline extension, a forecast turning into a real posted opportunity, an opportunity closing early, or a revised funding range. The last four are only watched when Enrich is on (the default), since they only exist on the enriched detail record. Charged at the same per-row price as a new opportunity. Each changed row is tagged with `_watchChangeType` (one or more of `closeDate`, `docType`, `oppStatus`, `awardCeiling`, `awardFloor`, `lastUpdatedDate`, `applicantEligibilityDesc`) plus the previous value(s) under `_watchPrevious`. IMPORTANT: do not also FILTER on a field you want change alerts on -- the change itself would drop the opportunity out of your match set and it could never be reported. In particular the default Opportunity statuses (forecasted + posted) hide every posted->closed transition: set all four statuses to catch closures. Widening is free to backfill (a new filter set gets a free 0-row baseline run). The run logs a warning and fills RUN\_SUMMARY.watchChangeBlindFilters whenever a filter is hiding changes this way. Off by default so existing watches keep their current behaviour.

## `webhookUrl` (type: `string`):

Optional. An http(s) URL to POST a small JSON summary to when the run finishes -- opportunities pushed, how many were newly enriched vs. thin, watch-label new/changed counts if Watch label is set, and the run's dataset ID so you can fetch the results. A convenience for callers who want a completion ping without setting up an Apify platform webhook (which needs separate Console/API configuration per Task, not per run). Best-effort: a failed or slow webhook is logged as a warning and never fails the run or affects charging -- it fires after every opportunity has already been pushed and charged. Leave empty to skip.

## Actor input object example

```json
{
  "keyword": "",
  "oppStatuses": [
    "forecasted",
    "posted"
  ],
  "sortBy": "",
  "enrich": true,
  "maxResults": 100,
  "watchChanges": false
}
```

# Actor output Schema

## `dataset` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("fetchsmith/grants-gov-scraper").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("fetchsmith/grants-gov-scraper").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 '{}' |
apify call fetchsmith/grants-gov-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fetchsmith/grants-gov-scraper"
        }
    }
}
```

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/Iso3mx8O6oBWJtrFr/builds/1T6dLWWcDpL1W9ZUW/openapi.json
