Skip to article

Federal subawards · Reported relationships

Link reported subawards to prime contracts with source identities and review flags intact.

Link 25 reported subawards to five prime awards. Download the executed Python notebook, source responses, edge table and explicit unmatched and review reports.

Published September 25, 202610 min readBy Daniel

To link USAspending subaward data to prime contracts, preserve the prime award key returned with each subaward, resolve that key against the award endpoint, and retain every unresolved row. Company names and contract numbers alone are insufficient join rules.

This Python example uses 25 reported contract-subaward records, collected from the official API on September 25, 2026. They resolve to five prime-award responses and contain 21 distinct subrecipient UEIs. One linked record needs review because its subaward date precedes the signing date returned for the prime award.

The result is a small, inspectable relationship dataset. It does not establish a complete subcontractor network, verify the underlying commercial relationships independently, or measure total subcontracting spend.

Define what the subaward sample can answer

The sample answers a narrow engineering question: can the prime identity returned on each selected subaward be resolved, while preserving the source record and its limitations? It uses the first page of 25 results, sorted by Sub-Award ID, for an action-date window of July 1–31, 2026.

That selection is neither random nor a complete July extract. The response explicitly says hasNext: true. July was chosen to avoid using the newest action window, but an older window does not guarantee complete reporting. Later corrections can still change an observation.

Treat the records as reported relationships. A missing company in this sample tells you nothing about whether it performs federal subcontract work. This page does not estimate reporting completeness, lower-tier relationships, or the proportion of each prime contract passed to subcontractors. USAspending's About the Data documentation is the starting point for checking source reporting and coverage limitations for a broader project.

For prime-award retrieval and financial interpretation, start with the USAspending API guide. For WebTruffle's published prime-award files, use the federal contract data Python tutorial. This example adds a separate relationship layer; it does not change those datasets' advertised coverage.

Retrieve a bounded contract-subaward sample

The request uses subawards: true, sets the spending level to subawards, and selects contract award types A, B, C and D. The official spending-by-award contract documents the requested fields and returned prime identifiers.

{
  "subawards": true,
  "spending_level": "subawards",
  "filters": {
    "award_type_codes": ["A", "B", "C", "D"],
    "time_period": [{"start_date": "2026-07-01", "end_date": "2026-07-31"}]
  },
  "fields": [
    "Sub-Award ID", "Sub-Awardee Name", "Sub-Recipient UEI",
    "Sub-Award Amount", "Sub-Award Date", "Sub-Award Type",
    "Prime Award ID", "Prime Recipient Name", "Prime Award Recipient UEI"
  ],
  "sort": "Sub-Award ID",
  "order": "asc",
  "page": 1,
  "limit": 25
}

POST this body to https://api.usaspending.gov/api/v2/search/spending_by_award/. The exact request, retrieval timestamp and response hash are in inputs/search-receipt.json. The download includes the original response, so replaying the tutorial makes no network calls.

The omitted date_type defaults to action_date for this subaward search. It is not a reporting-month filter. The official filter definitions also describe a last-modified date option; a production update process needs to evaluate that separately from the original action-date window.

If you collect a new observation, save it under a new snapshot name. Continue pagination when completeness within your query is required, retain each page and its receipt, and check for duplicates or changes between pages. A live paginated query should not be assumed to be an atomic snapshot.

The search response includes prime_award_generated_internal_id in addition to the display contract number. Keep this full value unchanged. Resolve each distinct key with GET /api/v2/awards/{key}/, URL-encoding the key when building the request.

The replay matches that value to generated_unique_award_id in the saved award response and cross-checks the returned numeric prime ID. A missing key, absent prime response or numeric ID disagreement produces an unmatched row. It never falls back to a company-name match.

key = row.get("prime_award_generated_internal_id")
prime = primes.get(key)
if not key:
    status = "missing_prime_key"
elif prime is None:
    status = "prime_not_in_snapshot"
elif str(row.get("prime_award_internal_id")) != str(prime["id"]):
    status = "prime_id_conflict"
else:
    status = "linked"

In this observation all 25 records resolve. The five saved prime responses are identity evidence from the same source, not an independent verification of reporting accuracy. unmatched.csv therefore contains a header and zero data rows. Synthetic tests exercise the three failure paths without adding invented records to the published sample.

Names remain useful display attributes. The Draper record illustrates why they should not determine the join: the search response names the prime CHARLES STARK DRAPER LABORATORY, INC., THE, while the award response uses THE CHARLES STARK DRAPER LABORATORY, INC. The generated award identity is unchanged.

