Skip to article

Federal grants data · API migration

Grants.gov API migration: Search2 to Simpler.
Grants.gov.

Migrate Grants.gov Search2 integrations to the Simpler.Grants.gov API with field mapping, pagination, XML reconciliation and a tested dual run.

Published August 7, 202624 min readBy DanielReviewed by Alexandra

Build new opportunity search on POST https://api.simpler.grants.gov/v1/opportunities/search, but do not delete the classic reader because of a deadline that Grants.gov has not published. Preserve both the Simpler UUID and the legacy numeric opportunity ID, dual-read matched populations, and use the daily Grants.gov XML extract as a separate bulk reconciliation path.

The timing is real. Simpler Search became the default funding-opportunity search in early 2026, and the official product roadmap says the Simpler.Grants.gov experience becomes the default Grants.gov experience in 2027. The modern API is live now. But the roadmap describes the user experience; it does not announce a retirement date for search2 or fetchOpportunity.

That distinction changes the migration plan. This is a measured contract transition, not an emergency endpoint rename.

The published Grants.gov transition
  1. Early 2026

    Simpler Search becomes default

    The new search experience becomes the primary way people find funding opportunities.

  2. Mid-2026

    The Simpler experience expands

    The roadmap targets broader application coverage and budget-form pilots while the public opportunity API continues to evolve.

  3. Now

    Run both data contracts

    Build the Simpler reader, preserve the official legacy identifiers, and reconcile it with Search2, fetchOpportunity, and the daily XML extract.

  4. 2027

    The user experience converges

    The roadmap says Simpler.Grants.gov becomes the default Grants.gov experience for all users.

The roadmap describes the product experience. It does not publish a retirement date for the classic Search2 or fetchOpportunity APIs.

Grants.gov API migration: the short answer

Use this implementation sequence:

  1. Define the population: opportunity statuses, agencies, Assistance Listings, funding categories, posted dates, and the exact retrieval cutoff.
  2. Treat Simpler.Grants.gov, classic Grants.gov REST endpoints, and the daily XML export as different access contracts over related source data.
  3. Generate a Simpler API key, keep it server-side, and send it only in the X-API-Key header.
  4. Start Simpler search at page_offset: 1, send an explicit page size and deterministic sort, and reconcile each page to pagination_info.
  5. Use search for structured opportunity data; call GET by UUID or legacy numeric ID when attachments or a record-specific retrieval are required.
  6. Keep search2 and fetchOpportunity running against matched filters during the parallel period; both remain public without authentication.
  7. Retain opportunity_id, legacy_opportunity_id, and opportunity_number as separate fields.
  8. Use Simpler extracts when search could hit 10,000 results, and use the daily Grants.gov XML archive as a separate bulk representation for bootstrap and reconciliation.
  9. Store raw responses before normalization, fingerprint the source schemas, and classify every mismatch.
  10. Switch production readers only after population, identity, dates, funding, eligibility, details, attachments, and failure behavior pass written gates.

This is a federal financial-assistance data migration, not public procurement, tenders, bids, or contract-award data. It does not turn Grants.gov into a database of grant awards, recipients, obligations, or payments. Those outcome records belong to federal spending and agency award systems, not an opportunity-search pipeline.

Separate the published roadmap from an API deadline

The official transition has three relevant facts:

  • Simpler Search is already the default funding-opportunity search.
  • Simpler.Grants.gov is expanding through 2026.
  • The roadmap targets 2027 for the Simpler experience to become the default Grants.gov experience for everyone.

None of those statements says that the current Grants.gov search2 or fetchOpportunity endpoints will stop working on a particular day. The current Grants.gov API guide still documents both endpoints, and their endpoint pages still say authentication is not required. Do not label them deprecated unless Grants.gov publishes that status.

At the same time, waiting for an endpoint shutdown would waste the safest migration window. The Simpler developer page says its REST API can search the full funding-opportunity catalog, perform fielded search, paginate and sort large result sets, and retrieve structured detail for one record. Its live OpenAPI document describes the service as stable for everyday use while still under active development and promises versioning with advance notice for breaking changes.

The practical conclusion is:

Build and test the new reader now. Keep the old reader as evidence until the new route is accepted and an official source decision justifies retirement.

This avoids two opposite mistakes: treating the modern API as experimental and unusable, or treating the classic API as already scheduled for deletion.

Choose Simpler, Search2, XML, or a normalized dataset

There is no universally best Grants.gov access route. Choose by the job the data must perform.

Choose the access path by job
01

New structured integration

Simpler.Grants.gov v1 API

X-API-Key

Structured opportunity data in search; GET supplies attachments and a record-specific representation

02

Full Simpler catalog

POST /v1/extracts

X-API-Key

Discover published opportunities_json or opportunities_csv files when a search could reach the 10,000-result cap

03

Classic compatibility

Search2 + fetchOpportunity

No authentication

Numeric legacy ID and classic response shapes; useful as a parallel-run witness

04

Daily bulk baseline

Grants.gov XML extract

Public file

Once-daily database export for bootstrap, replay, and separate-route reconciliation

05

Ready-to-use active feed

WebTruffle federal grants dataset

Public download

Normalized forecasted and posted opportunities in CSV, JSON, and JSONL

