Complete reference for all Tango Python SDK methods and functionality.
- 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
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 fromTANGO_API_KEYenvironment variable.base_url(str, optional): Base URL for the API. Defaults tohttps://tango.makegov.com.
Government agencies that award contracts and manage programs.
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 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 codename- Full agency nameabbreviation- Short namedepartment- Parent department (if applicable)
Federal agency 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 a specific office by code.
office = client.get_office(code="4732XX")Parameters:
code(str): Office code
Returns: Dictionary with office details
Federal organizations (hierarchical agency structure).
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 stringflat(bool): Flatten nested objects (default: False)flat_lists(bool): Flatten arrays with indexed keys (default: False)
Filter Parameters:
cgac- Filter by CGAC codeinclude_inactive- Include inactive organizationslevel- Filter by organization levelparent- Filter by parent organizationsearch- Search termtype- Filter by organization type
Returns: PaginatedResponse with organization dictionaries
Get a specific organization by fh_key.
org = client.get_organization(fh_key="ORG_KEY", shape=ShapeConfig.ORGANIZATIONS_MINIMAL)Parameters:
fh_key(str): Organization keyshape(str, optional): Response shape stringflat(bool): Flatten nested objects (default: False)flat_lists(bool): Flatten arrays with indexed keys (default: False)
Returns: Dictionary with organization details
Federal contract awards and procurement data.
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 fromresponse.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 keysflat_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 dateexpiring_lte- Expiring on or before date
Party Filters:
awarding_agency- Agency code (e.g., "4700" for GSA)funding_agency- Funding agency coderecipient_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 identifierpiid- Procurement Instrument Identifierdescription- Contract descriptionaward_date- Date awardedfiscal_year- Fiscal yeartotal_contract_value- Total valuetotal_obligated- Total obligated amountrecipient- 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)
Other Transaction Agreements — non-FAR-based awards.
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
ota = client.get_ota("OTA_KEY", shape=ShapeConfig.OTAS_MINIMAL)Other Transaction IDVs — umbrella OT agreements that can have child awards.
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
otidv = client.get_otidv("OTIDV_KEY", shape=ShapeConfig.OTIDVS_MINIMAL)Subcontract and subaward data under prime awards.
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 keyawarding_agency- Filter by awarding agency codefiscal_year- Exact fiscal yearfiscal_year_gte/fiscal_year_lte- Fiscal year rangefunding_agency- Filter by funding agency codeprime_uei- Filter by prime awardee UEIrecipient- Search by subrecipient namesub_uei- Filter by subrecipient UEI
Returns: PaginatedResponse with subaward dictionaries
Vehicles provide a solicitation-centric way to discover groups of related IDVs and (optionally) expand into the underlying awards via shaping.
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 termordering(str, optional): Server-side sort. Allowed:vehicle_obligations,latest_award_date. Prefix with-for descending.shape(str, optional): Shape string (defaults toShapeConfig.VEHICLES_MINIMAL)flat(bool): Flatten nested objects in shaped responseflat_lists(bool): Flatten arrays using indexed keysjoiner(str): Joiner used whenflat=True(default:".")
Returns: PaginatedResponse with vehicle dictionaries
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,
searchfilters expanded awardees when yourshapeincludesawardees(...)(it does not filter the vehicle itself).
List the IDV awardees for a vehicle.
awardees = client.list_vehicle_awardees(
uuid="00000000-0000-0000-0000-000000000001",
shape=ShapeConfig.VEHICLE_AWARDEES_MINIMAL,
)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 UUIDpage(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 toShapeConfig.VEHICLE_ORDERS_MINIMAL)flat,flat_lists,joiner: as on other vehicles methods
Returns: PaginatedResponse with order (Contract) dictionaries
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 |
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).
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 (indefinite delivery vehicles) are the parent “vehicle award” records that can have child awards/orders under them.
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.
idv = client.get_idv("SOME_IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)Lists child awards (contracts) under an IDV.
awards = client.list_idv_awards("SOME_IDV_KEY", limit=25)Lists child IDVs under an IDV.
children = client.list_idv_child_idvs("SOME_IDV_KEY", limit=25)tx = client.list_idv_transactions("SOME_IDV_KEY", limit=100)Vendors, recipients, and organizations doing business with the government.
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 numberlimit(int): Results per pageshape(str): Fields to returnflat(bool): Flatten nested objectsflat_lists(bool): Flatten arrays with indexed keys
Filter Parameters:
search- Full-text searchcage_code- Filter by CAGE codenaics- Filter by NAICS codename- Filter by entity namepsc- Filter by PSC codepurpose_of_registration_code- Filter by registration purposesocioeconomic- Filter by socioeconomic status; takes SAM business-type codes (e.g.OYBlack American Owned,A6SBA-certified 8(a),A2Woman Owned — seeGET /api/business_types/), not set-aside codes; accepts pipe-separated values for OR semantics, e.g.socioeconomic="OY|A2"state- Filter by statetotal_awards_obligated_gte/total_awards_obligated_lte- Obligation amount rangeuei- Filter by UEIzip_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 a specific entity by UEI or CAGE code.
entity = client.get_entity(key="ZQGGHJH74DW7", shape=None)Parameters:
key(str): UEI or CAGE codeshape(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 Identifiercage_code- CAGE codelegal_business_name- Official business namedisplay_name- Display namedba_name- Doing Business As namebusiness_types- Array of business type codesprimary_naics- Primary NAICS codephysical_address- Physical address (nested)mailing_address- Mailing address (nested)email_address- Contact emailentity_url- Website
Contract forecast and planning information.
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 numberlimit(int): Results per pageshape(str): Fields to returnflat(bool): Flatten nested objectsflat_lists(bool): Flatten arrays with indexed keys
Filter Parameters:
agency- Filter by agency codeaward_date_after/award_date_before- Expected award date rangefiscal_year- Exact fiscal yearfiscal_year_gte/fiscal_year_lte- Fiscal year rangemodified_after/modified_before- Last-modified date rangenaics_code- NAICS code (exact match)naics_starts_with- NAICS code prefixsearch- Full-text searchsource_system- Filter by source systemstatus- 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 identifiertitle- Forecast titledescription- Descriptionanticipated_award_date- Expected award datefiscal_year- Fiscal yearnaics_code- Industry codestatus- Current status
Active contract opportunities and solicitations.
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 numberlimit(int): Results per pageshape(str): Fields to returnflat(bool): Flatten nested objectsflat_lists(bool): Flatten arrays with indexed keys
Filter Parameters:
active- Filter by active status (bool)agency- Filter by agency codeawarded- Whether the opportunity has an award, either posted on it or linked to it (bool). Requires Tango API 5.5.0awardee_uei- Awardee UEI, case-insensitive; OR several with|. Requires Tango API 5.5.0first_notice_date_after/first_notice_date_before- First notice date rangelast_notice_date_after/last_notice_date_before- Last notice date rangenaics- NAICS codenotice_type- Filter by notice typeplace_of_performance- Filter by place of performancepsc- PSC coderesponse_deadline_after/response_deadline_before- Response deadline rangesearch- Full-text searchset_aside- Set-aside typesolicitation_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 identifiertitle- Opportunity titlesolicitation_number- Solicitation numberdescription- Descriptionresponse_deadline- Response deadlineactive- Is currently activenaics_code- Industry codepsc_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.
Contract award notices and modifications.
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 numberlimit(int): Results per pageshape(str): Fields to returnflat(bool): Flatten nested objectsflat_lists(bool): Flatten arrays with indexed keys
Filter Parameters:
active- Filter by active status (bool)agency- Filter by agency codenaics- NAICS codenotice_type- Filter by notice typeposted_date_after/posted_date_before- Posted date rangepsc- PSC coderesponse_deadline_after/response_deadline_before- Response deadline rangesearch- Full-text searchset_aside- Set-aside typesolicitation_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 identifiertitle- Notice titlesolicitation_number- Solicitation numberdescription- Descriptionposted_date- Date postednaics_code- Industry code
Federal grant opportunities and assistance listings.
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 numberlimit(int): Results per page (max 100)shape(str): Response shape stringflat(bool): Flatten nested objects in shaped responseflat_lists(bool): Flatten arrays using indexed keys
Filter Parameters:
agency- Filter by agency codeapplicant_types- Filter by applicant typecfda_number- Filter by CFDA numberfunding_categories- Filter by funding categoryfunding_instruments- Filter by funding instrumentopportunity_number- Filter by opportunity number (exact match)posted_date_after/posted_date_before- Posted date rangeresponse_date_after/response_date_before- Response date rangesearch- Full-text searchstatus- 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 identifieropportunity_number- Opportunity numbertitle- Grant titlestatus- Status information (nested object with code and description)agency_code- Agency codedescription- Descriptionlast_updated- Last updated timestampcfda_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 Schedule contracts from the GSA eLibrary.
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 numberkey- Filter by keypiid- Filter by PIIDschedule- Filter by GSA schedulesearch- Full-text searchsin- Filter by SIN (Special Item Number)uei- Filter by UEI
Returns: PaginatedResponse with GSA eLibrary contract dictionaries
Get a single GSA eLibrary contract by UUID.
contract = client.get_gsa_elibrary_contract("UUID_HERE")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 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 typeagency- Filter by protested agencycase_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 numberprotester- Search by protester namefiled_date_after/filed_date_before- Filed date rangedecision_date_after/decision_date_before- Decision date rangenaics_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 onlysearch- 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 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 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 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 leadingCBCA/ASBCAandNo./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 themdecision_type- CBCA only:"Decision","Dismissal","Order","Full Board Order", or the listing's own text. ASBCA rows carry no typedecision_date_after/decision_date_before- Decision date range (YYYY-MM-DD)listed- Whether the board's newest listing still carries the decisiondocument_id- The board's own document idsearch- Ranked full-text search over the appellant and the full decision text (min 2 characters); wrap in double quotes for a phraseordering-decision_date(the default, as-decision_date),appellant,first_listed_atorrank.rankrequires a non-emptysearch
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 a single decision by uuid.
appeal = client.get_contract_appeal(
"DECISION_UUID",
shape=ShapeConfig.CONTRACT_APPEALS_COMPREHENSIVE,
)Notes:
docket_numbersis a list — the dockets with the board prefix stripped (3288-R,59116), and a consolidated appeal carries several.docket_rawkeeps the listing's own text, anddocket_sourcesays where the dockets came from.- Reading
decision_textrequires 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 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 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 phrasedocument_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 mostlyUncategorized Document, so a type filter undercounts that eraagency- 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-Truefor documents whose comment period closes today or later,Falsefor those already closed; documents with no comment deadline match neithercfr_title- A CFR title number, e.g."40"cfr_part- A CFR part number, e.g."52". Requirescfr_title, and both must match the same CFR reference, socfr_title="40", cfr_part="52"finds 40 CFR 52significant- Significant under Executive Order 12866rin- A Regulation Identifier Number, e.g."2060-AV16"; OR several with|executive_order_number- Exact executive order numberordering-publication_date(the default, as-publication_date),effective_on,comments_close_on,document_numberorrank.rankrequires a non-emptysearch
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 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,docketsandtopicsare the Federal Register's own structures, served as published.agenciescarries 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 inshape(e.g.shape="uuid,title,full_text"). It can run to several MB, so neither default shape includes it.
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 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 localstatus-"open","closed","awarded","cancelled","unknown"active- Sugar for federal-shaped callers:Trueisstatus="open";Falseis its complement, so it includesunknownagency- Substring match on the buyer's published text (min 2 characters); no code resolution behind itsolicitation_number- The number a human would quote; null on roughly a third of the corpussolicitation_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 documentrevision_kind- Kind of the most recent substantive revisionnaics/nigp/unspsc/category- Exact match within one category scheme.naicsis thin on purpose: scheme tagging is mid-migration, so only a small share of entries are tagged NAICScategory_code- Match a code under ANY scheme, including the untagged pre-migration strings. The escape hatch when a scheme-specific filter returns less than expectedposted_after/posted_before- Posted-date rangeresponse_deadline_after/response_deadline_before- Deadline rangefirst_seen_after/first_seen_before- When Tango first observed it. The polling primitivechange_seen_after- When Tango observed the last substantive change. A scrape date, not an amendment datemodified_after/modified_before- When the Tango row last changedplatform/native_id/external_id- Support filters for reproducing a record with us. Not in any response shape and not stable valuessearch- 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.rankrequires a non-emptysearch
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 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_countcan be lower thanlen(row["attachments"]). Some portals auto-generate a cover sheet alongside the real documents; it is listed and flaggedis_generated_summary, but excluded from the count and fromhas_documents. The count answers "does this hold its solicitation package"; the array answers "what files exist".size_bytesandchar_countonly 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_atis when a portal dropped the solicitation from its listing before its deadline, and is whatstatus_reason="delisted"means. It is null when the solicitation was never delisted or has been seen again since.meta.jurisdiction_declaredsays whether the source stated the jurisdiction level itself.Falsemeans 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 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_atis the scrape that saw the change, not the date the agency made it. No state portal emits amendment notices, sokindis 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 servesenrichmentrows — Tango's own detail fetch filling in coverage rather than an agency amendment. Passkind="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_fieldsnames what moved at every plan.
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 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
statusfield, noactivefilter, and no open-only default. Currency is the caller's call fromestimated_advertisement_date. estimated_advertisement_dateis the start of the published quarter, not a posting date.estimated_advertisement_rawkeeps 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, somaxisNone— never read a missingmaxas an unbounded ceiling.rawis always there to check the parse against.incumbent_nameis published text, not a resolved Tango entity.ordering:rank,estimated_advertisement_date,first_seen_at,last_seen_at,modified.
forecast = client.get_sled_forecast("FORECAST_UUID")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 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 unlessorderingis givenrfq_id- Exact request id, e.g."RFQ1835158"reference_number- The buyer's own solicitation number; dashes are ignoredrequest_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 schedulebuyer_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.0contract_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 errorissue_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_seenormodified; 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:
statusis 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.Openmeans "open the last time it was seen", not "open now"; readlast_seenfor staleness.- The contract number a request was posted under is never returned in any payload.
buyer_agency_codeand some other buyer and contact fields are sparse on older requests.
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 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(aTangoValidationError) - The entry is an external link (is_link), not a stored document. The link is onerror.urlTangoNotFoundError- The request is unknown or outside your scope, the attachment does not exist, or its document has not been captured yet
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, sortedReturns: EbuyAccess with enabled (bool), reason ("tier_required", "no_contract_grant" or None; tier_required wins when both apply) and contracts (list of str).
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. 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 rangeagency_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):budgetaryorcredit_financingtoday; treat it as an open stringaccount_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 partialdata_through_period_gte/data_through_period_lte-data_through_periodrangedata_through_period_isnull-Truefor accounts with no File A data,Falsefor accounts with itsearch- Full-text search overaccount_title,agency_name,bureau_nameordering- Sort field; prefix with-for descending. The default is the latest fiscal year first, then largestenacted_bafirst, with accounts that have noenacted_balast.
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 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 toBUDGET_ACCOUNTS_MINIMAL.flat/flat_lists/joiner: See Shaping Guide.
Returns: A BudgetAccount record.
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 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 type classifications.
List available business type codes.
business_types = client.list_business_types(page=1, limit=25)Parameters:
page(int): Page numberlimit(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 codename- Business type namedescription- Description
NAICS (North American Industry Classification System) codes.
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 standardemployee_limit_gte/employee_limit_lte- Employee limit rangerevenue_limit- Exact revenue size standardrevenue_limit_gte/revenue_limit_lte- Revenue limit rangesearch- 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 a single NAICS code by code string.
naics = client.get_naics("541511")Returns: Dictionary with NAICS code details.
Get computed metrics for a NAICS code.
metrics = client.get_naics_metrics(code="541511", months=12, period_grouping="month")Product and Service Codes.
psc = client.list_psc(page=1, limit=25)psc = client.get_psc("D302")metrics = client.get_psc_metrics(code="D302", months=12, period_grouping="month")GSA Multiple Award Schedule Special Item Numbers.
sins = client.list_mas_sins(page=1, limit=25)sin = client.get_mas_sin("54151S")Catalog of Federal Domestic Assistance listings.
listings = client.list_assistance_listings(page=1, limit=25)listing = client.get_assistance_listing("10.310")depts = client.list_departments(page=1, limit=25)dept = client.get_department("097")Get a single business type by code.
bt = client.get_business_type("A6")Federal IT investments from the OMB IT Dashboard.
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:
searchis free;agency_code,type_of_investmentrequire Pro;agency_name,cio_rating,performance_riskrequire Business+. - Shape defaults to
ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL.
investment = client.get_itdashboard_investment("023-000001234")contracts = client.list_entity_contracts("ABCDEF123456", limit=25)idvs = client.list_entity_idvs("ABCDEF123456", limit=25)otas = client.list_entity_otas("ABCDEF123456", limit=25)
otidvs = client.list_entity_otidvs("ABCDEF123456", limit=25)subawards = client.list_entity_subawards("ABCDEF123456", limit=25)lcats = client.list_entity_lcats("ABCDEF123456", limit=25)metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")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).
lcats = client.list_idv_lcats("GS-00F-XXXX", limit=25)List contracts where the agency is the awarding agency.
contracts = client.list_agency_awarding_contracts("4700", limit=25)List contracts where the agency is the funding agency.
contracts = client.list_agency_funding_contracts("4700", limit=25)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
identifieranddisplay_name. - Pro+: up to 5 candidates with additional
match_tierfield.
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.
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 returninclude_extracted_text(bool, optional): Whether to include extracted text from attachments in results
Returns: dict with search results
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.
alerts = client.list_webhook_alerts(page=1, page_size=25)alert = client.get_webhook_alert("ALERT_UUID")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:
nameandquery_typeare required.query_typeis 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.
alert = client.update_webhook_alert("ALERT_UUID", name="Updated name")client.delete_webhook_alert("ALERT_UUID")version = client.get_version()keys = client.list_api_keys()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.
Discover supported event_type values.
info = client.list_webhook_event_types()
print(info.event_types[0].event_type)List your webhook endpoint(s).
endpoints = client.list_webhook_endpoints(page=1, limit=25)endpoint = client.get_webhook_endpoint("ENDPOINT_UUID")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)Send an immediate test webhook to your configured endpoint.
result = client.test_webhook_delivery()
print(result.success, result.status_code)Fetch Tango-shaped sample deliveries.
sample = client.get_webhook_sample_payload(event_type="alerts.contract.match")
print(sample["event_type"])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
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 401verify_signature returns False for missing/empty/malformed headers — it never raises. Comparison is constant-time.
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.
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="
)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.deliveriesConstructor (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.
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 returnedsimulate.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.
The tango[webhooks] extra also installs a tango console script. See docs/WEBHOOKS.md § CLI reference for the full command list.
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 dictionariesExample:
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")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.
The SDK provides specific exception types for different error scenarios.
from tango import (
TangoAPIError, # Base exception
TangoAuthError, # 401 - Authentication failed
TangoNotFoundError, # 404 - Resource not found
TangoValidationError, # 400 - Invalid parameters
TangoRateLimitError, # 429 - Rate limit exceeded
)Base exception for all Tango API errors.
Attributes:
message(str): Error messagestatus_code(int, optional): HTTP status code
Raised when authentication fails (401).
Common causes:
- Invalid API key
- Expired API key
- Missing API key for protected endpoint
Raised when a resource is not found (404).
Common causes:
- Invalid agency code
- Invalid entity key
- Resource doesn't exist
Raised when request parameters are invalid (400).
Attributes:
message(str): Error messagestatus_code(int): HTTP status code (400)details(dict): Validation error details from API
Raised when rate limit is exceeded (429).
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}")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.
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 += 1Filter 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
)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 appropriatelyNever 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()- 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