Nonprofit Grants.gov Database, Status Eligibility Award Details
Pricing
from $1.50 / 1,000 opportunity (enriched)s
Nonprofit Grants.gov Database, Status Eligibility Award Details
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.
Pricing
from $1.50 / 1,000 opportunity (enriched)s
Rating
0.0
(0)
Developer
Fetch Smith
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
19 hours ago
Last modified
Categories
Share
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/fetchOpportunityendpoints (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 defaultoppStatusesresult 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
attachmentsarray of the files the agency attached — the full NOFO PDF/DOCX, special notices, Q&A and amendment documents — each with a directdownloadUrl, plusfileName,description,mimeType,sizeBytes,folderName,folderTypeandpostedDate. 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. Codes are validated against all 712 live agency codes across every status — not just the 163 with a currently-open grant — so agencies whose grants are all closed or archived (ED,HHS-CDC,USAID,SBA, …) filter correctly too. An unrecognised code is dropped with a named warning, and a search where none of your codes is recognised fails fast rather than running unfiltered and charging you for every agency's rows. - Opportunity-number lookup ignores your other filters. Grants.gov ANDs
oppNumwith every other filter, including its own default status filter — looking up a closed or archived opportunity by its exact number normally returns nothing. SetoppNumand 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 singleoppNum. 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.postedWithinDaysfor 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/postedTofor 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") wherepostedWithinDays' relative-to-today window doesn't fit. If both are set,postedFrom/postedTowins andpostedWithinDaysis ignored (with a warning).closeDateFrom/closeDateTofilter 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% ofdocType: "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 leaveforecastedinoppStatuseswhile 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 ascloseDateFrom/closeDateTo), so a cron doesn't have to compute calendar dates each run. IfcloseDateFrom/closeDateTois also set, the absolute range wins andclosesWithinDaysis ignored (with a warning) — same precedence rule aspostedWithinDaysvspostedFrom/postedTo.minAwardAmount/maxAwardAmountfilter on award ceiling — forcesenrichon 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 ornull, 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. Don't pin a fixedpostedToon a watched query.openDateis assigned once when Grants.gov posts a synopsis under a givenidand is never revised in place — a genuinely revised posting ships under a brand-newidwith its ownopenDateinstead — so once the real calendar date passes a fixedpostedTo, no future opportunity can ever be dated on or before it again, and the watch goes permanently quiet while still logging "nothing new" every scheduled run.postedFromalone is safe to keep; only the upper bound is the trap. See the FAQ below.watchChanges— also catch a deadline extension, a status change, a funding-range revision, an eligibility rewrite, or a forecast turning real. Add this towatchLabeland 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) orapplicantEligibilityDescchanges since you last saw it — not just brand-new opportunities. The last 3 only get watched whenenrichis 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: 1on a cron and only pay for the handful of opportunities posted since yesterday, instead of re-scanning the whole index. - Deadline triage —
closeDateFrom/closeDateTo(orclosesWithinDays: 30for 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: 500000to surface only large awards, ormaxAwardAmountto find the small ones a single PI can realistically manage. - Grants-landscape research — filter by
agencies(parent codes expand to every sub-agency) andpostedFrom/postedToto reconstruct a fixed historical window, e.g. everything a department posted last quarter. - Enriching an existing list — set
oppNumfor one opportunity, oroppNumsfor 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+watchChangeson 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
watchChangesflag 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
{"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, ED, HHS-CDC, USDA-NIFA; validated against all 712 live codes (including agencies with only closed/archived grants), and parent codes are expanded to every sub-agency |
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)
{"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" },{ "number": "93.395", "title": "Cancer Treatment Research" },{ "number": "93.396", "title": "Cancer Biology 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)
{"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"])
{"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 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 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 84 listings with a comparable per-event price charge an Actor-start fee ($0.00005–$0.10) and 24 charge none, so a no-start-fee listing is a minority but not rare. Per-row prices run from $0.00 (two listings on Apify's FREE model) to $15.00, and 17 of those 84 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 springlike_meadowland/us-grant-opportunities-scraper (flat $0.001 + $0.00005 start) and jungle_synthesizer/grants-gov-crawler (flat $0.001 behind a steep $0.10 start fee) at $0.001/row flat. Two more in that group are actually TIERED, not flat, which an earlier version of this paragraph did not distinguish: shahidirfan/Grants-gov-Scraper runs $0.001 (FREE) down to $0.0008 (DIAMOND) behind a $0.0005 start, and chorelet/government-tenders-scraper runs $0.001→$0.0007 for the base tender row (its separate detail event is $0.002→$0.0014) behind a $0.00005 start — both still at or below our $0.0015 enriched rate at every plan either way, so the correction changes the wording, not the competitive position. Re-verified live 2026-10-03.
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 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. One listing beats even that: constant_quadruped/research-grant-aggregator queries NIH RePORTER, NSF Awards, Grants.gov and USASpending in one call and has pricingInfos: null — Apify's FREE pricing model, $0 per row at any volume. It's free but shallower on this niche specifically: no enrich/thin split (one flat record shape per row, not Grants.gov's own ceiling/floor + eligibility text + funding instrument/category detail fields), no Assistance Listing (CFDA) filter or validation, and no watch/change-detection mode — it unifies four sources at the cost of Grants.gov-specific depth. Pricing verified live 2026-10-02.
Five more rivals disclosed for completeness, found in a fresh 84-listing niche-size sweep — none undercuts us. scrapepilot/grant-foundation-opportunities-scraper genuinely reads Grants.gov itself (alongside fundsforngos.org and "any grant portal"), but as a thin 6-field export (grant ID, funder, amount, eligibility, deadline, link) with no agency/status/CFDA filters, enrich/thin split, attachments or watch mode, at $0.004/result plus a $0.05 Actor-start fee — 2.7x our enriched rate before the start fee is even counted, and it switched off a flat $7.99/month subscription to this per-event price on 2026-09-24. Two more share our own exact listing name, "Grants.gov Scraper": fortuitous_pirate/grants-gov-scraper advertises itself as "$4.38/1k" in its own title — $0.004375/result plus a $0.001 start, 2.9x our enriched rate — and parseforge/grants-gov-scraper runs a result/detail split like ours but tiered $0.0075→$0.007/result and $0.005→$0.00445/detail, 4.7x–5x our enriched rate even on its cheapest (DIAMOND) tier. aurumworks/us-federal-grant-scraper is a flat $0.009/result plus a $0.0005 start, 6x ours. signalcrawl/federal-grant-fit-finder is a scored-match product like fiery_dream/scholarship-intel above rather than a plain feed, at $0.002/result plus a $0.00005 start, 1.3x our enriched rate. Checked and deliberately left unnamed: pink_comic/irs-990-nonprofit-search (27 users, the single biggest listing this sweep turned up that we had not already priced) reads IRS Form 990 filings via ProPublica for charity/EIN lookup, not Grants.gov opportunities — a different data source for a different question ("is this nonprofit real and how is it funded" vs "what can I apply for"), so it is adjacent, not a head-on rival, despite name-dropping Grants.gov in its own listing copy. Pricing verified live 2026-10-03.
Nine more listings priced live 2026-10-04 after re-pricing all 84 matched listings end to end (not just the top 10 by users) — two of them genuinely undercut us and are disclosed here. martc03/grant-finder-mcp is an MCP server on Apify's FREE model — $0 per row at any volume — reading the same official Grants.gov search by agency, category, eligibility and status; like andrew_avina/grants-mcp it is an AI-assistant tool rather than a dataset product (no enrich/thin split, no CFDA validation, no watch/change-detection mode), but on price nothing can beat it. soilair/grants-gov-api is the closest head-on substitute this sweep found — it also pairs Grants.gov search2 with fetchOpportunity enrichment — and is tiered $0.0015 (FREE) down to $0.001 (GOLD/PLATINUM/DIAMOND) plus a $0.00005 start, so it matches our enriched rate on the lower plans and genuinely beats it on GOLD and above; we remain cheaper against its enriched rows at our $0.0007 thin rate, and it ships no watch/change-detection mode. vhsgreed/us-federal-contracts is a broader-scope aggregator (SAM.gov + USAspending + Grants.gov in one run) behind a $0.002 Actor-start fee, and its record event is tiered, not the flat $0.00125 an earlier version of this paragraph published: $0.00125 (FREE) / $0.00115 (BRONZE) / $0.00105 (SILVER) / $0.00095 (GOLD, PLATINUM, DIAMOND). The start fee still makes us cheaper on small runs, but the break-even is plan-dependent and the "roughly 8 rows per run" this paragraph used to quote was the FREE-tier figure only: against our enriched rate it is ~8 rows on FREE, ~6 on BRONZE, ~4 on SILVER and ~4 on GOLD and above, so a buyer on a paid plan crosses over about twice as early as we previously published. Our $0.0007 thin rate still beats it at every volume on every tier (its cheapest per-row rate, $0.00095, is above our thin rate before its start fee is even counted). Three more match our headline rate but cost more once the start fee is counted: brightpath-data/grants-gov-search ($0.0015 + $0.0001 start), gubidonius/grants-gov ($0.0015 + a steep $0.002 start) and upward_enterprises/grants-gov-opportunity-finder, which runs an enrich/thin split like ours and is also tiered rather than the flat "$0.003 full / $0.001 summary" an earlier version of this paragraph published: its full opportunity-detail row runs $0.003 (FREE/BRONZE) down to $0.0024 (GOLD+) and its opportunity summary row $0.001 (FREE/BRONZE) down to $0.0008 (GOLD+). The conclusion is unchanged and now verified tier by tier — it is dearer than us on both halves of the split at every plan ($0.0024 against our $0.0015 enriched, $0.0008 against our $0.0007 thin). Two listings that a headline-price scan reads as undercutters are not, and the correction is why the count above says 17 rather than 19: nimble_flash/grant-fit-scout carries a vestigial apify-default-dataset-item event at $0.00001 while its own README documents the real charge as qualified-opportunity at $0.10 per scored opportunity (67x our enriched rate), and adobeflex/grants-gov-lite prices search_run at $0.001 per run, not per row — its actual row event is notice_row at $0.002, dearer than us. Also checked and deliberately left unnamed: stefano_seggio/australia-grantconnect-monitor covers Australian GrantConnect (grants.gov.au) awards already paid, not US Grants.gov opportunities — a different country and a different question, matched only because its copy name-drops a "grants.gov" string. Pricing verified live 2026-10-04.
Re-verified live 2026-10-03 (full re-audit of all 17 named rivals, every event and tier, not just the headline price): zero price drift and zero user-count drift on every single one — solidcode, thoob, hridayrungta, andrew_avina, ayush.naa, springlike_meadowland, jungle_synthesizer, shahidirfan, chorelet, fiery_dream, alizarin_refrigerator-owner, constant_quadruped, scrapepilot, fortuitous_pirate, parseforge, aurumworks and signalcrawl all matched the numbers already published above, user count included. A fresh 15-term niche-size sweep still returns exactly 84 matching listings (re-confirmed again 2026-10-04), and all 10 of the niche's top-10-by-users listings are already named above or deliberately excluded (pink_comic) — no new top-of-niche rival to disclose. jungle_synthesizer/grants-gov-crawler's pricing entry dated effective 2026-10-04T09:05:27Z has now landed, re-verified live 2026-10-16: identical amounts ($0.10 start + $0.001/row) to the one it replaced, so no README change — just a note that the amounts never actually changed.
Three more listings priced live 2026-10-04 (cycle 1233) — none undercuts us. great_pistachio/grants-gov-scraper has no enrich/thin split and bills every row at a flat $0.01 plus a $0.00005 start, 6.7x our enriched rate and 14x our thin rate. preservable_mocha/us-federal-grants-aggregator is a flat $0.003/result plus a $0.00005 start, 2x our enriched rate and 4.3x our thin rate. caffein.dev/grants-actor (broader scope — queries NIH RePORTER and Duke Research Funding alongside Grants.gov in one call) charges $0.002 for its combined basic_result event (which is what a Grants.gov-only query bills) plus $0.008 for a separate duke_result event, behind a $0.00005 start — 1.3x our enriched rate and 2.9x our thin rate even on the cheaper of its two events.
Full 55-listing unnamed-tail sweep, live 2026-10-05 (cycle 1273) — the usual >=3-user cohort returned zero matches this round, so every one of the 55 unnamed listings was priced instead of applying that cut. A fresh 15-term sweep now finds 85 matching listings (up from 84). Four genuinely undercut our flat $0.0015 enriched rate, all only from the GOLD/DIAMOND tier up (none beats us below that), and all are 1–2-user listings: bakos_bence/grants-gov tiers its enriched result row $0.00249 (FREE) down to $0.001245 (GOLD/PLATINUM/DIAMOND) — cheaper than us from GOLD up, dearer below — behind its own real, non-negligible Actor-start fee ($0.01 FREE/BRONZE down to $0.003 GOLD+); datalayer/grants-gov-funding tiers its opportunity event (award range, cost-share flag, decoded eligibility — the same shape as our enriched row) $0.002 (FREE) down to $0.0014 (GOLD+), no start fee, undercutting from GOLD up only; publicrecords/govcon-opportunity-feed (broader SAM.gov+Grants.gov scope) tiers its opportunity-record event the same way, $0.002 down to $0.0014 (GOLD+), plus a near-zero $0.00005 start; and automation-lab/grants-gov-funding-opportunities-scraper tiers its item event $0.004692 (FREE) down to $0.0011424 — but only on DIAMOND, since PLATINUM is still $0.001632 (dearer than us) — behind a real $0.005 start fee. None of the four beats our $0.0007 thin rate at any tier. The other 51 of the 55 priced dearer at every tier. One pattern worth naming on its own: a single owner, nexgenwatch, accounts for 9 of the 55 — grant-funding-report, grants-qualification-mcp and seven separate us-grants-*-watch Actors, one apiece for a deadline amendment, an eligibility change, a forecast turning posted, a funding-range change, a brand-new opportunity, a cancellation and a document change — each billing $0.067–$0.10 per check, flat, with no per-row export at all. That is seven single-purpose listings covering almost exactly the six change types our own watchChanges flag detects in one Actor at the normal per-row price, live evidence for the README's existing "one Actor covers what some competitors ship as several separate single-purpose watch listings" claim. Pricing verified live 2026-10-05.
Full re-audit live 2026-10-06 (cycle 1320) — all 53 unnamed listings plus all 34 already-named rivals re-priced event by event and tier by tier. Two corrections to our own published prose, both made above. This paragraph's "zero undercutters among the unnamed tail" claim was wrong and is corrected in the 2026-10-07 paragraph below — it scored each rival on one number, its FREE tier, which can neither see a rival whose top tier reaches our rate nor weigh a flat row price against the start fee attached to it. A fresh 15-term sweep now finds 87 matching listings (up from 85 at cycle 1273 and 84 before that), of which 53 are not named in this README. The >=3-user cohort came back empty for the second audit running, so — per our standing method — the whole 53-listing tail was priced live rather than applying that cut. None of the 53 undercuts our $0.0015 enriched rate on any tier, and none is on Apify's FREE model. The single listing that a tier-scan flags as sub-$0.0015 is a false positive worth naming so it is not re-flagged later: tagadanar/us-grants-monitor (Grants.gov alerts + NIH award history) carries a $0.001 actor-start run start fee, not a row price — its real primary event, opportunity-found, is tiered $0.004 (FREE) down to $0.0028 (GOLD+), i.e. 1.9x–2.7x our enriched rate. On the named side there was no user-count drift and no per-row price drift on any of the 34, but two listings turned out to be tiered where we had published a flat number — vhsgreed/us-federal-contracts and upward_enterprises/grants-gov-opportunity-finder, both corrected in the paragraph above. The vhsgreed correction moves a real number in a buyer's favour to know about (its break-even against our enriched rate is ~4 rows on a paid plan, not the ~8 we published from its FREE tier), which is why it is called out rather than quietly patched. Pricing verified live 2026-10-06.
Full re-audit live 2026-10-07 (cycle 1356) — every tier of every listing in the unnamed tail, plus one-time start fees, scored instead of one headline number each. A fresh 15-term sweep now finds 89 matching listings (up from 87), of which 54 are not named in this README; the >=3-user cohort came back empty for the third audit running, so all 54 were priced live rather than applying that cut. 52 of the 54 quote a comparable per-row price: 49 are dearer than our $0.0015 enriched rate at every tier they offer, 2 reach it exactly at their top tier, and 1 genuinely undercuts it. None of the 54 beats our $0.0007 thin rate at any tier, and none is on Apify's FREE model.
The one undercutter: muzafferkadir/grants-gov-scraper. It bills a flat $0.001 per opportunity — a third below our enriched rate, with no tier ladder to climb — but attaches a $0.01 Actor-start fee we do not charge, so the comparison turns on rows per run rather than on the headline rate. Break-even is 20 rows: at 20 rows both cost $0.03, below that we are cheaper (10 rows: $0.015 against their $0.02), above it they are, and the gap widens with volume (1,000 rows: $1.01 against our $1.50). Our $0.0007 thin rate stays below their $0.001 at every volume, start fee aside. By its own description it returns title, agency, dates, Assistance Listing (CFDA) number and detail links, with no enrich/thin split, no CFDA validation and no watch/change-detection mode. It has charged this price since 2026-08-11, so it was live and in the tail during cycle 1320's sweep — publish the crossover, as this README already does for vhsgreed/us-federal-contracts and alizarin_refrigerator-owner, rather than letting a start fee stand in for a verdict.
A pricing shape none of our price checks can see: second_coming/gov-contract-monitor. Its only charge event is a scan flagged one-time at $0.02 per run, with no per-row event at all — so a run's cost does not move with the number of rows it returns. That makes it the cheapest option in this niche for any sizeable pull: it crosses under our enriched rate at roughly 14 rows per run and under our thin rate at roughly 29, and at 1,000 rows it is $0.02 against our $1.50. It is invisible to bin/check-price-superiority by construction, which skips a listing with no recurring event, and to any scan that reduces a rival to a per-row number. Scope is broader and shallower than ours: SAM.gov contract opportunities plus Grants.gov grants, filtered by keyword, agency, value and posting date, with no enrich/thin split, no CFDA validation and no watch/change-detection mode — and a flat per-run price means no per-row invoice to reconcile against the dataset. Pricing verified live 2026-10-07.
Two listings reach our flat rate at their top tier and neither beats it — the case cycle 1320's FREE-tier-only scan could not have seen. fetch_cat/grants-gov-opportunities-scraper tiers its item event $0.003 (FREE) / $0.00285 (BRONZE) / $0.00255 (SILVER) / $0.00225 (GOLD) / $0.0018 (PLATINUM) down to $0.0015 (DIAMOND) with no start fee, and logiover/grants-gov-scraper tiers $0.0025 (FREE) / $0.00225 (BRONZE) / $0.002 (SILVER) down to $0.0015 (GOLD, PLATINUM and DIAMOND) behind a $0.00005 start. Both are 1.7x–2x our rate on FREE and converge on it, not past it, at the top; our $0.0015 is the same number without a plan to qualify for, and our thin rate stays below both everywhere. thirdwatch/grants-gov-scraper runs the same pattern but stops short — $0.004 (FREE) down to $0.002 (GOLD+) plus a $0.00005 start, dearer than us on every tier.
Three notes from the same sweep. quarterly_jingo/grants-gov-scraper advertises "No Login, $4.38/1k" in its own listing title and bills exactly that — $0.004375 per result plus a $0.001 start, 2.9x our enriched rate — so its branding is honest about its price, which is worth recording because a title that quotes a rate is not always the rate the platform charges. flamboyant_liner/grants-opportunity-monitor is a delta-only product (new or changed opportunities only, daily schedule) billing two events with no primary flag between them, $0.005 per run plus $0.01 per opportunity, matching the "$10 per 1,000 alerts" in its own copy — 6.7x our enriched rate and 14x our thin rate, though its per-run row count is small by design. And the single-owner pattern this README named at cycle 1273 has grown: nexgenwatch now accounts for 10 of the 54 unnamed listings — a grant-funding report, an eligibility-screening MCP server and eight separate us-grants-*-watch Actors, one apiece for a new opportunity, a cancellation, a deadline amendment, an eligibility change, a funding-range change, a document change, a forecast turning posted and a general opportunity change. The eight watch listings bill $0.0201–$0.50 per check flat, tier-dependent, with no per-row export; the report listing bills $10.05–$15.00 a run and the MCP server $0.05. That is eight single-purpose listings covering change types our own watchChanges flag detects in one Actor at the normal per-row price. Pricing verified live 2026-10-07.
Full re-audit live 2026-10-09 (cycle 1461) — every charge event per rival scored this time, not just the one event our own unit_price() tool would select, closing this niche's slice of the steam-reviews-scraper blind spot (a multi-event rival's cheap secondary event can hide under a dearer primary one). A fresh 15-term sweep now finds 90 matching listings, of which 46 are not named in this README (down from 54 at cycle 1356 as more crossed into named territory); the >=3-user cohort again came back thin, so all 46 were scanned event-by-event and tier-by-tier. Result: still only the one undercutter already named above (muzafferkadir), and one new false positive worth recording so it is not re-flagged later. neverempty/grants-gov-opportunities-monitor carries a search-checked event flagged "Monitoring check" at a flat $0.0005 across every tier — below both our rates — but its own description says it only fires "for a run that read Grants.gov... and found nothing new," i.e. a per-POLL charge on a quiet run, not a per-opportunity price; its real primary event, grant-returned, is tiered $0.005 (FREE) down to $0.0035 (GOLD+), 2.3x–3.3x our enriched rate and dearer than our thin rate at every tier. Same container-noun shape, and same owner handle, as the neverempty/steam-reviews-price-monitor false positive this fleet first documented on a different Actor's niche (cycle 1452) — a *-checked/monitoring-style event name is now a standing reason to read the full event description before scoring it as a row price. nexgenwatch still accounts for 8 of the 46 unnamed listings (the us-grants-*-watch family already described above). No README price correction needed elsewhere; own ladder re-verified live first via check-own-price-freshness (24/0 fleet-wide, no drift). Pricing verified live 2026-10-09.
Full re-audit live 2026-10-16 (cycle 1797) — composition churn only, no new undercutter. A fresh 15-term sweep now finds 103 matching listings (up from 90 at cycle 1461), of which 59 are not named in this README (up from 46); the >=3-user cohort returned 4 listings this round (the prior three audits in a row came back empty), all priced live. moving_beacon-owner1/grants-gov-opportunity-scraper (3 users) bills a flat $0.00999/result, scrapemint/grant-opportunity-finder (3 users) a flat $0.01 (first 2 rows free), and redfoxxie/official-us-federal-grant-opportunities (3 users) a flat $0.005 — all 3.3x–6.7x our enriched rate. pink_comic/grants-gov-opportunities (3 users) — a different listing from the same owner's already-excluded irs-990-nonprofit-search — genuinely reads Grants.gov itself but bills $0.002/result plus a small start fee, 1.3x our enriched rate. Two more listings were checked only because the tool flags same-owner handles already named elsewhere: publicrecords/grants-gov-scraper (1 user, distinct from the already-named publicrecords/govcon-opportunity-feed) now bills a tiered Result row event, $0.004 (FREE) down to $0.0028 (GOLD+) — still 1.9x our enriched rate even at its cheapest tier; pink_comic/federal-audit-clearinghouse-single-audit-data (1 user) is a FAC single-audit/KYB product, not a Grants.gov opportunity feed, same different-shape ruling as its sibling listing. nexgenwatch still accounts for 8 of the 59 unnamed listings (the us-grants-*-watch family already described above) — no new listing from that owner. Own ladder re-verified live first via check-own-price-freshness (24/0 fleet-wide, no drift); fleet-wide check-price-superiority the same cycle found 0 undisclosed named-rival prices anywhere, confirming muzafferkadir remains the only disclosed undercutter. No README price correction needed. Pricing verified live 2026-10-16.
Full re-audit live 2026-10-23 (cycle 2134) — the niche grew sharply again: a fresh 15-term sweep now finds 124 matching listings (up from 103 at cycle 1797), of which 65 are not named in this README (up from 59); all 65 were priced live event-by-event and tier-by-tier. 11 real new rivals found, the biggest single haul since the original 17-listing sweep. 2 beat us at every row with no meaningful start fee: grit-77/grants-gov-funding-opportunities (2 users) bills a flat $0.00001 per opportunity with no Actor-start fee at all — the cheapest named rival in this niche after the three $0 FREE-model listings already disclosed above, 150x under our enriched rate; thriftykiwi/us-federal-grants-scraper bills a flat $0.001 with no start fee, cheaper than us from row 1. 2 more are cheaper from row 1 in practice because their start fee is negligible: opendatalabs/grants-gov-monitor tiers its enriched-equivalent opportunity event $0.0012 (FREE) down to $0.00102 (GOLD+) behind a $0.00005 start (crossover under 1 row on every tier); owlabee/gov-opportunity-feed (a broader SAM.gov + USAspending + Grants.gov combo) bills its primary opportunity-scraped row at $0.001 behind the same $0.00005 start, though its separate opportunity-detail enrichment event costs $0.002 on top — a thin pull is genuinely cheaper there, but a full detail pull ($0.001 + $0.002 = $0.003) is double our enriched rate. 4 undercut us only once a real start fee clears: automation_craft/grants-gov-scraper tiers $0.00069 (FREE/BRONZE) down to $0.00049 (GOLD+) behind a $0.0025 start — crossover ~3 rows on FREE, ~2.5 on GOLD+; one_dollar_store/grants-gov-opportunities-scraper (its own title reads "$1/1K") bills a flat $0.001 behind a $0.001 start, crossover 2 rows; and the same-owner pair amygraphics/us-federal-grants and amygraphics/grants-gov-monitor each bill a flat $0.001 behind a $0.001 start, crossover 2 rows apiece. 2 are tiered partial undercutters, cheaper only on upper plans: thequietstack/grants-gov-monitor ties FREE at $0.002 down to $0.0014 at GOLD+ (dearer below GOLD, cheaper from GOLD up) behind a near-zero $0.00005 start; publicmoney/grants-gov-scraper ties our enriched rate exactly at BRONZE ($0.0015), undercuts it at SILVER/GOLD/PLATINUM and ties our $0.0007 thin rate exactly at DIAMOND, dearer only at FREE ($0.002), behind a $0.00005 start. 1 genuine undercutter was hiding behind an ambiguous no-primary-flag event list: badboxx_newlife/sam-gov-grants-gov-scraper (SAM.gov + Grants.gov combined) bills a single result event at $0.0009 for either source behind a real $0.005 start fee — 40% below our enriched rate per row once past the break-even, crossover ~8 rows. 3 more ambiguous multi-event listings were hand-read and ruled out, all dearer than us on their Grants.gov-specific event: transparent_meteorite/federal-grants-contracts bills Grants.gov rows via opportunity-returned at $0.002 (its award-returned event, $0.0015, only fires for USAspending award rows, a different data type, and it carries a future pricing change effective 2026-10-30 not yet live); kempt_horizon_yzk/grants-gov-opportunities runs a summary/detail split at $0.002/$0.004, both dearer than either of our rates; signalscouter/us-federal-contracts-signals bills its Grants.gov-specific grant-result event at $0.002, also dearer. The remaining 51 of the 65 priced dearer than both our rates at every tier they offer, and none of the 65 is on Apify's FREE model. nexgenwatch still accounts for 8 of the niche's listings (the us-grants-*-watch family already described above) — no new listing from that owner this pass. Own ladder re-verified live first via check-own-price-freshness (25/0 fleet-wide, no drift). Pricing verified live 2026-10-23.
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 and listed in RUN_SUMMARY.unknownAgencies; RUN_SUMMARY.agencyCodesQueried tells you how many codes the expansion actually sent.
Can I filter by an agency that only has closed or archived grants? (fixed 2026-10-10)
Yes, and this was wrong before 2026-10-10 — re-run any historical agency search you did before that date. Grants.gov's agency list is scoped to the statuses you search, and its default is open grants only, so the validator used to recognise just 163 agency codes out of the 712 that really exist. Big ones were missing entirely: ED (Department of Education, 1,673 opportunities), HHS-CDC (1,728), USAID (763), SBA (253). Worse, a code the validator does not recognise is dropped, and an empty agency filter means "every agency" — so {"agencies": ["ED"], "oppStatuses": ["archived"]} quietly returned all 73,376 archived opportunities instead of ED's 1,600, billed per row. Two related bugs were fixed in the same build: parent codes under-expanded for the same reason (DOS resolved to 40 of its real 219 sub-agencies), and any parent that Grants.gov also lists as a sub-agency of itself — 15 of 46, including DOD, DOS, USDA, USAID, VA — had its expansion collapsed back to the bare parent code, which matches almost nothing. Measured: {"agencies": ["DOD"], "oppStatuses": ["archived"]} returned 114 rows before the fix and 4,231 after. Now: the agency list is loaded across all four statuses, parent expansion always wins over the self-listed sub-agency entry, and a search where none of your codes is recognised fails with a named error instead of running unfiltered and charging you for it (a failed run costs nothing).
Can I filter by a parent agency with hundreds of sub-agencies, like DOS?
Yes. Grants.gov's API has an undocumented cap on the agency list — a 193-code list works, a 194-code list returns hitCount: 0 with errorcode: 0 and "Webservice Succeeds", a silent empty result rather than an error (deterministic, 3/3 runs) — and DOS expands to 219 sub-agency codes, past it. Since 2026-10-11 the Actor splits a long agency list into several sub-searches (at most 100 codes / 900 characters each) and merges the results, so {"agencies": ["DOS"]} returns its real 7,639 matches across all four statuses. Between 2026-10-10 and 2026-10-11 that input failed with a named error instead; before 2026-10-10 it returned zero rows that looked like a genuine "no grants match", so re-run any DOS search you did before 2026-10-11. Chunking is safe because agency membership is exclusive — one opportunity carries exactly one agency code — which is measured, not assumed: chunked hitCounts are exactly additive (USDA 84 codes: 3,001 whole = 2,426 + 575; USAID 80: 3,074 = 1,896 + 1,178; DOD 75: 4,495 = 3,446 + 1,049), and a row-level check on a full USDA/posted walk returned the same 25 opportunity ids as the single-query walk with zero cross-chunk overlap. Two things to know: RUN_SUMMARY.agencyChunks tells you how many sub-searches ran (1 = not split, the normal case), and when it is above 1, sortBy is applied by Grants.gov within each sub-search rather than across the merged set — the rows are the same either way, but sort them yourself if you need a global order. USDA (84 codes / 1,213 chars) and DOD (75 / 986) also split today; USAID (80 / 795) and everything else run as a single query.
I named a specific DOT sub-agency code and got 0 rows — is that a bug?
Probably not. Ten of Grants.gov's own 712 agency codes contain a space (DOT-FAA-FAA ARG, DOT-FAA-FAA COE and 7 similar DOT/FAA codes) — real, recognised entries on Grants.gov's own agency list, not typos. Verified live (2026-10-11): 9 of those 10 return hitCount: 0 with no error when queried alone across all four statuses, i.e. a code that is validated and accepted but genuinely has no opportunities on file right now. Only DOT-FTA - TPM currently has live matches (197 across all statuses). This is harmless when you filter by the parent DOT or DOT-FAA — the other sub-agencies' rows still come through — but if you name one of the 9 directly, a 0-row result is Grants.gov's own data, not a filter failure.
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
{"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. watchDeadDateBound (null unless watchLabel is set) is true when a fixed postedTo is combined with watchLabel — see "why has my watch stopped finding anything new" above.
The run SUCCEEDED and returned 0 rows — where do I find out why?
On the run itself: the run status message (Console run detail, or statusMessage on GET /v2/actor-runs/<runId>) now always says which of the three zero-row cases you hit, so you do not have to read the log to tell them apart. (1) Watch mode, nothing new — watchLabel is set and every matching opportunity had already been delivered on an earlier run; the message names the label and how many already-seen matches it skipped. That is the expected result most of the time, and you were charged $0. (2) An exact opportunity-number lookup missed — oppNum/oppNums matched nothing; the message lists the number(s) that had no match, so check them on grants.gov/search-grants. (3) A plain search with no matches — if no cfda was set (or its own check couldn't run), the message lists the six causes in order of how often they are the real one (filters are ANDed; oppStatuses defaults to open/upcoming only; an unrecognised agency code is dropped, not guessed at; posted/close date windows are hard AND filters; the award-amount filter also excludes rows with no detail record; an unusable cfda returns nothing rather than an error). If cfda was set, the message skips that generic list and states the cfda verdict itself instead (matches nothing at all, or is usable and your other filters emptied it) — see the next question. A zero-row run is a complete, correct result, so it finishes SUCCEEDED and RUN_SUMMARY.complete is true with incompleteReason: null — do not read 0 rows as a failure. Before build 0.1.66 this explanation existed only as a log warning and the status message was empty, which made a genuinely-empty search indistinguishable from a broken run to anything reading the API.
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 (since build 0.1.68) the run status message itself states outright which case you're in — not just the log — 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. 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 ids 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.
I set watchLabel and a fixed postedTo — why has my watch stopped finding anything new?
Because the two don't mix. postedTo is applied client-side as an upper bound on each row's own openDate — the exact field that decides whether an opportunity is new enough to exist yet. openDate is assigned once when Grants.gov posts a synopsis under a given id and is never revised in place (a genuinely revised posting ships under a brand-new id with its own openDate instead — see the republished-row FAQ above), so once today's real calendar date passes your fixed cutoff, the filter can never again admit an opportunity posted after it. This run logs a warning (and sets RUN_SUMMARY.watchDeadDateBound: true) when it detects the combination. Drop postedTo from a watched query (postedFrom alone is fine, and postedWithinDays is always a rolling window so it's never affected) or drop watchLabel if you actually want a one-off historical window instead of an ongoing alert.
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
- Does a watchChanges deadline alert actually fire? We seeded 711 opportunities and forced it to find out — a live baseline, one deliberately altered stored snapshot, and the exact
_watchChangeType/_watchPreviousJSON the next run returned, plus the change-blind-filter warning firing live on this Actor's own defaultoppStatuses. - Grants.gov's search API has two opposite silent failures — and only one of them is safe — 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 — how
watchLabelis 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 — 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 — 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 — HTTP-only Actors for public data sources, no browser required.