Use Simpler.Grants.gov for a new structured search integration

The v1 read API has the cleanest forward-looking contract. It exposes:

  • keyword and fielded opportunity search;
  • filters for status, agency, top-level agency, applicant type, Assistance Listing number, funding category, funding instrument, dates, cost sharing, estimated funding, award floor, award ceiling, and expected awards;
  • explicit one-based pagination and up to five sort rules;
  • a Simpler UUID plus the numeric legacy ID;
  • structured opportunity detail and attachments; and
  • JSON or CSV responses.

The tradeoff is ownership. You must manage a key, pages, rate limits, detail calls, schema changes, raw evidence, retries, and reconciliation.

The official Simpler search guide says search returns the same opportunity data as GET except attachments and that search data is cached hourly. Use GET when attachments or a record-specific retrieval are required; do not spend one detail call per hit by default.

Use Simpler extracts for the full modern catalog

One Simpler search returns at most 10,000 opportunities. The official guide warns that matches beyond that boundary may be culled. A reported population at the cap is therefore a possible truncation signal, not proof of completeness.

Narrow interactive searches into explicit non-overlapping partitions. When the job needs the complete published catalog, call POST /v1/extracts to list available opportunities_json and opportunities_csv files, then download and validate the selected artifact. The extract documentation describes these as complete opportunity exports.

Keep Search2 for compatibility and comparison

The current POST https://api.grants.gov/v1/api/search2 endpoint returns lightweight hits using the classic request and response shape. POST https://api.grants.gov/v1/api/fetchOpportunity returns a richer record for a numeric opportunity ID. The Search2 and fetchOpportunity pages both state that they do not require authentication.

That makes the pair convenient for prototypes and valuable during a parallel run. It does not make the search response a complete, stable data product. The classic contract uses different field names, display-oriented dates, nested detail structures, and a success envelope that must be checked separately from HTTP status.

Use the XML extract for a daily bulk baseline

Grants.gov says its database is exported to an XML file once per day. That is a strong route for a bulk bootstrap, retained daily snapshot, or comparison through a separate representation of the same Grants.gov database. It is less suitable for a small interactive query because the client must download, validate, parse, filter, and store the file.

The XML extract page listed a 77.9 MB archive for August 6, 2026. Treat that as a point-in-time capacity observation, not a fixed file-size promise. Measure the actual response, archive members, decompressed bytes, record count, and parse time on every run.

Use a normalized dataset when the connector is not the product

If the decision depends on current opportunity data rather than owning three source integrations, use the free US federal grants dataset. It publishes a daily snapshot of active forecasted and posted opportunities in CSV, JSON, and JSONL, with source IDs, dates, eligibility, funding estimates, change fields, and official source links.

For a tested post-download workflow, continue to Federal grants data with Python. It verifies a pinned release, screens applicant-type labels, builds posted-deadline and funding-estimate watchlists, and compares two active snapshots without treating exits as cancellations.

That dataset deliberately excludes closed and archived opportunities, grant awards, recipients, and payments. Review its edition, schema, checks, and limitations before substituting it for a broader historical requirement.

Get and protect a Simpler.Grants.gov API key

The Simpler read API requires either an authenticated platform token or a developer key. For an external server integration, create a named API key through the Simpler.Grants.gov developer workflow and send it in:

X-API-Key: YOUR_API_KEY

The current developer guidance publishes two operational limits:

  • 60 requests per minute per key by default; and
  • 10,000 requests per day per key by default.

It also says an API key is automatically disabled after 30 days without use. Higher-throughput production integrations can contact the Simpler team, but a client must not assume an increase before it is granted.

Store the key in a server-side secret manager or protected environment variable. Do not put it in browser code, source control, notebooks, copied examples, request manifests, observability attributes, or error messages. Simpler uses a header, so there is no reason to place the key in a logged URL.

Budget the daily allowance before backfilling. One accepted opportunity can consume:

  • a search-page request;
  • a detail request;
  • retries after eligible transient failures;
  • validation queries; and
  • later re-reads during the parallel run.

At a 5,000-row search page, detail calls—not pagination—can dominate the allowance. Decide which downstream decisions truly need full details and attachments, then schedule the rest within an explicit budget.

The modern public API is read-only today. The developer page says public write operations do not support applying for funding, posting opportunities, or creating organization projects. Do not design an application-submission workflow around read credentials.

The current Simpler.Grants.gov read contract

Production base URL

https://api.simpler.grants.gov

The official description calls the API stable for everyday use while still under active development. Pin tests to the OpenAPI document instead of assuming the shape is frozen.

Reader boundary

Supported now: opportunity search, sorting, pagination, and structured detail retrieval.

Not supported publicly: applying for funding, creating opportunities, or other write operations.

01

Authentication

X-API-Key

Keep the key in a server-side secret store; inactive keys are disabled after 30 days.

02

Search

POST /v1/opportunities/search

Send JSON filters and pagination. The request requires a pagination object.

03

Page number

page_offset: 1

The first page is 1, not 0. Reconcile every response to pagination_info.

04

Page size

1–5,000

The August 7 OpenAPI contract allows up to 5,000 rows; keep a runtime contract test.

05

Search result cap

10,000

Partition a search below the documented cap or use the extracts endpoint for the full catalog.