The edge table preserves one row per search-result observation, including the original subaward identifier. 0000000588 keeps its leading zeros; 0000011731 MOD 3 keeps its suffix. Import identifiers as text in spreadsheet tools. The observation ID combines the response hash with the one-based row position. It is a trace-back key for this snapshot, not a permanent subaward business key.

Inspect the linked record that still needs review

The CHEVO LLC / MAX-CEVA SOLUTIONS row resolves to prime award 70CMSD26FR0000060. Its reported subaward amount is $174,441.60, with a subaward date of July 1, 2026. The saved prime response returns August 26, 2026 for date_signed and $0 for total_obligation.

The code retains the source link and adds two flags: subaward_before_prime_date_signed and prime_zero_obligation. These describe observed fields; they do not explain why the source returns them. The prime response also contains separate account-obligation information, reinforcing the need to inspect field meanings rather than substitute a convenient amount.

Do not silently rewrite either date, discard the relationship, or label the subaward fraudulent. Review the original record and source history before using it in an operational decision. A successful identity join and a reliable business interpretation are separate checks.

Four records also repeat subrecipient UEIs already present elsewhere in the sample. The two G2S records under the Lukos prime, for example, have different subaward IDs. Preserve them individually. Shared recipient identity does not establish that two reported rows are duplicates, incremental payments or independent contracts.

Keep reporting dates and financial grains separate

The search projection supplies the subaward action date. It does not expose a reporting timestamp or subaward modification timestamp in the pinned response. The edge table keeps reported_at and last_modified_at empty and labels their availability explicitly. The receipt's retrieved_at records when we observed the response.

Do not copy the prime award's modification date into the subaward's history. Do not convert a retrieval timestamp into a source publication date. Without the appropriate source timestamp, this example cannot measure how many days elapsed between a subaward action and its reporting.

The financial separation is equally important. edges.csv stores the reported subaward amount. primes.csv stores each prime's total obligation once. The script deliberately produces no combined spending total. Adding subaward amounts to prime obligations can double-count related funding and mix different financial measures.

Even summing prime obligations after a one-to-many join is unsafe: the Pantexas prime appears on 18 edges, so its prime amount would be repeated 18 times. Keep the prime table at one row per award. Before aggregating subaward amounts, establish how reporting revisions and repeated records should be interpreted; this small search projection does not resolve that accounting question.

Reproduce the edge table and unmatched report

Unzip the bundle, then run these commands from its directory with Python 3.10 or later:

python -B build_edges.py
python -B test_edges.py

The replay uses only the Python standard library. It verifies the SHA-256 of all six saved API responses before deriving outputs. Hash verification detects changed input bytes; it does not certify source completeness or factual accuracy.

Expected results are 25 source rows, 25 linked rows, zero unmatched rows, one flagged row, five primes and 21 distinct nonempty subrecipient UEIs. The saved summary also retains the fact that another search page exists.

Open subaward-links.ipynb in Jupyter to inspect the same workflow cell by cell. The supplied notebook has already been executed against the pinned files. Its tables show example edges, the flagged row and the separate prime table; its final assertions check row preservation and the unmatched result. Optional notebook execution dependencies are listed in requirements-notebook.txt.

Use the CSVs as inspection artifacts, not a finished supplier master. A UEI identifies the reported entity in this example; the tutorial does not merge corporate families or infer ownership. If your own data lacks a UEI, retain an unresolved entity record rather than substituting a normalized company name as a confident identity.

Define a subaward enrichment and update specification

A useful next step is a scoped acceptance test on the prime awards your application actually uses. Specify the agencies or award cohort, action window, required subrecipient fields and the acceptable unresolved-link rate. Decide whether customers need reported relationships, historical revisions or a current view, because those require different retention rules.

For recurring delivery, request immutable observations, original source keys, timestamps with availability flags, a documented change-detection method and an unmatched report. Plan overlapping refresh windows and periodic backfills for late reporting. A record's disappearance from a query should trigger investigation, not an automatic conclusion that a subcontract ended.

This tutorial demonstrates public-source linking. It is not a claim that WebTruffle's existing federal prime-award dataset already contains subaward coverage. A subaward enrichment project needs an agreed source scope and a verified sample before coverage or refresh commitments are made.

Frequently asked questions

Does USAspending subaward data show every federal subcontractor?

This example cannot establish that. It contains 25 reported records from one bounded query, with more results available. Reporting scope, missing records and relationships outside that scope must be evaluated separately before making a coverage claim.

Why is the unmatched CSV empty?

Every selected row resolves to a saved prime response with matching generated and numeric identities. The empty report is an observed result, not a guarantee for other queries. One linked row still has date and amount-context review flags.

Can I add the subaward amount to the prime contract value?

No. Keep the measures separate and establish their accounting meaning before aggregation. A relationship join can also repeat the same prime amount across many subaward rows.