usaspending-api

Query the USASpending.gov REST API for federal contract and award data. Use this skill whenever the user asks about federal spending data, contract descriptions, award details, transaction histories, vendor/recipient lookups, obligation amounts, agency spending breakdowns, PSC/NAICS code lookups, or any procurement research that involves USASpending, FPDS, or FAADC data. Also trigger when the user needs to enrich internal contract data (PRISM, FPDS, ALP, Contract Court) with base award descriptions, transaction histories, or award metadata. Trigger for any mention of USASpending, FPDS, federal awards, PIIDs, FAINs, award IDs, or federal spending analysis. This skill is essential for daily contract portfolio management, description enrichment, and procurement intelligence work.

USASpending.gov API Skill

Overview

The USASpending.gov API (https://api.usaspending.gov) provides free, no-auth access to all federal spending data sourced from FPDS, FAADC, and agency DATA Act submissions. No API key required.

Base URL: https://api.usaspending.gov

This skill is self-contained. Filter value tables, composite workflows, bulk download, loan search, reference endpoints, PSC codes, vendor dedup notes, and troubleshooting are all below in the same file.

Critical Rules

1. Award Type Code Groups (MOST COMMON ERROR)

Award type codes MUST NOT be mixed across groups. The API returns HTTP 422 if you mix them.

  • Contracts: ["A", "B", "C", "D"] (A=BPA Call, B=PO, C=DO, D=Definitive)
  • IDVs: ["IDV_A", "IDV_B", "IDV_B_A", "IDV_B_B", "IDV_B_C", "IDV_C", "IDV_D", "IDV_E"]
  • Grants: ["02", "03", "04", "05"]
  • Loans: ["07", "08"] (use "Loan Value" not "Award Amount")
  • Direct Payments: ["06", "10"]
  • Other: ["09", "11", "-1"]

2. PIID Type Detection

PIID formats vary by agency. Example: NAVSEA PIIDs start with N00024 (e.g., N0002425CXXXX). Other DoD prefixes: W91CRB (Army Contracting Command), FA8650 (AFRL). If the agency format is unknown, try contracts first, then IDVs if 0 results.

def detect_award_type(piid):
    if piid.startswith("N00024") and len(piid) >= 9:
        return ["IDV_B_B"] if piid[8] == 'D' else ["A", "B", "C", "D"]
    return ["A", "B", "C", "D"]

3. Sort Field Must Be in Fields Array

The sort value MUST appear in fields or the API returns HTTP 400.

4. Endpoint Limit Caps

  • Search endpoints: max limit = 100
  • Transactions endpoint: max limit = 5000
  • Paginate with page parameter

5. No Total Count in page_metadata

spending_by_award has no total field. Use spending_by_award_count for totals.

6. PSC/NAICS Filter Format

Do NOT include empty "exclude": []. Use simple arrays: "psc_codes": ["R499"].

7. Contract Award Type vs Pricing Type

Contract Award Type in search = how issued (BPA Call, PO, DO, Definitive). For pricing type (FFP, T&M, etc.), use award detail endpoint or contract_pricing_type_codes filter.

8. Generated Award IDs

Format: CONT_AWD_[PIID]_[AGENCY]_[PARENT_PIID]_[PARENT_AGENCY] or CONT_IDV_[PIID]_[AGENCY].


Core Endpoints

1. Award Search (PRIMARY WORKHORSE)

Endpoint: POST /api/v2/search/spending_by_award/

import urllib.request, json

def search_awards(keywords, award_type_codes, fields=None, limit=10, page=1, sort="Award Amount"):
    if fields is None:
        fields = ["Award ID", "Description", "Award Amount", "Recipient Name",
                  "Start Date", "End Date", "generated_internal_id",
                  "Awarding Agency", "Awarding Sub Agency", "PSC", "NAICS"]
    if sort not in fields:
        fields.append(sort)
    payload = json.dumps({
        "subawards": False, "limit": min(limit, 100), "page": page,
        "sort": sort, "order": "desc",
        "filters": {
            "keywords": keywords if isinstance(keywords, list) else [keywords],
            "award_type_codes": award_type_codes
        }, "fields": fields
    })
    url = "https://api.usaspending.gov/api/v2/search/spending_by_award/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Contract fields: Award ID, Recipient Name, Recipient UEI, Description, Awarding Agency, Awarding Sub Agency, Funding Agency, Funding Sub Agency, generated_internal_id, Start Date, End Date, Award Amount, Total Outlays, Contract Award Type, NAICS, PSC, def_codes, Last Modified Date, Base Obligation Date

IDV fields: Same base plus Last Date to Order (no End Date).

Loan fields: Use "Loan Value" and "Subsidy Cost" instead of "Award Amount". Sort by "Loan Value".

Note on award_ids filter: Use plain strings: "award_ids": ["N0002424C0085"]. Performs prefix match.

2. Award Detail

Endpoint: GET /api/v2/awards/<GENERATED_AWARD_ID>/

def get_award_detail(generated_id):
    url = f"https://api.usaspending.gov/api/v2/awards/{generated_id}/"
    req = urllib.request.Request(url)
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Returns: piid, description, total_obligation, date_signed, recipient, parent_award, latest_transaction_contract_data (competition, set-aside, pricing type), period_of_performance, place_of_performance, naics_hierarchy, psc_hierarchy, base_and_all_options, total_subaward_amount.

3. Transaction History

Endpoint: POST /api/v2/transactions/

def get_transactions(generated_award_id, limit=100, sort="action_date", order="asc"):
    payload = json.dumps({
        "award_id": generated_award_id, "page": 1,
        "sort": sort, "order": order, "limit": min(limit, 5000)
    })
    url = "https://api.usaspending.gov/api/v2/transactions/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Returns per transaction: id, type, action_date, action_type, modification_number, description, federal_action_obligation. Mod 0 = original base award description.

4. Agency-Filtered Search

filters = {
    "award_type_codes": ["A", "B", "C", "D"],
    "agencies": [{"type": "awarding", "tier": "subtier",
                  "name": "Naval Sea Systems Command",
                  "toptier_name": "Department of Defense"}],
    "time_period": [{"start_date": "2024-10-01", "end_date": "2025-09-30"}]
}

DoD constants: toptier code 097 (Defense). Navy subtier 1700. Example NAVSEA PIID format N00024[YY][type][NNNN].

5. IDV Child Awards

Endpoint: POST /api/v2/idvs/awards/

def get_idv_children(generated_idv_id, award_type="child_awards"):
    payload = json.dumps({
        "award_id": generated_idv_id, "type": award_type,
        "limit": 50, "page": 1,
        "sort": "period_of_performance_start_date", "order": "desc"
    })
    url = "https://api.usaspending.gov/api/v2/idvs/awards/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Field name differences: IDV children use piid (not "Award ID"), obligated_amount (not "Award Amount"), generated_unique_award_id (not "generated_internal_id").

6. Spending by Category

Endpoint: POST /api/v2/search/spending_by_category/<CATEGORY>/

Category MUST be in URL path. Categories: awarding_agency, awarding_subagency, funding_agency, funding_subagency, recipient, cfda, naics, psc, country, county, district, state_territory, federal_account, defc.

def spending_by_category(category, filters, limit=10):
    payload = json.dumps({"filters": filters, "limit": limit, "page": 1})
    url = f"https://api.usaspending.gov/api/v2/search/spending_by_category/{category}/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Recipient results: amount (net obligations, can be negative), name (ALL CAPS), recipient_id, uei. See Vendor Deduplication section below for caveats on large-company multi-UEI entries.

7. Spending Over Time

Endpoint: POST /api/v2/search/spending_over_time/

def spending_over_time(filters, group="fiscal_year"):
    payload = json.dumps({"group": group, "filters": filters})
    url = "https://api.usaspending.gov/api/v2/search/spending_over_time/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

CRITICAL: fiscal_year is returned as a string. Cast to int before numeric comparison.

8. Award Count

Endpoint: POST /api/v2/search/spending_by_award_count/

def get_award_counts(filters):
    payload = json.dumps({"filters": filters})
    url = "https://api.usaspending.gov/api/v2/search/spending_by_award_count/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Returns counts by group: contracts, idvs, grants, loans, direct_payments, other. Use with dimensional filters (contract_pricing_type_codes, extent_competed_type_codes, set_aside_type_codes) for aggregate analysis.

9. Autocomplete Endpoints

def autocomplete_psc(search_text):
    payload = json.dumps({"search_text": search_text, "limit": 10})
    url = "https://api.usaspending.gov/api/v2/autocomplete/psc/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode())

def autocomplete_naics(search_text):
    payload = json.dumps({"search_text": search_text, "limit": 10})
    url = "https://api.usaspending.gov/api/v2/autocomplete/naics/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=10) as resp:
        return json.loads(resp.read().decode())

PSC autocomplete works best with code prefixes ("R499") or single keywords ("professional").


Common Filter Object

filters = {
    "award_type_codes": ["A","B","C","D"],  # MUST be from same group
    "keywords": ["search term"],
    "time_period": [{"start_date": "2024-10-01", "end_date": "2025-09-30"}],
    "agencies": [{"type": "awarding", "tier": "subtier", "name": "..."}],
    "recipient_search_text": ["vendor name"],
    "award_ids": ["PIID1"],  # Plain strings, prefix match
    "psc_codes": ["R499"],  # Simple array, no empty exclude
    "naics_codes": ["541512"],
    "place_of_performance_locations": [{"country": "USA", "state": "MD"}],
    "award_amounts": [{"lower_bound": 1000000, "upper_bound": 10000000}],
    "set_aside_type_codes": ["SBA"],
    "extent_competed_type_codes": ["A"],
    "contract_pricing_type_codes": ["J"],
    "def_codes": ["L","M","N","O","P"]
}

Filter Value Reference Tables

contract_pricing_type_codes

CodeDescription
AFixed Price Redetermination
BFixed Price Level of Effort
JFirm Fixed Price
KFixed Price with Economic Price Adjustment
LFixed Price Incentive
MFixed Price Award Fee
RCost Plus Award Fee
SCost No Fee
TCost Sharing
UCost Plus Fixed Fee
VCost Plus Incentive Fee
YTime and Materials
ZLabor Hours

extent_competed_type_codes

CodeDescription
AFull and Open Competition
BNot Available for Competition
CNot Competed
DFull and Open after Exclusion of Sources
EFollow On to Competed Action
FCompeted under SAP
GNot Competed under SAP
CDOCompetitive Delivery Order
NDONon-Competitive Delivery Order

Useful groupings: competed = ["A","D","F","CDO"], not competed = ["B","C","E","G","NDO"].

set_aside_type_codes

CodeDescription
SBASmall Business Set-Aside (Total)
SBPSmall Business Set-Aside (Partial)
8A8(a) Competitive
8AN8(a) Sole Source
HZCHUBZone Set-Aside
HZSHUBZone Sole Source
SDVOSBSSDVOSB Set-Aside
SDVOSBCSDVOSB Sole Source
WOSBWOSB Set-Aside
WOSBSSWOSB Sole Source
EDWOSBEDWOSB Set-Aside
EDWOSBSSEDWOSB Sole Source
VSAVeteran Set-Aside

Useful groupings: all SB = ["SBA","SBP","8A","8AN","HZC","HZS","SDVOSBS","SDVOSBC","WOSB","WOSBSS","EDWOSB","EDWOSBSS","VSA"], 8(a) = ["8A","8AN"], SDVOSB = ["SDVOSBS","SDVOSBC"], WOSB/EDWOSB = ["WOSB","WOSBSS","EDWOSB","EDWOSBSS"].


Composite Workflows

Enrich Contract Court / PRISM Descriptions

Problem: PRISM overwrites the description field with the latest modification text.

def get_base_description(piid):
    """Get the original base award description for a PIID."""
    award_types = detect_award_type(piid)
    result = search_awards(piid, award_types,
                           fields=["Award ID", "Description", "Award Amount",
                                   "Recipient Name", "generated_internal_id"])
    awards = result.get('results', [])
    if not awards and 'IDV' not in str(award_types):
        award_types = ["IDV_A","IDV_B","IDV_B_A","IDV_B_B","IDV_B_C","IDV_C","IDV_D","IDV_E"]
        result = search_awards(piid, award_types,
                               fields=["Award ID", "Description", "Award Amount",
                                        "Recipient Name", "generated_internal_id"])
        awards = result.get('results', [])
    return awards[0] if awards else None

Batch PIID Lookup

import time

def batch_lookup_descriptions(piids):
    """Look up base descriptions for a list of PIIDs."""
    results = {}
    for piid in piids:
        try:
            award = get_base_description(piid)
            if award:
                results[piid] = {'description': award.get('Description'),
                    'amount': award.get('Award Amount'),
                    'recipient': award.get('Recipient Name'),
                    'internal_id': award.get('generated_internal_id')}
            else:
                results[piid] = None
        except Exception as e:
            results[piid] = {'error': str(e)}
        time.sleep(0.3)
    return results

Get Full Award Lifecycle

def get_award_lifecycle(piid):
    """Get complete award info: base description + all modifications."""
    award = get_base_description(piid)
    if not award:
        return None
    internal_id = award.get('generated_internal_id')
    detail = get_award_detail(internal_id)
    transactions = get_transactions(internal_id, limit=100)
    return {'award': detail, 'transactions': transactions.get('results', [])}

Dimensional Analysis with Award Counts

# Count FFP awards
ffp_filters = {**base_filters, "contract_pricing_type_codes": ["J"]}
ffp_count = get_award_counts(ffp_filters)["results"]["contracts"]

# Count competed awards
comp_filters = {**base_filters, "extent_competed_type_codes": ["A","D","F","CDO"]}
comp_count = get_award_counts(comp_filters)["results"]["contracts"]

# Count small business set-aside awards
sb_filters = {**base_filters, "set_aside_type_codes": ["SBA","SBP","8A","8AN"]}
sb_count = get_award_counts(sb_filters)["results"]["contracts"]

Top Small Business Vendors

sb_vendor_filters = {**base_filters, "set_aside_type_codes": ["SBA","SBP","8A","8AN","HZC","HZS","SDVOSBS","SDVOSBC","WOSB","WOSBSS","EDWOSB","EDWOSBSS","VSA"]}
sb_vendors = spending_by_category("recipient", sb_vendor_filters, limit=15)

Bulk Download

Endpoint: POST /api/v2/bulk_download/awards/

Async: returns download URL. Uses DIFFERENT filter structure than search endpoints.

def request_bulk_download(filters):
    payload = json.dumps({"filters": filters, "columns": [], "file_format": "csv"})
    url = "https://api.usaspending.gov/api/v2/bulk_download/awards/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode())

Key differences from search filters:

  • "prime_award_types" instead of "award_type_codes"
  • "date_range": {"start_date": "...", "end_date": "..."} (object) instead of "time_period" (array)
  • Requires "date_type" field (e.g., "action_date")

Loan Search

Loans use different field names. "Award Amount" returns HTTP 400; use "Loan Value".

def search_loans(keywords, limit=10):
    payload = json.dumps({
        "subawards": False, "limit": min(limit, 100), "page": 1,
        "sort": "Loan Value", "order": "desc",
        "filters": {
            "keywords": keywords if isinstance(keywords, list) else [keywords],
            "award_type_codes": ["07", "08"]
        },
        "fields": ["Award ID", "Recipient Name", "Recipient UEI", "Description",
                    "Loan Value", "Subsidy Cost", "Awarding Agency", "Awarding Sub Agency",
                    "generated_internal_id", "Last Modified Date"]
    })
    url = "https://api.usaspending.gov/api/v2/search/spending_by_award/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Award Funding (File C Data)

Endpoint: POST /api/v2/awards/funding/

Federal account, object class, and program activity data linked to an award.

def get_award_funding(generated_award_id):
    payload = json.dumps({"award_id": generated_award_id, "limit": 50, "page": 1})
    url = "https://api.usaspending.gov/api/v2/awards/funding/"
    req = urllib.request.Request(url, data=payload.encode('utf-8'),
                                 headers={'Content-Type': 'application/json'})
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode())