06

Details

GET /v1/opportunities/{id}

Pass a Simpler UUID or legacy integer to the same URL pattern; GET adds attachments.

07

Default limits

60/min · 10,000/day

Budget search, detail, retries, and validation calls against the per-key allowance.

Make the first Simpler.Grants.gov search request

The production search call is:

POST https://api.simpler.grants.gov/v1/opportunities/search

Send JSON and begin at page 1:

curl --request POST \
  --url https://api.simpler.grants.gov/v1/opportunities/search \
  --header "Content-Type: application/json" \
  --header "X-API-Key: YOUR_API_KEY" \
  --data '{
    "filters": {
      "opportunity_status": {
        "one_of": ["forecasted", "posted"]
      },
      "post_date": {
        "start_date": "2026-08-01"
      }
    },
    "pagination": {
      "page_offset": 1,
      "page_size": 100,
      "sort_order": [
        {
          "order_by": "opportunity_id",
          "sort_direction": "ascending"
        }
      ]
    }
  }'

The request contract currently requires pagination. Its page_offset starts at 1, and page_size accepts 1 through 5,000. The OpenAPI schema defaults sorting to opportunity ID descending if sort_order is omitted, but production collectors should send the sort explicitly.

The 5,000 value is a page-size limit, not a completeness guarantee. The search guide separately caps one search at 10,000 opportunities and warns that additional matches may be culled. If total_records reaches that boundary, narrow the population or use POST /v1/extracts.

The JSON response places opportunity rows in data and page evidence in pagination_info:

{
  "status_code": 200,
  "message": "Success",
  "data": [
    {
      "opportunity_id": "123e4567-e89b-12d3-a456-426614174000",
      "legacy_opportunity_id": 12345,
      "opportunity_number": "ABC-123",
      "opportunity_title": "Example funding opportunity",
      "opportunity_status": "posted",
      "agency_code": "US-ABC",
      "agency_name": "Department of Examples"
    }
  ],
  "pagination_info": {
    "page_offset": 1,
    "page_size": 100,
    "total_pages": 4,
    "total_records": 327,
    "sort_order": [
      {
        "order_by": "opportunity_id",
        "sort_direction": "ascending"
      }
    ]
  }
}

The values above are illustrative. Contract-test the live field inventory and types before accepting a run.

Treat CSV as another representation, not a completeness shortcut

The current OpenAPI contract offers two CSV paths:

  • send "format": "csv" to the main search endpoint to receive the requested page with the same pagination and sort behavior; or
  • call POST /v1/opportunities/search/csv for the current bulk CSV route, which the schema describes as up to 5,000 rows with a fixed post_date descending sort.

The dedicated search CSV route is limited to 5,000 rows. Use it for a bounded export; use POST /v1/extracts to discover a complete opportunity JSON or CSV extract. Preserve the extract metadata, download URL, creation time, type, bytes, checksum, row count, and schema fingerprint.

Paginate and reconcile the Simpler API

Pagination is complete only when the collector proves which pages it received and how those pages relate to the source totals. A loop that stops after len(data) < page_size can finish quietly on a changed or malformed response.

Pagination cannot cross the documented 10,000-result search ceiling. Treat any population at that boundary as potentially incomplete even when every returned page reconciles. Partition below the cap or move the full-catalog job to Simpler extracts.

For each request:

  1. save the redacted request body, response bytes, retrieval time, HTTP status, content type, and response hash;
  2. verify pagination_info.page_offset and page_size match the request;
  3. record total_pages and total_records from every page;
  4. hash the ordered list of opportunity_id values and reject a repeated non-empty page;
  5. retain duplicates as page evidence before emitting one normalized current record;
  6. stop only when the page is empty or the accepted page number reaches the reported total pages; and
  7. compare collected rows and unique IDs with the totals observed throughout the walk.

The public contract does not promise snapshot isolation while a multi-page search is running. A record added or changed during the walk can move page boundaries. Deterministic sorting reduces ambiguity, but it does not freeze the source.

For critical populations, run a second matched walk after the first, compare the ordered IDs and record versions, and accept only when the result converges or every difference is explained. Short date or agency partitions are cheaper to replay than one broad catalog query.

The collector below keeps the page contract, source evidence, totals, and retry policy visible for one bounded posted partition. Collect forecasted records through a separate bounded strategy because a planned opportunity may not have a source post date; use agency partitions or the complete extract instead of forcing forecasts through the posted-date filter.

import hashlib
import json
import os
import random
import time

import requests

ENDPOINT = "https://api.simpler.grants.gov/v1/opportunities/search"
API_KEY = os.environ["SIMPLER_GRANTS_API_KEY"]
PAGE_SIZE = 500
SEARCH_RESULT_CEILING = 10_000
MAX_ATTEMPTS = 6


def request_page(session, body):
    for attempt in range(MAX_ATTEMPTS):
        response = session.post(
            ENDPOINT,
            headers={"X-API-Key": API_KEY},
            json=body,
            timeout=60,
        )

        if response.status_code == 429 or 500 <= response.status_code < 600:
            if attempt == MAX_ATTEMPTS - 1:
                response.raise_for_status()
            retry_after = response.headers.get("Retry-After")
            if retry_after and retry_after.isdigit():
                delay = min(int(retry_after), 60)
            else:
                delay = min(2 ** attempt + random.random(), 60)
            time.sleep(delay)
            continue

        if response.status_code in {401, 403, 422}:
            raise RuntimeError(
                f"Non-retryable Simpler API response: {response.status_code}"
            )

        response.raise_for_status()
        return response, response.json()

    raise RuntimeError("Retry loop ended without a response")


