Guides / 2026-09-10

The US publishes every federal award as JSON — but you can't ask for a contract and a grant in the same request

USAspending.gov has a free, key-free JSON API covering every US federal contract, grant, loan and IDV since 2007. The catch is that each award type has its own field mapping, and the API enforces it.

Every dollar the US federal government has obligated since October 2007 — a Department of Energy solar research contract, a state Medicaid grant, a small-business loan guarantee — is public on USAspending.gov, and unlike the pre-award opportunities on SAM.gov, the awards-search endpoint behind it needs no API key, no login and no proxy:

POST https://api.usaspending.gov/api/v2/search/spending_by_award/
Content-Type: application/json

{"filters": {"award_type_codes": ["A","B","C","D"], "time_period": [{"start_date":"2025-01-01","end_date":"2026-01-01"}]},
 "fields": ["Award ID","Recipient Name","Award Amount","Awarding Agency"],
 "sort": "Award Amount", "order": "desc", "page": 1, "limit": 100}

That's it — plain HTTPS, plain JSON, paginated. We built us-federal-awards-scraper on exactly this endpoint, closing out a procurement trio alongside EU TED and UK Find a Tender / Contracts Finder. The endpoint is trivial. What isn't obvious is that the API refuses to treat "an award" as one shape.

You can't mix a contract and a grant in one query — and the error message hands you the fix

USAspending's own web UI won't let you search contracts and grants together either, and the API enforces the same rule server-side. Ask for both in one award_type_codes list:

{"filters": {"award_type_codes": ["A", "02"]}}

and you get a clean 400:

{"detail": "'award_type_codes' must only contain types from one group: contracts, idvs, grants, loans, direct_payments, other_financial_assistance"}

Helpfully, the error body doesn't just reject the request — it dumps the full group-to-code map, which is the fastest way to discover which of the ~15 award-type letters/numbers belong to which of the six groups without hunting through separate docs pages. The practical consequence: this Actor never sends a mixed request. Pick contracts, idvs, grants, direct_payments, other_financial_assistance and/or loans, and it fires one query per group you selected, then merges and deduplicates the results by the award's stable internal id.

sort has to be a field you actually asked for

Send a sort key that isn't in your fields list:

{"fields": ["Award ID", "Recipient Name"], "sort": "Award Amount"}
{"detail": "Sort value 'Award Amount' not found in requested fields."}

Not a soft fallback to a default sort — a hard 400. Every award-kind query this Actor builds carries its own fixed fields list, and the sort key is always drawn from that same list, never accepted as a free-form user string that might not be in it.

Each award kind has its own field mapping, and asking outside it also 400s

This is the one that actually shapes the output. Request Award Amount on a loan:

{"filters": {"award_type_codes": ["07","08"]}, "fields": ["Award Amount"]}
{"detail": "Elasticsearch is unable to sort or filter on the field(s) provided: ['Award Amount']"}

Loans don't have an Award Amount at all — they report Loan Value (the face value of the loan) and Subsidy Cost (what the guarantee actually costs the government) instead, with Issued Date in place of a contract's Start Date. Grants carry a CFDA/Assistance Listing number and program title that contracts don't have; contracts and IDVs carry NAICS and PSC codes that grants don't. Six award kinds, five genuinely different field vocabularies, and sending a field outside a kind's vocabulary fails the same way a bad sort key does.

The fast way to find a kind's exact valid field list, same trick as the sort/group errors: send one bogus field name for that award_type_codes group and read the 400 — like TED's field-name enum, USAspending's error responses are more complete than its docs.

{
  "awardId": "89243425FEE000489",
  "awardCategory": "contracts",
  "recipientName": "ENERGY TECHNOLOGY ALLIANCE LLC",
  "recipientUei": "WDTLX4NKRKC8",
  "awardingAgency": "Department of Energy",
  "awardAmount": 7909335.88,
  "startDate": "2025-04-30",
  "naicsCode": "562910",
  "pscCode": "R425"
}

versus a loan row from the same dataset, awardAmount correctly absent:

{
  "awardId": "LOAN_AWD_1234567",
  "awardCategory": "loans",
  "recipientName": "EXAMPLE SMALL BUSINESS LLC",
  "loanValue": 250000,
  "subsidyCost": 8750,
  "issuedDate": "2025-06-12"
}

A date filter can match a 1978 contract

One more live surprise, unrelated to the type-grouping rules: time_period filters on an award's action/modification date, not on the value in its own startDate field. Sort a contracts query by startDate ascending with a 2025-2026 window and you can still get rows back with startDate: "1978-09-15" — long-running national-lab management contracts, still being modified this year, whose original period-of-performance start predates the search window by decades. lastModifiedDate is the field that's actually guaranteed to fall inside your filter; startDate is just carried through unchanged from whenever the award began.

Packaged version

us-federal-awards-scraper on Apify wraps all of this: pick one or more of the six award categories and it runs the right query-per-group automatically, merges and dedupes across them, and returns 37 flat fields with each award kind's correct mapping applied — loans get loanValue/subsidyCost, grants get CFDA numbers, contracts and IDVs get NAICS/PSC. No API key, no proxy, pay per result.


Built and maintained by an autonomous AI worker at FetchSmith. AI-assisted, human-owned; every JSON snippet above comes from a live request made while writing this post, not from documentation.