Sort fields: reporting_fiscal_date, account_title, transaction_obligated_amount, object_class.


Reference Endpoints (GET)

EndpointUse
/api/v2/references/toptier_agencies/All top-tier agencies
/api/v2/references/naics/<CODE>/NAICS code details (2-6 digit)
/api/v2/references/filter_tree/psc/PSC hierarchy (drillable: /psc/Service/R/)
/api/v2/references/submission_periods/Available reporting periods
/api/v2/references/def_codes/Disaster/emergency fund codes
/api/v2/references/data_dictionary/Full data dictionary
/api/v2/references/glossary/Glossary terms
/api/v2/recipient/state/<FIPS>/State spending profile
/api/v2/agency/<CODE>/Agency overview (?fiscal_year=YYYY)
/api/v2/agency/<CODE>/awards/Agency award summary

PSC Code Reference (Common IT/Services Codes)

CodeDescription
AN11R&D: Health Care Services, Basic Research
AN91R&D: Other Research and Development, Basic Research
AJ11R&D: General Science/Technology, Basic Research
AJ12R&D: General Science/Technology, Applied Research
AZ11R&D: Other R&D, Basic Research
R499Other Professional Services
D399IT and Telecom: Other IT and Telecommunications
DA01IT and Telecom: Application Development Support
S112Utilities: Electric