def collect_posted_partition(posted_start, posted_end):
    session = requests.Session()
    page_offset = 1
    raw_pages = []
    seen_page_hashes = set()
    observed_totals = []
    search_ceiling_reached = False

    while True:
        body = {
            "filters": {
                "opportunity_status": {
                    "one_of": ["posted"]
                },
                "post_date": {
                    "start_date": posted_start,
                    "end_date": posted_end,
                },
            },
            "pagination": {
                "page_offset": page_offset,
                "page_size": PAGE_SIZE,
                "sort_order": [
                    {
                        "order_by": "opportunity_id",
                        "sort_direction": "ascending",
                    }
                ],
            },
        }

        response, payload = request_page(session, body)
        rows = payload.get("data") or []
        page_info = payload.get("pagination_info") or {}

        if page_info.get("page_offset") != page_offset:
            raise RuntimeError(f"Page mismatch at {page_offset}")
        if page_info.get("page_size") != PAGE_SIZE:
            raise RuntimeError(f"Page-size mismatch at {page_offset}")

        total_pages = int(page_info.get("total_pages") or 0)
        total_records = int(page_info.get("total_records") or 0)
        observed_totals.append(total_records)
        search_ceiling_reached |= total_records >= SEARCH_RESULT_CEILING

        ordered_ids = [str(row.get("opportunity_id")) for row in rows]
        page_hash = hashlib.sha256(
            json.dumps(ordered_ids, separators=(",", ":")).encode()
        ).hexdigest()

        if rows and page_hash in seen_page_hashes:
            raise RuntimeError(f"Repeated non-empty page {page_offset}")
        seen_page_hashes.add(page_hash)

        raw_pages.append({
            "page_offset": page_offset,
            "request_body": body,
            "http_status": response.status_code,
            "response_sha256": hashlib.sha256(response.content).hexdigest(),
            "raw_body": response.content,
            "payload": payload,
        })

        if not rows or page_offset >= total_pages:
            break
        page_offset += 1

    all_rows = [row for page in raw_pages for row in page["payload"]["data"]]
    unique_ids = {str(row.get("opportunity_id")) for row in all_rows}

    return {
        "raw_pages": raw_pages,
        "row_count": len(all_rows),
        "unique_id_count": len(unique_ids),
        "observed_total_records": observed_totals,
        "totals_changed_during_walk": len(set(observed_totals)) > 1,
        "search_ceiling_reached": search_ceiling_reached,
    }

This example intentionally returns evidence instead of declaring success. The caller still has to compare unique_id_count with the accepted source total, store raw bytes in durable storage, retrieve required attachments or record-specific GETs, and reject a run whose search_ceiling_reached flag is true or whose unexplained differences exceed the written policy.

Keep Search2 and fetchOpportunity as a migration witness

The current classic search endpoint is:

POST https://api.grants.gov/v1/api/search2

A minimal active-opportunity request can be written as:

{
  "rows": 100,
  "startRecordNum": 0,
  "keyword": "research",
  "oppStatuses": "forecasted|posted"
}

The response uses an outer errorcode and msg, then a data object containing hitCount, startRecord, oppHits, echoed search parameters, and facet options. A 200 HTTP response is not sufficient; validate the envelope and the expected data shape too.

Advance startRecordNum by the number of accepted hits, retain every page, and reconcile unique numeric IDs to hitCount. The official endpoint page does not publish a universal maximum row value or a snapshot-isolation guarantee. Keep row counts modest, detect repeated pages, and rerun matched partitions instead of assuming one large request is complete.

Retrieve each complete classic record with:

POST https://api.grants.gov/v1/api/fetchOpportunity
{
  "opportunityId": 289999
}

Expect the published example and production shape to drift

The published Search2 sample currently shows fields such as agencyName and alnist. In a production contract probe on August 7, 2026, the returned hit used agency and cfdaList instead. That point-in-time difference is not permission to guess which spelling will win.

Use a tolerant raw parser that inventories actual keys, but keep a strict acceptance layer that alerts when required semantics disappear or change type. Save the original field name alongside the normalized destination. Do not silently rename cfdaList and then lose the evidence needed to explain an Assistance Listing discrepancy.

The Grants.gov status-code page recommends exponential backoff for HTTP 429 and classifies 5xx responses as server errors. Retry 429 and eligible 5xx responses within a ceiling. Do not retry an unchanged bad request indefinitely, and do not publish an empty dataset when the endpoint failed.

Use the daily XML extract for bootstrap and reconciliation

The daily XML route solves a different problem from either search API: it provides a whole exported database file once per day.

For each archive, retain:

  • the source URL and extracted date/time shown by Grants.gov;
  • download start and finish times;
  • HTTP status, content type, compressed bytes, and checksum;
  • archive member names, decompressed bytes, and XML root/version evidence;
  • parsed record count and unique source-ID count;
  • counts by XML record type, Version, ArchiveDate, agency, Assistance Listing, and date;
  • invalid records, duplicate IDs, and parser warnings; and
  • the exact filter used to derive a downstream active subset.

