Skip to content

FFATA Sub-grants: Ingesting Pass-Through Assistance Data

Updated

When a state receives a federal grant and passes part of it to a county, a university or a nonprofit, that second hop is reported under FFATA. It is the larger half of federal assistance by both count and dollars, and almost nobody works with it. We held 604,078 sub-grant reports in production. This is how to build the pipeline, including the two things that will otherwise cost you a weekend.

Why this is a guide and not a product.

We retired our sub-grants endpoints in July 2026 to concentrate on federal contracting. The data is free at source. What follows is the operational knowledge, which is not.

The source

Same USAspending bulk_download API as prime awards, different filter block:

POST https://api.usaspending.gov/api/v2/bulk_download/awards/
Content-Type: application/json

{
  "file_format": "csv",
  "filters": {
    "sub_award_types": ["grant"],
    "date_type": "action_date",
    "date_range": {"start_date": "2025-10-01", "end_date": "2025-11-30"}
  }
}

Note sub_award_types, not prime_award_types. Using ["grant"] gives you the assistance side; ["procurement"] gives you contract sub-awards, which is a different dataset with different keys and should live in a different table. More on that below, because merging them is the expensive mistake.

Use action_date here, not last_modified_date. FFATA sub-award reporting is late and irregular by nature, and in our testing the action-date lane was the one that behaved predictably.

The assistance compiler is much slower, and this is the fix

USAspending compiles these files server-side on demand. The assistance sub-award compiler is far slower than the prime-award one, and the difference is not marginal:

Window Result
12 months, single request Ran 34 minutes and never finished
60 days, single chunk Can take 20+ minutes on its own
11 chunks, submitted serially Hours
11 chunks, submitted up front then polled together ~23 minutes

The key behaviour is that the API caches the compiled file per (start_date, end_date) pair. So a submit is not a request you wait on, it is a job you start. Submit every chunk first, then poll them all:

# WRONG: each chunk waits for the previous compile to finish
for start, end in chunks:
    job = submit_job(start, end)
    status = poll(job["status_url"])      # blocks for 20+ minutes
    download(status["file_url"])

# RIGHT: start every compile, then collect
jobs = [submit_job(start, end) for start, end in chunks]   # returns immediately
for job in jobs:
    status = poll(job["status_url"])       # most are already done by now
    download(status["file_url"])

Because the compile is cached, a poll that times out is not a failure. Exit, and the next run finds the file already built. That makes the job cron-safe: give it a bounded poll window, let it give up, and let tomorrow's run collect what today's run started.

Do not merge sub-grants into your contract sub-awards table

This is the mistake worth the most to avoid, and it fails silently rather than loudly.

If you run a reconciler that compares USAspending's per-day count against your own per-day row count, and you put both sub-grants and contract sub-awards in one table, your row counter now counts a larger population than the API's counter does:

ours (sub-contracts + sub-grants)  >  usaspending (sub-contracts only)

The comparison is then usa <= ours on every single day. Your reconciler reports "caught up" forever and goes blind. It does not error. It does not warn. It just stops finding gaps, and you learn about it months later when someone notices missing rows.

There is a second failure on top of it. Any per-day ceiling your reconciler already recorded was captured under the contracts-only regime. After the merge, a capped day skips its genuine sub-grant gap as a phantom and never fetches those rows at all.

Keep them in separate tables with separate jobs, even though the code looks nearly identical. We duplicated ours deliberately and revisited the decision twice, and both times duplication was correct.

Keys and dedup

Sub-grants key on FAIN and CFDA program, not PIID and NAICS. That is another reason they do not belong in a contract table: the join columns are different, and any query that assumes otherwise produces confident nonsense. Dedup on the sub-award report identifier and make the load idempotent so overlapping windows are free.

What it costs to run

Metric Value
Rows held 604,078 sub-grant reports
Storage 1.2 GB with indexes
Schedule Daily trailing action_date window
Scheduling note Offset it from your prime-award job so the two do not queue against USAspending together

We still hold this data.

We ran this pipeline through July 2026 and kept 604,078 FFATA sub-grant reports. Given the compile times above, a one-time export may be worth more than the backfill. Get in touch. Sold as a dated snapshot, not a feed.

Official sources

A REST API and MCP server for U.S. federal procurement data.

Get a free key, emailed instantly, or follow the quickstart.

Found something wrong, missing or confusing on this page?

Press Ctrl + Enter to send a note. Highlight text first and it travels with your note.