API Reference¶
Complete reference for all Tango Python SDK methods and functionality.
Table of Contents¶
- Client Initialization
- Agencies
- Offices
- Organizations
- Contracts
- IDVs
- OTAs
- OTIDVs
- Subawards
- Vehicles
- Entities
- Forecasts
- Opportunities
- Notices
- State \& Local (SLED)
- Grants
- GSA eLibrary Contracts
- Protests
- Contract Appeals
- Federal Register
- GSA eBuy
- Budget
- Business Types
- NAICS
- Webhooks
- Response Objects
- ShapeConfig (predefined shapes)
- Error Handling
Client Initialization¶
TangoClient¶
Initialize the Tango API client.
from tango import TangoClient
# With API key
client = TangoClient(api_key="your-api-key")
# From environment variable (TANGO_API_KEY)
client = TangoClient()
# Custom base URL (for testing or different environments)
client = TangoClient(api_key="your-api-key", base_url="https://custom.api.url")
Parameters: - api_key (str, optional): Your Tango API key. If not provided, will load from TANGO_API_KEY environment variable. - base_url (str, optional): Base URL for the API. Defaults to https://tango.makegov.com.
Agencies¶
Government agencies that award contracts and manage programs.
list_agencies()¶
List all federal agencies.
agencies = client.list_agencies(page=1, limit=25)
Parameters: - page (int): Page number (default: 1) - limit (int): Results per page (default: 25, max: 100) - search (str, optional): Search term to filter agencies by name
Returns: PaginatedResponse with Agency dataclass objects
Example:
agencies = client.list_agencies(limit=10)
print(f"Found {agencies.count} total agencies")
for agency in agencies.results:
print(f"{agency.code}: {agency.name}")
get_agency()¶
Get a specific agency by code.
agency = client.get_agency("GSA")
Parameters: - code (str): Agency identifier. Accepts CGAC ("097"), FPDS code ("4712"), short code ("GSA"), abbreviation, or canonical name. See Federal agency hierarchy for code semantics.
Returns: Agency dataclass with agency details
Example:
gsa = client.get_agency("GSA")
print(f"Name: {gsa.name}")
print(f"Abbreviation: {gsa.abbreviation or 'N/A'}")
if gsa.department:
print(f"Department: {gsa.department.name}")
Agency Fields: - code - Agency code - name - Full agency name - abbreviation - Short name - department - Parent department (if applicable)
Offices¶
Federal agency offices.
list_offices()¶
List offices with optional search.
offices = client.list_offices(page=1, limit=25, search="acquisitions")
Parameters: - page (int): Page number (default: 1) - limit (int): Results per page (default: 25, max: 100) - search (str, optional): Search term
Returns: PaginatedResponse with office dictionaries
get_office()¶
Get a specific office by code.
office = client.get_office(code="4732XX")
Parameters: - code (str): Office code
Returns: Dictionary with office details
Organizations¶
Federal organizations (hierarchical agency structure).
list_organizations()¶
List organizations with filtering and shaping.
organizations = client.list_organizations(
page=1,
limit=25,
shape=ShapeConfig.ORGANIZATIONS_MINIMAL,
# Filter parameters
cgac=None,
include_inactive=None,
level=None,
parent=None,
search=None,
type=None,
)
Parameters: - page (int): Page number (default: 1) - limit (int): Results per page (default: 25, max: 100) - shape (str, optional): Response shape string - flat (bool): Flatten nested objects (default: False) - flat_lists (bool): Flatten arrays with indexed keys (default: False)
Filter Parameters: - cgac - Filter by CGAC code - include_inactive - Include inactive organizations - level - Filter by organization level - parent - Filter by parent organization - search - Search term - type - Filter by organization type
Returns: PaginatedResponse with organization dictionaries
get_organization()¶
Get a specific organization by fh_key.
org = client.get_organization(fh_key="ORG_KEY", shape=ShapeConfig.ORGANIZATIONS_MINIMAL)
Parameters: - fh_key (str): Organization key - shape (str, optional): Response shape string - flat (bool): Flatten nested objects (default: False) - flat_lists (bool): Flatten arrays with indexed keys (default: False)
Returns: Dictionary with organization details
Contracts¶
Federal contract awards and procurement data.
list_contracts()¶
Search and filter contracts with extensive options.
contracts = client.list_contracts(
cursor=None, # keyset pagination token (not page number)
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
# Text search
keyword=None, # Mapped to 'search' API param
# Date filters
award_date_gte=None,
award_date_lte=None,
pop_start_date_gte=None,
pop_start_date_lte=None,
pop_end_date_gte=None,
pop_end_date_lte=None,
expiring_gte=None,
expiring_lte=None,
# Party filters
awarding_agency=None,
funding_agency=None,
recipient_name=None, # Mapped to 'recipient' API param
recipient_uei=None, # Mapped to 'uei' API param
# Classification
naics_code=None, # Mapped to 'naics' API param
psc_code=None, # Mapped to 'psc' API param
set_aside_type=None, # Mapped to 'set_aside' API param
# Type filters
fiscal_year=None,
fiscal_year_gte=None,
fiscal_year_lte=None,
award_type=None,
# Identifiers
piid=None,
solicitation_identifier=None,
# Sorting
sort=None, # Combined with 'order' into 'ordering' API param
order=None, # 'asc' or 'desc'
)
Common Parameters: - cursor (str, optional): Keyset pagination token from response.next (contracts use keyset pagination, not page numbers) - limit (int): Results per page (max: 100) - shape (str): Fields to return (see Shaping Guide) - flat (bool): Flatten nested objects to dot-notation keys - flat_lists (bool): Flatten arrays with indexed keys
Filter Parameters:
Text Search: - keyword - Search contract descriptions (automatically mapped to API's 'search' parameter)
Date Filters: - award_date_gte - Awarded on or after date (YYYY-MM-DD) - award_date_lte - Awarded on or before date (YYYY-MM-DD) - pop_start_date_gte - Period of performance start date ≥ - pop_start_date_lte - Period of performance start date ≤ - pop_end_date_gte - Period of performance end date ≥ - pop_end_date_lte - Period of performance end date ≤ - expiring_gte - Expiring on or after date - expiring_lte - Expiring on or before date
Party Filters: - awarding_agency - Agency code (e.g., "4700" for GSA) - funding_agency - Funding agency code - recipient_name - Vendor/recipient name (mapped to 'recipient' API param) - recipient_uei - Vendor UEI (mapped to 'uei' API param)
Classification: - naics_code - NAICS industry code (mapped to 'naics' API param) - psc_code - Product/Service code (mapped to 'psc' API param) - set_aside_type - Set-aside type (mapped to 'set_aside' API param)
Type Filters: - fiscal_year - Federal fiscal year (exact match) - fiscal_year_gte - Fiscal year ≥ - fiscal_year_lte - Fiscal year ≤ - award_type - Award type code
Identifiers: - piid - Procurement Instrument Identifier (exact match) - solicitation_identifier - Solicitation ID
Sorting: - sort - Field to sort by (e.g., "award_date", "obligated") - order - Sort order: "asc" or "desc" (default: "asc")
Returns: PaginatedResponse with contract dictionaries
Examples:
# Basic search
contracts = client.list_contracts(limit=10)
# Filter by agency
contracts = client.list_contracts(
awarding_agency="4700", # GSA agency code
limit=50
)
# Text search
contracts = client.list_contracts(
keyword="software development",
limit=50
)
# Date range
contracts = client.list_contracts(
award_date_gte="2023-01-01",
award_date_lte="2023-12-31",
limit=100
)
# Expiring contracts
contracts = client.list_contracts(
expiring_gte="2025-01-01",
expiring_lte="2025-12-31",
limit=50
)
# Multiple filters
contracts = client.list_contracts(
keyword="IT services",
awarding_agency="4700", # GSA
fiscal_year=2024,
naics_code="541511",
limit=100
)
# With shaping for performance
contracts = client.list_contracts(
shape="key,piid,recipient(display_name),total_contract_value,award_date",
awarding_agency="4700",
fiscal_year=2024,
limit=100
)
# Sorting results
contracts = client.list_contracts(
sort="award_date",
order="desc",
limit=100
)
Common Contract Fields: - key - Unique identifier - piid - Procurement Instrument Identifier - description - Contract description - award_date - Date awarded - fiscal_year - Fiscal year - total_contract_value - Total value - total_obligated - Total obligated amount - recipient - Vendor information (nested) - awarding_agency - Awarding agency (nested) - funding_agency - Funding agency (nested) - naics - Industry classification (nested) - psc - Product/service code (nested) - place_of_performance - Location (nested)
OTAs¶
Other Transaction Agreements — non-FAR-based awards.
list_otas()¶
List OTAs with keyset pagination, filtering, and shaping.
otas = client.list_otas(
limit=25,
cursor=None,
shape=ShapeConfig.OTAS_MINIMAL,
# Filter parameters (all optional)
award_date=None,
award_date_gte=None,
award_date_lte=None,
awarding_agency=None,
expiring_gte=None,
expiring_lte=None,
fiscal_year=None,
fiscal_year_gte=None,
fiscal_year_lte=None,
funding_agency=None,
ordering=None,
piid=None,
pop_end_date_gte=None,
pop_end_date_lte=None,
pop_start_date_gte=None,
pop_start_date_lte=None,
psc=None,
recipient=None,
search=None,
uei=None,
)
Notes: - Uses keyset pagination (cursor + limit) rather than page numbers. - Filter parameters mirror those on list_contracts.
Returns: PaginatedResponse with OTA dictionaries
get_ota()¶
ota = client.get_ota("OTA_KEY", shape=ShapeConfig.OTAS_MINIMAL)
OTIDVs¶
Other Transaction IDVs — umbrella OT agreements that can have child awards.
list_otidvs()¶
List OTIDVs with keyset pagination, filtering, and shaping.
otidvs = client.list_otidvs(
limit=25,
cursor=None,
shape=ShapeConfig.OTIDVS_MINIMAL,
# Same filter parameters as list_otas()
)
Notes: - Uses keyset pagination (cursor + limit) rather than page numbers. - Filter parameters are identical to list_otas().
Returns: PaginatedResponse with OTIDV dictionaries
get_otidv()¶
otidv = client.get_otidv("OTIDV_KEY", shape=ShapeConfig.OTIDVS_MINIMAL)
Subawards¶
Subcontract and subaward data under prime awards.
list_subawards()¶
List subawards with filtering and shaping.
subawards = client.list_subawards(
page=1,
limit=25,
shape=ShapeConfig.SUBAWARDS_MINIMAL,
# Filter parameters (all optional)
award_key=None,
awarding_agency=None,
fiscal_year=None,
fiscal_year_gte=None,
fiscal_year_lte=None,
funding_agency=None,
prime_uei=None,
recipient=None,
sub_uei=None,
)
Filter Parameters: - award_key - Filter by prime award key - awarding_agency - Filter by awarding agency code - fiscal_year - Exact fiscal year - fiscal_year_gte / fiscal_year_lte - Fiscal year range - funding_agency - Filter by funding agency code - prime_uei - Filter by prime awardee UEI - recipient - Search by subrecipient name - sub_uei - Filter by subrecipient UEI
Returns: PaginatedResponse with subaward dictionaries
Vehicles¶
Vehicles provide a solicitation-centric way to discover groups of related IDVs and (optionally) expand into the underlying awards via shaping.
list_vehicles()¶
List vehicles with optional vehicle-level full-text search and ordering.
vehicles = client.list_vehicles(
page=1,
limit=25,
search="GSA schedule",
ordering="-vehicle_obligations",
shape=ShapeConfig.VEHICLES_MINIMAL,
flat=False,
flat_lists=False,
)
Parameters: - page (int): Page number (default: 1) - limit (int): Results per page (default: 25, max: 100) - search (str, optional): Vehicle-level search term - ordering (str, optional): Server-side sort. Allowed: vehicle_obligations, latest_award_date. Prefix with - for descending. - shape (str, optional): Shape string (defaults to ShapeConfig.VEHICLES_MINIMAL) - flat (bool): Flatten nested objects in shaped response - flat_lists (bool): Flatten arrays using indexed keys - joiner (str): Joiner used when flat=True (default: ".")
Returns: PaginatedResponse with vehicle dictionaries
get_vehicle()¶
Get a single vehicle by UUID.
vehicle = client.get_vehicle(
uuid="00000000-0000-0000-0000-000000000001",
shape=ShapeConfig.VEHICLES_COMPREHENSIVE,
)
Notes: - On the vehicle detail endpoint, search filters expanded awardees when your shape includes awardees(...) (it does not filter the vehicle itself).
list_vehicle_awardees()¶
List the IDV awardees for a vehicle.
awardees = client.list_vehicle_awardees(
uuid="00000000-0000-0000-0000-000000000001",
shape=ShapeConfig.VEHICLE_AWARDEES_MINIMAL,
)
list_vehicle_orders()¶
List task orders under a vehicle's IDVs (/api/vehicles/{uuid}/orders/). Optimized for fast pagination over large vehicles.
orders = client.list_vehicle_orders(
uuid="00000000-0000-0000-0000-000000000001",
limit=25,
ordering="-obligated",
shape=ShapeConfig.VEHICLE_ORDERS_MINIMAL,
)
Parameters: - uuid (str): Vehicle UUID - page (int): Page number (default: 1) - limit (int): Results per page (default: 25, max: 100) - ordering (str, optional): Server-side sort. Allowed: award_date (default), obligated, total_contract_value. Prefix with - for descending. - shape (str, optional): Shape string (defaults to ShapeConfig.VEHICLE_ORDERS_MINIMAL) - flat, flat_lists, joiner: as on other vehicles methods
Returns: PaginatedResponse with order (Contract) dictionaries
Vehicle response fields¶
The post-cutover (May 2026) vehicle response includes these top-level fields, all addressable via the shape parameter:
| Field | Type | Notes |
|---|---|---|
uuid | str | Stable identifier. |
solicitation_identifier | str | Solicitation shared by underlying IDVs. |
is_synthetic_solicitation | bool | True for GWAC orphans recovered via ACRO: prefix. |
agency_id | str | From IDV award-key suffix. |
program_acronym | str | None | New post-cutover field. |
organization_id | str | None | Awarding organization. |
organization | dict | None | Live awarding-org snapshot {organization_id, office_code, office_name, agency_code, agency_name, department_code, department_name}. Selected as a leaf field (shape=...,organization); not currently sub-selectable. |
vehicle_type, who_can_use, type_of_idc, contract_type | dict | None | Returned as {code, description}. |
description | str | None | Common text across IDV descriptions. |
descriptions | list[str] | None | Distinct IDV descriptions. |
idv_count, order_count | int | None | Denormalized rollups. |
holder_count | int | None | Distinct companies holding one of the vehicle's IDVs. |
order_winner_count | int | None | Distinct companies that have won a task order under the vehicle. |
awardee_count | int | None | Deprecated. Same value as order_winner_count; use that instead. Removed at the next major API version. |
total_obligated, vehicle_obligations, vehicle_contracts_value | Decimal | None | Denormalized rollups. |
award_date, latest_award_date, last_date_to_order | date | None | |
solicitation_title, solicitation_description, solicitation_date, opportunity_id | str / date / None | From SAM.gov via the linked Opportunity. |
naics_code, psc_code, set_aside, fiscal_year | int / str / None |
Vehicle shape expansions¶
awardees(...)— underlying IDV awards. Supports nestedorders(...).metrics(*)— bundled computed metrics:avg_offers_received,award_concentration_hhi,order_concentration_hhi,competed_rate,using_agency_count,avg_order_value,max_order_value,top_recipient_share,recent_obligations_24mo,recent_orders_24mo,days_since_last_order,obligation_to_ceiling_ratio. Defaults included inShapeConfig.VEHICLES_COMPREHENSIVE.organization— live awarding-org snapshot (selected as a leaf field; not sub-selectable).
Deprecated shape fields¶
The following fields and expansions are still served by the API (recomputed at request time from the underlying IDVs) but the API now returns a Deprecation: true response header for them. They will be removed in a future tango API release.
agency_details(top-level field andagency_details(*)expansion)competition_details(top-level field andcompetition_details(*)expansion)opportunity(*)expansion (use the new top-levelsolicitation_*andopportunity_idfields instead)
If you pass any of these in shape=..., the SDK will emit a Python DeprecationWarning. The default shapes (VEHICLES_MINIMAL, VEHICLES_COMPREHENSIVE) no longer include them.
IDVs¶
IDVs (indefinite delivery vehicles) are the parent “vehicle award” records that can have child awards/orders under them.
list_idvs()¶
idvs = client.list_idvs(
limit=25,
cursor=None,
shape=ShapeConfig.IDVS_MINIMAL,
awarding_agency="4700",
)
Notes:
- This endpoint uses keyset pagination (
cursor+limit) rather than page numbers.
get_idv()¶
idv = client.get_idv("SOME_IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)
list_idv_awards()¶
Lists child awards (contracts) under an IDV.
awards = client.list_idv_awards("SOME_IDV_KEY", limit=25)
list_idv_child_idvs()¶
Lists child IDVs under an IDV.
children = client.list_idv_child_idvs("SOME_IDV_KEY", limit=25)
list_idv_transactions()¶
tx = client.list_idv_transactions("SOME_IDV_KEY", limit=100)
Entities¶
Vendors, recipients, and organizations doing business with the government.
list_entities()¶
List and search for entities (vendors/recipients).
entities = client.list_entities(
page=1,
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
search=None,
cage_code=None,
naics=None,
name=None,
psc=None,
purpose_of_registration_code=None,
socioeconomic=None,
state=None,
total_awards_obligated_gte=None,
total_awards_obligated_lte=None,
uei=None,
zip_code=None,
)
Parameters: - page (int): Page number - limit (int): Results per page - shape (str): Fields to return - flat (bool): Flatten nested objects - flat_lists (bool): Flatten arrays with indexed keys
Filter Parameters: - search - Full-text search - cage_code - Filter by CAGE code - naics - Filter by NAICS code - name - Filter by entity name - psc - Filter by PSC code - purpose_of_registration_code - Filter by registration purpose - socioeconomic - Filter by socioeconomic status; takes SAM business-type codes (e.g. OY Black American Owned, A6 SBA-certified 8(a), A2 Woman Owned — see GET /api/business_types/), not set-aside codes; accepts pipe-separated values for OR semantics, e.g. socioeconomic="OY|A2" - state - Filter by state - total_awards_obligated_gte / total_awards_obligated_lte - Obligation amount range - uei - Filter by UEI - zip_code - Filter by ZIP code
Returns: PaginatedResponse with entity dictionaries
Example:
entities = client.list_entities(search="Booz Allen", limit=20)
for entity in entities.results:
print(f"{entity['legal_business_name']}")
print(f"UEI: {entity.get('uei', 'N/A')}")
if entity.get('business_types'):
print(f"Types: {', '.join(bt['code'] for bt in entity['business_types'])}")
get_entity()¶
Get a specific entity by UEI or CAGE code.
entity = client.get_entity(key="ZQGGHJH74DW7", shape=None)
Parameters: - key (str): UEI or CAGE code - shape (str, optional): Fields to return
Returns: Dictionary with entity details
Example:
entity = client.get_entity("ZQGGHJH74DW7")
print(f"Name: {entity['legal_business_name']}")
print(f"UEI: {entity['uei']}")
if entity.get('physical_address'):
addr = entity['physical_address']
print(f"Location: {addr.get('city')}, {addr.get('state_code')}")
Common Entity Fields: - uei - Unique Entity Identifier - cage_code - CAGE code - legal_business_name - Official business name - display_name - Display name - dba_name - Doing Business As name - business_types - Array of business type codes - primary_naics - Primary NAICS code - physical_address - Physical address (nested) - mailing_address - Mailing address (nested) - email_address - Contact email - entity_url - Website
Forecasts¶
Contract forecast and planning information.
list_forecasts()¶
List contract forecasts.
forecasts = client.list_forecasts(
page=1,
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
agency=None,
award_date_after=None,
award_date_before=None,
fiscal_year=None,
fiscal_year_gte=None,
fiscal_year_lte=None,
modified_after=None,
modified_before=None,
naics_code=None,
naics_starts_with=None,
search=None,
source_system=None,
status=None,
)
Parameters: - page (int): Page number - limit (int): Results per page - shape (str): Fields to return - flat (bool): Flatten nested objects - flat_lists (bool): Flatten arrays with indexed keys
Filter Parameters: - agency - Filter by agency code - award_date_after / award_date_before - Expected award date range - fiscal_year - Exact fiscal year - fiscal_year_gte / fiscal_year_lte - Fiscal year range - modified_after / modified_before - Last-modified date range - naics_code - NAICS code (exact match) - naics_starts_with - NAICS code prefix - search - Full-text search - source_system - Filter by source system - status - Filter by status
Returns: PaginatedResponse with forecast dictionaries
Example:
forecasts = client.list_forecasts(agency="GSA", fiscal_year=2025, limit=20)
for forecast in forecasts.results:
print(f"{forecast['title']}")
print(f"Anticipated: {forecast.get('anticipated_award_date', 'TBD')}")
print(f"Fiscal Year: {forecast.get('fiscal_year', 'N/A')}")
Common Forecast Fields: - id - Forecast identifier - title - Forecast title - description - Description - anticipated_award_date - Expected award date - fiscal_year - Fiscal year - naics_code - Industry code - status - Current status
Opportunities¶
Active contract opportunities and solicitations.
list_opportunities()¶
List contract opportunities/solicitations.
opportunities = client.list_opportunities(
page=1,
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
active=None,
agency=None,
awarded=None,
awardee_uei=None,
first_notice_date_after=None,
first_notice_date_before=None,
last_notice_date_after=None,
last_notice_date_before=None,
naics=None,
notice_type=None,
place_of_performance=None,
psc=None,
response_deadline_after=None,
response_deadline_before=None,
search=None,
set_aside=None,
solicitation_number=None,
)
Parameters: - page (int): Page number - limit (int): Results per page - shape (str): Fields to return - flat (bool): Flatten nested objects - flat_lists (bool): Flatten arrays with indexed keys
Filter Parameters: - active - Filter by active status (bool) - agency - Filter by agency code - awarded - Whether the opportunity has an award, either posted on it or linked to it (bool). Requires Tango API 5.5.0 - awardee_uei - Awardee UEI, case-insensitive; OR several with |. Requires Tango API 5.5.0 - first_notice_date_after / first_notice_date_before - First notice date range - last_notice_date_after / last_notice_date_before - Last notice date range - naics - NAICS code - notice_type - Filter by notice type - place_of_performance - Filter by place of performance - psc - PSC code - response_deadline_after / response_deadline_before - Response deadline range - search - Full-text search - set_aside - Set-aside type - solicitation_number - Solicitation number (exact match)
Returns: PaginatedResponse with opportunity dictionaries
Example:
opportunities = client.list_opportunities(agency="DOD", active=True, limit=20)
for opp in opportunities.results:
print(f"{opp['title']}")
print(f"Solicitation: {opp.get('solicitation_number', 'N/A')}")
print(f"Deadline: {opp.get('response_deadline', 'Not specified')}")
print(f"Active: {opp.get('active', False)}")
Common Opportunity Fields: - opportunity_id - Unique identifier - title - Opportunity title - solicitation_number - Solicitation number - description - Description - response_deadline - Response deadline - active - Is currently active - naics_code - Industry code - psc_code - Product/service code
Document roles — attachments(doc_role, doc_role_alt):
On a Pro plan or above, each attachment can say what the document is for. doc_role is one of requirement, instructions, pricing, terms, reference or unknown; doc_role_alt is a runner-up role, and is null on nearly every attachment.
opp = client.get_opportunity(
opportunity_id,
shape="opportunity_id,title,attachments(name,url,doc_role,doc_role_alt)",
)
for attachment in opp["attachments"]:
print(attachment["name"], attachment.get("doc_role"))
- You have to name them.
attachments(*)does not include either field, and no default shape does. - The keys are absent, not null, on an attachment Tango has not classified. Use
attachment.get("doc_role"). - A Free-plan request that names them gets the response without them, plus an entry in
meta.upgrade_hints. - There is no filter on document role.
Award fields:
Tango API 5.5.0 adds award information to opportunities. None of it is in the SDK's default shape, so name the fields you want.
opp = client.get_opportunity(
opportunity_id,
shape=(
"opportunity_id,title,awarded,award_date,award_amount,awardee,awardee_uei,"
"award_count,solicitation_opportunity_id,"
"awards(opportunity_id,notice_id,award_number,award_date,award_amount,awardee,awardee_uei)"
),
)
awarded = client.list_opportunities(
awardee_uei="ABCDEF123456",
shape="opportunity_id,title,award_date,awardee",
)
award_date,award_amount,awardeeandawardee_ueiare filled only where SAM.gov posted an award notice; elsewhere they are null.award_amountis the text SAM.gov published, not a number.- SAM.gov often posts an award as its own opportunity. When that award notice references its solicitation, the award's
solicitation_opportunity_idpoints back to the solicitation, and the solicitation'sawards(...)lists up to ten of the most recent linked awards;award_countis the full count. An award notice that does not reference its solicitation is not linked. awardedis true when the opportunity has an award of its own or has linked awards.- The
awardedandawardee_ueifilters search every opportunity, not just active ones, so passactive=Trueto narrow to open opportunities. - Notices accept
award_date,award_amount,awardeeandawardee_ueiinshapetoo.
Notices¶
Contract award notices and modifications.
list_notices()¶
List contract notices.
notices = client.list_notices(
page=1,
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
active=None,
agency=None,
naics=None,
notice_type=None,
posted_date_after=None,
posted_date_before=None,
psc=None,
response_deadline_after=None,
response_deadline_before=None,
search=None,
set_aside=None,
solicitation_number=None,
)
Parameters: - page (int): Page number - limit (int): Results per page - shape (str): Fields to return - flat (bool): Flatten nested objects - flat_lists (bool): Flatten arrays with indexed keys
Filter Parameters: - active - Filter by active status (bool) - agency - Filter by agency code - naics - NAICS code - notice_type - Filter by notice type - posted_date_after / posted_date_before - Posted date range - psc - PSC code - response_deadline_after / response_deadline_before - Response deadline range - search - Full-text search - set_aside - Set-aside type - solicitation_number - Solicitation number (exact match)
Returns: PaginatedResponse with notice dictionaries
Example:
notices = client.list_notices(agency="GSA", notice_type="Presolicitation", limit=20)
for notice in notices.results:
print(f"{notice['title']}")
print(f"Solicitation: {notice.get('solicitation_number', 'N/A')}")
print(f"Posted: {notice.get('posted_date', 'N/A')}")
Common Notice Fields: - notice_id - Notice identifier - title - Notice title - solicitation_number - Solicitation number - description - Description - posted_date - Date posted - naics_code - Industry code
Grants¶
Federal grant opportunities and assistance listings.
list_grants()¶
List grant opportunities.
grants = client.list_grants(
page=1,
limit=25,
shape=None,
flat=False,
flat_lists=False,
# Filter parameters (all optional)
agency=None,
applicant_types=None,
cfda_number=None,
funding_categories=None,
funding_instruments=None,
opportunity_number=None,
posted_date_after=None,
posted_date_before=None,
response_date_after=None,
response_date_before=None,
search=None,
status=None,
)
Parameters: - page (int): Page number - limit (int): Results per page (max 100) - shape (str): Response shape string - flat (bool): Flatten nested objects in shaped response - flat_lists (bool): Flatten arrays using indexed keys
Filter Parameters: - agency - Filter by agency code - applicant_types - Filter by applicant type - cfda_number - Filter by CFDA number - funding_categories - Filter by funding category - funding_instruments - Filter by funding instrument - opportunity_number - Filter by opportunity number (exact match) - posted_date_after / posted_date_before - Posted date range - response_date_after / response_date_before - Response date range - search - Full-text search - status - Filter by status
Returns: PaginatedResponse with grant dictionaries
Example:
grants = client.list_grants(agency="HHS", status="F", limit=20) # F = Forecasted, P = Posted
for grant in grants.results:
print(f"{grant['title']}")
print(f"Opportunity: {grant.get('opportunity_number', 'N/A')}")
print(f"Status: {grant.get('status', {}).get('description', 'N/A')}")
Common Grant Fields: - grant_id - Grant identifier - opportunity_number - Opportunity number - title - Grant title - status - Status information (nested object with code and description) - agency_code - Agency code - description - Description - last_updated - Last updated timestamp - cfda_numbers - CFDA numbers (list of objects with number and title) - applicant_types - Applicant types (list of objects with code and description) - funding_categories - Funding categories (list of objects with code and description) - funding_instruments - Funding instruments (list of objects with code and description) - category - Category (object with code and description) - important_dates - Important dates (list) - attachments - Attachments (list of objects)
Example with Expanded Fields:
# Get grants with expanded status and CFDA numbers
grants = client.list_grants(
shape="grant_id,title,opportunity_number,status(*),cfda_numbers(number,title)",
limit=10
)
for grant in grants.results:
print(f"Grant: {grant['title']}")
if grant.get('status'):
print(f"Status: {grant['status'].get('description')}")
if grant.get('cfda_numbers'):
for cfda in grant['cfda_numbers']:
print(f"CFDA: {cfda.get('number')} - {cfda.get('title')}")
GSA eLibrary Contracts¶
GSA Schedule contracts from the GSA eLibrary.
list_gsa_elibrary_contracts()¶
List GSA eLibrary contracts with filtering and shaping.
contracts = client.list_gsa_elibrary_contracts(
page=1,
limit=25,
shape=ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL,
# Filter parameters (all optional)
contract_number=None,
key=None,
piid=None,
schedule=None,
search=None,
sin=None,
uei=None,
)
Filter Parameters: - contract_number - Filter by contract number - key - Filter by key - piid - Filter by PIID - schedule - Filter by GSA schedule - search - Full-text search - sin - Filter by SIN (Special Item Number) - uei - Filter by UEI
Returns: PaginatedResponse with GSA eLibrary contract dictionaries
get_gsa_elibrary_contract()¶
Get a single GSA eLibrary contract by UUID.
contract = client.get_gsa_elibrary_contract("UUID_HERE")
Protests¶
Bid protest records from three venues: GAO, the U.S. Court of Federal Claims (COFC), and the SBA Office of Hearings and Appeals (SBA OHA).
list_protests()¶
List bid protests with filtering and shaping.
protests = client.list_protests(
page=1,
limit=25,
shape=ShapeConfig.PROTESTS_MINIMAL,
# Filter parameters (all optional)
source_system=None,
outcome=None,
case_type=None,
agency=None,
case_number=None,
solicitation_number=None,
protester=None,
filed_date_after=None,
filed_date_before=None,
decision_date_after=None,
decision_date_before=None,
naics_code=None,
search=None,
)
Filter Parameters: - source_system - Filter by source system: "gao", "cofc", or "sba_oha" - outcome - Filter by outcome (e.g., "Denied", "Dismissed", "Withdrawn", "Sustained") - case_type - Filter by case type - agency - Filter by protested agency - case_number - Filter by base case number, matched case-insensitively (e.g., "b-423274" for GAO, "26-1391" for COFC, "SIZ-6100" for SBA OHA) - solicitation_number - Filter by solicitation number - protester - Search by protester name - filed_date_after / filed_date_before - Filed date range - decision_date_after / decision_date_before - Decision date range - naics_code - The NAICS code at issue in an SBA OHA size or NAICS appeal (e.g., "541519"). GAO and COFC protests carry no NAICS code, so this filter returns SBA OHA records only - search - Full-text search
Returns: PaginatedResponse with protest dictionaries
Example:
protests = client.list_protests(
source_system="gao",
outcome="Sustained",
filed_date_after="2024-01-01",
shape="case_id,case_number,title,outcome,filed_date,dockets(docket_number,outcome)",
limit=25,
)
for protest in protests.results:
print(f"{protest['case_number']}: {protest['title']} — {protest['outcome']}")
get_protest()¶
Get a single protest by case_id (UUID).
protest = client.get_protest(
"CASE_UUID",
shape="case_id,case_number,title,source_system,outcome,filed_date,dockets(*)",
)
Notes: - Use shape=...,dockets(...) to include nested docket records.
Contract Appeals¶
Contract Disputes Act appeal decisions from the Civilian Board of Contract Appeals (CBCA) and the Armed Services Board of Contract Appeals (ASBCA).
These are not bid protests. A protest challenges an award or a solicitation before performance; an appeal here challenges a contracting officer's final decision under an existing contract — a claim, a termination, a differing-site-conditions dispute. Protests live at Protests and share no identifiers with this resource.
One row is one decision as the board's own listing publishes it, so several fields describe the listing rather than the dispute. listed goes false once the board's newest listing stops carrying the decision; the row is kept, never dropped.
list_contract_appeals()¶
List appeal decisions with filtering and shaping.
appeals = client.list_contract_appeals(
page=1,
limit=25,
shape=ShapeConfig.CONTRACT_APPEALS_MINIMAL,
# Filter parameters (all optional)
board=None,
docket=None,
appellant=None,
judge=None,
decision_type=None,
decision_date_after=None,
decision_date_before=None,
listed=None,
document_id=None,
search=None,
ordering=None,
)
Filter Parameters: - board - "cbca" or "asbca"; OR both with "cbca|asbca" - docket - Exact docket match, e.g. "3288-R", "59116" or "7092-C(6682, 6765, 6767)". A leading CBCA / ASBCA and No. / Nos. are ignored, so a docket can be pasted as it is cited. Commas are part of a docket, never a separator; OR several with | - appellant - Case-insensitive substring match on the appellant's name (min 2 characters) - judge - Case-insensitive exact match on the judge as the listing names them - decision_type - CBCA only: "Decision", "Dismissal", "Order", "Full Board Order", or the listing's own text. ASBCA rows carry no type - decision_date_after / decision_date_before - Decision date range (YYYY-MM-DD) - listed - Whether the board's newest listing still carries the decision - document_id - The board's own document id - search - Ranked full-text search over the appellant and the full decision text (min 2 characters); wrap in double quotes for a phrase - ordering - decision_date (the default, as -decision_date), appellant, first_listed_at or rank. rank requires a non-empty search
Returns: PaginatedResponse with appeal-decision dictionaries
Example:
appeals = client.list_contract_appeals(
board="asbca",
search="differing site conditions",
decision_date_after="2025-01-01",
ordering="rank",
limit=25,
)
for appeal in appeals.results:
dockets = ", ".join(appeal["docket_numbers"])
print(f"{appeal['board'].upper()} {dockets}: {appeal['appellant']} ({appeal['decision_date']})")
get_contract_appeal()¶
Get a single decision by uuid.
appeal = client.get_contract_appeal(
"DECISION_UUID",
shape=ShapeConfig.CONTRACT_APPEALS_COMPREHENSIVE,
)
Notes: - docket_numbers is a list — the dockets with the board prefix stripped (3288-R, 59116), and a consolidated appeal carries several. docket_raw keeps the listing's own text, and docket_source says where the dockets came from. - Reading decision_text requires an Enterprise plan, and below that tier the key is absent rather than null — read it with .get(), not [...]. Neither default shape names it: the body runs to roughly 100K characters, so asking for it on every row is rarely what you want. - Searching that text is free at every plan. search= matches inside the decision body and returns no fragment of it, so buying the body does not change what you can find.
Federal Register¶
Federal Register documents — rules, proposed rules, notices and presidential documents published since 1994.
A document is identified by uuid. document_number is not unique on its own: the Federal Register reused some numbers before 2016, so document_number= can return more than one document.
list_federal_register_documents()¶
List documents with filtering and shaping.
documents = client.list_federal_register_documents(
page=1,
limit=25,
shape=ShapeConfig.FEDERAL_REGISTER_MINIMAL,
# Filter parameters (all optional)
search=None,
document_number=None,
type=None,
agency=None,
fr_agency=None,
publication_date_after=None,
publication_date_before=None,
effective_on_after=None,
effective_on_before=None,
comments_close_on_after=None,
comments_close_on_before=None,
comments_open=None,
cfr_title=None,
cfr_part=None,
significant=None,
rin=None,
executive_order_number=None,
ordering=None,
)
Filter Parameters: - search - Ranked full-text search over the title, abstract and action; wrap in double quotes for a phrase - document_number - Exact FR document number, e.g. "2016-31922"; OR several with | - type - "Notice", "Rule", "Proposed Rule", "Presidential Document", "Correction", "Sunshine Act Document" or "Uncategorized Document" (case-insensitive); OR several with |. An unknown type is an error, not an empty page. Documents published before 2008 are mostly Uncategorized Document, so a type filter undercounts that era - agency - A Tango agency name, abbreviation, code or organization key, e.g. "EPA". Matches a document when any agency it lists falls within that organization, so a department includes its sub-agencies; OR several with | - fr_agency - The Federal Register's own agency slug, e.g. "environmental-protection-agency"; OR several with | - publication_date_after / publication_date_before - Publication date range (YYYY-MM-DD) - effective_on_after / effective_on_before - Effective date range (YYYY-MM-DD) - comments_close_on_after / comments_close_on_before - Comment-deadline range (YYYY-MM-DD) - comments_open - True for documents whose comment period closes today or later, False for those already closed; documents with no comment deadline match neither - cfr_title - A CFR title number, e.g. "40" - cfr_part - A CFR part number, e.g. "52". Requires cfr_title, and both must match the same CFR reference, so cfr_title="40", cfr_part="52" finds 40 CFR 52 - significant - Significant under Executive Order 12866 - rin - A Regulation Identifier Number, e.g. "2060-AV16"; OR several with | - executive_order_number - Exact executive order number - ordering - publication_date (the default, as -publication_date), effective_on, comments_close_on, document_number or rank. rank requires a non-empty search
Returns: PaginatedResponse with document dictionaries
Example:
documents = client.list_federal_register_documents(
type="Proposed Rule",
comments_open=True,
agency="EPA",
limit=25,
)
for doc in documents.results:
print(f"{doc['document_number']} ({doc['publication_date']}): {doc['title']}")
print(f" comments close {doc['comments_close_on']}")
get_federal_register_document()¶
Get a single document by uuid. To look one up by its document number, use list_federal_register_documents(document_number=...).
document = client.get_federal_register_document(
"DOCUMENT_UUID",
shape=ShapeConfig.FEDERAL_REGISTER_COMPREHENSIVE,
)
Notes: - agencies, cfr_references, dockets and topics are the Federal Register's own structures, served as published. agencies carries Federal Register agency slugs, not Tango organization keys. - full_text, the document's plain text, is available on this method only and only when named in shape (e.g. shape="uuid,title,full_text"). It can run to several MB, so neither default shape includes it.
State & Local (SLED)¶
State, local and education procurement — solicitations that never appear on SAM.gov because they were never federal. Beta: coverage is partial and grows one jurisdiction at a time.
This data does not join to the federal data. There is no UEI, no PIID, no agency-hierarchy key and no NAICS/PSC crosswalk; organization(*) here is three strings, not the federal 7-key office payload.
list_sled_opportunities()¶
List SLED solicitations with filtering and shaping.
solicitations = client.list_sled_opportunities(
page=1,
limit=25,
shape=ShapeConfig.SLED_OPPORTUNITIES_MINIMAL,
# Filter parameters (all optional)
state=None,
jurisdiction=None,
status=None,
active=None,
agency=None,
solicitation_number=None,
solicitation_type=None,
has_documents=None,
revision_kind=None,
naics=None,
nigp=None,
unspsc=None,
category=None,
category_code=None,
posted_after=None,
posted_before=None,
response_deadline_after=None,
response_deadline_before=None,
first_seen_after=None,
first_seen_before=None,
change_seen_after=None,
modified_after=None,
modified_before=None,
search=None,
ordering=None,
)
Two defaults worth knowing before your first call:
- Passing neither
statusnoractivereturns open solicitations only. Only about a fifth of the corpus is open, and a portal drops a closed solicitation rather than restating it, so the API defaults the list tostatus=open. Pass an explicitstatusto page the whole corpus.status="unknown"(standing rosters, dateless RFIs) is hidden by that default — reach it withstatus="open|unknown".get_sled_opportunity()returns the solicitation whatever its status. statusis Tango's answer, not the portal's. It is derived from the portal's word, the deadline and the clock, and refreshed every fifteen minutes. The portal's own word is served assource_statusand is frozen at last capture — most of what it calls open already has a passed deadline. Never filter liveness on it.
Filter Parameters: - state - Two-letter state or territory code. Multi-value: "TX|OK" - jurisdiction - "state", "local", "education", or "unknown" for aggregator rows that cannot tell state from local - status - "open", "closed", "awarded", "cancelled", "unknown" - active - Sugar for federal-shaped callers: True is status="open"; False is its complement, so it includes unknown - agency - Substring match on the buyer's published text (min 2 characters); no code resolution behind it - solicitation_number - The number a human would quote; null on roughly a third of the corpus - solicitation_type - "rfp", "ifb", "rfq", "rfi", "itb", "sole_source", "grant", "other". "null" (portal states no type) is a distinct answer from "other" - has_documents - Whether the solicitation advertises at least one document - revision_kind - Kind of the most recent substantive revision - naics / nigp / unspsc / category - Exact match within one category scheme. naics is thin on purpose: scheme tagging is mid-migration, so only a small share of entries are tagged NAICS - category_code - Match a code under ANY scheme, including the untagged pre-migration strings. The escape hatch when a scheme-specific filter returns less than expected - posted_after / posted_before - Posted-date range - response_deadline_after / response_deadline_before - Deadline range - first_seen_after / first_seen_before - When Tango first observed it. The polling primitive - change_seen_after - When Tango observed the last substantive change. A scrape date, not an amendment date - modified_after / modified_before - When the Tango row last changed - platform / native_id / external_id - Support filters for reproducing a record with us. Not in any response shape and not stable values - search - Ranked full-text search over title, agency, identifiers, category labels and description, widened by the solicitations whose attachment text matched (min 2 characters) - ordering - rank, response_deadline, posted_date, first_seen_at, last_seen_at, last_change_seen_at, modified. rank requires a non-empty search
Returns: PaginatedResponse with solicitation dictionaries
Example:
# Open Texas solicitations closing this month, newest first
page = client.list_sled_opportunities(
state="TX",
response_deadline_before="2026-10-01",
ordering="response_deadline",
limit=25,
)
for row in page.results:
print(f"{row['state']} {row.get('solicitation_number') or '—'}: {row['title']}")
# Full-text search puts the matching passage on each row that matched on its body
hits = client.list_sled_opportunities(
search="environmental mitigation",
shape="opportunity_id,title,state,snippet,response_deadline",
)
snippet is present only under search=, and only on rows that matched on their description — a title-or-agency match honestly carries none. Attachment matching contributes ids only: a caller learns that a document matched, never what it said.
get_sled_opportunity()¶
Get a single solicitation by opportunity_id, whatever its status.
row = client.get_sled_opportunity(
"OPPORTUNITY_UUID",
shape="opportunity_id,title,status,attachments(*),revisions(*)",
)
Notes: - meta.attachment_count can be lower than len(row["attachments"]). Some portals auto-generate a cover sheet alongside the real documents; it is listed and flagged is_generated_summary, but excluded from the count and from has_documents. The count answers "does this hold its solicitation package"; the array answers "what files exist". - size_bytes and char_count only mean something as a pair — 3 MB that yielded no characters is a scan awaiting OCR. - raw(*) needs a Small plan or above, and is explicitly unstable: its shape varies by portal platform. - delisted_at is when a portal dropped the solicitation from its listing before its deadline, and is what status_reason="delisted" means. It is null when the solicitation was never delisted or has been seen again since. - meta.jurisdiction_declared says whether the source stated the jurisdiction level itself. False means Tango classified it from the issuer's name, which is the common case on a state's central portal.
Reading a document body — attachments(extracted_text):
row = client.get_sled_opportunity(
opportunity_id,
shape="opportunity_id,attachments(name,size_bytes,extracted_text)",
)
Needs a Small plan or above, and Tango API 4.25.1 or later (client.get_version() reports what you are calling). Three rules:
- You have to name it.
attachments(*)does not carry the body, and neither default shape names it — the API only resolves it for a caller who asked, so a default would make every detail fetch pay for a document nobody wanted to read. - The key is absent, not null, whenever the text is not being served to you: below Small (where it is withheld and named in
meta.upgrade_hints), on a contested document, or where the text cannot be resolved. Useattachment.get("extracted_text"). - A contested document never returns text, at any plan — its stored bytes disagree with what the record advertised, so its text is not reliably that record's.
Searching document text and reading it are separate things. search= matches inside attachment text on every plan and returns no fragment of it; extracted_text is a per-record read on Small and above. Buying the body does not change search.
list_sled_opportunity_revisions()¶
List one solicitation's observed revision history.
revisions = client.list_sled_opportunity_revisions(
"OPPORTUNITY_UUID",
kind=None,
source_declared=None,
observed_after=None,
observed_before=None,
)
Notes: - observed_at is the scrape that saw the change, not the date the agency made it. No state portal emits amendment notices, so kind is Tango's inference from the diff on about 95% of revisions, resolution is that state's crawl cadence, and history starts when Tango began reading the jurisdiction. - Unlike the revisions(*) expand, this route serves enrichment rows — Tango's own detail fetch filling in coverage rather than an agency amendment. Pass kind="enrichment" for only those. - changes (the per-field before and after) needs a Small plan; it is left out of the default shape for that reason. changed_fields names what moved at every plan.
get_sled_coverage()¶
Get the per-state coverage rollup. Takes no parameters; not shaped or paginated.
coverage = client.get_sled_coverage()
print(coverage["totals"])
for row in coverage["states"]:
print(row["state"], row["total_count"], row["by_status"], row["last_change_observed_at"])
Call this before treating a per-state count as market size. A thin result for a state is at least as likely to be a portal Tango does not read as a quiet market, and that is the ambiguity this endpoint exists to resolve. Every state row carries all five status buckets whether or not they have rows, so a total and two buckets never invite subtraction.
list_sled_forecasts()¶
List planned state procurements.
forecasts = client.list_sled_forecasts(
state=None,
agency=None,
procurement_category=None,
procurement_method=None,
contract_number=None,
incumbent_name=None,
advertisement_after=None,
advertisement_before=None,
first_seen_after=None,
first_seen_before=None,
modified_after=None,
modified_before=None,
search=None,
ordering=None,
)
Notes: - Forecasts carry no liveness at all — no deadline to have passed, so there is no status field, no active filter, and no open-only default. Currency is the caller's call from estimated_advertisement_date. - estimated_advertisement_date is the start of the published quarter, not a posting date. estimated_advertisement_raw keeps the portal's own words ("Q3 (Jan.-March 2027)"), and a large share of rows publish no quarter at all. - estimated_value(min,max,raw) is parsed from a free-text award band at serve time. A band naming one number is a floor, so max is None — never read a missing max as an unbounded ceiling. raw is always there to check the parse against. - incumbent_name is published text, not a resolved Tango entity. - ordering: rank, estimated_advertisement_date, first_seen_at, last_seen_at, modified.
get_sled_forecast()¶
forecast = client.get_sled_forecast("FORECAST_UUID")
GSA eBuy¶
GSA eBuy requests for quotes, proposals and information (RFQs, RFPs, RFIs), keyed by rfq_id.
Access is scoped to your account. You see only requests posted under the GSA schedule contracts linked to your account, and the endpoints require the Pro tier or above (below it they return 403). With no linked contract, list_ebuy_requests() returns an empty page rather than an error, and get_ebuy_request() raises TangoNotFoundError for a request outside your scope, the same as for an id that does not exist. Use get_ebuy_access() to tell "no access" from "no matches".
list_ebuy_requests()¶
List requests with filtering and shaping.
requests = client.list_ebuy_requests(
page=1,
limit=25,
shape=ShapeConfig.EBUY_REQUESTS_MINIMAL,
# Filter parameters (all optional)
search=None,
rfq_id=None,
reference_number=None,
request_type=None,
status=None,
sin=None,
schedule=None,
buyer_agency=None,
agency=None,
contract_number=None,
issue_date_after=None,
issue_date_before=None,
close_date_after=None,
close_date_before=None,
ordering=None,
)
Filter Parameters: - search - Full-text search over the title, description, reference number, request id and attachment text. Results rank by relevance unless ordering is given - rfq_id - Exact request id, e.g. "RFQ1835158" - reference_number - The buyer's own solicitation number; dashes are ignored - request_type - "RFQ", "RFP" or "RFI" - status - "Open" or "Cancelled", as last seen (see the note below) - sin - Special Item Number, e.g. "54151S" - schedule - GSA schedule - buyer_agency - The buyer agency as eBuy names it (free text) - agency - A Tango agency name, abbreviation, code or organization key, e.g. "GSA". Matches the whole organization subtree, so a department includes its sub-agencies. Requires Tango API 5.3.0 - contract_number - Narrow to requests posted under one of your own linked contracts. A contract not linked to your account returns an empty page, not an error - issue_date_after / issue_date_before - Issue date range (YYYY-MM-DD, inclusive) - close_date_after / close_date_before - Close date range (YYYY-MM-DD, inclusive) - ordering - issue_date (the default, as -issue_date), close_date, last_seen or modified; prefix - for descending
String filters accept several values joined with | (OR).
Returns: PaginatedResponse with request dictionaries
Example:
requests = client.list_ebuy_requests(status="Open", sin="54151S", ordering="close_date")
for req in requests.results:
print(f"{req['rfq_id']} closes {req['close_date']}: {req['title']}")
print(f" last seen {req['last_seen']}")
Notes: - status is frozen at the last state the request was seen in. Only currently-active requests are carried, so a request that closes stops appearing rather than getting a final row. Open means "open the last time it was seen", not "open now"; read last_seen for staleness. - The contract number a request was posted under is never returned in any payload. - buyer_agency_code and some other buyer and contact fields are sparse on older requests.
get_ebuy_request()¶
Get a single request by rfq_id.
request = client.get_ebuy_request(
"RFQ1835158",
shape=ShapeConfig.EBUY_REQUESTS_COMPREHENSIVE,
)
for attachment in request["attachments"]:
print(attachment["doc_seq_num"], attachment["doc_name"], attachment["is_link"])
The default shape returns every field plus two expands: organization (the buyer office, with the same seven keys as other resources' organization expand) and attachments. Each attachment carries doc_seq_num, doc_name, doc_type, doc_path, is_link and doc_session_date. is_link=True means doc_path is an outbound URL with no stored document behind it. amendments, line_items and addresses are lists of objects, served as eBuy publishes them.
get_ebuy_attachment_url()¶
Get a short-lived download URL for one stored attachment.
url = client.get_ebuy_attachment_url("RFQ1835158", doc_seq_num=1)
Returns: The signed URL the API redirects to, as a string. The SDK reads the redirect without following it, so no document is downloaded. The URL expires after about five minutes: fetch it promptly, and call this again rather than storing it.
Raises: - TangoAttachmentLinkError (a TangoValidationError) - The entry is an external link (is_link), not a stored document. The link is on error.url - TangoNotFoundError - The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet
get_ebuy_access()¶
Check whether your account can read eBuy requests.
access = client.get_ebuy_access()
if not access.enabled:
print(access.reason) # "tier_required" or "no_contract_grant"
print(access.contracts) # your own linked contracts, sorted
Returns: EbuyAccess with enabled (bool), reason ("tier_required", "no_contract_grant" or None; tier_required wins when both apply) and contracts (list of str).
Budget¶
Federal account × fiscal year budget rollups, covering the full budget lifecycle (requested → enacted → apportioned → obligated → outlayed), pre-computed ratios and trends, the contract / assistance / unlinked breakdown, and request-vs-actual spend.
list_budget_accounts()¶
List budget accounts. One row per (federal_account_symbol, fiscal_year).
accounts = client.list_budget_accounts(
page=1,
limit=25,
shape=ShapeConfig.BUDGET_ACCOUNTS_MINIMAL,
# Filter parameters (all optional)
federal_account_symbol=None,
fiscal_year=None,
fiscal_year_gte=None,
fiscal_year_lte=None,
agency_code=None,
bureau_name=None,
account_title=None,
bea_category=None,
on_off_budget=None,
subfunction_code=None,
account_category=None,
account_category_in=None,
data_through_period=None,
data_through_period_gte=None,
data_through_period_lte=None,
data_through_period_isnull=None,
search=None,
ordering=None,
)
Filter Parameters: - federal_account_symbol - Exact federal account symbol (e.g., "097-0100") - fiscal_year - Fiscal year (exact) - fiscal_year_gte / fiscal_year_lte - Fiscal year range - agency_code - Agency code (exact) - bureau_name - Bureau name (exact) - account_title - Account title (case-insensitive substring match) - bea_category - BEA category (exact) - on_off_budget - On/off budget flag (exact) - subfunction_code - Subfunction code (exact) - account_category - Account category (exact): budgetary or credit_financing today; treat it as an open string - account_category_in - Comma-separated account categories to match any of (e.g. "budgetary,credit_financing") - data_through_period - File A period (1-12) the account-year's figures run through (exact); below 12 the year is partial - data_through_period_gte / data_through_period_lte - data_through_period range - data_through_period_isnull - True for accounts with no File A data, False for accounts with it - search - Full-text search over account_title, agency_name, bureau_name - ordering - Sort field; prefix with - for descending. The default is the latest fiscal year first, then largest enacted_ba first, with accounts that have no enacted_ba last.
Source anomalies: the default shape includes account_category and source_anomalies. source_anomalies is a list of problems found in the source data behind the account, and [] when there are none. Each element is a BudgetAccountSourceAnomaly (a TypedDict; every key is optional and may be None, so read keys with .get()): code, field, bound_field, action (capped or flagged), reported_value, served_value, likely_cause, affected_fields, message, and source (dataset, fiscal_year, and rows of fiscal_period, piid, parent_piid, tas, reporting_agency_id, transaction_obligated_amount, file_c_source). Today code is one of contract_exceeds_obligations, assistance_exceeds_obligations or contract_without_obligations; treat it as an open string. There is no filter on anomalies.
for acct in client.list_budget_accounts(fiscal_year=2025).results:
for anomaly in acct["source_anomalies"] or []:
print(acct["federal_account_symbol"], anomaly.get("code"), anomaly.get("action"))
Returns: PaginatedResponse of BudgetAccount records (see ShapeConfig for the default shape).
Example:
accounts = client.list_budget_accounts(
agency_code="097",
fiscal_year_gte=2023,
ordering="-enacted_ba",
limit=10,
)
for acct in accounts.results:
print(f"{acct.federal_account_symbol} FY{acct.fiscal_year}: "
f"enacted ${acct.enacted_ba:,}")
get_budget_account()¶
Get a single budget account by id.
account = client.get_budget_account(
12345,
shape=ShapeConfig.BUDGET_ACCOUNTS_MINIMAL,
)
Parameters: - id (str | int): Budget account id. - shape (str, optional): Response shape. Defaults to BUDGET_ACCOUNTS_MINIMAL. - flat / flat_lists / joiner: See Shaping Guide.
Returns: A BudgetAccount record.
get_budget_account_quarters()¶
Get quarterly TAS-grain flow for a budget account. FY21+ only.
quarters = client.get_budget_account_quarters(12345, limit=25)
Parameters: - id (str | int): Budget account id. - tas (str, optional): Narrow to a single Treasury Account Symbol. - limit (int): Results per page (max 100).
Returns: PaginatedResponse of quarterly flow records.
get_budget_account_recipients()¶
Get funding-office × recipient contract-flow detail for a budget account.
recipients = client.get_budget_account_recipients(
12345,
funding_organization_id=None,
limit=25,
)
Parameters: - id (str | int): Budget account id. - funding_organization_id (str, optional): Narrow to a single funding office (Organization UUID). - limit (int): Results per page (max 100).
Returns: PaginatedResponse of (funding_office, recipient) flow records.
Business Types¶
Business type classifications.
list_business_types()¶
List available business type codes.
business_types = client.list_business_types(page=1, limit=25)
Parameters: - page (int): Page number - limit (int): Results per page
Returns: PaginatedResponse with business type dictionaries
Example:
business_types = client.list_business_types(limit=50)
for biz_type in business_types.results:
print(f"{biz_type.code}: {biz_type.name}")
Business Type Fields: - code - Business type code - name - Business type name - description - Description
NAICS¶
NAICS (North American Industry Classification System) codes.
list_naics()¶
List NAICS codes with optional filtering.
naics = client.list_naics(
page=1,
limit=25,
# Filter parameters (all optional)
employee_limit=None,
employee_limit_gte=None,
employee_limit_lte=None,
revenue_limit=None,
revenue_limit_gte=None,
revenue_limit_lte=None,
search=None,
)
Filter Parameters: - employee_limit - Exact employee size standard - employee_limit_gte / employee_limit_lte - Employee limit range - revenue_limit - Exact revenue size standard - revenue_limit_gte / revenue_limit_lte - Revenue limit range - search - Full-text search (code or description)
Returns: PaginatedResponse with NAICS dictionaries
Example:
naics = client.list_naics(search="software", limit=10)
for code in naics.results:
print(f"{code['code']}: {code['description']}")
get_naics()¶
Get a single NAICS code by code string.
naics = client.get_naics("541511")
Returns: Dictionary with NAICS code details.
get_naics_metrics()¶
Get computed metrics for a NAICS code.
metrics = client.get_naics_metrics(code="541511", months=12, period_grouping="month")
PSC¶
Product and Service Codes.
list_psc()¶
psc = client.list_psc(page=1, limit=25)
get_psc()¶
psc = client.get_psc("D302")
get_psc_metrics()¶
metrics = client.get_psc_metrics(code="D302", months=12, period_grouping="month")
MAS SINs¶
GSA Multiple Award Schedule Special Item Numbers.
list_mas_sins()¶
sins = client.list_mas_sins(page=1, limit=25)
get_mas_sin()¶
sin = client.get_mas_sin("54151S")
Assistance Listings (CFDA)¶
Catalog of Federal Domestic Assistance listings.
list_assistance_listings()¶
listings = client.list_assistance_listings(page=1, limit=25)
get_assistance_listing()¶
listing = client.get_assistance_listing("10.310")
Departments¶
list_departments()¶
depts = client.list_departments(page=1, limit=25)
get_department()¶
dept = client.get_department("097")
Business Types (by code)¶
get_business_type()¶
Get a single business type by code.
bt = client.get_business_type("A6")
IT Dashboard¶
Federal IT investments from the OMB IT Dashboard.
list_itdashboard_investments()¶
investments = client.list_itdashboard_investments(
page=1,
limit=25,
search=None,
agency_code=None,
type_of_investment=None,
# Pro/Business+ tier-gated filters available
)
Notes: - Filter tier-gating: search is free; agency_code, type_of_investment require Pro; agency_name, cio_rating, performance_risk require Business+. - Shape defaults to ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL.
get_itdashboard_investment()¶
investment = client.get_itdashboard_investment("023-000001234")
Entity Sub-resources¶
list_entity_contracts()¶
contracts = client.list_entity_contracts("ABCDEF123456", limit=25)
list_entity_idvs()¶
idvs = client.list_entity_idvs("ABCDEF123456", limit=25)
list_entity_otas() / list_entity_otidvs()¶
otas = client.list_entity_otas("ABCDEF123456", limit=25)
otidvs = client.list_entity_otidvs("ABCDEF123456", limit=25)
list_entity_subawards()¶
subawards = client.list_entity_subawards("ABCDEF123456", limit=25)
list_entity_lcats()¶
lcats = client.list_entity_lcats("ABCDEF123456", limit=25)
get_entity_metrics()¶
metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")
get_entity_budget_flows()¶
Get budget flows for an entity (/api/entities/{uei}/budget-flows/) — the federal accounts that funded contracts and assistance awarded to this entity.
flows = client.get_entity_budget_flows("ABCDEF123456", fiscal_year=2024)
for row in flows.results:
print(row["federal_account_symbol"], row["contract_obligated"])
# next page
more = client.get_entity_budget_flows("ABCDEF123456", page=2, fiscal_year=2024)
Parameters: - uei (str): Entity UEI. Required. - page (int): Page number. Default 1. - limit (int): Results per page. Default 25, max 100. - fiscal_year (int | None): Optional fiscal year filter.
Returns: PaginatedResponse[dict[str, Any]] — standard count / next / previous / results. Result rows are raw dicts from the API (not shape-controlled).
IDV LCATs¶
list_idv_lcats()¶
lcats = client.list_idv_lcats("GS-00F-XXXX", limit=25)
Agency Sub-resources¶
list_agency_awarding_contracts()¶
List contracts where the agency is the awarding agency.
contracts = client.list_agency_awarding_contracts("4700", limit=25)
list_agency_funding_contracts()¶
List contracts where the agency is the funding agency.
contracts = client.list_agency_funding_contracts("4700", limit=25)
Resolve / Validate¶
resolve()¶
Resolve a free-text name to ranked entity or organization candidates.
result = client.resolve(
name="Lockheed Martin",
target_type="entity", # or "organization"
state="MD", # optional
city="Bethesda", # optional
context="defense contractor", # optional
)
for candidate in result.candidates:
print(candidate.identifier, candidate.display_name)
Notes: - Free-tier: up to 3 candidates with identifier and display_name. - Pro+: up to 5 candidates with additional match_tier field.
validate()¶
Validate the format of a PIID, solicitation number, or UEI.
result = client.validate(identifier_type="uei", value="ABCDEF123456")
# identifier_type is one of: "piid", "solicitation", "uei"
Note: The parameter is named identifier_type (not type) to avoid shadowing the Python builtin.
Opportunities (attachments)¶
search_opportunity_attachments()¶
Semantic search over opportunity attachments. q is required.
results = client.search_opportunity_attachments(
q="cybersecurity",
top_k=10,
include_extracted_text=False,
)
Parameters: - q (str): Search query (required) - top_k (int, optional): Number of top results to return - include_extracted_text (bool, optional): Whether to include extracted text from attachments in results
Returns: dict with search results
Webhook Alerts¶
The Alerts API is the canonical (and only) write surface for webhook subscriptions. Every alert maps to one of the five alerts.*.match event types and delivers when its saved-search filters match new or modified records.
list_webhook_alerts()¶
alerts = client.list_webhook_alerts(page=1, page_size=25)
get_webhook_alert()¶
alert = client.get_webhook_alert("ALERT_UUID")
create_webhook_alert()¶
alert = client.create_webhook_alert(
name="New cloud IT contracts",
query_type="contract",
filters={"naics": "541511"},
)
For multi-endpoint accounts, pin the delivery target with endpoint=:
alert = client.create_webhook_alert(
name="New cloud IT contracts",
query_type="contract",
filters={"naics": "541511"},
endpoint="ENDPOINT_UUID",
)
Notes: - name and query_type are required. query_type is singular (e.g. "contract", not "contracts"). - endpoint= is optional and only required when the account has multiple webhook endpoints; for single-endpoint accounts the server auto-resolves.
update_webhook_alert()¶
alert = client.update_webhook_alert("ALERT_UUID", name="Updated name")
delete_webhook_alert()¶
client.delete_webhook_alert("ALERT_UUID")
Utility¶
get_version()¶
version = client.get_version()
list_api_keys()¶
keys = client.list_api_keys()
Webhooks¶
Webhook APIs let Large / Enterprise users manage delivery endpoints and discover the supported event-type catalog. Filter subscriptions (alerts) live in the Webhook Alerts section above.
For testing, signing, and a CLI tool, see
docs/WEBHOOKS.md. This section covers SDK method signatures only.
list_webhook_event_types()¶
Discover supported event_type values.
info = client.list_webhook_event_types()
print(info.event_types[0].event_type)
list_webhook_endpoints()¶
List your webhook endpoint(s).
endpoints = client.list_webhook_endpoints(page=1, limit=25)
get_webhook_endpoint()¶
endpoint = client.get_webhook_endpoint("ENDPOINT_UUID")
create_webhook_endpoint() / update_webhook_endpoint() / delete_webhook_endpoint()¶
In production, MakeGov provisions the initial endpoint for you. These are most useful for dev/self-service.
endpoint = client.create_webhook_endpoint("https://example.com/tango/webhooks")
endpoint = client.update_webhook_endpoint(endpoint.id, is_active=False)
client.delete_webhook_endpoint(endpoint.id)
test_webhook_delivery()¶
Send an immediate test webhook to your configured endpoint.
result = client.test_webhook_delivery()
print(result.success, result.status_code)
get_webhook_sample_payload()¶
Fetch Tango-shaped sample deliveries.
sample = client.get_webhook_sample_payload(event_type="alerts.contract.match")
print(sample["event_type"])
Deliveries / redelivery¶
The API does not currently expose a public /api/webhooks/deliveries/ or redelivery endpoint. Use:
test_webhook_delivery()for connectivity checksget_webhook_sample_payload()for building handlers
Receiving webhooks (signature verification)¶
Every delivery includes an HMAC signature header:
X-Tango-Signature: sha256=<hex digest>
Compute the digest over the raw request body bytes using your shared secret.
The SDK ships a stdlib-only verifier that mirrors the Tango server's signing scheme byte-for-byte. Use it instead of hand-rolling — it's importable from a default install (no extras needed):
from tango.webhooks import verify_signature
if not verify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
return 401
verify_signature returns False for missing/empty/malformed headers — it never raises. Comparison is constant-time.
Webhook tooling (tango.webhooks)¶
The tango.webhooks subpackage adds testing and developer-tooling primitives on top of the API methods above. Signing helpers ship with the default install; the receiver and CLI ship with pip install 'tango-python[webhooks]'. See docs/WEBHOOKS.md for usage guides; this section is the import-level reference.
Signing (default install)¶
from tango.webhooks import (
verify_signature, # (body: bytes, secret: str, header: str | None) -> bool
generate_signature, # (body: bytes, secret: str) -> str ("sha256=<hex>" wire form)
parse_signature_header, # (header: str | None) -> str | None (strips "sha256=")
SIGNATURE_HEADER, # "X-Tango-Signature"
SIGNATURE_PREFIX, # "sha256="
)
WebhookReceiver (with [webhooks] extra)¶
A stdlib-based local HTTP receiver, useful in tests and during local development.
from tango import WebhookReceiver, Delivery # exported from top-level tango package
# or: from tango.webhooks.receiver import WebhookReceiver, Delivery
with WebhookReceiver(secret="dev").run() as rx:
# ... cause something to POST to rx.url ...
deliveries: list[Delivery] = rx.deliveries
Constructor (all keyword arguments):
| Arg | Default | Meaning |
|---|---|---|
secret | "" | Shared secret. Empty means signatures are not verified. |
path | /tango/webhooks | URL path to accept POSTs on. |
host | 127.0.0.1 | Bind address. |
port | 0 | TCP port. 0 = OS picks a free port. |
forward_to | None | Optional URL to mirror each delivery to. |
max_history | 256 | Cap on the in-memory deliveries deque. |
on_delivery | None | Callback fired for every delivery (verified or not). |
require_signature | None | Override default (require iff secret is set). |
Each Delivery is a dataclass: received_at, path, signature_header, body_bytes, body_json, verified, remote_addr, forward_status, forward_error.
simulate.sign and simulate.deliver¶
from tango.webhooks import sign, SignedRequest
from tango.webhooks import simulate
# Offline — produce the signed wire form without POSTing:
signed: SignedRequest = sign({"events": [{"event_type": "..."}]}, secret="s")
signed.body # bytes you would put on the wire
signed.signature # bare lowercase hex
signed.headers # {"Content-Type": ..., "X-Tango-Signature": "sha256=..."}
# With delivery — sign and POST to a target URL:
result = simulate.deliver(target_url="http://localhost:8011/tango/webhooks",
payload={...}, secret="s")
result.status_code # status from the receiver
result.signature # bare hex
result.sent_bytes # exact bytes that were POSTed
result.response_body # body the receiver returned
simulate.deliver and simulate.sign accept payloads as dict, list, str, or raw bytes. Dicts/lists are serialized via json.dumps(..., sort_keys=True, separators=(",", ":")) so signatures are reproducible across runs.
CLI entry point¶
The tango[webhooks] extra also installs a tango console script. See docs/WEBHOOKS.md § CLI reference for the full command list.
Response Objects¶
PaginatedResponse¶
All list methods return a PaginatedResponse object with the following attributes:
response = client.list_contracts(limit=25)
# Attributes
response.count # Total number of results
response.next # URL to next page (or None)
response.previous # URL to previous page (or None)
response.results # List of result dictionaries
Example:
contracts = client.list_contracts(limit=25)
print(f"Total contracts: {contracts.count:,}")
print(f"Results on this page: {len(contracts.results)}")
# Iterate through results
for contract in contracts.results:
print(contract['piid'])
# Check for more pages (contracts use keyset pagination via cursor)
if contracts.next:
next_page = client.list_contracts(cursor=contracts.cursor, limit=25)
Pagination Example (contracts use keyset pagination, not page numbers):
cursor = None
all_results = []
page_num = 1
while True:
response = client.list_contracts(cursor=cursor, limit=100)
all_results.extend(response.results)
print(f"Batch {page_num}: {len(response.results)} results")
if not response.next:
break
cursor = response.cursor # use cursor for next page
page_num += 1
print(f"Total collected: {len(all_results)} results")
ShapeConfig (predefined shapes)¶
The SDK provides predefined shape strings as constants on ShapeConfig. Use them as the shape argument for list/get methods when you want a consistent, validated set of fields without building a custom shape string.
from tango import TangoClient, ShapeConfig
client = TangoClient()
# List methods default to the minimal shape when shape is omitted
contracts = client.list_contracts(limit=10) # uses CONTRACTS_MINIMAL
# Or pass the constant explicitly
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL, limit=10)
entity = client.get_entity("UEI_KEY", shape=ShapeConfig.ENTITIES_COMPREHENSIVE)
Available constants (by resource):
| Constant | Used by | Description |
|---|---|---|
CONTRACTS_MINIMAL | list_contracts | key, piid, award_date, recipient(display_name), description, total_contract_value |
ENTITIES_MINIMAL | list_entities | uei, legal_business_name, cage_code, business_types |
ENTITIES_COMPREHENSIVE | get_entity | Full entity profile (addresses, naics, psc, obligations, etc.) |
FORECASTS_MINIMAL | list_forecasts | id, title, anticipated_award_date, fiscal_year, naics_code, status |
OPPORTUNITIES_MINIMAL | list_opportunities | opportunity_id, title, solicitation_number, response_deadline, active |
NOTICES_MINIMAL | list_notices | notice_id, title, solicitation_number, posted_date |
GRANTS_MINIMAL | list_grants | grant_id, opportunity_number, title, status(*), agency_code |
IDVS_MINIMAL | list_idvs, list_vehicle_awardees | key, piid, award_date, recipient(display_name,uei), description, total_contract_value, obligated, idv_type |
IDVS_COMPREHENSIVE | get_idv | Full IDV with offices, place_of_performance, competition, transactions, etc. |
VEHICLES_MINIMAL | list_vehicles | uuid, solicitation_identifier, is_synthetic_solicitation, program_acronym, organization_id, organization, vehicle_type, description, idv_count, holder_count, order_winner_count, awardee_count, order_count, total_obligated, vehicle_obligations, vehicle_contracts_value, latest_award_date, solicitation_title, solicitation_date |
VEHICLES_COMPREHENSIVE | get_vehicle | Full vehicle with competition_details, fiscal_year, set_aside, etc. |
VEHICLE_AWARDEES_MINIMAL | list_vehicle_awardees | uuid, key, piid, award_date, title, order_count, idv_obligations, idv_contracts_value, recipient(display_name,uei) |
ORGANIZATIONS_MINIMAL | list_organizations | key, fh_key, name, level, type, short_name |
OTAS_MINIMAL | list_otas | key, piid, award_date, recipient(display_name,uei), description, total_contract_value, obligated |
OTIDVS_MINIMAL | list_otidvs | key, piid, award_date, recipient(display_name,uei), description, total_contract_value, obligated, idv_type |
SUBAWARDS_MINIMAL | list_subawards | award_key, prime_recipient(uei,display_name), subaward_recipient(uei,display_name) |
GSA_ELIBRARY_CONTRACTS_MINIMAL | list_gsa_elibrary_contracts | uuid, contract_number, schedule, recipient(display_name,uei), idv(key,award_date) |
PROTESTS_MINIMAL | list_protests | case_id, case_number, title, source_system, outcome, filed_date |
CONTRACT_APPEALS_MINIMAL | list_contract_appeals | uuid, board, docket_numbers, decision_date, appellant, judge, decision_type, url |
CONTRACT_APPEALS_COMPREHENSIVE | get_contract_appeal | The list fields plus docket_raw, docket_source, decision_date_repaired, decision_type_raw, listing_year, first_listed_at, listed, text_status, text_char_count (omits decision_text, which needs an Enterprise plan) |
FEDERAL_REGISTER_MINIMAL | list_federal_register_documents | uuid, document_number, publication_date, type, subtype, title, abstract, action, agencies, cfr_references, citation, significant, comments_close_on, effective_on, html_url, pdf_url |
FEDERAL_REGISTER_COMPREHENSIVE | get_federal_register_document | The list fields plus dates, signing_date, start_page, end_page, volume, docket_ids, dockets, regulation_id_numbers, topics, correction_of, corrections, executive_order_number, presidential_document_number, proclamation_number, comment_url, regulations_dot_gov_url, raw_text_url, body_html_url (omits full_text) |
EBUY_REQUESTS_MINIMAL | list_ebuy_requests | rfq_id, request_type, title, schedule, sin, status, buyer_name, buyer_agency, buyer_agency_code, reference_number, issue_date, close_date, attachment_count, link_count, last_seen |
EBUY_REQUESTS_COMPREHENSIVE | get_ebuy_request | Every field, plus the organization and attachments expands |
BUDGET_ACCOUNTS_MINIMAL | list_budget_accounts, get_budget_account | id, federal_account_symbol, fiscal_year, data_through_period, agency_code/name, bureau_name, account_title, bea_category, on_off_budget, subfunction_code, account_category, lifecycle (requested/enacted/apportioned/obligated/outlayed/unobligated), contract & assistance rollups, key ratios, next-year growth, source_anomalies |
VEHICLE_ORDERS_MINIMAL | list_vehicle_orders | key, piid, award_date, recipient(display_name,uei), total_contract_value, obligated |
ITDASHBOARD_INVESTMENTS_MINIMAL | list_itdashboard_investments | Minimal IT Dashboard investment fields |
ITDASHBOARD_INVESTMENTS_COMPREHENSIVE | get_itdashboard_investment | Full investment fields: uii, agency_code, agency_name, bureau_code, bureau_name, investment_title, type_of_investment, part_of_it_portfolio, updated_time, url |
SLED_OPPORTUNITIES_MINIMAL | list_sled_opportunities | opportunity_id, solicitation_number, solicitation_type, title, state, jurisdiction, agency, status, status_reason, delisted_at, posted_date, response_deadline, source_url, has_documents, first_seen_at, last_change_seen_at (no description — detail-only on the API) |
SLED_OPPORTUNITIES_COMPREHENSIVE | get_sled_opportunity | Full solicitation with description, the raw portal status, the delisting timestamp, both deadlines, bid opening, category codes, and organization / contact / meta / attachments / revisions |
SLED_REVISIONS_MINIMAL | list_sled_opportunity_revisions | observed_at, sequence, kind, changed_fields, source_declared (omits changes, which needs a Small plan) |
SLED_FORECASTS_MINIMAL | list_sled_forecasts | forecast_id, state, agency, title, estimated_advertisement_date, estimated_advertisement_raw, procurement_category, procurement_method, contract_number, incumbent_name, source_url, estimated_value(*) |
SLED_FORECASTS_COMPREHENSIVE | get_sled_forecast | Full forecast with description, contract_term, mbe_dbe_goal, delivery_location, and organization / contact / estimated_value |
All predefined shapes are validated at SDK release time (see Developer Guide). For custom shapes, see the Shaping Guide.
Error Handling¶
The SDK provides specific exception types for different error scenarios.
Exception Types¶
from tango import (
TangoAPIError, # Base exception
TangoAuthError, # 401 - Authentication failed
TangoNotFoundError, # 404 - Resource not found
TangoValidationError, # 400 - Invalid parameters
TangoRateLimitError, # 429 - Rate limit exceeded
)
TangoAPIError¶
Base exception for all Tango API errors.
Attributes: - message (str): Error message - status_code (int, optional): HTTP status code
TangoAuthError¶
Raised when authentication fails (401).
Common causes: - Invalid API key - Expired API key - Missing API key for protected endpoint
TangoNotFoundError¶
Raised when a resource is not found (404).
Common causes: - Invalid agency code - Invalid entity key - Resource doesn't exist
TangoValidationError¶
Raised when request parameters are invalid (400).
Attributes: - message (str): Error message - status_code (int): HTTP status code (400) - details (dict): Validation error details from API
TangoRateLimitError¶
Raised when rate limit is exceeded (429).
Error Handling Examples¶
from tango import (
TangoClient,
TangoAPIError,
TangoAuthError,
TangoNotFoundError,
TangoValidationError,
TangoRateLimitError,
)
client = TangoClient(api_key="your-api-key")
# Handle specific errors
try:
agency = client.get_agency("INVALID")
except TangoNotFoundError:
print("Agency not found")
except TangoAuthError:
print("Authentication failed - check your API key")
except TangoAPIError as e:
print(f"API error: {e.message}")
# Handle validation errors with details
try:
contracts = client.list_contracts(
award_date_gte="invalid-date"
)
except TangoValidationError as e:
print(f"Validation error: {e.message}")
if e.response_data:
print(f"Details: {e.response_data}")
# Handle rate limiting
try:
contracts = client.list_contracts(limit=100)
except TangoRateLimitError:
print("Rate limit exceeded - please wait before retrying")
# Implement exponential backoff here
# Catch-all for any API error
try:
result = client.list_contracts()
except TangoAPIError as e:
print(f"An error occurred: {e.message}")
if e.status_code:
print(f"Status code: {e.status_code}")
Best Practices¶
1. Use Response Shaping¶
Always use response shaping for better performance:
# ❌ Without shaping (slow, large response)
contracts = client.list_contracts(limit=100)
# ✅ With shaping (fast, small response)
contracts = client.list_contracts(
shape="key,piid,recipient(display_name),total_contract_value",
limit=100
)
See Shaping Guide for details.
2. Handle Pagination Properly¶
Don't fetch all results at once - paginate responsibly:
# ✅ Good - process batch by batch (contracts use keyset/cursor pagination)
cursor = None
batches = 0
while batches < 10: # Limit to 10 batches
contracts = client.list_contracts(cursor=cursor, limit=100)
process_contracts(contracts.results)
if not contracts.next:
break
cursor = contracts.cursor
batches += 1
3. Use Filters to Narrow Results¶
Filter on the server side instead of client side:
# ❌ Don't do this
all_contracts = client.list_contracts(limit=1000)
gsa_contracts = [c for c in all_contracts.results if c['awarding_agency']['code'] == 'GSA']
# ✅ Do this instead
gsa_contracts = client.list_contracts(
awarding_agency="GSA",
limit=100
)
4. Handle Errors Gracefully¶
Always wrap API calls in try-except blocks:
try:
contracts = client.list_contracts(limit=10)
except TangoAPIError as e:
logger.error(f"Failed to fetch contracts: {e.message}")
# Handle error appropriately
5. Use Environment Variables for API Keys¶
Never hardcode API keys:
# ❌ Don't do this
client = TangoClient(api_key="sk_live_abc123...")
# ✅ Do this instead
import os
client = TangoClient(api_key=os.getenv("TANGO_API_KEY"))
# Or just use the default (loads from environment)
client = TangoClient()
Additional Resources¶
- Shaping Guide - Response shaping syntax, examples, and field reference
- Developer Guide - Dynamic models, predefined shapes, and SDK conformance (maintainers)
- Quick Start - Interactive notebook with examples
- GitHub Repository - Source code and examples
- Tango API Documentation - Full API documentation