Do not compare an XML snapshot with a live API response taken hours later and call every difference a defect. Align source cutoffs as closely as possible, record the lag, and classify records that changed between observations.

The official XML field guide does not expose the APIs' four-state forecasted | posted | closed | archived enum directly. Any XML-to-API status comparison therefore needs a documented derivation rule based on the available XML record and date evidence, and the derived value must remain distinguishable from a source-native API status.

The XML filename currently includes v2, but a filename is not a complete schema contract. Fingerprint element paths, namespaces, required fields, enum values, and representative fixtures. Fail visibly when the file or schema changes instead of emitting zero rows.

The full extract is especially useful for answering three migration questions:

  1. Did either API omit records that belong to the declared population after the XML-to-API scope rule is applied?
  2. Did a source field or lifecycle state move into a different structure?
  3. Can the normalized output be reproduced through a separately collected representation of the same Grants.gov database?

Map identifiers before mapping fields

The most important migration rule is simple:

legacy numeric ID != Simpler UUID != opportunity number

The Simpler search model exposes:

  • opportunity_id: the Simpler UUID;
  • legacy_opportunity_id: the numeric ID used by classic Grants.gov routes; and
  • opportunity_number: the human-facing funding opportunity number.

Store all three. Do not overwrite the legacy numeric ID with the UUID, and do not promote the opportunity number into a database key without source-backed uniqueness evidence.

A practical normalized identity can look like this:

{
  "source": "grants_gov",
  "source_opportunity_id": "289999",
  "simpler_opportunity_id": "123e4567-e89b-12d3-a456-426614174000",
  "legacy_opportunity_id": 289999,
  "opportunity_number": "ABC-123",
  "source_url": "https://simpler.grants.gov/opportunity/123e4567-e89b-12d3-a456-426614174000"
}

Use the legacy ID as the cross-contract bridge only after verifying that the Simpler record and classic detail represent the same source opportunity. Preserve the matching evidence and any exceptions. If a record has no legacy ID, keep that null visible; do not synthesize one from the opportunity number.

Map the two API contracts without losing meaning

The legacy and Simpler shapes differ in naming, typing, nesting, identifiers, and status-specific fields. A field crosswalk must document more than destination names.

Starter Search2-to-Simpler crosswalk
01idlegacy_opportunity_id

Keep the legacy integer and the new opportunity_id UUID as separate source identifiers

02numberopportunity_number

Human-facing funding opportunity number; do not substitute it for either source key

03titleopportunity_title

Preserve the source value before text cleanup or search indexing

04agencyCodeagency_code

Store the returned top-level agency fields separately from the owning agency

05agency / agencyNameagency_name

The current response and published sample have used different legacy field names

06oppStatusopportunity_status

Retain forecasted, posted, closed, and archived as source lifecycle states

07openDatesummary.post_date

Candidate match; validate against synopsis.postingDate and XML PostDate while retaining the raw value

08closeDatesummary.close_date

Candidate match; validate by status and retain blank or display values separately

09cfdaList / alnistopportunity_assistance_listings[]

Preserve legacy labels and map Assistance Listing numbers explicitly

10synopsis.awardCeilingsummary.award_ceiling

Legacy text becomes a nullable integer; reject silent parse failures

11synopsisAttachmentFoldersattachments[]

Candidate enrichment map; preserve legacy folder hierarchy and IDs instead of flattening them away

This is a migration test set, not a complete schema map. Generate the full mapping from saved legacy responses and the current Simpler OpenAPI document.

For the complete mapping, record:

  • legacy JSON path and observed aliases;
  • Simpler JSON path and OpenAPI schema reference;
  • source type, nullable behavior, and enum vocabulary;
  • normalized field and target type;
  • transformation rule;
  • forecasted versus posted applicability;
  • source fixture IDs;
  • exact, normalized, expected-different, or unsupported comparison rule; and
  • owner and resolution for exceptions.

Do not equate timestamps by convenient names

The modern record has top-level created_at and updated_at, while its nested summary also has timestamps and a version_number. The legacy detail contains multiple created, posting, modified, and display-formatted dates. Similar labels do not prove identical business meaning.

Retain every source timestamp first. Then define explicit destinations such as:

  • source record observed time;
  • opportunity posted date;
  • forecasted post date;
  • posted close date;
  • forecasted close date;
  • source summary updated time; and
  • collector retrieval time.

Never use the ingestion timestamp as a substitute for a source update date.

Keep raw codes and normalized labels together

Legacy applicant, funding-instrument, and funding-category values can appear as compact codes with descriptions. Simpler exposes normalized enum strings. Store the raw legacy code, raw label, Simpler enum, and versioned mapping. That makes a later vocabulary change auditable instead of rewriting history.

Preserve null, absent, empty, and failed parse

These states are different:

  • the field does not apply to a forecast;
  • the source returned null;
  • the source returned an empty string;
  • the detail request failed;
  • the XML parser did not recognize the field;
  • the amount could not be parsed; and
  • the record was never enriched beyond the search hit.