Vendor Deduplication Notes

The recipient category returns results by unique entity (UEI). Large companies often appear multiple times due to subsidiaries, divisions, or re-registrations. When using for vendor landscape analysis:

  • Vendor counts may overstate unique companies
  • Market share may be split across multiple entries
  • No built-in dedup; apply case-insensitive name matching if precision matters
  • Name normalization catches suffix variations but not acquisitions/rebrands (e.g., Cerner to Oracle Health Government Services; Northrop Grumman acquired Peraton)
  • Vendor names return in ALL CAPS; title-case for reports but preserve LLC/INC/LLP in uppercase

Rate Limiting

  1. No auth needed; API is public and read-only
  2. Add 0.3s delays between batch requests
  3. Search max limit = 100; transactions max limit = 5000
  4. API silently accepts invalid field names (returns null); verify spelling
  5. Empty keywords: [] returns HTTP 400; omit entirely if not needed

Troubleshooting

ErrorCauseFix
422 "award_type_codes must only contain types from one group"Mixed contract + IDV codesSeparate into two requests
422 "Invalid value in filters|psc_codes"Empty "exclude": []Remove exclude key or use simple array
422 "Field 'limit' value above max"limit > 100 (search) or > 5000 (transactions)Reduce limit; paginate
400 "Sort value not found in requested fields"Sort field not in fields arrayAdd sort field to fields
400 "Sort value not found in Loan Award mappings""Award Amount" with loan codesUse "Loan Value" for loans
400 "Invalid value in filters|keywords"Empty keywords arrayOmit keywords filter entirely
Empty results for known PIIDWrong award type groupTry contracts then IDVs
Null values for valid fieldsTypo in field name (API accepts anything)Verify field spelling
Truncated descriptionFPDS 4000-char limitNormal; use what's available
Stale data (non-DoD)FPDS update lagNon-DoD typically available within 5 business days
Stale data (DoD/USACE)90-day FPDS reporting delayDoD and USACE procurement data has a 90-day lag; plan accordingly

MIT © James Jenrette / 1102tools. Source: github.com/1102tools/federal-contracting-skills