Collapsing them to one blank value hides whether the gap belongs to the agency, source API, mapper, or collector.

Build a three-lane migration collector

A reliable migration stores each source route independently before producing one consumer-facing table.

Three collection lanes, one accepted dataset

Simpler API lane

01
one-based search pages + detail records

Store UUID and legacy ID, page metadata, raw JSON, detail state, attachments, and the OpenAPI fingerprint used by the run.

Acceptance evidence

Distinct page hashes, total_pages and total_records reconciliation, valid IDs, and detail coverage.

Classic witness lane

02
Search2 + fetchOpportunity

Collect matched filters and numeric IDs without translating away legacy field names or envelope errors.

Acceptance evidence

Matched cutoff, hitCount, unique IDs, detail success, raw field inventory, and classified differences.

Bulk baseline lane

03
daily Grants.gov XML extract

Verify the archive, retain raw XML, profile record type, version, and archive-date evidence, and document any derived status rule.

Acceptance evidence

File hash, member list, parse count, duplicate IDs, record-type and archive-date counts, and source-date manifest.

Normalize only after the evidence for each lane is durable. A difference can then be traced to source timing, source semantics, or collector behavior.

Use separate immutable namespaces such as:

raw/grants_gov/simpler_api/2026-08-07/run-.../
raw/grants_gov/classic_api/2026-08-07/run-.../
raw/grants_gov/xml_extract/2026-08-07/run-.../

Each run manifest should include:

  • source route and environment;
  • redacted request, filter, sort, page size, and cutoff;
  • source-document or OpenAPI fingerprint;
  • start, finish, and source timestamps;
  • pages, rows, unique IDs, details, and attachments attempted;
  • retry and response-status counts;
  • raw-object hashes and storage references;
  • schema and enum differences;
  • comparison results against other lanes; and
  • final state: accepted, quarantined, failed, or superseded.

Normalization runs only from accepted raw evidence. Downstream publication runs only from an accepted normalized edition. A failed API call cannot become a successful zero-row feed.

Separate collection from comparison

Do not make the Simpler collector call Search2 and fail whenever the classic endpoint is unavailable. Collect the lanes independently, then compare accepted source runs with compatible cutoffs. This preserves freshness while keeping evidence about a delayed witness.

Derive changes from retained snapshots

Neither search route should be treated as a guaranteed amendment event stream. Retain accepted snapshots and compare source-relevant fields to derive:

  • newly observed opportunity;
  • status changed;
  • post or close date changed;
  • funding estimate changed;
  • eligibility changed;
  • Assistance Listing changed;
  • attachment set changed; and
  • record absent from the current declared snapshot.

Absence is not automatically deletion. It can reflect a status filter, source cutoff, failed page, archive transition, or source issue. Classify it using the complete evidence set.

Keep forecasted, posted, closed, and archived distinct

Grants.gov defines four search statuses with different meanings:

  • Forecasted: a planned opportunity that is not yet an official funding announcement and may never become one.
  • Posted: an announced opportunity that is currently open and accepting applications.
  • Closed: an announcement whose due date has passed.
  • Archived: a historical record of a closed announcement.

Those definitions come from the official Search Grants help. Preserve the source status; do not flatten forecasted and posted into one open=true field.

Forecast dates and posted dates also need separate columns. A forecasted close date is an agency estimate. A posted close date belongs to an announced opportunity. When a forecast becomes posted, retain both source observations and the transition rather than overwriting the forecast with final dates.

Even a posted status is not a substitute for reading the official Notice of Funding Opportunity, amendments, and application instructions. Deadline descriptions, time zones, package requirements, and agency clarifications can be decisive. A feed supports discovery and routing; the linked source materials govern the application.

Retrieve details, attachments, and source evidence

Simpler search currently returns the same opportunity data as GET except attachments, and the search representation is cached hourly. Use GET when attachments or a record-specific retrieval are required. Classic Search2 remains the lighter search representation. The governing application evidence still lives in the linked Notice of Funding Opportunity, amendments, and instructions.

The Simpler detail URL pattern accepts either a Simpler UUID or a legacy numeric ID; the OpenAPI document describes the two identifier types as separate operations over the same visible path shape. The current detail schema includes summary fields, Assistance Listings, attachments, and opportunity-level identity. Classic fetchOpportunity returns its own nested summary, contacts, categories, instruments, eligibility, history, and attachment folders.

For every required detail request, record:

  • requested UUID and legacy ID;
  • HTTP and application-level status;
  • retrieval time and raw-response hash;
  • returned identifiers and whether they match the search hit;
  • detail schema fingerprint;
  • required-field coverage;
  • attachment inventory; and
  • retry, missing, unauthorized, malformed, or accepted state.

For attachments, preserve source metadata before download, then record URL, filename, media type, bytes, checksum, retrieval time, and access result. Treat HTML returned for a document URL, zero-byte files, changed filenames, duplicate payloads, and malware-scanner blocks as explicit states.

Do not distribute an attachment merely because it was publicly linked. Apply the source terms, content-specific rights, security controls, file-safety policy, and downstream purpose.

The classic Grants.gov API terms permit searching, displaying, analyzing, and retrieving grants data, prohibit implying endorsement, and reserve the right to limit service. They say services using that API should prominently display this notice:

This product uses the Grants.gov API but is not endorsed or certified by the U.S. Department of Health and Human Services.

For a product using the covered classic API, keep that notice prominent and review the current terms rather than copying a stale legal interpretation from this article. The cited page does not name every Simpler endpoint, so confirm which current terms govern that integration too.

Validate population, semantics, and operational behavior

Run validation at four levels.

1. Transport and contract

Check:

  • status code and content type;
  • parseable JSON, CSV, ZIP, or XML as expected;
  • required envelope and pagination fields;
  • requested versus returned page metadata;
  • source identifiers and types;
  • key redaction; and
  • OpenAPI or XML-schema fingerprint.

2. Population and completeness

Measure:

  • rows and unique IDs by source lane;
  • source totals observed on every page;
  • duplicates and repeated-page fingerprints;
  • status, agency, Assistance Listing, post-date, and close-date distributions;
  • search-hit-to-detail coverage;
  • attachments attempted and accepted; and
  • records present in only one lane at the matched cutoff.

3. Semantic parity

Compare:

  • UUID-to-legacy-ID mappings;
  • opportunity numbers and titles;
  • agency and top-level agency;
  • forecasted, posted, closed, and archived state;
  • posting, forecast, close, archive, and update dates;
  • estimated program funding, expected awards, floor, and ceiling;
  • cost sharing and applicant eligibility;
  • funding instruments and categories;
  • Assistance Listings; and
  • attachment identity and availability.

Amounts require special care. Null, zero, unlimited, unavailable, and failed numeric parsing are different. Estimated program funding and an award ceiling are not interchangeable, and neither is proof that money was obligated or paid.

4. Operational behavior

Exercise:

  • 401 handling for an invalid or inactive key;
  • 422 handling for a bad Simpler request;
  • 429 backoff without a retry storm;
  • bounded retries for 5xx responses;
  • XML download interruption and corrupt archives;
  • a changed field name or enum;
  • partial detail and attachment failure;
  • replay from the last accepted checkpoint; and
  • publication blocking when a required partition fails.

Every exception should land in a classified report, not disappear inside a percentage.

Dual-run before switching production readers

Run legacy and Simpler collection against the same declared population for multiple editions. Align cutoffs, keep both raw stores, and compare at the legacy numeric ID before switching consumers.

Classify differences as:

  1. source timing or moving-page difference;
  2. intentional filter or status-scope difference;
  3. field-name or type migration;
  4. expected vocabulary normalization;
  5. legacy parser defect;
  6. Simpler parser defect;
  7. detail or attachment retrieval difference;
  8. XML extraction or parsing difference; or
  9. unresolved source discrepancy.

Do not force exact equality between observations taken at different times. Do require every material variance to have a source-backed explanation or an unresolved state that blocks the affected use case.

Cut over only when all six gates pass
01

Population

Matched statuses, agencies, Assistance Listings, dates, and cutoffs reconcile across the selected source routes.

02

Identity

Every migrated record retains the Simpler UUID, legacy numeric ID, opportunity number, and a tested join path.

03

Semantics

Forecast dates, posted dates, close dates, funding estimates, eligibility, and null states survive without coercion.

04

Details

Summaries, contacts, Assistance Listings, and attachments meet written coverage and retrieval thresholds.

05

Operations

Keys, rate limits, retry ceilings, raw evidence, schema-drift alerts, replay, and failure publication are exercised.

06

Rollback

Readers can switch between accepted legacy and Simpler datasets without deleting either source history.

The classic APIs can remain a validation witness after reader cutover. Retire them because evidence and an official source decision support it—not because the product roadmap was mistaken for an endpoint deadline.

Cut over readers, not history

Use this release sequence:

  1. freeze accepted legacy, Simpler, and XML checkpoints;
  2. publish a comparison report and approve documented exceptions;
  3. switch a small downstream reader cohort to the Simpler-derived view;
  4. compare search, alerts, eligibility filters, and analyst outputs;
  5. expand the reader cohort;
  6. retain a fast rollback to the last accepted legacy-backed view; and
  7. retire collection only after the rollback window and an official source decision support it.

Rollback changes which accepted edition readers consume. It must not delete source responses, rewrite IDs, or destroy the evidence needed to explain a past decision.

Choose the API, XML, or a ready daily dataset

Use the Simpler API when you need current fielded search, structured details, source-native control, or an application that queries the live catalog.

Use Search2 and fetchOpportunity when you maintain a classic integration or need a separate compatibility witness during migration. Do not start a long-lived new architecture around their shapes without a transition plan.

Use Simpler extracts when a search could reach the 10,000-result cap or the job needs a complete published JSON or CSV opportunity export.

Use the daily XML extract when you need a whole-database bootstrap, reproducible daily file, or coverage reconciliation through a separate representation from paginated search.

Use the WebTruffle daily dataset when active forecasted and posted opportunity records in a normalized, downloadable schema solve the decision and owning the connector would only add operational work.

If the requirement needs retained closed history, additional authorized sources, custom eligibility rules, a different cadence, or delivery into your own stack, continue to the operational data feeds scope after testing the public edition.

Grants.gov API migration checklist

Scope and source choice

  • [ ] The required population names statuses, agencies, Assistance Listings, categories, dates, and cutoff.
  • [ ] Funding opportunities are kept separate from awards, recipients, obligations, and payments.
  • [ ] Simpler API, classic APIs, XML, and normalized-dataset roles are written down.
  • [ ] The project does not claim a classic API shutdown date that Grants.gov has not published.

Simpler API contract

  • [ ] A named API key is stored server-side and absent from source control, browser code, logs, and manifests.
  • [ ] Key inactivity, rotation, per-minute, daily, detail-call, and retry budgets have owners.
  • [ ] Search starts at page_offset: 1 with an explicit page size and sort order.
  • [ ] Returned page metadata, totals, unique IDs, and repeated-page hashes reconcile.
  • [ ] A search at the 10,000-result cap is rejected or repartitioned; full-catalog jobs use Simpler extracts.
  • [ ] The current OpenAPI document is fingerprinted and contract-tested.
  • [ ] 401, 403, 422, 429, and 5xx handling has been exercised.

Classic API and XML evidence

  • [ ] Search2 pages reconcile to hitCount and application-level errorcode is checked.
  • [ ] fetchOpportunity detail coverage and failures are measured.
  • [ ] Observed legacy aliases such as agency and Assistance Listing fields remain traceable.
  • [ ] XML downloads retain source time, compressed hash, member inventory, parse count, and schema fingerprint.
  • [ ] API and XML comparisons use compatible cutoffs and a documented XML-to-API status derivation.

Identity and semantics

  • [ ] Simpler UUID, legacy numeric ID, and opportunity number are stored separately.
  • [ ] Every migrated join has evidence and unmapped records remain visible.
  • [ ] Forecasted and posted dates remain distinct.
  • [ ] Null, absent, empty, non-applicable, failed request, and failed parse remain distinct.
  • [ ] Funding estimates, expected awards, floor, ceiling, and awarded spending are not conflated.
  • [ ] Codes, labels, enums, contacts, Assistance Listings, details, and attachments have versioned maps.

Publication and cutover

  • [ ] Raw source responses are immutable and normalized editions identify their source runs.
  • [ ] A failed required partition cannot publish as a valid zero-row edition.
  • [ ] Dual-run differences are classified by cause and material unresolved differences block cutover.
  • [ ] Downstream search, filters, alerts, and exports pass reader-level acceptance tests.
  • [ ] Reader rollout, rollback, and retained checkpoints have been rehearsed.
  • [ ] Required Grants.gov attribution is displayed and current source terms have been reviewed.

Frequently asked questions

Is the classic Grants.gov Search2 API deprecated?

The current Grants.gov documentation still presents search2 and fetchOpportunity as available public endpoints without authentication. The Simpler roadmap says the new experience becomes the default Grants.gov experience in 2027, but it does not publish a retirement date for those APIs. Build the new reader now without inventing a shutdown date.

Which Grants.gov API should a new integration use?

Prefer the Simpler.Grants.gov v1 read API for a new structured search integration. It exposes fielded filters, one-based pagination, sorting, a UUID plus the official legacy ID, and structured detail. Keep the daily XML extract or classic endpoints as reconciliation evidence when completeness matters.

Does the Simpler.Grants.gov API require an API key?

Yes. External requests use an API key in the X-API-Key header. The current default limits are 60 requests per minute and 10,000 requests per day per key, and inactive keys are disabled after 30 days. Keep the key server-side.

Does the classic Search2 API require a key?

No. Grants.gov currently documents both search2 and fetchOpportunity as available without authentication. The response still needs transport, envelope, schema, pagination, and completeness validation.

Does Simpler pagination start at zero or one?

One. The current OpenAPI schema says page_offset starts at 1. It requires a pagination object with both page_offset and page_size. Validate returned pagination_info against every request.

What is the Simpler.Grants.gov maximum page size?

The OpenAPI contract checked on August 7, 2026 allows 1 through 5,000 records per search page. The separate search-result ceiling is 10,000 opportunities, and matches beyond it may be culled. Narrow a search below that boundary or use POST /v1/extracts for a complete opportunity export.

Should I use the API or the daily XML extract?

Use search for fielded queries below its 10,000-result cap. Use Simpler extracts for complete modern JSON or CSV opportunity files. Use the once-daily classic XML file for a bulk bootstrap, retained daily snapshot, or separate-representation reconciliation. These are different contracts over related Grants.gov data, not independent underlying sources.

Can Grants.gov opportunity data show who received a grant?

No. Opportunity records describe funding that organizations may apply for. They are not grant awards, recipient records, obligations, or payment data. Use the appropriate federal spending or agency award source for outcome analysis.

What is the difference between forecasted and posted?

A forecasted opportunity is a plan and is not yet an official funding announcement; it may change or never be posted. A posted opportunity is an announced opportunity that is currently open according to Grants.gov. Preserve both states and their different date fields.

Which identifier should become my database key?

Keep the Simpler UUID, legacy numeric ID, and opportunity number separately. The legacy numeric ID is the most useful bridge during migration, but verify the match and retain its evidence. Do not overwrite one identifier with another or assume the opportunity number is globally unique.

Can I apply for funding through the Simpler API?

Not through the current public read API. The developer page says public write operations do not support applying, posting opportunities, or creating organization projects. Use the official application workflow and current agency instructions.