<!-- doc: index.md -->

# Welcome to Tango API Documentation

The Tango API provides comprehensive access to federal procurement and assistance data, including contracts, grants, entities, and opportunities. This documentation will help you integrate with our API quickly and effectively.

## What is Tango?

Tango is an integrated platform that consolidates federal procurement and assistance data from multiple sources (FPDS, USAspending, SAM) into a unified system. Our API provides:

- **Contract Data**: Federal contract awards, modifications, and transactions
- **Grant Data**: Financial assistance awards and transactions
- **Entity Information**: Vendor and recipient details with business classifications
- **Opportunity Data**: Contract opportunities and solicitation notices (SAM.gov)
- **Agency Data**: Government department, agency, and office information
- **Procurement Forecasts**: Upcoming procurement opportunities across many agencies
- **Webhooks**: Near-real-time notifications when new data is available (awards, opportunities, entities, grants, forecasts)
- And much more to come...

## Getting Started

### [Quick Start Guide](getting-started/quick-start.md)

Get up and running in 5 minutes.

### [Python SDK](sdks/python/index.md)

Install `tango-python` for a batteries-included Python client with response shaping, type hints, and pagination helpers.

```bash
pip install tango-python
```

### [Swagger documentation](https://tango.makegov.com/api/)

See the full documentation for the endpoints.

### [OpenAPI spec (JSON)](https://tango.makegov.com/api/openapi.json)

Full endpoint definitions, parameters, and response models in one fetch — for tools, scripts, and AI consumers.

### [API Reference](api-reference/index.md)

Filtering, ordering, and pagination by endpoint (curated reference; Swagger remains canonical for full surface area).

### [Data Dictionary](data-dictionary/index.md)

Field definitions for API resources.

### [Rate limits](guides/patterns/rate-limits.md)

Learn how to read rate limit headers, handle 429s, and retry responsibly.

**Machine-readable:** For AI and scripts, use the [OpenAPI spec (JSON)](https://tango.makegov.com/api/openapi.json), the [single-page HTML reference](https://docs.makegov.com/print_page/), or the [combined markdown](https://docs.makegov.com/all.md) (all docs in one file).

## Key Features

### 🔐 Flexible Authentication

- **API Keys**: Simple server-to-server integration
- **OAuth2**: For web applications and user tokens

### 📊 Rich Data Model

- **Comprehensive Coverage**: Contracts, grants, entities, opportunities
- **Historical Data**: Years of federal spending data
- **Real-time Updates**: Frequent data refreshes + optional webhooks for near-real-time notifications

### ⚡ High Performance

- **Smart Filtering**: Efficient query optimization
- **Pagination**: Handle large result sets
- **Caching**: Fast response times
- **Rate Limiting**: Fair usage policies

### 🔍 Advanced Search

- **Full-text Search**: Search across titles and descriptions
- **Complex Filtering**: OR/AND patterns for precise queries
- **Geographic Search**: Location-based filtering
- **Date Ranges**: Flexible temporal queries

### 🎯 Response Shaping

- **Custom Field Selection**: Request only the data you need to reduce payload size
- **Nested Expansions**: Include related objects (recipients, offices, transactions) in a single request
- **Flattening Support**: Convert nested JSON to flat structures for easier consumption
- **Flexible Aliasing**: Rename fields to match your application's naming conventions

Learn more in the [Response Shaping Guide](guides/patterns/response-shaping.md). For field definitions, see the [Data Dictionary](data-dictionary/index.md).

### 🔔 Webhooks

Subscribe to event notifications when new data is available (awards, opportunities, entities, grants, forecasts).

Learn more in the [Webhooks Partner Guide](webhooks/index.md).

### MCP (AI agents)

Use Tango from Claude Desktop, Cursor, or other MCP-compatible clients. The Tango MCP server exposes 4 tools to discover, search, and get details on contracts, opportunities, entities, and more.

Learn more in the [MCP (AI agents) guide](mcp/index.md).

## Data Sources

Tango integrates data from authoritative federal sources:

- **FPDS** (Federal Procurement Data System) - Contract awards and modifications
- **USAspending** - Financial assistance and sub-award data
- **SAM.gov** - Entity registrations and contract opportunities
- **Grants.gov** - Federal grant opportunities
- **Agency Systems** - Direct agency data feeds including the [Federal Hierarchy](api-reference/concepts/federal-agency-hierarchy.md)

### [Provenance](api-reference/concepts/provenance.md)

Learn more about our data sources and how we use them in [Provenance](api-reference/concepts/provenance.md).

## [Changelog](CHANGELOG.md)

See what's new and what's changed in each release in the [Changelog](CHANGELOG.md).

---

<!-- doc: CHANGELOG.md -->

<!-- markdownlint-disable MD024, MD013 -->
# Changelog

This is the most up-to-date change log for [Tango](https://tango.makegov.com/) and the [Tango API](https://tango.makegov.com/api/).

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

[![Production Badge](https://tango.makegov.com/badges/version.svg?label=production)](https://tango.makegov.com/)

## [Unreleased]

### Added

### Changed

### Breaking

### Fixed

### Internal / Infra

## [4.6.7] - 2026-05-24

### Added

- **Entity metrics now include award counts per period.** `GET /api/entities/{uei}/metrics/{months}/{period_grouping}/` results now carry `awards_count` (distinct prime awards) and `subawards_count` (distinct subawards) alongside the existing `awards_obligated` and `subawards_obligated` dollar sums. Counts participate in `?group_by=agency` / `?group_by=department`, so a single call returns per-agency award counts for a UEI — no more N+1 calls to the contracts / subawards endpoints. Counts are aggregated per month at the source; if you request a coarser `period_grouping` (`quarter`, `year`), the counts in each bucket sum the underlying monthly counts — read them as "award-months of activity" in that window rather than a global distinct count.

### Fixed

- **Attachment-text search is reliably fast on common terms.** `GET /api/opportunities/?search=…` with attachment-text search enabled previously could take several minutes on cold cache for broad terms (e.g. `data governance`) — long enough to hit the API timeout. Cold-path wall time on those searches now drops by roughly 30× (e.g. ~5 min → ~10 s for `data governance`), comfortably under the timeout. The set of matching opportunities and per-page results are unchanged.

- **`GET /api/opportunities/?search=` is reliably fast on common terms.** Searches matching thousands of opportunities (e.g. `data governance`, `department of state`) — including paired with `ordering=-last_notice_date` / `response_deadline` / `first_notice_date`, deep pagination (`?page=10`), and field-selection via `shape=` — previously could hit the API timeout when the database cache was cold. Cold-path wall time on these searches now drops by roughly 6-7× and stays well under the timeout. The set of matching opportunities and the per-page results are unchanged. **One caveat for clients that exhaustively crawl results via `?page=N` while attachment-text search is enabled**: the approximate `count` field can come back lower than before, which lowers the maximum `?page=` you can navigate to. If you want the precise total — or need to crawl all matches without missing pages — pass `?exact=true` (this was already the documented way to opt out of approximate counts; nothing about its behavior has changed).

## [4.6.6] - 2026-05-23

### Added

- **Follow one or many resources with a single webhook alert.** Webhook alerts for opportunities, contracts (including IDVs / OTAs / OTIDVs), grants, and forecasts now accept the same identifier you get from each resource's detail endpoint — and you can pass several at once. Use `opportunity_id`, `key` (for contracts and IDV / OTA / OTIDV), `grant_id`, or `id` (for forecasts) in your alert's `filters`, either as a single value or as a pipe-OR list (e.g. `"key": "key1|key2|key3"`). One alert that follows a set of resources is more efficient than one alert per resource, and counts as a single subscription against your tier's alert limit. Up to 500 IDs per filter; the same identifiers are also accepted on the corresponding list endpoints (e.g. `GET /api/contracts/?key=k1|k2|k3`).

## [4.6.5] - 2026-05-22

### Added

### Changed

### Breaking

### Fixed

- **`GET /api/opportunities/?search=` no longer times out on common terms.** Searches like `data governance` or `department of state` previously could return a gateway timeout; they now return results reliably.

### Internal / Infra

## [4.6.2] - 2026-05-18

### Added

- **`GET /api/opportunities/?search=` matches attachment text.** `?search=` now searches text from opportunity attachments in addition to notice titles and descriptions. Matched results include a `snippet` centered on the hit.

## [4.6.1] - 2026-05-13

### Fixed

- **`GET /api/vehicles/?search=` matches text from member-IDV descriptions.** Vehicles whose `solicitation_identifier` is a single run-on token (e.g. `RFPCMS2016SPARC`) now match substring queries like `?search=SPARC` via the underlying IDV descriptions. The endpoint's default ordering (`solicitation_identifier, agency_id, uuid`) is unchanged. (issue #2224)

## [4.6.0] - 2026-05-12

### Added

- **Shape aliases: `naics_code(...)` and `psc_code(...)` now work as expand forms.** `?shape=naics_code(code,description)` is accepted as a synonym for `naics(...)` on `/api/contracts/` and `/api/idvs/` (the only award endpoints with a NAICS expand). `?shape=psc_code(code,description)` is accepted as a synonym for `psc(...)` across all award endpoints (`/api/contracts/`, `/api/idvs/`, `/api/otas/`, `/api/otidvs/`). The expanded object comes back under the canonical key (`naics` / `psc`). Bare scalar `naics_code` / `psc_code` is unchanged — it still returns the raw code value. (issue #2257)
- **`POST /api/webhooks/endpoints/test-delivery/` now accepts `endpoint`.** The canonical field name matches `POST /api/webhooks/subscriptions/`, so SDKs and docs can use one identifier across both calls. The legacy `endpoint_id` field is still accepted as a deprecated alias; if both are provided, `endpoint` wins. `endpoint_id` will be removed in the next major release. (issue #2252)
- **`POST /api/webhooks/alerts/` accepts an optional `endpoint` (UUID) field.** Multi-endpoint accounts can disambiguate without dropping to `/api/webhooks/subscriptions/`; single-endpoint accounts can still omit it. (issue #2256)

### Breaking

- **Subject-based webhook subscriptions removed (#2267).** Subscriptions now match by saved filters (`/api/webhooks/alerts/`) only — `subject_type`, `subject_ids`, and the legacy `resource_ids` / `entity_type` / `entity_ids` / `change_types` fields are no longer accepted by `POST /api/webhooks/subscriptions/` (or `PATCH`). Existing integrations that still send these fields will see them silently dropped. See the alerts guide on [docs.makegov.com](https://docs.makegov.com/) for migration.

### Fixed

- **OpenAPI now matches runtime for `ordering`.** `/api/notices/` no longer advertises `ordering` (the endpoint never accepted it — every value 400s), and `/api/subawards/` advertises its `ordering` enum as `last_modified_date` / `-last_modified_date` only. SDKs generated from the schema no longer surface an `ordering` kwarg on notices, and the subawards kwarg is typed to the actual allowlist. (issue #2254)
- **OpenAPI `ordering` now has a real `enum` on the remaining endpoints.** Completes the schema/runtime parity work started in #2258: `/api/contracts/`, `/api/idvs/`, `/api/opportunities/`, `/api/forecasts/`, `/api/vehicles/`, `/api/vehicles/{uuid}/orders/`, and `/api/gsa_elibrary_contracts/` now declare an explicit `enum` matching each viewset's runtime allowlist (`ordering_fields` ∪ `ordering_fields_map.keys()` + their `-` variants). SDK generators emit a typed kwarg instead of a free-form `str`, so invalid values fail at type-check time rather than as a runtime 400. (issue #2262)
- **`POST /api/webhooks/alerts/` no longer returns a sibling endpoint's subscription row** when the same user has the same filter `(query_type, filters)` on two different endpoints. The post-create lookup is now scoped by the resolved endpoint instead of user — a latent dedup bug that became reachable once multi-endpoint disambiguation was added. (issue #2256)
- **`GET /api/webhooks/endpoints/sample-payload/` no longer 500s.** The endpoint previously referenced a subject-type attribute that was removed when subject-based webhooks were deprecated. Returns the alert-only sample set as expected.

## [4.5.2] - 2026-05-11

### Changed

- **Public docs moved to [docs.makegov.com](https://docs.makegov.com/).** The in-app MkDocs site at `tango.makegov.com/docs/*` is gone; legacy URLs 301-redirect to the new host path-preserving (`/docs/getting-started/pricing/` → `docs.makegov.com/getting-started/pricing/`, etc.). Bookmarks keep working. Pricing-tier upgrade responses (`upgrade_url`) and the welcome-email "Docs" CTA now point at the new host directly.

### Fixed

- **`/api/otas/` and `/api/otidvs/` pagination no longer 400s on page 2.** Cursor-based pagination on these endpoints used to return HTTP 400 whenever the page-1 anchor row had a NULL `award_date` — common on OTAs/OTIDVs because many leading rows have no resolvable award date. Page 2 now succeeds and returns the next slice as expected. No client changes required. (issue #2139)

## [4.5.1] - 2026-05-09

### Changed

- **`/api/notices/`: `?shape=office(organization_id)` is now valid.** The office payload on notices gains `organization_id` as a 7th key, matching the surface already exposed on `/api/opportunities/` and the awards endpoints. The other six keys (`office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`) are unchanged.

### Performance

- **`/api/notices/` is materially faster.** The `office` expand no longer fetches full `Organization` rows on every request. Uncached `?limit=100` drops from ~0.36 s to ~0.07–0.1 s. No query parameter changes needed; same shapes work faster. (issue #2236)

## [4.5.0] - 2026-05-09

### Fixed

- **`?shape=case_id,dockets` no longer 500.** Naming `dockets` as a leaf in the protests shape — without an inner expand like `dockets(case_number)` — used to raise an internal serializer error. The leaf is now equivalent to `dockets(*)` and returns the default docket projection. Detail endpoint mirrors list. (issue #2222)

### Performance

- **Five more endpoints are materially faster.** The organization expand on `/api/forecasts/`, `/api/grants/`, `/api/itdashboard/`, `/api/opportunities/` (the `office` expand), and awards (`/api/contracts/`, `/api/idvs/`, `/api/otas/`, `/api/otidvs/`) no longer fetches full `Organization` rows from the database. Uncached `?limit=100` timing across eight endpoints: ~8.7 s total → ~1.9 s total (−78%); opportunities alone 3.4 s → 0.4 s (−88%), forecasts 2.1 s → 0.15 s (−93%). No query parameter changes needed — same shapes work faster. (issue #2225, PR #2233)

- **`/api/contracts/`, `/api/idvs/`, `/api/otas/`, `/api/otidvs/`: `?shape=awarding_office(organization_id)` and `?shape=funding_office(organization_id)` are now valid.** The office payload gains `organization_id` as a 7th key (the other six — `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name` — are unchanged). Rows that hit the legacy JSON fallback return `{"organization_id": null, ...}`.

- **`/api/protests/` is materially faster.** End-to-end on a default `?limit=100` request, latency drops from ~3.8 s to ~80 ms. No API changes; same response shape.

## [4.4.0] - 2026-05-08

### Added

- **Agency filter accepts more identifier forms.** The `agency` / `awarding_agency` / `funding_agency` filters on every endpoint that exposes one now resolve the same way: hand them a name, abbreviation, or code (CGAC, FPDS, AAC, and others) and Tango figures out the matching agency subtree. See the new [Agency search guide](guides/patterns/agency-search.md). (issue #2207)

### Changed

- **Trimmed `?ordering=` whitelist on `/api/vehicles/`.** Removed `awardee_count` and `vehicle_contracts_value` from the v4.3.0 ordering surface — both were `Subquery` annotations on `VehicleStats` with no backing index, and their matching `*_min/max` filters were intentionally deferred. The remaining surface is `vehicle_obligations`, `latest_award_date`, `total_obligated`, `award_date`, `last_date_to_order`, `fiscal_year`, `idv_count`, `order_count`. (issue #2202 follow-up)

### Breaking

- **Comma `,` is no longer an `AND` separator in filter values; multi-value uses `|` only.** Across every endpoint that accepts smart-text filter values (`naics`, `psc`, set-asides, business types, entity / agency text searches, etc.), a comma is now a literal character in the token value rather than an `AND` separator. `?awarding_agency=HHS,DOD` was previously treated as `HHS AND DOD` (which was always empty — a single record can't equal two different values) and is now treated as a single literal value `"HHS,DOD"` (also always empty, but for the literal reason). For union semantics use `|`: `?awarding_agency=HHS|DOD` returns rows in either subtree. For intersection semantics, repeat the query parameter or use multiple separate filter parameters — `AND`-joining values inside one parameter has no clear use case in this API. Mixed inputs like `?field=A,B|C` no longer raise a `400 "Cannot combine AND and OR in the same query"` error; they're parsed as `"A,B"` OR `"C"`. The change also fixes the long-standing footgun where federal entity / agency names containing literal commas (`"ENERGY, DEPARTMENT OF"`, `"GENERAL DYNAMICS, INC."`) silently produced empty results when pasted into a filter. Out of scope: `/api/gsa_elibrary_contracts/?sin=`, `?uei=`, `?search=` continue to honor the prior comma-as-AND grammar (separate code path). (issue #2214)

### Removed

- **AND-via-comma is fully removed from documentation, swagger, and help text.** Comma-as-AND no longer works anywhere downstream of `BaseSmartFilter` / `BasePerformantFilter` and the swagger / public docs no longer advertise it. Multi-value uses `|` (OR) only. Consumers using `,` for intersection semantics should migrate to multiple distinct query parameters (e.g. `?naics=541511&fiscal_year=2024` rather than trying to AND values inside a single param). The swagger description-builder (`make_description`) now raises `ValueError` if a future internal call site passes `"conjunctive": True`, so the option cannot be reintroduced silently. Public docs (`docs/guides/patterns/agency-search.md`, `docs/webhooks-user-guide.md`, `docs/index.md`, forecasts API user guide) rewritten to drop OR-vs-AND framing in favor of OR-only language. (issue #2214)

### Fixed

- **Multi-value `?agency=A|B` returned zero results.** Pipe-separated values on `agency` / `awarding_agency` / `funding_agency` now correctly return the union of both subtrees (`?awarding_agency=HHS|DOD` returns rows from either) on every endpoint. Single-value queries are unchanged. (issue #2210, also reported externally as `makegov/tango-public#51`)

- **Numeric Federal Hierarchy keys (`fh_key`) now resolve to the canonical agency.** Previously, passing an L2 `fh_key` (e.g. NIH's `100019747`) could resolve to a deep descendant office (an L3 within NIH whose name happened to contain "AGENCY") instead of NIH itself. The agency now picks the canonical match and the subtree expansion gives the full agency's records. Same behavior change applies to numeric CGAC-style codes when the leading zero is stripped (`?agency=89` now resolves to DOE rather than a sub-office under DOE).

- **Long pipe-separated agency queries no longer return HTTP 400 on award endpoints.** `?awarding_agency=DOD|HHS|VA|DHS|DOE|TREAS|USDA|DOJ|DOT|GSA` (and similar 10-token OR queries) now return HTTP 200 with the union of every named department's awards on `/api/contracts/`, `/api/idvs/`, and other award endpoints. Empty / pipe-only inputs (`|||`) and all-unresolvable inputs (`FAKE1|FAKE2|FAKE3`) likewise return HTTP 200 with an empty result set rather than 400. A single typo'd agency name still returns 400 (e.g. `?awarding_agency=DEPRTMENTOFENERGY`).

- **Single-word agency abbreviations no longer accidentally match an unrelated office.** Some L3+ offices in the Federal Hierarchy data have a `name` literally equal to a department abbreviation (e.g. an L3 office named `"HHS"`); previously, querying `?agency=HHS` could match that office instead of the canonical L1 HHS department, returning a narrow result set. Abbreviations now consistently resolve to the canonical agency. Multi-word canonical names like `?agency=ENERGY, DEPARTMENT OF` are unchanged.

- **`?agency=A|B` on `/api/forecasts/`, `/api/grants/`, `/api/protests/` no longer over-matches via partial-text fallback.** Where a token resolves cleanly to an agency, only that agency's subtree is matched. The previous behavior fell back to a partial-text match on the legacy text column when paired with multi-value input, which over-matched compound codes (e.g. `?agency=HHS|...` on grants matched every grant with `"HHS-..."` in its `agency_code` — 1500+ instead of 40). Unresolvable tokens still fall back to the legacy text column as before.

## [4.3.0] - 2026-05-07

### Added

- **Vehicle list filtering (`/api/vehicles/`).** New query parameters cover enum / code fields (`vehicle_type`, `type_of_idc`, `contract_type`, `set_aside`, `who_can_use`), reference codes (`naics_code`, `psc_code`, `program_acronym`), org hierarchy (`agency`, `organization_id`), numeric ranges (`total_obligated_min/max`, `idv_count_min/max`, `order_count_min/max`), and dates (`fiscal_year`, `award_date_after/before`, `last_date_to_order_after/before`). `vehicle_type`, `type_of_idc`, and `contract_type` accept pipe-separated multi-value selections via the `OR` smart-filter grammar (e.g. `?vehicle_type=A|B|C`). The `?ordering=` whitelist is expanded with `total_obligated`, `award_date`, `last_date_to_order`, `fiscal_year`, `idv_count`, `order_count`, `awardee_count`, and `vehicle_contracts_value` alongside the existing `vehicle_obligations` and `latest_award_date`. Examples: `GET /api/vehicles/?vehicle_type=A`, `GET /api/vehicles/?vehicle_type=A|B`, `GET /api/vehicles/?naics_code=541512&total_obligated_min=1000000`, `GET /api/vehicles/?ordering=-total_obligated`. (issue #2202)
- **Awardee search (`/api/vehicles/{uuid}/awardees/?search=`).** Filter the awardees response by entity-aware full-text search across IDV fields (PIID, key, solicitation identifier, NAICS, PSC) and the recipient entity (legal name, address, etc.). Examples: `GET /api/vehicles/{uuid}/awardees/?search=ACCENTURE`, `GET /api/vehicles/{uuid}/awardees/?search=GS-35F-0119Y`. The response count reflects filtered results. (issue #2202)
- **`/api/forecasts/`: new `organization` shape expand.** Each forecast row is now linked to a canonical `agencies.Organization` resolved deterministically from the existing `agency` text field (12 distinct production acronyms — `HHS`, `DHS`, `DOI`, `GSA`, `DOE`, `DOT`, `VA`, `DOL`, `NRC`, `NSF`, `COMMERCE`, `TREASURY`). Use `?shape=organization(*)` for the canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). The `organization` expand is also in the default response shape (no `?shape=` needed). The legacy `agency` text field and filter are unchanged.
- **`/api/grants/`: new `organization` shape expand.** Each grant opportunity is now linked to a canonical `agencies.Organization` resolved from the free-text `agency_code` field — the resolver walks compound codes like `HHS-NIH11` / `HHS-FDA` / `HHS-CDC-GHC` / `DOS-ECA` / `USDOJ-OJP-BJA` segment-wise to the most-specific aliased sub-agency (NIH, FDA, CDC, the State Dept bureau, BJA, etc.) rather than collapsing to the parent department. Codes whose only recognizable token is the dept prefix (`DOS-MEX` country codes, etc.) resolve to the L1 department. `?shape=organization(*)` returns the canonical 7-key office payload; `organization` is in the default response shape. The legacy `agency` text field and filter are unchanged.
- **`/api/itdashboard/`: new `organization` shape expand** (free tier — same gating as `agency_name`). Each IT Dashboard investment is now linked to a canonical `agencies.Organization`, resolved deterministically from `(agency_code, bureau_code)` (preferred) and `(agency_name, bureau_name)` (fallback). `?shape=organization(*)` returns the canonical 7-key office payload. A small minority of investments (e.g., U.S. Army Corps of Engineers) currently resolve to `null` due to gaps in the canonical org data — tracked in #2176.
- **`/api/protests/`: new `organization` shape expand** (free tier — same gating as the existing `agency` text field). Each protest case is now linked to a canonical `agencies.Organization` resolved deterministically from the free-text `agency` field — handles hierarchical names (`Department of the Army : NAVSEA : Surface Warfare`), DoD branches (Army / Navy / Air Force resolve to their AGENCY-typed orgs under DoD, not to DoD itself), and synthetic GAO buckets (`Independent Government Entities` / `Legislative Agencies` skip the dept lookup). `?shape=organization(*)` returns the canonical 7-key office payload. The deterministic FK is the canonical "what agency is this protest against?" signal — distinct from (and complementary to) the existing Bayesian `?shape=resolved_agency(*)` expand which returns a best-guess match with confidence and rationale. Both signals can be requested in the same call.

### Changed

- **`organization` is now in the default response shape** for `/api/forecasts/`, `/api/grants/`, `/api/itdashboard/`, and `/api/protests/`. Previously opt-in via `?shape=organization(*)`; now always present. Renders as the canonical 7-key office payload — same shape as `awards`, `opportunities`, and `vehicles`. Drop `organization` from your `?shape=` parameter if you don't want it.
- **`/api/opportunities/` `office` payload now includes `organization_id`** (was 6 keys, now 7). Additive change — same UUID is resolvable as before, just no longer requires a separate query to round-trip from the office to the underlying Organization. Other six keys (`office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`) unchanged.
- **Bare `?shape=<expand-name>` syntax now works on every dataset that has the expand registered** with `relation="select"` or `"prefetch"`. Previously rejected with a 400 `unknown_field` error on most endpoints — `?shape=organization` returns 200 on forecasts / grants / itdashboard / protests / vehicles / opportunities. The parens form (`organization(*)`) was always working and is unchanged.

## [4.2.2] - 2026-05-05

### Fixed

- **`?shape=opportunity(...)` on `/api/vehicles/` now returns the full Opportunity shape instead of a 400.** The v4.2.1 implementation shipped a 4-field stub that only allowed `opportunity_id`, `solicitation_title`, `solicitation_description`, and `solicitation_date`. Any consumer requesting a field the pre-cutover `vehicle.opportunity` FK exposed — `title`, `sam_url`, `first_notice_date`, `office(...)`, or any other Opportunity field — received a 400 from shape validation. The fix resolves the full `Opportunity` by `solicitation.opportunity_id` and routes through the complete Opportunity shape so every field and nested expand (including `office(office_code,office_name,agency_code,agency_name,department_code,department_name)` and `set_aside(code,description)`) works as it did pre-cutover. Available on both list and retrieve; opt-in via `?shape=opportunity(...)` (not in the default response shape). On list pages, all Opportunity rows for the page are fetched in a single batched query, so `opportunity(...)` is safe to request alongside other shapes. Closes #2193.
- **`solicitation_identifier` no longer leaks the internal `ACRO:` storage prefix on synthetic GWAC vehicles.** Previously, certain GWAC vehicles returned values like `"solicitation_identifier": "ACRO:SEWP"`. The `ACRO:` prefix is an internal storage detail (collision avoidance with real solicitation numbers in the unique index); the API response now strips it, so SEWP-family vehicles surface as `"SEWP"`, `"SEWP IV"`, `"SEWP V"`, etc. Use `is_synthetic_solicitation` (boolean) and `program_acronym` (string) to identify GWAC-recovery vehicles. Refs #2193.

### Changed

- **`Deprecation: true` HTTP header removed from `/api/vehicles/`.** v4.2.1 added an unconditional `Deprecation: true` header on every list and retrieve response, advertising deprecation of `agency_details`, `competition_details`, and `?shape=opportunity(...)`. We're walking it back: there are no committed first-class replacements for `agency_details.funding_office` or for unique `competition_details` keys (`extent_competed`, `set_aside`, `solicitation_procedures`, `number_of_offers_received`), no sunset date, and `?shape=opportunity(...)` is now restored to full pre-cutover parity. The fields continue to work; tooling that was watching for the header should expect it to be absent until we publish a real migration plan.

### Breaking

- **`?shape=opportunity(solicitation_title)` / `opportunity(solicitation_description)` / `opportunity(solicitation_date)` no longer validate.** These three fields are top-level Vehicle fields, not Opportunity model fields — the v4.2.1 stub erroneously accepted them inside `opportunity(...)`. Use `?shape=solicitation_title,solicitation_description,solicitation_date` (top-level, no nesting) instead — these come back in the default list shape and are cheaper than the full `opportunity(...)` expand. `opportunity(opportunity_id)` continues to work — `opportunity_id` is also an Opportunity model field, AND it's available as a top-level Vehicle field. SDK consumers (`tango-python`, `tango-node`) verified clean. If you adopted the v4.2.1 stub shape between #2177 and #2193 landing, move the three companion fields to top-level.

## [4.2.1] - 2026-05-03

### Added

- `/api/vehicles/` responses now include `is_synthetic_solicitation` (boolean). True for a small set of GWAC vehicles where the underlying IDVs lacked a real solicitation identifier but had a recognizable `program_acronym` (`8ASTARS`, `SEWP IV`, `COMMITS NEXGEN`, etc.) — the API keys the vehicle off the program acronym so these GWACs surface in results instead of being dropped. SAM enrichment (`solicitation_title`/`description`/`date`/`opportunity_id`) is always null on synthetic rows. Use this flag to distinguish them in your own filtering or display logic.
- `/api/vehicles/` responses now include three new top-level fields on both list and retrieve: `program_acronym` (the vehicle's program acronym, e.g. `"OASIS+"`, when present), `idv_count` (number of IDVs in the vehicle), `total_obligated` (sum of obligations across the vehicle's IDVs). Retrieve responses also include a nested `metrics` object with 12 new metrics: `competed_rate`, `award_concentration_hhi`, `order_concentration_hhi`, `top_recipient_share`, `avg_offers_received`, `avg_order_value`, `max_order_value`, `using_agency_count`, `recent_obligations_24mo`, `recent_orders_24mo`, `days_since_last_order`, `obligation_to_ceiling_ratio`. Request the bag via `?shape=metrics(*)` or specific keys via `?shape=metrics(competed_rate,top_recipient_share)`.
- **`organization` field on `/api/vehicles/` list and retrieve responses** — the awarding Organization expanded to the canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). The same payload now also populates `agency_details.awarding_office` on both list and detail responses (was previously null on list and sometimes null on detail when per-IDV office signals were missing). Cached for 1 year per Organization.
- **`description` (string) and `descriptions` (array)** on `/api/vehicles/` are populated again. Both had been returning null since v4.2.0; that's now fixed. The legacy compose-a-single-string behavior (`description` returns the longest common substring across `descriptions`) is unchanged.

### Changed

- **Response keys are now alphabetized** at every nesting level on `/api/vehicles/` list and detail. Leaves and nested expand objects interleave by key name. The order is stable and independent of the order keys appear in your `?shape=` parameter — `?shape=uuid,organization` and `?shape=organization,uuid` produce the same JSON. Affects every shape-using endpoint (vehicles, contracts, opportunities, entities, etc.) — if you've been relying on response-key order, switch to explicit key access. The OpenAPI schema (Swagger UI) and SDK type definitions are unchanged.

### Changed (internal data path; API surface preserved)

- `/api/vehicles/{uuid}/` `agency_details` and `competition_details` JSON blobs and the `?shape=opportunity(...)` expand are now recomputed at request time (from the vehicle's IDVs and the `awards_vehicle_solicitation` companion respectively) instead of being stored columns. **No deprecation announced**: see the #2193 / 4.2.2 entry for the walk-back. The fields keep working. List responses include `agency_details.awarding_office` populated from the same source as the top-level `organization` field — `agency_details.funding_office` is null at the vehicle grain (see below).
- **Top-level `organization_id`** on `/api/vehicles/` retrieve responses is dropped from the default shape — it's redundant with `organization.organization_id` (the same UUID, nested inside the office payload). Still requestable explicitly via `?shape=organization_id`, but the default response no longer surfaces it.
- **Funding-side organization signal removed from vehicles.** `agency_details.funding_office` is null at the vehicle grain. Vehicles are not the right grain for funding-org analysis: a single vehicle is funded by many agencies via task orders, and task-order data already carries funding-side detail. Consumers needing funding-org rollups should query `/api/vehicles/<uuid>/orders/` and aggregate from there.

## [4.2.0] - 2026-04-30

### Added

- G2X integration (#1881): new `/api/news/` and `/api/events/` endpoints (G2X User group required), a `/api/company/rag/` endpoint for unified company/people/news lookups, and `g2x_about` / `g2x_ai_summary` / `g2x_employee_count` fields available via `?shape=` on `/api/entities/` when a linked G2X profile exists
- eBuy opportunities now available on `/api/opportunities/` via `?domain=ebuy` (eBuy only) or `?domain=sam,ebuy` (union with SAM). eBuy results are served live from the G2X GraphQL API — no stored snapshot; filter by `request_type`, `contract_type`, `set_aside_type`, `buyer_department`, `closes_after`, or `search`. On upstream outage, `?domain=sam,ebuy` degrades to SAM-only with an `upstream_warnings` field so SAM queries stay up
- `vehicle` is now available as a shape expand on `/api/contracts/`. Use `?shape=vehicle(uuid,solicitation_title)` to attach the contract's parent vehicle to each result, or `?shape=vehicle(*)` for the curated field set (`uuid`, `solicitation_identifier`, `solicitation_title`, `solicitation_description`, `agency_id`, `vehicle_type`, `type_of_idc`, `contract_type`, `who_can_use`, `solicitation_date`, `award_date`, `last_date_to_order`, `fiscal_year`, `naics_code`, `psc_code`, `set_aside`, `description`). Contracts without a parent IDV — or whose parent IDV is not part of any vehicle — return `null`. For richer vehicle detail (awardees, opportunity, totals), follow the returned `uuid` to `/api/vehicles/{uuid}/`. Resolution adds one query per page, not per row.

## [4.1.1] - 2026-04-27

### Added

- `cage` is now an alias for `cage_code` across the API. Query entities with `?cage=` or `?cage_code=` (sending both is rejected — pick one). Award `recipient` shapes accept either key: `recipient(cage)` returns `cage`, `recipient(cage_code)` returns `cage_code`, and `recipient(*)` returns `cage_code` only. If you want the short `cage` key, ask for it explicitly (refs #1350).

### Fixed

- Filtering subawards by `?awarding_agency=<name|code>` or `?funding_agency=<name|code>` now uses the same hierarchical resolution as the contracts and opportunities endpoints. Department queries (e.g. `?awarding_agency=DOE`, `?awarding_agency=089`) return all subawards under that department's subtree; agency codes (e.g. `?awarding_agency=8900` for DOE, `?awarding_agency=7530` for CMS) return that agency's subtree; abbreviations (e.g. `?awarding_agency=CMS`) resolve via the canonical alias table. Result counts will shift compared to the previous literal-code-match behavior — most notably, name-based queries that previously returned 0 (e.g. `?awarding_agency=CMS`) now return the full agency subtree, semantically aligned with `/api/contracts/?awarding_agency=...`. Closes #2076.

## [4.1.0] - 2026-04-26

### Internal / Infra

- Internal ETL pipeline rewrite for awards data freshness. No API surface or query-behavior changes (refs #2082).

## [4.0.14] - 2026-04-26

### Changed

- Latency spikes on `/metrics/` endpoints during scheduled refreshes should be noticeably smaller after an internal pipeline rewrite. API surface is unchanged — the same `/metrics/` endpoints with `?group_by=agency` / `?group_by=department` parameters continue to work.
- Filtering opportunities or notices by `?agency=<name|code>` now uses the same hierarchical resolution as the contracts agency filter. Department queries (e.g. `?agency=DOE`, `?agency=089`) return the whole department subtree; agency codes (e.g. `?agency=8900` for DOE, `?agency=8960` for FERC) return just that agency's subtree. Result counts will shift compared to the previous full-text-search behavior — the new counts are semantically correct subtree matches rather than token co-occurrence. Closes #2058.

### Fixed

- Forecasts loader: reduced log noise and false Sentry alerts. `sanitize_year` data-quality events are now DEBUG-level; empty records is a no-op rather than an error; partial validation failures log as warnings (visible in admin, no Sentry alert). Closes #1974, closes #1935.
- Filtering contracts by a department's full legal name (e.g. `?awarding_agency=ENERGY, DEPARTMENT OF`) now correctly returns the whole department's contracts instead of 0 results. The embedded comma in federal inverted names was being misinterpreted as a target/context separator. Queries for `?awarding_agency=DOE` and the numeric CGAC/FPDS codes are unchanged. Closes #2079.
- Filtering contracts by `awarding_agency` or `funding_agency` using a real (but currently inactive) agency code such as `5706`, `8933`, or `8944` now returns an HTTP 200 response with the matching contracts, instead of an HTTP 400 "No agency found matching" error. Queries for agency codes that don't exist at all continue to return 400 as before. Closes #2057.
- Filtering contracts by a numeric agency code (e.g. `awarding_agency=8900`, `funding_agency=8900`) now returns the correct subtree of contracts instead of narrowing to a single sub-office. A 4-digit FPDS code resolves to that agency and its descendants; a 3-digit CGAC code resolves to the whole department (matching what `awarding_agency=DOE` returns). Name queries are unchanged. Closes #2056.
- Filtering opportunities or notices by a numeric agency code (e.g. `?agency=8900`) now resolves to the correct subtree instead of matching agencies whose search index happens to contain that token. Fixes over-broad matches for short numeric queries and makes sibling-agency queries (like DOE `8900` vs FERC `8960` under the same department) correctly distinct. Closes #2058.

## [4.0.12] - 2026-04-22

### Added

- The `/welcome/` page now displays your active API key(s) with copy-to-clipboard and visibility toggle — no longer need to navigate to your profile to find your key after signing up

### Changed

- The welcome page's API keys card now matches the richer layout used on your profile: full-width card at the top of the page with your daily requests remaining, per-key name + "Active" badge, click-to-copy, show/hide, and created date + rate limits. "Your plan" and "Quick links" now sit side-by-side below it so nothing important is hidden below the fold

## [4.0.11] - 2026-04-21

- No user-facing changes

## [4.0.10] - 2026-04-21

- No user-facing changes

## [4.0.9] - 2026-04-20

- No user-facing changes

## [4.0.8] - 2026-04-20

- No user-facing changes

## [4.0.7] - 2026-04-18

- No user-facing changes

## [4.0.6] - 2026-04-17

- No user-facing changes

---

<!-- doc: api-reference/agencies.md -->

# Agencies

!!! warning "Deprecated endpoint"
    This endpoint is deprecated. Use [Organizations](organizations.md) for the unified federal hierarchy. This endpoint will be removed in a future version.

!!! warning "Deprecated scoped contract endpoints"
    The `/api/agencies/{code}/contracts/awarding/` and `/api/agencies/{code}/contracts/funding/` endpoints are deprecated. Use [`/api/contracts/?awarding_agency={code}`](contracts.md) or [`/api/contracts/?funding_agency={code}`](contracts.md) instead.

Agencies are federal organizations exposed at `/api/agencies/` (with a small amount of structure via `department`).

If you need the unified Federal Hierarchy model (departments/agencies/offices all as one tree), see [Organizations](organizations.md). For field definitions, see the [Organizations Data Dictionary](../data-dictionary/organizations.md).

## Endpoints

- `GET /api/agencies/` (list)
- `GET /api/agencies/{code}/` (detail)

Agency-scoped contract lists (these behave like `/api/contracts/`, but scoped by agency code):

- `GET /api/agencies/{code}/contracts/awarding/`
- `GET /api/agencies/{code}/contracts/funding/`

## Filtering

| Param | What it does |
| --- | --- |
| `search` | Full-text search over agencies (best-effort, vector-backed). |

## Ordering

`/api/agencies/` does **not** support `ordering=...`.

## Pagination

Agencies use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `code`, `name`, `abbreviation`
- Expansions:
  - `department(code,name,abbreviation,description,cgac,website,congressional_justification)`

Default shape (no `?shape=` param): `code,name,abbreviation,department(name,code)`

```bash
# Just codes and names
/api/agencies/?shape=code,name

# Include department info
/api/agencies/4700/?shape=code,name,department(name,code)

# All department fields
/api/agencies/4700/?shape=code,name,department(*)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

### List agencies

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_agencies(limit=25)

    for a in resp.results:
        print(a.code, a.name)
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listAgencies({ limit: 25 });

    for (const a of resp.results) {
      console.log(a.code, a.name);
    }
    ```

### Awarding/funding contracts for an agency

The SDKs don’t yet expose a first-class method for these scoped endpoints. Use the contracts endpoint with awarding/funding agency filters, or call the scoped path directly.

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    data = client._get(
        "/api/agencies/4700/contracts/awarding/",
        params={"limit": 10, "shape": ShapeConfig.CONTRACTS_MINIMAL},
    )

    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/agencies/4700/contracts/awarding/", {
      limit: 10,
      shape: ShapeConfig.CONTRACTS_MINIMAL,
    });

    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/assistance-listings.md -->

# Assistance listings

Assistance listings (CFDA) reference data is exposed at `/api/assistance_listings/`. This is **not** the `assistance` transactions endpoint; it’s just the static reference list. For field definitions, see the [Assistance Listings Data Dictionary](../data-dictionary/assistance-listings.md).

## Endpoints

- `GET /api/assistance_listings/` (list)
- `GET /api/assistance_listings/{number}/` (detail; `{number}` supports digits and `.`)

## Filtering

None.

## Ordering

`/api/assistance_listings/` does **not** support `ordering=...`.

## Pagination

Assistance listings use standard page-number pagination:

- `page` (default 1)
- `limit` (default 1000, max 10000)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `number`, `title`, `published_date`, `archived_date`, `popular_name`, `objectives`, `applicant_eligibility`, `benefit_eligibility`
- No expansions.

Default shape (list, no `?shape=` param): `number,title`

**Note:** Detail responses (`/api/assistance_listings/{number}/`) use the full serializer (includes all fields) when no `?shape=` param is provided.

```bash
# Include extra fields
/api/assistance_listings/?shape=number,title,objectives

# Detail with specific fields
/api/assistance_listings/10.001/?shape=number,title,objectives,popular_name
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don’t yet expose a first-class `list_assistance_listings()` / `listAssistanceListings()` method.
You can still call the endpoint via the SDK’s internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/assistance_listings/", params={"page": 1, "limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/assistance_listings/", { page: 1, limit: 25 });
    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/business-types.md -->

# Business types

Business type reference data is exposed at `/api/business_types/`. For field definitions, see the [Business Types Data Dictionary](../data-dictionary/business-types.md).

## Endpoints

- `GET /api/business_types/` (list)
- `GET /api/business_types/{code}/` (detail)

## Value formats

- **`code`**: 2-character alphanumeric, typically uppercase (examples: `A6`, `JT`, `XX`, `2R`).
- **`certifier`**: In practice you’ll see a small set of values: `SBA`, `AbilityOne`, `Government`, `Self`.

## Filtering

None.

## Ordering

`/api/business_types/` does **not** support `ordering=...`.

## Pagination

Business types use page-number pagination (static reference pagination defaults to larger pages):

- `page` (default 1)
- `limit` (default 1000, max 10000)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `name`, `code`
- No expansions.

Default shape (no `?shape=` param): `name,code`

```bash
# Select a single field
/api/business_types/?shape=name
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_business_types(limit=25)
    print("count:", resp.count)
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listBusinessTypes({ limit: 25 });
    console.log("count:", resp.count);
    ```

---

<!-- doc: api-reference/concepts/federal-agency-hierarchy.md -->

# Finding Federal Organizations: The Challenge of Incomplete Data Sources

When building applications that work with federal procurement and assistance data, one of the first challenges you'll encounter is finding the right organization identifiers. The federal government maintains multiple overlapping but incomplete data sources, each with their own strengths and gaps. In this post, we'll explore why SAM's Federal Hierarchy is authoritative but incomplete, how USAspending fills some gaps but misses others, and how Tango's unified approach helps you find organizations reliably.

## The Authoritative Source: SAM Federal Hierarchy

The [Federal Hierarchy from SAM.gov](https://sam.gov/content/entity-registration) is the official, authoritative source for federal organization structure. It provides a comprehensive tree of departments, agencies, and offices with unique `orgKey` identifiers and maintains the canonical parent-child relationships.

However, the Federal Hierarchy has a critical limitation: **it's missing entire organizations that appear in transaction data**.

For example, USAspending's office file contains thousands of organizations that don't exist in Federal Hierarchy. These include:

- **Office `AC6091`** - "W462 USA AERONAUTICAL SVCS AGG" (Department of Defense office that appears in USAspending financial assistance data but is missing from Federal Hierarchy)
- **Many DOD offices** - Hundreds of Department of Defense offices with codes like `W81YDE`, `W914J4`, `W90LDH` that appear in USAspending transaction data but aren't in Federal Hierarchy's structure
- **Subtier agencies** - Organizations like "Center for Nutrition Policy and Promotion" (subtier code `12F3`) and "Federal Library and Information Center Committee" (subtier code `0363`) that exist in USAspending's subtier file but may be missing from Federal Hierarchy

When you're working with contract data from FPDS or financial assistance from USAspending, you'll encounter codes like:

- **CGAC codes** (like `069` for Department of Transportation)
- **FPDS codes** (4-digit agency identifiers used in contract transactions)
- **Subtier codes** (like `12F3` for Center for Nutrition Policy and Promotion)
- **Office codes** (like `15JCRM` for the Criminal Division)

The Federal Hierarchy may have the organization's name and structure, but it often lacks these operational codes that appear in actual transaction data.

## USAspending: Filling Some Gaps

USAspending's database has data that complement the Federal Hierarchy, divided into top tier, subtier, and offices.

USASpending contains data that are missing from Federal Hierarchy. For example, this includes abbreviations like `DOT`, `DHS`, `USDA`, as well as full names and mission statements.

However, **USAspending has a critical gap: it doesn't include all FPDS contract data**. FPDS uses its own set of organization identifiers that don't always map cleanly to USAspending's structure. When you're working with contract transactions, you'll encounter organizations that exist in neither Federal Hierarchy nor USAspending:

- **Legacy FPDS offices** - Historical contract offices that were used in FPDS transactions but have since been reorganized or decommissioned. These appear in FPDS contract data with office codes that don't match any organization in Federal Hierarchy or USAspending's reference files.
- **Contract-specific organization IDs** - FPDS transaction data includes `fpds_org_id` values that are specific to contract processing workflows. These identifiers may reference organizations that were valid at the time of the contract but no longer exist in current organization reference data.
- **Historical agency structures** - FPDS contains contract transactions from years past that reference agency codes and department IDs that have changed over time. The organization that awarded a contract in 2010 might have a different code structure today, and the old codes may not appear in either Federal Hierarchy or USAspending's current reference files.

These FPDS identifiers appear in millions of contract transactions but aren't present in USAspending's organization files. Tango addresses this by maintaining legacy organization data from historical FPDS sources, ensuring that even old contract transactions can be properly linked to their awarding organizations.

## Tango's Unified Approach

Tango consolidates all these sources into a single `Organization` model with a priority-based field provenance system. Here's how it works:

### Data Source Priority

Tango loads organizations from multiple sources in priority order:

1. **Federal Hierarchy (Top Priority)** - The authoritative structure
2. **USAspending (Next Priority)** - Fills in missing codes and details
3. **Legacy models (Historical FPDS)** - Backfills from historical data

Higher-priority sources won't be overwritten by lower-priority ones, ensuring that Federal Hierarchy's authoritative structure is preserved while USAspending fills in the operational codes.

### Deduplication

When multiple Federal Hierarchy records map to the same organizational unit (same canonical code), Tango elects the best representative based on data completeness (name, codes, source priority, freshness) and demotes the others. Demoted records are preserved in the database for historical reference but excluded from API search and list results. Their `fh_key` values are recorded in the canonical record's history so lookups by any historical identifier still resolve correctly.

### Finding Organizations in Tango

Tango provides several ways to find organizations, each optimized for different use cases:

#### 1. Search by Name or Alias

The `/api/organizations/` endpoint supports a `search` query parameter that uses a multi-stage search strategy:

```bash
# Search by abbreviation, acronym, or name
GET /api/organizations/?search=FEMA
GET /api/organizations/?search=Department of Transportation
GET /api/organizations/?search=Treasury OIG  # Context-aware search
```

Or programmatically using the search library:

```python
from agencies.lib.search import search_organizations

# Finds organizations by abbreviation, acronym, or name
results = search_organizations("FEMA")
results = search_organizations("Department of Transportation")
results = search_organizations("Treasury OIG")  # Context-aware search
```

The search handles:

- **Exact alias matches** - Catches abbreviations like "CIO", "OIG", "FEMA"
- **Trigram similarity** - Handles typos like "FMEA" → "FEMA"
- **Full-text search** - Finds organizations by keywords in names
- **Context-aware queries** - "Treasury OIG" finds the OIG within Treasury

#### 2. Filter or look up by code

The list endpoint supports a single code-based filter — **`cgac`** (department-level CGAC code). Other codes (FPDS, office, sub-tier) are not exposed as filters; use `?search=` or a path lookup instead:

```bash
# Filter by CGAC (department-level)
GET /api/organizations/?cgac=069

# Search across aliases / full-text — works for FPDS codes, office codes,
# names, and abbreviations (multi-stage: aliases → trigram → FTS → fuzzy)
GET /api/organizations/?search=2100
GET /api/organizations/?search=15JCRM
GET /api/organizations/?search=FEMA

# Direct lookup by Federal Hierarchy key (zero-padded 9-char string) or UUID
GET /api/organizations/{fh_key}/
GET /api/organizations/{key}/
```

> **Note:** `?fpds_code=` and `?code=` query parameters are silently ignored (the response includes a `meta.warnings` entry). `fpds_code`, `fpds_org_id`, and the office-level `code` are available as response **leaves**, not filters — request them via `?shape=...,fpds_code,fpds_org_id,code`.

#### 3. Lookup by fh_key

The Federal Hierarchy's `orgKey` (mapped to `fh_key` in Tango) is used for direct lookups:

```bash
GET /api/organizations/{fh_key}/
```

**Note**: Tango uses two identifiers:

- `key` (UUID) - The primary key for the Organization model, stable across reloads
- `fh_key` (BigInteger) - The Federal Hierarchy's `orgKey` identifier, used for API lookups and cross-referencing with SAM.gov

#### 4. Hierarchy Navigation

Each organization includes parent relationships and flattened hierarchy paths:

```json
{
  "key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "fh_key": 123456,
  "name": "Federal Emergency Management Agency",
  "short_name": "FEMA",
  "parent_fh_key": 789012,
  "l1_name": "Department of Homeland Security",
  "l1_short_name": "DHS",
  "full_parent_path_name": "Department of Homeland Security > Federal Emergency Management Agency"
}
```

### Field Provenance

Every organization tracks which source provided each field and when it was last updated:

```json
{
  "field_provenance": {
    "name": {
      "source": "federal_hierarchy",
      "modified_at": "2024-12-01T10:00:00Z"
    },
    "cgac": {
      "source": "usaspending",
      "modified_at": "2024-12-01T10:00:00Z"
    },
    "fpds_code": {
      "source": "usaspending",
      "modified_at": "2024-12-01T10:00:00Z"
    },
    "code": {
      "source": "legacy",
      "modified_at": "2024-12-01T10:00:00Z"
    }
  }
}
```

This transparency helps us ensure the reliability of each field and make informed decisions about which identifiers to use.

## Practical Examples

### Example 1: Finding an Organization from a Contract Transaction

When processing an FPDS contract transaction, you might see:

- `agencyID`: `2100`
- `departmentID`: `097`

In Tango, you can find the organization by department CGAC, by searching for the FPDS code (search picks it up), or by name:

```bash
# By CGAC (department-level filter)
GET /api/organizations/?cgac=097

# By FPDS code via search (the multi-stage search picks it up via aliases / FTS)
GET /api/organizations/?search=2100

# Or search by name if you know it
GET /api/organizations/?search=Department of the Army
```

### Example 2: Finding an Office from USAspending Data

USAspending financial assistance data might reference:

- `awarding_sub_agency_code`: `1501`
- `awarding_office_code`: `15JCRM`

You can find the office via search:

```bash
GET /api/organizations/?search=15JCRM
```

The response will include the full hierarchy, so you can see it's part of the Department of Justice (CGAC `015`).

### Example 3: Context-Aware Search

If you know you're looking for "Treasury OIG" but aren't sure of the exact code:

```bash
GET /api/organizations/?search=Treasury OIG
```

Tango's contextual search will find the OIG (Office of Inspector General) within the Treasury Department, even if there are multiple OIGs across different departments.

## Best Practices

1. **Use `key` (UUID) for API references** - The UUID `key` is the primary identifier for storing references to organizations in your application
2. **Use `fh_key` for Federal Hierarchy lookups** - When cross-referencing with SAM.gov or other federal data sources that use the Federal Hierarchy, use `fh_key`
3. **Use code lookups for transaction matching** - When matching transactions, use the specific code type (CGAC, FPDS code, office code) that appears in your data
4. **Leverage search for user-facing features** - The `search` query parameter handles abbreviations, typos, and context better than exact code matching
5. **Check field_provenance for data quality** - Understand which source provided each field to assess reliability

## Conclusion

Federal organization data is fragmented across multiple sources, each with its own strengths and gaps. SAM's Federal Hierarchy provides the authoritative structure but lacks operational codes. USAspending fills in many codes but doesn't include FPDS contract identifiers. Tango unifies these sources with a priority-based system that preserves authoritative data while filling in the gaps, giving you a single, reliable way to find and reference federal organizations.

Whether you're matching contract transactions, processing financial assistance awards, or building user-facing search features, Tango's unified `Organization` model and flexible search capabilities help you find the right organization identifiers, regardless of which source your data comes from.

---

<!-- doc: api-reference/concepts/provenance.md -->

# Provenance & auditing

Tango ingests data from multiple upstream systems (SAM.gov, USAspending, FPDS, agency feeds, etc.). Those sources overlap, disagree, and update on different cadences.

To keep the dataset consistent (and debuggable), Tango tracks two related concepts:

- **Field provenance**: “Which source last updated this specific field, and when?”
- **Change logs (CDC)**: “Which fields changed on this row, by what operation (INSERT/UPDATE), and from what source?”

Even though we **do not currently expose** provenance or changelog tables in the public API, this tracking matters to API consumers because it helps us:

- **Explain behavior**: why a value changed (or why it didn’t).
- **Prevent regressions**: avoid overwriting authoritative sources with lower-quality sources.
- **Improve reliability**: tighten guarantees around “data is ready / stable” moments for downstream consumers.

## Terminology

- **CDC (Change Data Capture)**: a pattern for recording row-level changes (insert/update/delete) as data is ingested.
- **`change_source`**: a normalized identifier of the ingest source (e.g. `"sam"`, `"dsbs"`, `"usaspending"`).
- **`changed_fields`**: the list of field names that were changed by an operation.
  - For **UPDATE**, this is a true diff.
  - For **INSERT**, this is a best-effort list of fields set by the ingest path.
- **`batch_id` / `etl_job_id`**: identifiers used to correlate changes to a specific loader run.

## What models have this (so far)

Today, Tango tracks provenance/auditing for:

- **Organizations**
- **Entities**
- **Opportunities**

## How it works (high level)

At ingest time, Tango typically writes incoming data into a staging representation, computes a diff against the current table, records the audit log, and then applies the update.

```mermaid
flowchart LR
  A[Upstream source file/API] --> B[Loader parses + normalizes]
  B --> C[(Staging / temp representation)]
  C --> D[Compute diff vs target]
  D --> E[(ChangeLog rows)]
  D --> F[(Field provenance updates)]
  C --> G[Apply upsert/update to target]
```

## Why this is useful

- **Multiple sources, different trust levels**: provenance lets us define “who wins” per field (and why).
- **Better debugging**: we can query “what changed the name for UEI X?” or “why did this org’s code change?”
- **Future-facing**: this lays groundwork for exposing provenance in a controlled way (e.g. support tooling, exports, or opt-in API surfaces).

## Example questions we can answer

- “What last updated an Organization’s `name` (and from which source)?”
- “What last updated `legal_business_name` for an entity UEI?”
- “Which fields changed on organization FH key 12345 in the last sync?”
- “Did loader X actually change data, or was it a no-op?”

---

<!-- doc: api-reference/contracts.md -->

# Contracts

Contracts are awards (FPDS “definitive contracts”) exposed at `/api/contracts/`. For field definitions, see the [Contracts Data Dictionary](../data-dictionary/contracts.md).

## Endpoints

- `GET /api/contracts/` (list + filtering + search + ordering)
- `GET /api/contracts/{key}/` (detail)
- `GET /api/contracts/{key}/transactions/` (detail transactions)
- `GET /api/contracts/{key}/subawards/` (subawards scoped to a contract)

Related “scoped” list endpoints that behave like `/api/contracts/`:

- `GET /api/entities/{uei}/contracts/`
- `GET /api/idvs/{key}/awards/` (child awards under an IDV)
- `GET /api/vehicles/{uuid}/orders/` (task orders under a vehicle)

## Filtering

Swagger is the canonical list of query parameters. These are the core filters most people use day-to-day.

### Text / parties

| Param | What it does |
| --- | --- |
| `search` | Full-text-ish search across contract search vectors (good for keywords). |
| `recipient` | Search by recipient/vendor name. |
| `uei` | Filter by recipient UEI (exact). |
| `piid` | Filter by PIID (case-insensitive). |
| `solicitation_identifier` | Filter by solicitation identifier (exact). |

### Agencies

| Param | What it does |
| --- | --- |
| `awarding_agency` | Filter by awarding agency (code/name/abbrev; best-effort matching). Implemented via organization UUID for performance; response `awarding_office` shape unchanged. |
| `funding_agency` | Filter by funding agency (code/name/abbrev; best-effort matching). Implemented via organization UUID for performance; response `funding_office` shape unchanged. |

### Codes / set-asides

| Param | What it does |
| --- | --- |
| `naics` | Filter by NAICS. |
| `psc` | Filter by PSC. |
| `set_aside` | Filter by set-aside. |

### Dates / fiscal years

All date filters require `YYYY-MM-DD` format. Invalid dates or inverted ranges return **400**. See [Date filters](shared.md#date-filters) for details.

| Param | What it does |
| --- | --- |
| `award_date` | Award date (exact). |
| `award_date_gte`, `award_date_lte` | Award date range. |
| `fiscal_year` | Fiscal year (exact, `YYYY`). |
| `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year range (`YYYY`). |

### Period of performance

| Param | What it does |
| --- | --- |
| `pop_start_date_gte`, `pop_start_date_lte` | Period of performance **start** date range. |
| `pop_end_date_gte`, `pop_end_date_lte` | Period of performance **current end** date range. |
| `expiring_gte`, `expiring_lte` | Ultimate completion date range (useful for “expiring soon”). |

### Dollars / types

| Param | What it does |
| --- | --- |
| `obligated_gte`, `obligated_lte` | Obligated amount range (USD). |
| `award_type` | Award type code. |
| `ordering` | Sort results (allowlist: `award_date`, `obligated`, `total_contract_value`; prefix with `-` for descending). |

### Multi-value filter syntax

Use `|` (or the literal word `OR`) to match any of several values:

```
naics=541511|541512
```

The same syntax works on every filter that accepts multiple values.

## Ordering

Contracts support `ordering=` with a strict allowlist:

- `award_date`
- `obligated`
- `total_contract_value`

Examples:

- Newest first: `GET /api/contracts/?ordering=-award_date`
- Largest obligations first: `GET /api/contracts/?ordering=-obligated`

## Vehicle linkage

Contracts that are part of an IDIQ / GWAC vehicle can opt-in to a curated subset of vehicle fields via `?shape=...,vehicle(*)` (allowed leaves include `vehicle_type`, plus a curated subset of solicitation / vehicle metadata). The full vehicle detail (awardees, opportunity, totals) stays on `/api/vehicles/{uuid}/`. The expansion is not in the default response shape; opt in when needed.

## Pagination

High-volume award endpoints use **cursor-based (keyset) pagination**.

- Use `limit` to control page size.
- Follow the `next` / `previous` URLs in responses.
- If you’re constructing requests manually, you’ll typically pass a `cursor` parameter from the `next` URL.

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

### Search + ordering + shaping

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    resp = client.list_contracts(
        keyword="cloud services",  # SDK maps to API param: search
        awarding_agency="4700",  # GSA
        fiscal_year=2024,
        sort="award_date",  # SDK maps (sort+order) -> API ordering
        order="desc",
        limit=10,
        shape=ShapeConfig.CONTRACTS_MINIMAL,
    )

    for c in resp.results:
        print(c.piid, c.award_date, c.recipient.display_name)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    const resp = await client.listContracts({
      keyword: "cloud services", // SDK maps to API param: search
      awarding_agency: "4700", // GSA
      fiscal_year: 2024,
      sort: "award_date", // SDK maps (sort+order) -> API ordering
      order: "desc",
      limit: 10,
      shape: ShapeConfig.CONTRACTS_MINIMAL,
    });

    for (const c of resp.results) {
      console.log(c.piid, c.award_date, c.recipient.display_name);
    }
    ```

### Cursor pagination (using the `next` URL)

=== "Python (tango-python)"

    ```python
    import os
    from urllib.parse import parse_qs, urlparse

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    resp = client.list_contracts(limit=25, sort="award_date", order="desc")
    print("next:", resp.next)

    if resp.next:
        qs = parse_qs(urlparse(resp.next).query)
        cursor = (qs.get("cursor") or [None])[0]
        if cursor:
            resp2 = client.list_contracts(limit=25, cursor=cursor, sort="award_date", order="desc")
            print("page 2 results:", len(resp2.results))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    const resp = await client.listContracts({ limit: 25, sort: "award_date", order: "desc" });
    console.log("next:", resp.next);

    if (resp.next) {
      const url = new URL(resp.next);
      const cursor = url.searchParams.get("cursor");
      if (cursor) {
        const resp2 = await client.listContracts({ limit: 25, cursor, sort: "award_date", order: "desc" });
        console.log("page 2 results:", resp2.results.length);
      }
    }
    ```

---

<!-- doc: api-reference/departments.md -->

# Departments

!!! warning "Deprecated endpoint"
    This endpoint is deprecated. Use [Organizations](organizations.md) for the unified federal hierarchy. This endpoint will be removed in a future version.

Departments are top-level organizations exposed at `/api/departments/`.

If you need the full organization tree + richer search, see [Organizations](organizations.md). For field definitions, see the [Organizations Data Dictionary](../data-dictionary/organizations.md).

## Endpoints

- `GET /api/departments/` (list)
- `GET /api/departments/{code}/` (detail)

## Filtering

None.

## Ordering

`/api/departments/` does **not** support `ordering=...`.

## Pagination

Departments use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `code`, `name`, `abbreviation`, `description`, `cgac`, `website`, `congressional_justification`
- No expansions.

Default shape (no `?shape=` param): `code,name,abbreviation`

```bash
# Select specific fields
/api/departments/?shape=code,name

# Extended department info
/api/departments/1400/?shape=code,name,description,website,congressional_justification
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The SDKs don’t yet expose a first-class `list_departments()` / `listDepartments()` method.
You can still call the endpoint via the SDK’s internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/departments/", params={"page": 1, "limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;
    const data = await http.get("/api/departments/", { page: 1, limit: 25 });
    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/entities.md -->

# Entities

Entities are vendors/recipients exposed at `/api/entities/` (UEI is canonical). For field definitions, see the [Entities Data Dictionary](../data-dictionary/entities.md).

## Endpoints

- `GET /api/entities/` (list + filtering + search)
- `GET /api/entities/{uei}/` (detail; UEI lookup is case-insensitive)

Related “scoped” list endpoints (awards for an entity):

- `GET /api/entities/{uei}/contracts/`
- `GET /api/entities/{uei}/idvs/`
- `GET /api/entities/{uei}/otas/`
- `GET /api/entities/{uei}/otidvs/`
- `GET /api/entities/{uei}/subawards/`

**Metrics:** Time-series obligation/award metrics for an entity: `GET /api/entities/{uei}/metrics/<months>/<period_grouping>/`. See [Metrics](metrics.md) for parameters and behavior.

## Response Shaping

Entities default to the shaping pipeline — all responses go through shaping even without `?shape=`. Use `?shape=` to customize which fields are returned.

- **List default:** `uei`, `legal_business_name`, `dba_name`, `entity_url`, `cage_code`, `business_types(*)`, `sba_business_types(*)`, `primary_naics`, `physical_address(*)`, `purpose_of_registration(*)`, `relationships(*)`
- **Detail default:** All list fields plus `display_name`, `uuid`, `dodaac`, `description`, `email_address`, `capabilities`, `congressional_district`, `keywords`, `registered`, `registration_status`, `federal_obligations(*)`, `naics_codes(*)`, `psc_codes`, `highest_owner(*)`, `immediate_owner(*)`, `mailing_address(*)`, all date fields, and structured code/description expands: `entity_structure(*)`, `entity_type(*)`, `profit_structure(*)`, `organization_structure(*)`, `state_of_incorporation(*)`, `country_of_incorporation(*)`.
- **On-demand expand:** `past_performance(*)` — aggregated contract history with `summary` and `top_agencies`. Accepts `[top=N]` bracket param (default 5, max 100). Example: `?shape=uei,past_performance[top=10](*)`.

Complex fields are **normalized** by the shaping pipeline: address keys are always snake_case (address normalization now includes `county`, `county_code`, `fips_code` when available from DSBS), `business_types` and `sba_business_types` are always `[{code, description, ...}]` arrays, `naics_codes` are always `[{code, sba_small_business}]` objects, and code/description pairs (entity_structure, purpose_of_registration, etc.) are always `{code, description}` objects with map-based description fallback.

See [Response Shaping](../guides/patterns/response-shaping.md) for full syntax and the [Entities Data Dictionary](../data-dictionary/entities.md) for field definitions.

## Filtering

Swagger is the canonical list of query parameters. These are the core filters most people use day-to-day.

### Identity / text search

| Param | What it does |
| --- | --- |
| `search` | Search entities (name + aliases + other indexed fields). |
| `uei` | Filter by UEI (exact, case-insensitive). |
| `cage_code` | Filter by CAGE code (exact, case-insensitive). |
| `name` | Filter by legal business name (substring match). |

### Location

| Param | What it does |
| --- | --- |
| `state` | Filter by entity physical address state/province code. |
| `zip_code` | Filter by ZIP code (exact). |

### Classification

| Param | What it does |
| --- | --- |
| `naics` | Filter by NAICS. |
| `psc` | Filter by PSC. |
| `socioeconomic` | Filter by business type / socioeconomic code. |

### Other

| Param | What it does |
| --- | --- |
| `purpose_of_registration_code` | Filter by purpose-of-registration code (when present). |
| `total_awards_obligated_gte` | Filter entities with total obligated amount greater than or equal to a USD value. |
| `total_awards_obligated_lte` | Filter entities with total obligated amount less than or equal to a USD value. |

### Multi-value filter syntax

Use `|` (or the literal word `OR`) to match any of several values:

```
socioeconomic=8A|WOSB
```

The same syntax works on every filter that accepts multiple values.

## Ordering

`/api/entities/` does **not** currently support `ordering=...`.

If you send `?ordering=...` anyway, Tango returns **HTTP 400** (ordering is opt-in per endpoint).

## Pagination

Entities use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

### List entities (search + filters + shaping)

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    resp = client.list_entities(
        search="acme",
        state="VA",
        naics="541511",
        limit=10,
        shape=ShapeConfig.ENTITIES_MINIMAL,
    )

    for e in resp.results:
        print(e.uei, e.legal_business_name)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    const resp = await client.listEntities({
      search: "acme",
      state: "VA",
      naics: "541511",
      limit: 10,
      shape: ShapeConfig.ENTITIES_MINIMAL,
    });

    for (const e of resp.results) {
      console.log(e.uei, e.legal_business_name);
    }
    ```

### Get an entity (detail)

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    entity = client.get_entity(
        "ZQGGHJH74DW7",
        shape=ShapeConfig.ENTITIES_COMPREHENSIVE,
    )

    print(entity.uei, entity.legal_business_name)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    const entity = await client.getEntity("ZQGGHJH74DW7", {
      shape: ShapeConfig.ENTITIES_COMPREHENSIVE,
    });

    console.log(entity.uei, entity.legal_business_name);
    ```

---

<!-- doc: api-reference/forecasts.md -->

# Forecasts

Forecasts are procurement forecasts exposed at `/api/forecasts/`. For field definitions, see the [Forecasts Data Dictionary](../data-dictionary/forecasts.md). For which agency sources we ingest (HHS, DHS, GSA, COMMERCE, DOE, TREASURY, DOI, DOL, DOT, VA, NRC), see [Forecasts we track](../data-dictionary/forecasts.md#forecasts-we-track).

## Endpoints

- `GET /api/forecasts/` (list + filtering + search + ordering)
- `GET /api/forecasts/{pk}/` (detail)

## Response Shaping

Forecasts default to the shaping pipeline — all responses go through shaping even without `?shape=`. Use `?shape=` to customize which fields are returned.

- **List default:** `id`, `source_system`, `external_id`, `agency`, `title`, `description`, `anticipated_award_date`, `fiscal_year`, `naics_code`, `is_active`, `status`, `primary_contact`, `place_of_performance`, `estimated_period`, `set_aside`, `contract_vehicle`, `organization(*)`
- **Detail default:** Same as list, plus `raw_data` and `display(*)`

The `organization(*)` expand is the canonical 7-key office payload, resolved deterministically from the forecast's `agency` text using a 12-acronym alias map covering: HHS, DHS, DOI, GSA, DOE, DOT, VA, DOL, NRC, NSF, COMMERCE, TREASURY. Forecasts whose `agency` value doesn't map to one of these returns `organization: null`.

See [Response Shaping](../guides/patterns/response-shaping.md) for full syntax and the [Forecasts Data Dictionary](../data-dictionary/forecasts.md) for field definitions.

## Filtering

Core filters (multi-value filters accept `|` for OR; date filters require `YYYY-MM-DD` format — invalid dates or inverted ranges return **400**, see [Date filters](shared.md#date-filters)):

| Param | What it does |
| --- | --- |
| `agency` | Filter by agency acronym. |
| `source_system` | Filter by source system identifier. |
| `naics_code` | Filter by exact NAICS. |
| `naics_starts_with` | Filter by NAICS prefix (e.g. `54`). |
| `fiscal_year` | Filter by exact fiscal year. |
| `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year range. |
| `award_date_after`, `award_date_before` | Anticipated award date range (`YYYY-MM-DD`). |
| `modified_after`, `modified_before` | Modified-in-Tango date range (`YYYY-MM-DD`). |
| `status` | Status (case-insensitive, partial match). |
| `search` | Full-text search over title/description (vector-backed). |

## Ordering

Forecasts support `ordering=` with a strict allowlist:

- `anticipated_award_date`
- `fiscal_year`
- `title`

Examples:

- Soonest award first: `GET /api/forecasts/?ordering=anticipated_award_date`
- Most recent fiscal year first: `GET /api/forecasts/?ordering=-fiscal_year`

## Pagination

Forecasts use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_forecasts(
        agency="GSA",
        naics_starts_with="54",
        ordering="-anticipated_award_date",
        limit=10,
        shape=ShapeConfig.FORECASTS_MINIMAL,
    )

    for f in resp.results:
        print(f.title, f.anticipated_award_date)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listForecasts({
      agency: "GSA",
      naics_starts_with: "54",
      ordering: "-anticipated_award_date",
      limit: 10,
      shape: ShapeConfig.FORECASTS_MINIMAL,
    });

    for (const f of resp.results) {
      console.log(f.title, f.anticipated_award_date);
    }
    ```

---

<!-- doc: api-reference/grants.md -->

# Grants

Grant opportunities are exposed at `/api/grants/`. For field definitions, see the [Grants Data Dictionary](../data-dictionary/grants.md).

## Endpoints

- `GET /api/grants/` (list + filtering + search + ordering)
- `GET /api/grants/{grant_id}/` (detail)

## Response Shaping

Grants default to the shaping pipeline — all responses go through shaping even without `?shape=`. Use `?shape=` to customize which fields are returned.

- **List default:** `grant_id`, `opportunity_number`, `agency_code`, `title`, `description`, `applicant_eligibility_description`, `funding_activity_category_description`, `grantor_contact`, `last_updated`, plus expanded `organization(*)`, `status(*)`, `category(*)`, `cfda_numbers(*)`, `applicant_types(*)`, `funding_categories(*)`, `funding_instruments(*)`, `funding_details(*)`, `important_dates(*)`, `additional_info(*)`
- **Detail default:** Same as list, plus `synopsis`, `forecast`, `opportunity_history`

The `organization(*)` expand is the canonical 7-key office payload, resolved from the grant's `agency_code` via a segment-walk (e.g. `HHS-NIH11` → NIH; `USDOJ-OJP-BJA` → BJA; trailing fiscal-cycle digits stripped where applicable). Grants from agencies without a canonical organization mapping return `organization: null`.

See [Response Shaping](../guides/patterns/response-shaping.md) for full syntax and the [Grants Data Dictionary](../data-dictionary/grants.md) for field definitions.

## Filtering

Core filters (date filters require `YYYY-MM-DD` format; invalid dates or inverted ranges return **400** — see [Date filters](shared.md#date-filters)):

| Param | What it does |
| --- | --- |
| `search` | Full-text search (vector-backed). |
| `agency` | Filter by agency abbreviation (substring). |
| `opportunity_number` | Exact opportunity number. |
| `cfda_number` | CFDA number (substring match). |
| `status` | Opportunity status (case-insensitive choice). |
| `applicant_types` | Eligibility/applicant types (case-insensitive choice). |
| `funding_categories` | Funding category codes (case-insensitive choice). |
| `funding_instruments` | Funding instruments (case-insensitive choice). |
| `posted_date_after`, `posted_date_before` | Posted date range (inclusive, `YYYY-MM-DD`). |
| `response_date_after`, `response_date_before` | Response/deadline date range. |

## Ordering

Grants support `ordering=` with a strict allowlist:

- `posted_date` (also accepts `last_updated`)
- `deadline_current`
- `rank` (relevance ranking; most useful when `search` is present)

Examples:

- Most recently posted first: `GET /api/grants/?ordering=-posted_date`
- Soonest deadline first: `GET /api/grants/?ordering=deadline_current`

## Pagination

Grants use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_grants(
        agency="HHS",
        search="opioid",
        ordering="-posted_date",
        limit=10,
        shape=ShapeConfig.GRANTS_MINIMAL,
    )

    for g in resp.results:
        print(g.grant_id, g.opportunity_number, g.title)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listGrants({
      agency: "HHS",
      search: "opioid",
      ordering: "-posted_date",
      limit: 10,
      shape: ShapeConfig.GRANTS_MINIMAL,
    });

    for (const g of resp.results) {
      console.log(g.grant_id, g.opportunity_number, g.title);
    }
    ```

---

<!-- doc: api-reference/gsa-elibrary-contracts.md -->

# GSA eLibrary contracts

Persisted GSA eLibrary contract metadata is exposed directly at `/api/gsa_elibrary_contracts/`.

For field definitions, see the [GSA eLibrary Contracts Data Dictionary](../data-dictionary/gsa-elibrary-contracts.md).

These endpoints exist in addition to the `gsa_elibrary` enrichment on `/api/idvs/`.

## Endpoints

- `GET /api/gsa_elibrary_contracts/` (list + filtering + ordering)
- `GET /api/gsa_elibrary_contracts/{uuid}/` (detail)

## Response shape (non-shaped)

Each result includes:

- `uuid`, `schedule`, `contract_number`
- `sins`, `cooperative_purchasing`, `disaster_recovery_purchasing`, `file_urls`
- `recipient` (cached lookup by UEI; fields: `uei`, `display_name`)
- `idv` (best-effort; fields: `key`, `award_date`)

## Filtering

Core filters:

| Param | What it does |
| --- | --- |
| `schedule` | Filter by schedule (exact). Example: `MAS` |
| `contract_number` | Filter by contract number (exact, case-insensitive). Example: `GS-07F-0251W` |
| `uei` | Filter by recipient UEI (from eLibrary provenance; exact). Multi-value: use `\|` for OR. |
| `sin` | Filter by SIN code (exact). Multi-value: use `\|` for OR. |
| `search` | Search across schedule, contract_number, UEI, SIN, and linked IDV fields. Multi-value: use `\|` for OR. |
| `key` | Filter by linked IDV key (exact, case-insensitive). |
| `piid` | Filter by linked IDV PIID (exact, case-insensitive). |

## Ordering

Supports `ordering=` with:

- `last_updated`
- `schedule`
- `contract_number`

Example:

```bash
/api/gsa_elibrary_contracts/?schedule=MAS&ordering=-last_updated
```

## Pagination

Uses page-number pagination.

## Response shaping

Like other endpoints, shaping is available via `shape=`:

```bash
/api/gsa_elibrary_contracts/?contract_number=47QRAA26D003K&shape=uuid,schedule,contract_number,sins,file_urls,recipient(uei,display_name),idv(key,award_date)
```

---

<!-- doc: api-reference/idvs.md -->

# IDVs

IDVs (Indefinite Delivery Vehicles) are awards exposed at `/api/idvs/`.

If you’re trying to work at the “solicitation that produced many IDV awards” level, also see [Vehicles](vehicles.md). For field definitions, see the [IDVs Data Dictionary](../data-dictionary/idvs.md).

IDV responses include a vehicle UUID when the IDV is grouped under a Tango Vehicle, which you can use to fetch vehicle details at `GET /api/vehicles/{uuid}/`.

## Endpoints

- `GET /api/idvs/` (list + filtering + search + ordering)
- `GET /api/idvs/{key}/` (detail)
- `GET /api/idvs/{key}/transactions/` (detail transactions)
- `GET /api/idvs/{key}/awards/` (child awards / task orders under an IDV; behaves like `/api/contracts/`)
- `GET /api/idvs/{key}/idvs/` (child IDVs under an IDV; behaves like `/api/idvs/`)

Removed (legacy solicitation grouping — these routes no longer exist):

- `GET /api/idvs/{identifier}/summary/` — returns 404
- `GET /api/idvs/{identifier}/summary/awards/` — returns 404

## Filtering

Swagger is the canonical list of query parameters. These are the core filters most people use day-to-day.

### Text / parties

| Param | What it does |
| --- | --- |
| `search` | Full-text-ish search across IDV search vectors (good for keywords). |
| `recipient` | Search by recipient/vendor name. |
| `uei` | Filter by recipient UEI (exact). |
| `piid` | Filter by PIID (case-insensitive). |
| `solicitation_identifier` | Filter by solicitation identifier (exact). |
| `obligated_gte`, `obligated_lte` | Obligated amount range (USD). |
| `ordering` | Sort results (allowlist: `award_date`, `obligated`, `total_contract_value`; prefix with `-` for descending). |

### Agencies

| Param | What it does |
| --- | --- |
| `awarding_agency` | Filter by awarding agency (code/name/abbrev; best-effort matching). |
| `funding_agency` | Filter by funding agency (code/name/abbrev; best-effort matching). |

### Codes / set-asides / types

| Param | What it does |
| --- | --- |
| `naics` | Filter by NAICS. |
| `psc` | Filter by PSC. |
| `set_aside` | Filter by set-aside. |
| `idv_type` | Filter by IDV type code. |

### Dates / fiscal years

All date filters require `YYYY-MM-DD` format. Invalid dates or inverted ranges return **400**. See [Date filters](shared.md#date-filters) for details.

| Param | What it does |
| --- | --- |
| `award_date` | Award date (exact). |
| `award_date_gte`, `award_date_lte` | Award date range. |
| `fiscal_year` | Fiscal year (exact, `YYYY`). |
| `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year range (`YYYY`). |

### Period of performance / ordering window

IDVs differ from contracts:

- They do **not** have contract-style period-of-performance end dates.
- The ordering window is typically expressed via last-date-to-order fields.

| Param | What it does |
| --- | --- |
| `pop_start_date_gte`, `pop_start_date_lte` | Period of performance **start** date range. |
| `last_date_to_order_gte`, `last_date_to_order_lte` | Last date to order range (primary IDV “expiring” concept). |
| `expiring_gte`, `expiring_lte` | Alias for last-date-to-order range filters. |

### Multi-value filter syntax

Use `|` (or the literal word `OR`) to match any of several values:

```
naics=541511|541512
```

The same syntax works on every filter that accepts multiple values.

## Ordering

IDVs support `ordering=` with a strict allowlist:

- `award_date`
- `obligated`
- `total_contract_value`

Examples:

- Newest first: `GET /api/idvs/?ordering=-award_date`
- Largest obligations first: `GET /api/idvs/?ordering=-obligated`

## Pagination

High-volume award endpoints use **cursor-based (keyset) pagination**.

- Use `limit` to control page size.
- Follow the `next` / `previous` URLs in responses.
- If you’re constructing requests manually, you’ll typically pass a `cursor` parameter from the `next` URL.

## Response shaping

IDV responses use the shaping pipeline by default — even without an explicit `?shape=` parameter, responses go through the shaping system with a default shape that includes all standard fields plus expanded choice fields (`idv_type(*)`, `type_of_idc(*)`, `multiple_or_single_award_idv(*)`), relationships, and period of performance.

Use `?shape=` to customize the response. See [Response Shaping](../guides/patterns/response-shaping.md#idvs-apiidvs) for the full field reference.

```bash
# Expand idv_type to structured code/description
/api/idvs/?shape=key,piid,idv_type(code,description)

# Period of performance (uses last_date_to_order, not end_date)
/api/idvs/?shape=key,piid,period_of_performance(start_date,last_date_to_order)

# Resolved NAICS/PSC lookups
/api/idvs/?shape=key,piid,naics(code,description),psc(code,description)
```

## GSA eLibrary enrichment

IDV list and detail responses include best-effort enrichment from GSA eLibrary under `gsa_elibrary` (or `null` when no matching eLibrary row exists).

You can use shaping to request only the subfields you care about:

```bash
/api/idvs/?piid=<PIID>&shape=key,piid,gsa_elibrary(schedule,contract_number,sins,file_urls)
```

You can also query persisted eLibrary rows directly via `GET /api/gsa_elibrary_contracts/`.

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don’t yet expose a first-class `list_idvs()` / `listIdvs()` method.
Until they do, you can still call the endpoint via the SDK’s internal HTTP helper.

<!-- markdownlint-disable MD046 -->
=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    # Workaround until the SDK ships a first-class list_idvs()
    data = client._get(
        "/api/idvs/",
        params={
            "search": "SEWP",
            "ordering": "-award_date",
            "limit": 10,
        },
    )

    print("count:", data.get("count"))
    print("first key:", (data.get("results") or [{}])[0].get("key"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    // Workaround until the SDK ships a first-class listIdvs()
    const http = (client as any).http;
    const data = await http.get("/api/idvs/", {
      search: "SEWP",
      ordering: "-award_date",
      limit: 10,
    });

    console.log("count:", data.count);
    console.log("first key:", data?.results?.[0]?.key);
    ```

---

<!-- doc: api-reference/index.md -->

# API Reference

These pages are a curated reference for **filtering**, **ordering**, and **pagination** on Tango's highest-traffic endpoints, with copy/pasteable examples (including SDK usage).

Swagger is still the canonical surface-area reference, but these pages aim to be faster to use day-to-day.

- Swagger (OpenAPI): [https://tango.makegov.com/api/](https://tango.makegov.com/api/)
- For field definitions, see the [Data Dictionary](../data-dictionary/index.md).

## Awards

Definitive contracts, indefinite-delivery vehicles, other transactions, subawards, and solicitation-level groupings.

- [Contracts](contracts.md) — Definitive contract awards. [Response shaping →](../guides/patterns/response-shaping.md#contracts-apicontracts)
- [IDVs](idvs.md) — Indefinite delivery vehicles (GWACs, IDIQs, etc.). [Response shaping →](../guides/patterns/response-shaping.md#idvs-apiidvs)
- [OTAs](otas.md) — Other Transaction Agreements from FPDS. [Response shaping →](../guides/patterns/response-shaping.md#otas-apiotas)
- [OTIDVs](otidvs.md) — Other Transaction IDVs from FPDS. [Response shaping →](../guides/patterns/response-shaping.md#otidvs-apiotidvs)
- [Subawards](subawards.md) — Sub-contracts and sub-grants under prime awards. [Response shaping →](../guides/patterns/response-shaping.md#subawards-apisubawards)
- [Vehicles](vehicles.md) — Solicitation-centric grouping of IDVs. [Response shaping →](../guides/patterns/response-shaping.md#vehicles-apivehicles)
- [GSA eLibrary contracts](gsa-elibrary-contracts.md) — Persisted GSA eLibrary contract metadata (direct endpoint).

## Entities

- [Entities](entities.md) — Vendors and recipients in federal contracting and assistance; UEI is the canonical identifier (from SAM.gov). [Response shaping →](../guides/patterns/response-shaping.md#entities-apientities)

## Forecasts

- [Forecasts](forecasts.md) — Upcoming procurement opportunities from agency feeds (e.g., HHS, DHS) before they appear as SAM.gov solicitations. [Response shaping →](../guides/patterns/response-shaping.md#forecasts-apiforecasts)

## Grants

- [Grants](grants.md) — Grant opportunities from Grants.gov (funding opportunities, not assistance transactions). [Response shaping →](../guides/patterns/response-shaping.md#grants-apigrants)

## IT Dashboard

- [IT Dashboard](itdashboard.md) — Federal IT investment data from [itdashboard.gov](https://www.itdashboard.gov/), keyed by Unique Investment Identifier (UII). Filters and shape expansions are tier-gated. [Response shaping →](../guides/patterns/response-shaping.md#it-dashboard-apiitdashboard)

## Opportunities

SAM.gov contract opportunity and notice data.

- [Opportunities](opportunities.md) — Contract opportunities aggregated by parent. [Response shaping →](../guides/patterns/response-shaping.md#opportunities-apiopportunities)
- [Notices](notices.md) — Individual SAM.gov notice records (amendments, updates) under an opportunity. [Response shaping →](../guides/patterns/response-shaping.md#notices-apinotices)

## Metrics

- [Metrics](metrics.md) — Time-series obligation and award-count metrics for entities, NAICS codes, and PSC codes (same URL pattern and query params for all).

## Reference data

Lookup tables and codes used across awards and entities.

- [Assistance listings (CFDA)](assistance-listings.md) — CFDA program numbers and titles for federal grants/assistance. [Response shaping →](../guides/patterns/response-shaping.md#assistance-listings-apiassistance_listings)
- [Business types](business-types.md) — SAM.gov business type codes (e.g., small business, 8(a), HUBZone). [Response shaping →](../guides/patterns/response-shaping.md#business-types-apibusiness_types)
- [MAS SINs](mas-sins.md) — MAS Special Item Numbers (SINs) reference data. [Response shaping →](../guides/patterns/response-shaping.md#mas-sins-apimas_sins)
- [NAICS](naics.md) — North American Industry Classification System codes. [Response shaping →](../guides/patterns/response-shaping.md#naics-apinaics)
- [Organizations](organizations.md) — Unified federal hierarchy (departments, agencies, offices in one tree with search and obligations). [Response shaping →](../guides/patterns/response-shaping.md#organizations-apiorganizations)
- [PSC](psc.md) — Product/Service Codes for federal contracting. [Response shaping →](../guides/patterns/response-shaping.md#psc-apipsc)

## Shared

Objects and legacy endpoints shared across multiple resources. These data structures appear in response shaping expands on many endpoints (e.g., `awarding_office(...)`, `department(...)`, `set_aside(...)`).

- [Shared response objects](shared.md) — Common expanded objects (set-aside, office) returned across endpoints.
- [Agencies](agencies.md) *(deprecated)* — Federal agencies (legacy endpoint; use [Organizations](organizations.md) instead). [Response shaping →](../guides/patterns/response-shaping.md#agencies-apiagencies)
- [Departments](departments.md) *(deprecated)* — Top-level federal departments (legacy endpoint; use [Organizations](organizations.md) instead). [Response shaping →](../guides/patterns/response-shaping.md#departments-apidepartments)
- [Offices](offices.md) *(deprecated)* — Sub-agency offices (legacy endpoint; use [Organizations](organizations.md) instead). [Response shaping →](../guides/patterns/response-shaping.md#offices-apioffices)
- [Set-aside codes](set-aside.md) — Set-aside type codes and descriptions.

---

<!-- doc: api-reference/itdashboard.md -->

# IT Dashboard

Federal IT investment data from [itdashboard.gov](https://www.itdashboard.gov/) exposed at `/api/itdashboard/`. For field definitions, see the [IT Dashboard Data Dictionary](../data-dictionary/itdashboard.md).

## Endpoints

- `GET /api/itdashboard/` (list + filtering + search)
- `GET /api/itdashboard/{uii}/` (detail by Unique Investment Identifier)

## Filtering

### Free tier

| Param | What it does |
| --- | --- |
| `search` | Full-text search over UII, investment title, description, agency name, and bureau name (vector-backed). |
| `agency_name` | Text search on agency name (e.g., `?agency_name=defense`). |

### Pro+ tier

| Param | What it does |
| --- | --- |
| `agency_code` | Filter by numeric agency code (e.g., `?agency_code=21` for the Department of Transportation). |
| `type_of_investment` | Filter by investment type (case-insensitive, e.g., `?type_of_investment=Major IT Investment`). |
| `updated_time_after`, `updated_time_before` | Filter by updated time range (e.g., `?updated_time_after=2026-01-01`). |

### Business+ tier

| Param | What it does |
| --- | --- |
| `cio_rating` | Filter by exact CIO risk rating: 1=High Risk, 2=Moderately High, 3=Medium, 4=Moderately Low, 5=Low Risk. |
| `cio_rating_max` | Filter investments at or below a CIO rating threshold (e.g., `?cio_rating_max=2` returns High + Moderately High Risk). |
| `performance_risk` | Boolean. `?performance_risk=true` returns investments with at least one NOT MET performance metric. |

!!! note "Tier gating"
    Filters marked Pro+ or Business+ require the corresponding subscription tier. Lower-tier users attempting to use a gated filter receive a 403 response with structured upgrade information.

## Pagination

IT Dashboard uses page-number pagination (`page`, `limit`) and returns `count`, `next`, `previous`, and `results`.

## Response Shaping

IT Dashboard supports the `?shape=` query parameter. When no `?shape=` is provided, the endpoint returns a default shape.

**Default shape (list):** `uii,agency_name,bureau_name,investment_title,type_of_investment,part_of_it_portfolio,updated_time,url,organization(*)`

**Default shape (detail):** adds `agency_code` and `bureau_code` (otherwise identical to list).

### Available fields (all tiers)

| Field | Type | Description |
| --- | --- | --- |
| `uii` | String | Unique Investment Identifier (e.g., `021-488119819`). |
| `agency_code` | Integer | Numeric agency code. |
| `agency_name` | String | Agency name. |
| `bureau_code` | Integer | Numeric bureau code. |
| `bureau_name` | String | Bureau name. |
| `investment_title` | String | Investment title. |
| `type_of_investment` | String | Investment type (e.g., "Major IT Investment"). |
| `part_of_it_portfolio` | String | IT portfolio classification. |
| `updated_time` | DateTime | Last updated timestamp from source. |
| `url` | String | Canonical URL on itdashboard.gov. |
| `organization_id` | UUID | Underlying foreign key to `agencies.Organization`; `null` when no canonical org match. |

### Tier-gated leaf field

| Field | Tier | Description |
| --- | --- | --- |
| `business_case_html` | Business+ | Full business case HTML from IT Dashboard. |

### Available expansions

All shape expansions are available to every tier — the underlying source data is public from itdashboard.gov. Only the `business_case_html` **leaf** field requires Business+.

| Expansion | Description |
| --- | --- |
| `organization(*)` | Canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). Resolved deterministically from `(agency_code, bureau_code)`. Included in default shape. |
| `funding(*)` | Fiscal-year funding breakdown (FY2020–FY2025 internal funding and contributions). |
| `details(*)` | Extended metadata: description, previous/current UII, classification, business case URL, public URLs. |
| `cio_evaluation(*)` | CIO risk ratings over time (rating, comment, date, latest indicator). |
| `contracts(*)` | Associated IT contracts. |
| `projects(*)` | Project details under the investment. |
| `cost_pools_towers(*)` | Cost pool and tower breakdowns. |
| `funding_sources(*)` | Funding source details. |
| `performance_metrics(*)` | Performance metrics with targets and actuals. |
| `performance_actual(*)` | Historical performance actuals. |
| `operational_analysis(*)` | Operational analysis data. |

### Examples

```bash
# Free tier: basic search
GET /api/itdashboard/?search=navy

# Pro+ tier: filter by agency + expand funding
GET /api/itdashboard/?agency_code=7&shape=uii,investment_title,url,funding(*)

# Business+ tier: find high-risk investments with full details
GET /api/itdashboard/?cio_rating_max=2&shape=uii,investment_title,agency_name,cio_evaluation(*),performance_metrics(*)

# Business+ tier: underperforming investments
GET /api/itdashboard/?performance_risk=true&shape=uii,investment_title,agency_name,url,performance_metrics(*)

# Detail with everything
GET /api/itdashboard/021-488119819/?shape=uii,investment_title,agency_name,url,funding(*),details(*),cio_evaluation(*),contracts(*),projects(*)
```

See [Response Shaping](../guides/patterns/response-shaping.md) for syntax and examples.

---

<!-- doc: api-reference/lcats.md -->

# CALC Labor Category Rates

CALC (Contract Awarded Labor Category) labor rates are exposed as nested endpoints under IDVs and entities. For field definitions, see the [CALC Labor Rates Data Dictionary](../data-dictionary/lcats.md).

**Access:** Requires Pro tier or above.

## Endpoints

- `GET /api/idvs/{key}/lcats/` (rates for a specific IDV)
- `GET /api/entities/{uei}/lcats/` (rates for a specific entity, via IDV recipient linkage)

These are nested endpoints -- there is no top-level `/api/lcats/` route. Use the IDV key or entity UEI to scope the results.

## Filtering

Core filters:

| Param | What it does |
| --- | --- |
| `search` | Full-text search across vendor name, labor category, category, and subcategory. |
| `vendor_name` | Case-insensitive partial match on vendor name. |
| `labor_category` | Case-insensitive partial match on labor category title. |
| `price_gte`, `price_lte` | Filter by current price range (decimal). |
| `education_level` | Exact match on education level (case-insensitive). |
| `min_years_experience_gte`, `min_years_experience_lte` | Filter by minimum years of experience range. |
| `security_clearance` | Exact match on security clearance requirement (case-insensitive). |
| `worksite` | Exact match on work location type (case-insensitive). |
| `business_size` | Exact match on business size code (case-insensitive). |
| `sin` | Exact match on Special Item Number (case-insensitive). |
| `category` | Case-insensitive partial match on service category. |
| `idv_piid` | Exact match on the stored PIID from CALC. |
| `key` | IDV key (case-insensitive). |
| `piid` | IDV PIID (case-insensitive). |

## Ordering

CALC labor rates support `ordering=` with a strict allowlist:

- `current_price`
- `vendor_name`
- `labor_category`
- `education_level`
- `min_years_experience`
- `modified`

Prefix with `-` for descending order.

Examples:

- Cheapest first: `GET /api/idvs/{key}/lcats/?ordering=current_price`
- Most experienced first: `GET /api/idvs/{key}/lcats/?ordering=-min_years_experience`

## Pagination

Uses page-number pagination:

- `page` (default 1)
- `limit` (default 10, max 100)

## Response shaping

Use `?shape=` to customize the response. Root fields:

- `uuid`, `labor_category`, `current_price`
- `education_level`, `min_years_experience`, `experience_range`
- `security_clearance`, `worksite`
- `sin`, `category`, `subcategory`, `schedule`
- `contract_start`, `contract_end`

Expansions:

- `idv(key, piid, award_date)` -- linked IDV details (Pro tier required)
- `recipient(uei, display_name)` -- entity details via IDV recipient (Pro tier required)
- `business_size(code, description)` -- business size with human-readable description

A default shape is applied when no `?shape=` parameter is provided, so `?flat=true` works without needing an explicit shape.

Example:

```bash
/api/idvs/{key}/lcats/?shape=uuid,labor_category,current_price,idv(key,piid)
```

Flat example:

```bash
/api/idvs/{key}/lcats/?flat=true
```

## Rate limiting

These endpoints have separate premium query rate limits. See your account dashboard for current limits.

---

<!-- doc: api-reference/mas-sins.md -->

# MAS SINs

MAS SIN reference data is exposed at `/api/mas_sins/`. For field definitions, see the [MAS SINs Data Dictionary](../data-dictionary/mas-sins.md).

## Endpoints

- `GET /api/mas_sins/` (list)
- `GET /api/mas_sins/{sin}/` (detail)

## Value formats and normalization

- **`sin`**: String SIN code (examples: `334310`, `541611`, `561210FS`, `ANCILLARY`).
- **`large_category_code`**: Single-letter category code (example: `A`).
- **`sub_category_code`**: 3-character subcategory code (example: `A01`).
- **`set_aside_code`**: One of `Y`, `N`, `B`.
- **`service_comm_code`**: One of `B`, `C`, `S`, `P`.
- **`naics_codes`**: List of NAICS code integers. Some SINs may use special values (e.g. `0` for `ANCILLARY`).
- **`expiration_date`**: Parsed to an ISO date. The source uses a `YYYYDDD` (year + day-of-year) numeric format in some rows (example: `2022181` → 2022-06-30).

## Filtering

- `search`: matches against SIN code, NAICS code, PSC code, title, and description.

Examples:

- Search by SIN: `GET /api/mas_sins/?search=334310`
- Search by NAICS: `GET /api/mas_sins/?search=541611`
- Search by PSC: `GET /api/mas_sins/?search=R799`
- Search by text: `GET /api/mas_sins/?search=facilities`

## Ordering

`/api/mas_sins/` does **not** support `ordering=...`.

## Pagination

MAS SINs use page-number pagination (static reference pagination defaults to larger pages):

- `page` (default 1)
- `limit` (default 1000, max 10000)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `sin`, `large_category_code`, `large_category_name`, `sub_category_code`, `sub_category_name`, `psc_code`, `state_local`, `set_aside_code`, `service_comm_code`, `expiration_date`, `tdr`, `olm`, `naics_codes`, `title`, `description`
- No expansions.

Default shape (no `?shape=` param): all 15 fields above.

```bash
# Select a subset of fields
/api/mas_sins/?shape=sin,title,description
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

<!--
The official SDKs don’t yet expose a first-class `list_mas_sins()` / `listMasSins()` method.
We’ll add SDK examples once those methods ship.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/mas_sins/", params={"limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/mas_sins/", { limit: 25 });
    console.log("count:", data.count);
    ```
-->

---

<!-- doc: api-reference/metrics.md -->

# Metrics

Metrics endpoints return **time-series obligation and award counts** for a specific entity, NAICS code, or PSC code. They share the same URL pattern and query parameters, so this page describes behavior once, then lists the available endpoints.

## How metrics work

Each metrics endpoint is:

`GET /api/{resource}/{id}/metrics/<months>/<period_grouping>/`

- **Path parameters** (required):
  - `{id}` — The resource identifier (entity UEI, NAICS code, or PSC code).
  - `months` — Lookback window in months (positive integer). Example: `24` for the last 24 months.
  - `period_grouping` — How to bucket time: `year`, `quarter`, or `month`.

- **Query parameters** (optional):
  - `group_by` — Comma-separated list of dimensions to break out: `agency`, `department`. When used, each result row includes the grouped dimension(s). Rolling averages are not supported when grouping; if both are used, rolling is skipped and a `warning` is returned.
  - `fiscal_year` — If `true` or `1`, buckets use federal fiscal year (Oct–Sep). Default is calendar year.
  - `rolling` — If `true` or `1`, adds a rolling average to each result (when not using `group_by`).

Response shape (same for all three):

- `count` — Number of result rows.
- `description` — Human-readable description of the time range and grouping.
- `warning` — Present only when e.g. rolling was requested with `group_by` (rolling skipped).
- The resource object — `recipient` (entity), `naics_code`, or `psc_code` (the one you queried).
- `results` — Array of objects. Each has:
  - `year` (and optionally `month`, `quarter` depending on `period_grouping`),
  - `awards_obligated` — obligation amount (USD) for the period,
  - `subawards_obligated` — subaward obligation amount (entity endpoints only),
  - `awards_count` — number of awards (NAICS and PSC endpoints only),
  - `rolling_avg` (if `rolling=true` and no `group_by`),
  - `department`, `agency` (if `group_by` includes them).

Invalid `period_grouping`, non-positive `months`, or invalid `group_by` values return HTTP 400 with an `error` message.

## Available endpoints

| Resource | Endpoint | Identifier |
| --- | --- | --- |
| **Entity** | `GET /api/entities/{uei}/metrics/<months>/<period_grouping>/` | Entity UEI. See [Entities](entities.md). |
| **NAICS** | `GET /api/naics/{code}/metrics/<months>/<period_grouping>/` | NAICS code. See [NAICS](naics.md). |
| **PSC** | `GET /api/psc/{code}/metrics/<months>/<period_grouping>/` | PSC code. See [PSC](psc.md). |

All three support the same optional query parameters: `group_by`, `fiscal_year`, `rolling`.

## Example

Entity obligations for the last 12 months, by month, with rolling average:

```http
GET /api/entities/ABC123DEF456/metrics/12/month/?rolling=true
```

NAICS code `541512` for the last 24 months by quarter, grouped by department:

```http
GET /api/naics/541512/metrics/24/quarter/?group_by=department
```

## SDK

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don’t yet expose first-class methods for metrics. Use the HTTP helper with the paths above:

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get(
        "/api/entities/ABC123DEF456/metrics/12/month/",
        params={"rolling": "true"},
    )
    print("count:", data.get("count"), "results:", len(data.get("results", [])))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/entities/ABC123DEF456/metrics/12/month/", {
      rolling: "true",
    });
    console.log("count:", data.count, "results:", data.results?.length ?? 0);
    ```

---

<!-- doc: api-reference/naics.md -->

# NAICS

NAICS codes are exposed at `/api/naics/`. For field definitions, see the [NAICS Data Dictionary](../data-dictionary/naics.md).

## External references

- [Official NAICS information (U.S. Census Bureau)](https://www.census.gov/naics/)
- [NAICS Association (third-party reference)](https://www.naics.com/)

## Endpoints

- `GET /api/naics/` (list + filtering)
- `GET /api/naics/{code}/` (detail)

**Metrics:** Time-series obligation/award metrics for a NAICS code: `GET /api/naics/{code}/metrics/<months>/<period_grouping>/`. See [Metrics](metrics.md) for parameters and behavior.

## Filtering

| Param | What it does |
| --- | --- |
| `search` | Matches `code__startswith` or `description__icontains`. |
| `revenue_limit` | Filter by SBA revenue size standard (in **millions** of USD). |
| `revenue_limit_gte`, `revenue_limit_lte` | Revenue size standard range (in **millions**). |
| `employee_limit` | Filter by SBA employee size standard. |
| `employee_limit_gte`, `employee_limit_lte` | Employee size standard range. |

Example:

- NAICS starting with 54: `GET /api/naics/?search=54`
- NAICS with revenue limit \(\le 15M\): `GET /api/naics/?revenue_limit_lte=15`

## Ordering

`/api/naics/` does **not** support `ordering=...`.

## Pagination

NAICS uses page-number pagination (static reference pagination defaults to larger pages):

- `page` (default 1)
- `limit` (default 1000, max 10000)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `code`, `description`
- Expansions:
  - `size_standards(employee_limit,revenue_limit)` — SBA size standards. `revenue_limit` is in whole dollars.
  - `federal_obligations(total,active)` — obligation rollups with `awards_obligated` and `awards_count`.

Default shape (list, no `?shape=` param): `code,description`

Default shape (detail or `?show_limits=1`): `code,description,size_standards(*),federal_obligations(*)`

**Deprecation note:** The legacy `?show_limits=1` parameter is still supported but produces the same result as `?shape=code,description,size_standards(*),federal_obligations(*)`. Prefer `?shape=` directly.

```bash
# List defaults
/api/naics/?shape=code,description

# Full detail equivalent (replaces ?show_limits=1)
/api/naics/?shape=code,description,size_standards(*),federal_obligations(*)

# Size standards only
/api/naics/541330/?shape=code,description,size_standards(*)

# Just the total obligations
/api/naics/541330/?shape=code,federal_obligations(total)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don’t yet expose a first-class `list_naics()` / `listNaics()` method.
You can still call the endpoint via the SDK’s internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/naics/", params={"search": "5415", "limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/naics/", { search: "5415", limit: 25 });
    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/notices.md -->

# Notices

Notices are individual SAM.gov notice records exposed at `/api/notices/`.

If you want “one row per solicitation/opportunity”, use [Opportunities](opportunities.md). For field definitions, see the [Notices Data Dictionary](../data-dictionary/notices.md).

## Endpoints

- `GET /api/notices/` (list + filtering)
- `GET /api/notices/{notice_id}/` (detail)

## Filtering

Swagger is the canonical list of query parameters. These are the core filters most people use day-to-day. All date filters require `YYYY-MM-DD` format; invalid dates or inverted ranges return **400**. See [Date filters](shared.md#date-filters).

!!! warning "Search temporarily unavailable"
    The `?search=` parameter is currently returning HTTP 400 due to a missing database migration (`search_vector` field). Use `solicitation_number`, `naics`, or `psc` filters instead until this is resolved.

| Param | What it does |
| --- | --- |
| `agency` | Filter by agency or department (vector-backed). |
| `naics` | Filter by NAICS. |
| `psc` | Filter by PSC. |
| `set_aside` | Filter by set-aside. |
| `notice_type` | Filter by notice type (valid values are validated). |
| `solicitation_number` | Filter by solicitation number (exact). |
| `posted_date_after`, `posted_date_before` | Posted date range (`YYYY-MM-DD`). |
| `response_deadline_after`, `response_deadline_before` | Response deadline range (`YYYY-MM-DD`). |
| `active` | Filter active/inactive. |

## Ordering

`/api/notices/` does **not** support `ordering=...`.

## Pagination

Notices use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## Response Shaping

Notices support the `?shape=` query parameter. When no `?shape=` is provided, the endpoint returns a default shape.

**Default shape (list):** `active,address(*),attachment_count,award_number,description,last_updated,meta(*),naics_code,notice_id,office(*),opportunity(*),place_of_performance(*),posted_date,psc_code,response_deadline,sam_url,set_aside,solicitation_number,title`

**Default shape (detail):** adds `archive(*)`, `primary_contact(*)`, `secondary_contact(*)`

`set_aside` is available as a leaf (returns the description text) or as an expansion `set_aside(code,description)` for the structured form.

See [Response Shaping](../guides/patterns/response-shaping.md) for syntax and examples.

### Notices shaping notes

- **New leaf**: `opportunity_id` is available directly as a leaf (UUID).
- **Bare expand shorthand**: common expansions can be requested without parentheses when the runtime must expand them for safety (e.g., `office` behaves like `office(*)`, and `opportunity` behaves like `opportunity(*)`).

Examples:

```bash
# Leaf opportunity_id + bare office/opportunity expansions
/api/notices/?shape=notice_id,opportunity_id,office,opportunity&limit=1
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_notices(
        agency="DOD",
        notice_type="Presolicitation",
        limit=10,
        shape=ShapeConfig.NOTICES_MINIMAL,
    )

    for n in resp.results:
        print(n.notice_id, n.title)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listNotices({
      agency: "DOD",
      notice_type: "Presolicitation",
      limit: 10,
      shape: ShapeConfig.NOTICES_MINIMAL,
    });

    for (const n of resp.results) {
      console.log(n.notice_id, n.title);
    }
    ```

---

<!-- doc: api-reference/offices.md -->

# Offices

!!! warning "Deprecated endpoint"
    This endpoint is deprecated. Use [Organizations](organizations.md) for the unified federal hierarchy. This endpoint will be removed in a future version.

Offices are sub-agency organizations exposed at `/api/offices/`.

If you need the unified Federal Hierarchy model (departments/agencies/offices all as one tree), see [Organizations](organizations.md). For field definitions, see the [Organizations Data Dictionary](../data-dictionary/organizations.md).

## Endpoints

- `GET /api/offices/` (list)
- `GET /api/offices/{code}/` (detail)

## Filtering

| Param | What it does |
| --- | --- |
| `search` | Full-text search over offices (vector-backed). |

## Ordering

`/api/offices/` does **not** support `ordering=...`.

## Pagination

Offices use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `code`, `name`, plus flat aliases `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`
- Expansions:
  - `agency(code,name,abbreviation)`
  - `department(code,name,abbreviation,description,cgac,website,congressional_justification)`

Default shape (no `?shape=` param): `office_code,office_name,agency_code,agency_name,department_code,department_name` (flat aliases)

```bash
# Default flat format
/api/offices/?shape=office_code,office_name,agency_name

# Direct leaves with nested expansion
/api/offices/4700/?shape=code,name,agency(code,name,abbreviation)

# Office with department details
/api/offices/4700/?shape=code,name,department(name,description,website)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The SDKs don’t yet expose a first-class `list_offices()` / `listOffices()` method.
You can still call the endpoint via the SDK’s internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/offices/", params={"search": "OIG", "page": 1, "limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;
    const data = await http.get("/api/offices/", { search: "OIG", page: 1, limit: 25 });
    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/opportunities.md -->

# Opportunities

Opportunities are SAM.gov opportunities/solicitations exposed at `/api/opportunities/` (with a latest-notice pointer and history behind the scenes). For field definitions, see the [Opportunities Data Dictionary](../data-dictionary/opportunities.md).

## Endpoints

- `GET /api/opportunities/` (list + filtering + search + ordering)
- `GET /api/opportunities/{opportunity_id}/` (detail)

## Notice UUID Resolution

If you pass a **notice UUID** (instead of an opportunity UUID) to the detail endpoint, the API automatically redirects (HTTP 302) to the parent opportunity. This is useful when you have a subsequent notice UUID (e.g., from an amendment) and want to reach the parent opportunity directly.

```bash
# Using a notice UUID redirects to the parent opportunity
GET /api/opportunities/{notice_id}/  →  302 /api/opportunities/{opportunity_id}/
```

If the notice UUID has no parent opportunity, the endpoint returns 404.

## Filtering

Swagger is the canonical list of query parameters. These are the core filters most people use day-to-day. All date filters require `YYYY-MM-DD` format; invalid dates or inverted ranges return **400**. See [Date filters](shared.md#date-filters).

| Param | What it does |
| --- | --- |
| `search` | Full-text search over opportunities (vector-backed). |
| `agency` | Filter by agency or department (vector-backed). |
| `naics` | Filter by NAICS. |
| `psc` | Filter by PSC. |
| `set_aside` | Filter by set-aside. |
| `notice_type` | Filter by notice type (valid values are validated). |
| `solicitation_number` | Filter by solicitation number. |
| `first_notice_date_after`, `first_notice_date_before` | First notice date range (inclusive, `YYYY-MM-DD`). |
| `last_notice_date_after`, `last_notice_date_before` | Last notice date range (inclusive, `YYYY-MM-DD`). |
| `response_deadline_after`, `response_deadline_before` | Response deadline range (inclusive, `YYYY-MM-DD`). |
| `place_of_performance` | Full-text-ish filter over place-of-performance text. |
| `active` | Filter active/inactive (default true). |

### Multi-value filter syntax

Use `|` (or the literal word `OR`) to match any of several values:

```
naics=541511|541512
agency=DOD|GSA
```

The same syntax works on every filter that accepts multiple values.

## Ordering

Opportunities support `ordering=` with a strict allowlist:

- `first_notice_date` (alias: `posted_date`)
- `last_notice_date`
- `response_deadline`

Examples:

- Newest posted first: `GET /api/opportunities/?ordering=-posted_date`
- Soonest deadline first: `GET /api/opportunities/?ordering=response_deadline`

Note: if you use `search` **without** an explicit `ordering=...`, results default to relevance ordering (rank).

## Pagination

Opportunities use page-number pagination (`page`, `limit`) and return `next`/`previous` URLs.

## Response Shaping

Opportunities support the `?shape=` query parameter. When no `?shape=` is provided, the endpoint returns a default shape.

**Default shape (list):** `active,award_number,first_notice_date,last_notice_date,meta(*),naics_code,office(*),opportunity_id,place_of_performance(*),psc_code,response_deadline,sam_url,set_aside,solicitation_number,title`

**Default shape (detail):** adds `attachments(...)`, `description`, `notice_history(*)`, `primary_contact(*)`. The default `attachments(...)` expansion lists `attachment_id, file_size, mime_type, name, posted_date, resource_id, type, url` — note that the **`extracted_text` sub-field is Pro+ tier** and is *not* in the default. Free-tier callers who request it receive the response without it plus a `meta.upgrade_hints` entry.

The `office(*)` expansion is the canonical 7-key office payload — `organization_id, office_code, office_name, agency_code, agency_name, department_code, department_name` — same shape used on awards, vehicles, forecasts, grants, IT Dashboard, and protests.

`set_aside` is available as a leaf (returns the code string) or as an expansion `set_aside(code,description)` for the full label.

See [Response Shaping](../guides/patterns/response-shaping.md) for syntax and examples.

### Opportunities shaping notes

- **New leaves**: `latest_notice_id`, `archive_date`, plus relation id leaves `agency_id`, `department_id`, `office_id`.
- **New expansions**: `agency(*)`, `department(*)`, `latest_notice(notice_id,link)`.
- **Bare expand shorthand**: common expansions can be requested without parentheses when the runtime must expand them for safety (e.g., `office` behaves like `office(*)`, and `attachments` behaves like `attachments(*)`).

Examples:

```bash
# Include latest notice pointer + org expansions
/api/opportunities/?shape=opportunity_id,title,latest_notice_id,latest_notice(*),agency(*),department(*),office&limit=1

# Attachments without specifying subfields (shorthand)
/api/opportunities/{opportunity_id}/?shape=opportunity_id,title,attachments
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

### Search opportunities (with shaping)

=== "Python (tango-python)"

    ```python
    import os

    from tango import ShapeConfig, TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_opportunities(
        search="cybersecurity",
        agency="DOD",
        limit=10,
        shape=ShapeConfig.OPPORTUNITIES_MINIMAL,
    )

    for o in resp.results:
        print(o.opportunity_id, o.title)
    ```

=== "Node (tango-node)"

    ```ts
    import { ShapeConfig, TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listOpportunities({
      search: "cybersecurity",
      agency: "DOD",
      limit: 10,
      shape: ShapeConfig.OPPORTUNITIES_MINIMAL,
    });

    for (const o of resp.results) {
      console.log(o.opportunity_id, o.title);
    }
    ```

---

<!-- doc: api-reference/organizations.md -->

# Organizations

Organizations are the unified Federal Hierarchy model exposed at `/api/organizations/` (departments, agencies, offices, and other intermediate nodes).

If you’re looking for field definitions, see the [Organizations Data Dictionary](../data-dictionary/organizations.md).

## Endpoints

- `GET /api/organizations/` (list + filtering + search)
- `GET /api/organizations/{identifier}/` (detail — accepts either an integer Federal Hierarchy key or a UUID primary key)

## Filtering

| Param | What it does |
| --- | --- |
| `search` | Multi-stage org search (aliases → trigram → full-text → fuzzy). |
| `type` | Filter by organization type (lenient allowlist; invalid values are ignored + warnings may be returned). |
| `level` | Filter by hierarchy level (1 = department, 2 = agency, etc.). |
| `cgac` | Filter by CGAC code. |
| `parent` | Filter by parent organization (accepts organization key or common names/abbreviations). |
| `include_inactive` | Include inactive orgs in list results (default false). Detail lookups always return the org regardless of active status. |

## Response shaping

Organizations support `shape=...` (see [Response Shaping](../guides/patterns/response-shaping.md)).

**Default shape** (no `?shape=` param):

`key, fh_key, name, short_name, type, level, is_active, code, fpds_code, cgac, canonical_code, parent_fh_key, full_parent_path_name`

All responses go through the shaping pipeline, even without a `?shape=` parameter.

**Root fields** available via shaping:

| Field | Description |
| --- | --- |
| `key` | Tango's internal UUID primary key. |
| `fh_key` | Zero-padded 9-character string (e.g., `"000000001"`). Accepted for detail lookups alongside `key` (UUID). |
| `name` | Full official name. |
| `short_name` | Abbreviation or common name (e.g., `"DOD"`). |
| `type` | Organization type (`DEPARTMENT`, `AGENCY`, `OFFICE`, etc.). |
| `level` | Hierarchy depth (1 = department). |
| `is_active` | Whether the organization is currently active. |
| `code` | Office or agency code. |
| `fpds_code` | FPDS 4-digit agency code. |
| `cgac` | CGAC 3-character code. |
| `canonical_code` | Level-prefixed canonical code (e.g., `"L1:097"`). |
| `fpds_org_id` | FPDS organization identifier. |
| `aac_code` | Activity Address Code. |
| `parent_fh_key` | Parent's `fh_key` (zero-padded). |
| `full_parent_path_name` | Human-readable hierarchy path. |
| `l1_fh_key` .. `l8_fh_key` | Ancestor `fh_key` at each hierarchy level (zero-padded). |
| `start_date` | Organization start date from Federal Hierarchy. |
| `end_date` | Organization end date from Federal Hierarchy. |
| `logo` | Logo URL, when available. |
| `summary` | Short description or mission summary. |
| `mod_status` | Modification status (`ACTIVE`, `INACTIVE`). |
| `description` | Organization description. |
| `obligations` | Total rolled-up obligations (alias for `tree_obligations`). |
| `total_obligations` | Direct obligations for this organization. |
| `tree_obligations` | Obligations for the entire subtree. |
| `obligation_rank` | Percentile ranking (1–100) by obligations. |

**Expansions:**

| Expand | Fields | Notes |
| --- | --- | --- |
| `parent(...)` | `key`, `fh_key`, `name`, `short_name`, `type`, `level`, `is_active`, `code`, `cgac` | Immediate parent org. |
| `ancestors(...)` | `fh_key`, `name`, `short_name`, `level` | All ancestors from L1 down to immediate parent. |
| `children(...)` | `key`, `fh_key`, `name`, `short_name`, `type`, `level`, `is_active`, `code`, `cgac` | Immediate child organizations. |
| `department(...)` | `code`, `name`, `abbreviation` | L1 ancestor as a department. Returns `null` for level 1 orgs (they are the department). |
| `agency(...)` | `code`, `name`, `abbreviation` | L2 ancestor as an agency. Returns `null` for level 1 and 2 orgs (no agency above them). |

```bash
# Default response (no shape param)
/api/organizations/

# Select specific fields
/api/organizations/?shape=fh_key,name,type,level

# Include department and agency expands
/api/organizations/?shape=fh_key,name,department(code,name),agency(code,name,abbreviation)

# Include enriched ancestors
/api/organizations/?shape=fh_key,name,ancestors(fh_key,name,short_name,level)
```

!!! note "`department` and `agency` expand level behavior"
    The `department` expand returns `null` for level 1 organizations (a department doesn't have a department — it is one). The `agency` expand returns `null` for level 1 or 2 organizations (departments and agencies don't have an agency above them). This means list responses with mixed levels work without errors.

## Ordering

`/api/organizations/` does **not** support `ordering=...`.

(Results are returned in a stable way; searches preserve their own relevance ordering, and non-search lists are returned in a consistent key order.)

## Pagination

Organizations use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_organizations(
        search="Treasury",
        type="DEPARTMENT|AGENCY",
        include_inactive=False,
        limit=25,
    )
    print("count:", resp.count)
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listOrganizations({
      search: "Treasury",
      type: "DEPARTMENT|AGENCY",
      include_inactive: false,
      limit: 25,
    });

    console.log("count:", resp.count);
    ```

---

<!-- doc: api-reference/otas.md -->

# OTAs

OTAs (Other Transaction Awards) are awards exposed at `/api/otas/`. For field definitions, see the [OTAs Data Dictionary](../data-dictionary/otas.md).

## Endpoints

- `GET /api/otas/` (list + filtering + search + ordering)
- `GET /api/otas/{key}/` (detail)

Entity-scoped list:

- `GET /api/entities/{uei}/otas/` (behaves like `/api/otas/` scoped to an entity)

## Filtering

OTAs share most of the "award" filtering surface with contracts/IDVs, except they do not support some contracts-specific filters (like NAICS).

Common filters you can use:

| Param | What it does |
| --- | --- |
| `search` | Full-text-ish search over award search vectors. |
| `recipient` | Search by recipient/vendor name. |
| `uei` | Filter by recipient UEI (exact). |
| `piid` | Filter by PIID (case-insensitive). |
| `awarding_agency`, `funding_agency` | Filter by awarding/funding agency (best-effort matching). |
| `psc` | Filter by PSC. |
| `award_date`, `award_date_gte`, `award_date_lte` | Award date filters. |
| `fiscal_year`, `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year filters. |
| `pop_start_date_gte`, `pop_start_date_lte` | Period of performance start date range (when present). |
| `pop_end_date_gte`, `pop_end_date_lte` | Period of performance end date range (when present). |
| `expiring_gte`, `expiring_lte` | Expiration window range filters. |
| `obligated_gte`, `obligated_lte` | Obligated amount range (USD). |
| `ordering` | Sort results (allowlist: `award_date`, `obligated`, `total_contract_value`; prefix with `-` for descending). |

## Ordering

OTAs support `ordering=` with a strict allowlist:

- `award_date`
- `obligated`
- `total_contract_value`

Examples:

- Newest first: `GET /api/otas/?ordering=-award_date`
- Largest obligations first: `GET /api/otas/?ordering=-obligated`

## Pagination

High-volume award endpoints use **cursor-based (keyset) pagination**:

- `limit`
- `cursor` (follow `next` / `previous` URLs)

## Response Shaping

OTA endpoints support the `?shape=` parameter for customizing response fields. See [Response Shaping](../guides/patterns/response-shaping.md) for full syntax.

### Shapeable fields

| Field | Type | Notes |
| --- | --- | --- |
| `key` | scalar | Award key |
| `piid` | scalar | |
| `award_date` | scalar | |
| `award_type` | scalar or expand | Expandable to `{code, description}` (OTA-specific: "O" = Other Transaction Non-Research, "R" = Other Transaction for Research) |
| `fiscal_year` | scalar | |
| `obligated` | scalar | |
| `total_contract_value` | scalar | |
| `description` | scalar | |
| `base_and_exercised_options_value` | scalar | |
| `psc_code` | scalar | |
| `consortia` | scalar | |
| `consortia_uei` | scalar | |
| `dod_acquisition_program` | scalar | |
| `non_governmental_dollars` | scalar | |
| `non_traditional_government_contractor_participation` | scalar | |
| `parent_award_modification_number` | scalar | |
| `type_of_ot_agreement` | scalar or expand | Expandable to `{code, description}` |
| `extent_competed` | scalar or expand | Expandable to `{code, description}` |
| `transactions` | scalar | Raw snapshot list |

### Expandable fields

| Expand | Fields |
| --- | --- |
| `recipient(*)` | `uei`, `display_name`, `legal_business_name`, `cage`, `duns` |
| `place_of_performance(*)` | `country_code`, `country_name`, `state_code`, `state_name`, `city_name`, `zip_code` |
| `awarding_office(*)` | `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name` |
| `funding_office(*)` | same as `awarding_office` |
| `parent_award(key,piid)` | `key`, `piid` |
| `period_of_performance(*)` | `start_date`, `current_end_date`, `ultimate_completion_date` |
| `transactions(*)` | `modification_number`, `transaction_date`, `obligated`, `description`, `action_type` |
| `psc(*)` | `code`, `description` |
| `award_type(*)` | `code`, `description` (OTA-specific choices) |
| `type_of_ot_agreement(*)` | `code`, `description` |
| `extent_competed(*)` | `code`, `description` |

### Examples

Request only key fields:

```
GET /api/otas/?shape=key,piid,award_date,obligated
```

Expand recipient and extent competed with description:

```
GET /api/otas/?shape=key,piid,recipient(uei,display_name),extent_competed(code,description)
```

Expand type of OT agreement:

```
GET /api/otas/?shape=key,type_of_ot_agreement(code,description)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don't yet expose a first-class `list_otas()` / `listOTAs()` method.
You can still call the endpoint via the SDK's internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get(
        "/api/otas/",
        params={"search": "DARPA", "ordering": "-award_date", "limit": 10},
    )
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/otas/", {
      search: "DARPA",
      ordering: "-award_date",
      limit: 10,
    });

    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/otidvs.md -->

# OTIDVs

OTIDVs (Other Transaction IDVs) are awards exposed at `/api/otidvs/`. For field definitions, see the [OTIDVs Data Dictionary](../data-dictionary/otidvs.md).

## Endpoints

- `GET /api/otidvs/` (list + filtering + search + ordering)
- `GET /api/otidvs/{key}/` (detail)
- `GET /api/otidvs/{key}/awards/` (child OTAs under an OTIDV; behaves like `/api/otas/` scoped to the OTIDV)

Entity-scoped list:

- `GET /api/entities/{uei}/otidvs/` (behaves like `/api/otidvs/` scoped to an entity)

## Filtering

OTIDVs share most of the "award" filtering surface with contracts/IDVs, except they do not support some contracts-specific filters (like NAICS).

Common filters you can use:

| Param | What it does |
| --- | --- |
| `search` | Full-text-ish search over award search vectors. |
| `recipient` | Search by recipient/vendor name. |
| `uei` | Filter by recipient UEI (exact). |
| `piid` | Filter by PIID (case-insensitive). |
| `awarding_agency`, `funding_agency` | Filter by awarding/funding agency (best-effort matching). |
| `psc` | Filter by PSC. |
| `award_date`, `award_date_gte`, `award_date_lte` | Award date filters. |
| `fiscal_year`, `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year filters. |
| `pop_start_date_gte`, `pop_start_date_lte` | Period of performance start date range (when present). |
| `pop_end_date_gte`, `pop_end_date_lte` | Period of performance end date range (when present). |
| `expiring_gte`, `expiring_lte` | Expiration window range filters. |
| `obligated_gte`, `obligated_lte` | Obligated amount range (USD). |
| `ordering` | Sort results (allowlist: `award_date`, `obligated`, `total_contract_value`; prefix with `-` for descending). |

## Ordering

OTIDVs support `ordering=` with a strict allowlist:

- `award_date`
- `obligated`
- `total_contract_value`

Examples:

- Newest first: `GET /api/otidvs/?ordering=-award_date`
- Largest obligations first: `GET /api/otidvs/?ordering=-obligated`

## Pagination

High-volume award endpoints use **cursor-based (keyset) pagination**:

- `limit`
- `cursor` (follow `next` / `previous` URLs)

## Response Shaping

OTIDV endpoints support the `?shape=` parameter for customizing response fields. See [Response Shaping](../guides/patterns/response-shaping.md) for full syntax.

### Shapeable fields

| Field | Type | Notes |
| --- | --- | --- |
| `key` | scalar | Award key |
| `piid` | scalar | |
| `award_date` | scalar | |
| `idv_type` | scalar | Raw code |
| `fiscal_year` | scalar | |
| `obligated` | scalar | |
| `total_contract_value` | scalar | |
| `description` | scalar | |
| `base_and_exercised_options_value` | scalar | |
| `psc_code` | scalar | |
| `consortia` | scalar | |
| `consortia_uei` | scalar | |
| `dod_acquisition_program` | scalar | |
| `non_governmental_dollars` | scalar | |
| `non_traditional_government_contractor_participation` | scalar | |
| `type_of_ot_agreement` | scalar or expand | Expandable to `{code, description}` |
| `extent_competed` | scalar or expand | Expandable to `{code, description}` |
| `transactions` | scalar | Raw snapshot list |

### Expandable fields

| Expand | Fields |
| --- | --- |
| `recipient(*)` | `uei`, `display_name`, `legal_business_name`, `cage`, `duns` |
| `place_of_performance(*)` | `country_code`, `country_name`, `state_code`, `state_name`, `city_name`, `zip_code` |
| `awarding_office(*)` | `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name` |
| `funding_office(*)` | same as `awarding_office` |
| `period_of_performance(*)` | `start_date`, `current_end_date`, `ultimate_completion_date` |
| `transactions(*)` | `modification_number`, `transaction_date`, `obligated`, `description`, `action_type` |
| `psc(*)` | `code`, `description` |
| `type_of_ot_agreement(*)` | `code`, `description` |
| `extent_competed(*)` | `code`, `description` |

### Examples

Request only key fields:

```
GET /api/otidvs/?shape=key,piid,award_date,obligated
```

Expand recipient and extent competed with description:

```
GET /api/otidvs/?shape=key,piid,recipient(uei,display_name),extent_competed(code,description)
```

Expand type of OT agreement:

```
GET /api/otidvs/?shape=key,type_of_ot_agreement(code,description)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don't yet expose a first-class `list_otidvs()` / `listOTIDVs()` method.
You can still call the endpoint via the SDK's internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get(
        "/api/otidvs/",
        params={"search": "DARPA", "ordering": "-award_date", "limit": 10},
    )
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/otidvs/", {
      search: "DARPA",
      ordering: "-award_date",
      limit: 10,
    });

    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/protests.md -->

# Protests

Protests are bid protest records exposed at `/api/protests/`. For field definitions, see the [Protests Data Dictionary](../data-dictionary/protests.md).

**Authentication**: Protests endpoints require authentication (API key or OAuth2). Unauthenticated requests receive HTTP 401.

## Endpoints

- `GET /api/protests/` (list + filtering + search + pagination)
- `GET /api/protests/{case_id}/` (detail by deterministic case UUID)

Both list and detail return **case-level** objects identified by `case_id` (a deterministic UUID derived from `source_system` + `base_case_number`). Subordinate dockets are available via `?shape=...,dockets(...)` expansion. The path segment must be a valid RFC 4122 UUID (version 1–5, variant 8/9/a/b).

## Filtering

Core filters:

| Param | What it does |
| --- | --- |
| `source_system` | Filter by source system (for example `gao`). Multi-value: use `|` for OR. |
| `outcome` | Filter by protest outcome (for example `Denied`, `Dismissed`, `Withdrawn`, `Sustained`). Multi-value: use `|` for OR. |
| `case_type` | Filter by case type (for example `Bid Protest`, `Bid Protest: Cost`). Multi-value: use `|` for OR. |
| `agency` | Filter by protested agency text. Multi-value: use `|` for OR. |
| `case_number` | Filter by case number (B-number, for example `b-423274`). Multi-value: use `|` for OR. |
| `solicitation_number` | Filter by exact solicitation number. |
| `protester` | Filter by protester name text. Multi-value: use `|` for OR. |
| `filed_date_after`, `filed_date_before` | Filed date range filters. |
| `decision_date_after`, `decision_date_before` | Decision date range filters. |
| `search` | Full-text search over protest searchable fields. |

## Ordering

Protests do not support custom ordering via `ordering=`.

Results are returned in a fixed timeline order:

- `decision_date` when present, otherwise `filed_date` (newest first)
- UUID descending as a deterministic tie-breaker

If `ordering` is provided, the API returns HTTP 400.

## Pagination

Protests use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## List response: one result per case

The list endpoint returns **one result per case** (distinct `base_case_number`), not one per docket. Each result includes case-level fields from the most recent docket in the case. `case_id` is a deterministic UUID from `(source_system, base_case_number)` used for grouping and detail lookup. Pagination and filters apply to dockets first; results are then grouped, so `count` is the total number of matching **cases** across all pages.

The default (unshaped) response does **not** include nested dockets. To get dockets, use the `dockets` expansion via `?shape=...,dockets(...)`.

## Detail response: case-level

The detail endpoint (`GET /api/protests/{case_id}/`) returns a single **case-level** object, the same structure as a list item. Use `?shape=...,dockets(...)` to include nested dockets.

## Shaping

Protests support `shape` on both list and detail endpoints:

- `GET /api/protests/?shape=...`
- `GET /api/protests/{case_id}/?shape=...`

Both list and detail return case-level objects. You can request case-level fields and the `dockets` expansion (e.g. `shape=case_id,case_number,title,dockets(case_number,docket_number,filed_date)`).

Allowed shape fields (case-level for both list and detail):

- `case_id` (deterministic case UUID; use for detail lookup)
- `source_system`, `case_number` (base B-number), `title`, `protester`, `agency`
- `solicitation_number`, `case_type`, `outcome`
- `filed_date`, `posted_date`, `decision_date`, `due_date`
- `docket_url`, `decision_url`
- `digest` (opt-in only; from `raw_data.digest`, e.g. decision summary text)
- `dockets` (expand with docket fields, e.g. `dockets(case_number,docket_number,filed_date)`)
- `organization` (expand: deterministic resolution of the protested agency to a canonical Tango organization — included in the **default** response shape; canonical 7-key office payload)
- `resolved_protester` (expand: Bayesian entity resolution for the protester name; **Pro+ tier**)
- `resolved_agency` (expand: Bayesian organization resolution for the agency name; **Pro+ tier**)

Allowed shape fields inside `dockets(...)`:

- `source_system`, `case_number` (base B-number), `docket_number` (specific docket, e.g. `b-424046.1`)
- `title`, `protester`, `agency`, `solicitation_number`, `case_type`, `outcome`
- `filed_date`, `posted_date`, `decision_date`, `due_date`
- `docket_url`, `decision_url`
- `digest`

The `case_number` field (e.g. `b-424214`) identifies the case. The `docket_number` field (e.g. `b-424214.1`, `b-424214.2`) identifies the specific sub-docket within a case and is only available inside `dockets(...)`.

### Organization (deterministic): `organization(...)`

The default response shape includes an `organization(*)` expansion: the canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`) resolved deterministically at ingest time to a canonical Tango organization. This is the same payload used on awards, opportunities, vehicles, forecasts, grants, and IT Dashboard responses.

The deterministic `organization` expand and the Bayesian `resolved_agency` expand **coexist** intentionally:

- `organization` is the canonical "what does our matching pipeline think?" answer — fast, cached, and suitable for joining against the rest of your data.
- `resolved_agency` (Pro+) carries `match_confidence` (`"confident"` / `"review"`) and a human-readable `rationale`. Useful when you need to know *why* a particular org was chosen or to surface review-worthy matches to a human.

When the two disagree, prefer `organization` for canonical use and consult `resolved_agency.match_confidence` / `.rationale` to understand the discrepancy.

### Entity Resolution: `resolved_protester(...)`

When a protester name has been resolved to a canonical entity in Tango, you can expand the match via `resolved_protester(...)`. Only high-confidence (`confident`) and medium-confidence (`review`) matches are returned; unresolved or low-confidence names return `null`.

Allowed fields inside `resolved_protester(...)`:

- `uei` — Unique Entity Identifier of the matched entity
- `name` — Display name of the matched entity
- `match_confidence` — `"confident"` (auto-linkable) or `"review"` (needs human review)
- `rationale` — Human-readable explanation of why the match was made

### Organization Resolution: `resolved_agency(...)`

When an agency name has been resolved to a canonical organization in Tango, you can expand the match via `resolved_agency(...)`. Same confidence rules as `resolved_protester`.

Allowed fields inside `resolved_agency(...)`:

- `key` — UUID key of the matched organization
- `name` — Display name of the matched organization
- `match_confidence` — `"confident"` or `"review"`
- `rationale` — Human-readable explanation

Not exposed in the API (internal only):

- `raw_data`, `field_provenance`, `external_id`, `data_quality`, `source_last_updated`, `created`, `modified`
- internal search/index fields (for example `search_vector`)

Examples:

```bash
# Case-level fields and nested dockets
GET /api/protests/?shape=case_id,case_number,title,dockets(case_number,docket_number,filed_date,outcome)

# Case-level only
GET /api/protests/?shape=case_id,source_system,case_number,title,outcome,filed_date
```

More examples:

```bash
# Timeline-focused list payload
GET /api/protests/?source_system=gao&shape=case_id,title,outcome,filed_date,decision_date

# Link-focused payload
GET /api/protests/?shape=case_id,docket_url,decision_url

# Detail by case UUID with shaped response
GET /api/protests/550e8400-e29b-41d4-a716-446655440000/?shape=case_id,source_system,title,agency,protester,filed_date,decision_date

# Detail with nested dockets
GET /api/protests/550e8400-e29b-41d4-a716-446655440000/?shape=case_id,title,dockets(docket_number,filed_date,outcome)

# Entity resolution for protester
GET /api/protests/?shape=case_id,protester,resolved_protester(*)

# Organization resolution for agency
GET /api/protests/?shape=case_id,agency,resolved_agency(*)

# Selective resolution fields
GET /api/protests/?shape=case_id,resolved_protester(uei,match_confidence)
```

Notes:

- Use the `dockets` expansion to request nested docket fields (e.g. `dockets(docket_number,case_number)`).
- `*` is only valid inside expansions. Use explicit field lists at the root.
- Invalid `shape` fields return HTTP 400 with structured validation errors.

---

<!-- doc: api-reference/psc.md -->

# Product and Service Code (PSC)

Product and Service Code (PSC) reference data is exposed at `/api/psc/`. For field definitions, see the [PSC Data Dictionary](../data-dictionary/psc.md).

## External references

- [Product and Service Code Manual](https://www.acquisition.gov/psc-manual)

## Endpoints

- `GET /api/psc/` (list)
- `GET /api/psc/{code}/` (detail)

**Metrics:** Time-series obligation/award metrics for a PSC code: `GET /api/psc/{code}/metrics/<months>/<period_grouping>/`. See [Metrics](metrics.md) for parameters and behavior.

## Filtering

| Param | What it does |
| --- | --- |
| `has_awards` | If truthy, restrict to PSCs that appear in award data. |

Response options:

| Param | What it does |
| --- | --- |
| `complete` | If truthy, returns a “complete” representation (serializer context-dependent). |

## Ordering

`/api/psc/` does **not** support `ordering=...`.

## Pagination

PSC uses page-number pagination (static reference pagination defaults to larger pages):

- `page` (default 1)
- `limit` (default 1000, max 10000)

**Note:** Reference-data endpoints like `/api/psc/` allow larger page sizes than transactional award endpoints because the underlying datasets are relatively small and static, and larger pages reduce client round-trips.

## Response shaping

This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter.

- Leaves: `code`, `parent`, `category`, `level_1_category`, `level_1_category_code`, `level_2_category`, `level_2_category_code`
- Expansions:
  - `current(name,active,start_date,end_date,description,includes,excludes)` — active or latest description
  - `historical(name,active,start_date,end_date,description,includes,excludes)` — all descriptions

Default shape (no `?shape=` param): `code,current(*),parent,category,level_1_category,level_1_category_code,level_2_category,level_2_category_code`

```bash
# Just code and current description name
/api/psc/?shape=code,current(name)

# Code with current and historical descriptions
/api/psc/AD12/?shape=code,current(name,active),historical(name,active)
```

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don’t yet expose a first-class `list_psc()` / `listPsc()` method.
You can still call the endpoint via the SDK’s internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get("/api/psc/", params={"has_awards": "true", "limit": 25})
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/psc/", { has_awards: "true", limit: 25 });
    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/resolve.md -->

# Name Resolution

Resolve an entity or organization name to ranked candidates using Tango's Bayesian resolver. This endpoint is useful for data enrichment workflows, entity linking, and research.

**Endpoint**: `POST /api/resolve/`

## Overview

Given a name and target type, the endpoint returns a ranked list of entity or organization candidates. Results are ranked by quality and filtered based on your access tier:

- **Free tier**: Up to 3 candidates (identifier and display name only)
- **Pro tier and above**: Up to 5 candidates (includes match quality tier)

The resolver learns from location context (state, city), industry signals (NAICS, PSC codes), and any additional information you provide in the `context` field. More context typically leads to better matches.

**Authentication required:** API key or OAuth2 token (all callers must be authenticated; unauthenticated requests receive HTTP 401).

**Rate limiting**: Standard API rate limits apply (no additional premium throttling for this endpoint).

## Request

```http
POST /api/resolve/
Content-Type: application/json
X-API-KEY: <key>

{
  "name": "Lockheed Martin",
  "target_type": "entity",
  "state": "MD",
  "city": "Bethesda",
  "context": "fighter aircraft maintenance, NAICS 336411"
}
```

### Request body fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `name` | string | Yes | Name to resolve (must be non-blank). |
| `target_type` | string | Yes | Type of target: `"entity"` or `"organization"`. |
| `state` | string | No | 2-letter state code (e.g., `"MD"`, `"CA"`). |
| `city` | string | No | City name. |
| `context` | string | No | Freeform text with additional context for better matching. Can include industry references, contract descriptions, agency names, solicitation details, or any other relevant information. More context generally produces more accurate matches. The API does not document which specific signals are extracted from this field. |

**Constraints:**

- `name` cannot be empty or whitespace-only (400 error)
- `target_type` must be exactly `"entity"` or `"organization"` (400 error)
- `state` should be a valid 2-letter code, but is not validated
- `city` and `context` are free-form text with no constraints

## Response

### Success (HTTP 200)

```json
{
  "candidates": [
    {
      "identifier": "ABC123DEF456",
      "display_name": "Lockheed Martin Corporation"
    },
    {
      "identifier": "GHI789JKL012",
      "display_name": "Lockheed Martin Aeronautics"
    },
    {
      "identifier": "MNO345PQR678",
      "display_name": "LMC Holdings LLC"
    }
  ],
  "count": 3
}
```

**Free tier**: Returns up to 3 candidates with `identifier` and `display_name` fields only.

**Pro tier and above**: Returns up to 5 candidates with an additional `match_tier` field:

```json
{
  "candidates": [
    {
      "identifier": "ABC123DEF456",
      "display_name": "Lockheed Martin Corporation",
      "match_tier": "high"
    },
    {
      "identifier": "GHI789JKL012",
      "display_name": "Lockheed Martin Aeronautics",
      "match_tier": "medium"
    },
    {
      "identifier": "MNO345PQR678",
      "display_name": "LMC Holdings LLC",
      "match_tier": "low"
    },
    {
      "identifier": "STU901VWX234",
      "display_name": "Lockheed Martin Space",
      "match_tier": "low"
    },
    {
      "identifier": "YZA567BCD890",
      "display_name": "Lockheed Martin Rotary",
      "match_tier": "low"
    }
  ],
  "count": 5
}
```

The `match_tier` field indicates the quality of the match:

- `"high"` — Confident match, likely the intended target
- `"medium"` — Reasonable match, but consider alternatives
- `"low"` — Possible match, but verify before using

When fewer candidates are returned than the tier limit, `count` reflects the actual number returned.

### No matches (HTTP 200)

```json
{
  "candidates": [],
  "count": 0
}
```

### Validation errors (HTTP 400)

Missing or blank `name`:

```json
{
  "error": "name is required and must not be blank",
  "code": "validation_error"
}
```

Invalid `target_type`:

```json
{
  "error": "target_type must be 'entity' or 'organization'",
  "code": "validation_error"
}
```

### Authentication error (HTTP 401)

Standard 401 response for unauthenticated requests.

### Server error (HTTP 500)

If an unexpected error occurs during resolution (rare), the response is:

```json
{
  "error": "An unexpected error occurred during resolution",
  "code": "server_error"
}
```

## Examples

### Example 1: Entity resolution with full context

```bash
curl -X POST https://tango.makegov.com/api/resolve/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Boeing",
    "target_type": "entity",
    "state": "WA",
    "city": "Seattle",
    "context": "aerospace defense contractor, NAICS 336414, Department of Defense supplier"
  }'
```

**Response (Pro tier):**

```json
{
  "candidates": [
    {
      "identifier": "ABC123DEF456",
      "display_name": "The Boeing Company",
      "match_tier": "high"
    },
    {
      "identifier": "GHI789JKL012",
      "display_name": "Boeing Defense, Space and Security",
      "match_tier": "high"
    },
    {
      "identifier": "MNO345PQR678",
      "display_name": "Boeing Commercial Airplanes",
      "match_tier": "medium"
    }
  ],
  "count": 3
}
```

### Example 2: Minimal request (free tier)

```bash
curl -X POST https://tango.makegov.com/api/resolve/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corp",
    "target_type": "entity"
  }'
```

**Response (Free tier):**

```json
{
  "candidates": [
    {
      "identifier": "ABC123DEF456",
      "display_name": "Acme Corporation"
    }
  ],
  "count": 1
}
```

### Example 3: Organization resolution

```bash
curl -X POST https://tango.makegov.com/api/resolve/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Naval Sea Systems Command",
    "target_type": "organization",
    "context": "Department of the Navy"
  }'
```

**Response:**

```json
{
  "candidates": [
    {
      "identifier": "N6233A00",
      "display_name": "Naval Sea Systems Command",
      "match_tier": "high"
    }
  ],
  "count": 1
}
```

## Best practices

1. **Provide context**: The `context` field significantly improves match quality. Include any relevant details: industry (NAICS/PSC codes), agency, contract purpose, solicitation details, or other business context.

2. **Use the right target_type**: Use `"entity"` for vendors/contractors and `"organization"` for government agencies or organizational units.

3. **Location helps**: When available, provide `state` and/or `city` to disambiguate between entities with similar names in different regions.

4. **Check match_tier** (Pro tier): Use the `match_tier` to gauge confidence. High matches are generally safe to use; medium and low matches should be verified.

5. **Handle no matches**: Plan for empty result sets. When no candidates are returned, consider trying alternative names or reducing specificity in the `context` field.

## SDK support

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

Both SDKs expose a typed `resolve()` method. The Python version returns a `ResolveResult` dataclass with `candidates` (list of `ResolveCandidate`) and `count`; the Node version returns the same envelope as a typed object.

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    result = client.resolve(
        name="Lockheed Martin",
        target_type="entity",
        state="MD",
        context="aerospace defense",
    )
    print("Found", result.count, "candidates")
    for candidate in result.candidates:
        print(f"  - {candidate.display_name} ({candidate.identifier}) [{candidate.match_tier}]")

    # Older SDK versions without client.resolve():
    #   response = client._post("/api/resolve/", {"name": "...", "target_type": "entity", ...})
    #   for c in response["candidates"]:
    #       print(c["display_name"], c["identifier"])
    ```

=== "Node (tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const response = await client.resolve({
      name: "Lockheed Martin",
      target_type: "entity",
      state: "MD",
      context: "aerospace defense",
    });
    console.log(`Found ${response.count} candidates`);
    response.candidates.forEach((candidate) => {
      console.log(`  - ${candidate.display_name} (${candidate.identifier})`);
    });
    ```

## Tier-based access

All requests must be authenticated. Free tier and pro+ tier users receive different response shapes:

| Aspect | Free | Pro+ |
|--------|------|------|
| Max candidates | 3 | 5 |
| Fields | identifier, display_name | identifier, display_name, match_tier |
| Rate limit | Standard | Standard |

See [Plans & Data Access](../getting-started/pricing.md) for tier definitions.

---

<!-- doc: api-reference/set-aside.md -->

# Set-aside codes

This page documents the set-aside codes used across Tango’s awards and opportunities APIs (including shaped `set_aside(code,description)` expansions).

## Codes

| Code | Meaning |
| --- | --- |
| `NONE` | No set aside used |
| `SBA` | Small Business Set Aside - Total |
| `8A` | 8a Competed |
| `SBP` | Small Business Set Aside - Partial |
| `HMT` | HBCU or MI Set Aside - Total |
| `HMP` | HBCU or MI Set Aside - Partial |
| `VSB` | Very Small Business |
| `ESB` | Emerging Small Business Set-Aside |
| `HZC` | HUBZone Set Aside |
| `SDVOSBC` | Service-Disabled Veteran-Owned Small Business Set Aside |
| `BI` | Buy Indian |
| `IEE` | Indian Economic Enterprise |
| `ISBEE` | Indian Small Business Economic Enterprise |
| `HZS` | HUBZone Sole Source |
| `SDVOSBS` | SDVOSB Sole Source |
| `8AN` | 8(a) Sole Source |
| `RS` | Reserved for Small Business |
| `HS3` | 8(a) with HUBZone Preference |
| `VSA` | Veteran Set Aside |
| `VSS` | Veteran Sole Source |
| `WOSB` | Women-Owned Small Business |
| `EDWOSB` | Economically Disadvantaged Women-Owned Small Business |
| `WOSBSS` | Women-Owned Small Business Sole Source |
| `EDWOSBSS` | Economically Disadvantaged Women-Owned Small Business Sole Source |

## Source of truth

Tango maintains a canonical mapping internally and publishes it here for API consumers.

---

<!-- doc: api-reference/shared.md -->

# Shared response objects

This page defines shared objects returned across Tango API endpoints, especially when using **response shaping** (`?shape=`).

## Set-aside

Many endpoints expose a `set_aside` field. When expanded (e.g. `set_aside(code,description)`), it returns an object:

```json
{
  "code": "SBA",
  "description": "Small Business Set Aside - Total"
}
```

If the underlying value is missing, the API returns `null` for `set_aside`.

For the full list of codes and their meanings, see [Set-aside codes](set-aside.md).

## Date filters

All date range filters across the API require **YYYY-MM-DD** format (e.g. `2024-06-15`). The API validates date inputs and returns **HTTP 400** for:

- **Invalid format**: any value that is not a valid `YYYY-MM-DD` string (e.g. `2024-13-01`, `not-a-date`, `01/15/2024`).
- **Inverted range**: when the "after" / "gte" value exceeds the "before" / "lte" value (e.g. `award_date_gte=2025-01-01&award_date_lte=2024-01-01`).

The error response body includes a human-readable message identifying the invalid parameter and value.

Endpoints use two naming conventions for range pairs, but validation is identical:

| Convention | Example params |
| --- | --- |
| `_gte` / `_lte` | `pop_start_date_gte`, `pop_start_date_lte` |
| `_after` / `_before` | `posted_date_after`, `posted_date_before` |

Fiscal year filters (`fiscal_year`, `fiscal_year_gte`, `fiscal_year_lte`) use **YYYY** format and are validated separately.

## Office

Several endpoints expand an office into a common office object (for example: `office(...)`, `awarding_office(...)`, or `funding_office(...)` depending on the endpoint).

When expanded, office objects use this shape:

```json
{
  "office_code": "1234AB",
  "office_name": "Office Name",
  "agency_code": "12",
  "agency_name": "Agency Name",
  "department_code": "1",
  "department_name": "Department Name"
}
```

For **awarding offices**, `office_code` returns the 6-character Activity Address Code (AAC) that matches positions 1-6 of the contract PIID, with a fallback to the local office code. For **funding offices**, `office_code` returns the local office code.

---

<!-- doc: api-reference/subawards.md -->

# Subawards

Subawards (FSRS/USAspending subawards) are exposed at `/api/subawards/`. For field definitions, see the [Subawards Data Dictionary](../data-dictionary/subawards.md).

## Endpoints

- `GET /api/subawards/` (list + filtering + ordering)
- `GET /api/subawards/{key}/` (detail)

Scoped lists:

- `GET /api/contracts/{key}/subawards/` (subawards under a contract)
- `GET /api/entities/{uei}/subawards/` (subawards for an entity)

## Default response

All subaward responses use the shaping pipeline by default. No `?shape=` parameter is required to get a shaped response.

Default fields (equivalent to requesting):

```
award_key,awarding_office(*),fsrs_details(*),funding_office(*),key,piid,
place_of_performance(*),prime_recipient(uei,display_name),
subaward_details(*),subaward_recipient(uei,display_name)
```

## Shaping

Use `?shape=` to request a custom field set. Subawards support the following leaves and expansions.

### Leaves

Identity and prime award scalar fields:

| Field | Description |
| --- | --- |
| `key` | Subaward primary key |
| `award_key` | Prime award identifier |
| `piid` | Prime award PIID (mapped from `prime_award_piid`) |
| `prime_award_amount` | Total prime award amount |
| `prime_award_total_outlayed_amount` | Prime award total outlayed amount |
| `prime_award_base_action_date` | Prime award base action date |
| `prime_award_base_action_date_fiscal_year` | Fiscal year of base action date |
| `prime_award_latest_action_date` | Prime award latest action date |
| `prime_award_latest_action_date_fiscal_year` | Fiscal year of latest action date |
| `prime_award_base_transaction_description` | Prime award description |
| `prime_award_project_title` | Prime award project title |
| `prime_award_naics_code` | NAICS code |
| `prime_award_naics_description` | NAICS description |
| `prime_awardee_uei` | Prime awardee UEI |
| `prime_awardee_name` | Prime awardee name |
| `prime_awardee_parent_uei` | Prime awardee parent UEI |
| `prime_awardee_parent_name` | Prime awardee parent name |
| `subawardee_uei` | Subawardee UEI |
| `subawardee_duns` | Subawardee DUNS |
| `subawardee_name` | Subawardee name |
| `subawardee_dba_name` | Subawardee DBA name |
| `subawardee_parent_uei` | Subawardee parent UEI |
| `subawardee_parent_duns` | Subawardee parent DUNS |
| `subawardee_parent_name` | Subawardee parent name |
| `subawardee_business_types` | Subawardee business types |
| `usaspending_permalink` | USASpending.gov link |

### Expansions

| Expansion | Fields |
| --- | --- |
| `subaward_details(*)` | `description`, `type`, `number`, `amount`, `action_date`, `fiscal_year` |
| `fsrs_details(*)` | `last_modified_date`, `id`, `year`, `month` |
| `place_of_performance(*)` | `city`, `state`, `zip`, `country_code` |
| `highly_compensated_officers(*)` | `name`, `amount` — list of up to 5 officers |
| `prime_recipient(uei,display_name)` | Prime awardee identity, resolved against the Entities resource when available (falling back to the subaward's own recipient fields when no match exists) |
| `subaward_recipient(uei,display_name,duns)` | Subawardee identity, resolved against the Entities resource when available (falling back to the subaward's own recipient fields when no match exists) |
| `awarding_office(*)` | `organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name` |
| `funding_office(*)` | Same fields as `awarding_office` |

### Examples

```bash
# Default response (no shape needed)
/api/subawards/

# Minimal fields
/api/subawards/?shape=key,piid,award_key

# Subaward details with recipient info
/api/subawards/?shape=key,piid,subaward_details(*),subaward_recipient(uei,display_name)

# Office hierarchy for awarding office
/api/subawards/?shape=key,awarding_office(office_code,office_name,agency_name)

# All offices
/api/subawards/?shape=key,awarding_office(*),funding_office(*)

# Place of performance
/api/subawards/?shape=key,place_of_performance(city,state,country_code)

# FSRS report details
/api/subawards/?shape=key,fsrs_details(id,year,month,last_modified_date)

# Highly compensated officers
/api/subawards/?shape=key,highly_compensated_officers(name,amount)

# Discover available fields
/api/subawards/?show_shapes=true
```

## Filtering

Core filters:

| Param | What it does |
| --- | --- |
| `award_key` | Filter by prime award key (exact). |
| `prime_uei` | Filter by prime awardee UEI (exact). |
| `sub_uei` | Filter by subawardee UEI (exact). |
| `recipient` | Search by recipient name (best-effort mapping to UEIs). |
| `awarding_agency` | Filter by prime awarding agency code (exact). |
| `funding_agency` | Filter by prime funding agency code (exact). |
| `fiscal_year` | Filter by subaward action-date fiscal year (exact). |
| `fiscal_year_gte`, `fiscal_year_lte` | Fiscal year range. |

## Ordering

Subawards support `ordering=` with a strict allowlist:

- `last_modified_date`

Examples:

- Most recently modified first: `GET /api/subawards/?ordering=-last_modified_date`

## Pagination

Subawards use standard page-number pagination:

- `page` (default 1)
- `limit` (max 100)

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The official SDKs don't yet expose a first-class `list_subawards()` / `listSubawards()` method.
You can still call the endpoint via the SDK's internal HTTP helper.

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    data = client._get(
        "/api/subawards/",
        params={"recipient": "deloitte", "ordering": "-last_modified_date", "limit": 10},
    )
    print("count:", data.get("count"))
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const http = (client as any).http;

    const data = await http.get("/api/subawards/", {
      recipient: "deloitte",
      ordering: "-last_modified_date",
      limit: 10,
    });

    console.log("count:", data.count);
    ```

---

<!-- doc: api-reference/validate.md -->

# Identifier Validation

Validate the format of federal procurement identifiers. This endpoint is useful for intake forms, data cleaning pipelines, or integrations that need to check identifiers before submission.

**Endpoint**: `POST /api/validate/`

## Overview

Given an identifier type and value, the endpoint returns a `result` of `"valid"`, `"not_valid"`, or `"low_confidence"`. No database lookups are performed — this is pure format validation.

Supported identifier types:

| Type | Description | Rules |
| ---- | ----------- | ----- |
| `piid` | Procurement Instrument Identifier | FAR 4.201: 13–17 alphanumeric chars, valid AAC + fiscal year + instrument type + serial |
| `solicitation` | Solicitation number | 5–30 chars, contains both letters and digits, matches known procurement ID patterns |
| `uei` | Unique Entity Identifier | Exactly 12 alphanumeric chars, must not contain letters I or O |

### Result values

| Result | Meaning |
| ------ | ------- |
| `valid` | Matches a known format with high confidence |
| `not_valid` | Does not match any recognized format |
| `low_confidence` | Plausible identifier but doesn't match a named pattern (solicitations only) |

The `low_confidence` result only applies to solicitation numbers. Solicitation formats vary widely across agencies — some pass basic structural checks (alphanumeric, right length, mixed letters and digits) but don't match any of the ~25 known patterns derived from FPDS data. PIIDs and UEIs have strict formats, so they are always `valid` or `not_valid`.

**Access**: Requires Pro tier (Micro+). See [Plans & Data Access](../getting-started/pricing.md) for tier definitions.

**Rate limiting**: Standard API rate limits apply.

## Request

```http
POST /api/validate/
Content-Type: application/json
X-API-KEY: <key>

{
  "type": "piid",
  "value": "ABCDEF24C1234"
}
```

### Request body fields

| Field | Type | Required | Description |
| ----- | ---- | -------- | ----------- |
| `type` | string | Yes | Identifier type: `"piid"`, `"solicitation"`, or `"uei"`. |
| `value` | string | Yes | Identifier value to validate (must be non-blank). |

## Response

### Valid identifier (HTTP 200)

```json
{
  "result": "valid",
  "type": "piid",
  "value": "ABCDEF24C1234"
}
```

### Invalid identifier (HTTP 200)

For PIIDs, the response includes an `errors` array with specific validation failures:

```json
{
  "result": "not_valid",
  "type": "piid",
  "value": "ABC",
  "errors": [
    "PIID must be 13-17 characters, got 3"
  ]
}
```

For solicitation numbers and UEIs, the response has no `errors` array:

```json
{
  "result": "not_valid",
  "type": "uei",
  "value": "A1B2C3D4E5I6"
}
```

### Low confidence (HTTP 200, solicitations only)

```json
{
  "result": "low_confidence",
  "type": "solicitation",
  "value": "W911NF21C000100000000000000005"
}
```

### Validation errors (HTTP 400)

Missing or invalid `type`:

```json
{
  "error": "type must be one of: piid, solicitation, uei",
  "code": "validation_error"
}
```

Missing or blank `value`:

```json
{
  "error": "value is required and must not be blank",
  "code": "validation_error"
}
```

## Examples

### Validate a PIID

```bash
curl -X POST https://tango.makegov.com/api/validate/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"type": "piid", "value": "W58RGZ25F1234"}'
```

### Validate a solicitation number

```bash
curl -X POST https://tango.makegov.com/api/validate/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"type": "solicitation", "value": "SPE7A619T4321"}'
```

### Validate a UEI

```bash
curl -X POST https://tango.makegov.com/api/validate/ \
  -H "X-API-KEY: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"type": "uei", "value": "A1B2C3D4E5F6"}'
```

---

<!-- doc: api-reference/vehicles.md -->

# Vehicles

Vehicles are a solicitation-centric grouping of multiple IDVs (the "thing people usually mean" when they say "this vehicle"), exposed at `/api/vehicles/`.

If you want the conceptual model first, start with [Getting Started – Vehicles](../guides/patterns/vehicles.md). For field definitions, see the [Vehicles Data Dictionary](../data-dictionary/vehicles.md).

## Endpoints

- `GET /api/vehicles/` (list + filtering + search + ordering)
- `GET /api/vehicles/{uuid}/` (detail; supports shaping)
- `GET /api/vehicles/{uuid}/awardees/` (the underlying IDVs; supports `?search=` and shaping)
- `GET /api/vehicles/{uuid}/orders/` (task orders / contracts under all IDVs in the vehicle; behaves like `/api/contracts/`)

## Filtering

The list endpoint supports a rich filter set across enums, reference codes, org hierarchy, numeric ranges, and dates. Multi-value enum filters use pipe (`|`) for OR semantics, e.g. `?vehicle_type=A|B|C`.

| Param | What it does |
| --- | --- |
| `search` | Full-text vehicle search (solicitation identifier + aggregated award terms). |
| `vehicle_type` | Vehicle (IDV) type code (e.g. `A`, `B`). Multi-value via `\|`. Case-insensitive. |
| `type_of_idc` | Type of IDC code. Multi-value via `\|`. |
| `contract_type` | Contract type code (`J`, `M`, etc.). Multi-value via `\|`. |
| `set_aside` | Set-aside type (`8A`, `8AN`, `BICiv`, etc.). Multi-value via `\|`. |
| `who_can_use` | Who-can-use code. |
| `naics_code` | NAICS code (exact integer). |
| `psc_code` | Product / Service Code. |
| `program_acronym` | Program acronym (`SEWP`, `OASIS`, `GSA Schedule`, etc.). |
| `agency` | Awarding agency or department. Examples: `GSA`, `DOD`, `DHS`. Multi-value via `\|`. |
| `organization_id` | Awarding organization UUID (exact match). |
| `total_obligated_min` / `total_obligated_max` | Total obligated USD lower / upper bound. |
| `idv_count_min` / `idv_count_max` | Number of child IDVs lower / upper bound. |
| `order_count_min` / `order_count_max` | Number of task orders lower / upper bound. |
| `fiscal_year` | Fiscal year (YYYY). |
| `award_date_after` / `award_date_before` | Award-date range (YYYY-MM-DD). Invalid dates or inverted ranges return **400**. |
| `last_date_to_order_after` / `last_date_to_order_before` | Last-date-to-order range (YYYY-MM-DD). |

> Range filters are not exposed for `vehicle_obligations` or `awardee_count`. Use the corresponding `total_obligated_min/max` and `order_count_min/max` parameters instead — they cover the same use cases against the precomputed rollup fields.

### Vehicle awardee filtering and search

`GET /api/vehicles/{uuid}/awardees/` supports `?search=` for entity-aware full-text search across the underlying IDVs (PIID, key, solicitation_identifier, NAICS, PSC, recipient name / address). Pagination is page-based.

On the **detail** endpoint (`GET /api/vehicles/{uuid}/`), `?search=` does *not* search vehicles — it filters the expanded awardees when your `?shape=` includes `awardees(...)`.

Example:

```bash
GET /api/vehicles/{uuid}/?shape=uuid,solicitation_identifier,awardees(key,piid,recipient(display_name,uei))&search=deloitte
```

## Ordering

Vehicles support `?ordering=` with a strict allowlist (8 fields). Prefix with `-` for descending; combine with commas for multi-field sort.

- `vehicle_obligations` — sum of obligations across child orders (computed at query time)
- `latest_award_date` — most recent IDV award date in the vehicle (computed at query time)
- `total_obligated` — direct column rollup
- `award_date` — earliest IDV award date
- `last_date_to_order` — latest "last date to order" across IDVs
- `fiscal_year` — vehicle fiscal year
- `idv_count` — precomputed count of IDVs in the vehicle
- `order_count` — precomputed count of orders against the vehicle's IDVs

Examples:

- Most obligations first: `?ordering=-vehicle_obligations`
- Soonest stop-orders first: `?ordering=last_date_to_order`
- Combined: `?ordering=-total_obligated,-award_date`

When no `?ordering=` is provided, results are returned in `solicitation_identifier, agency_id, uuid` order.

## Pagination

Vehicle lists use **page-number** pagination:

- `page` (default 1)
- `limit` (max 100)

Vehicle awardees and vehicle orders also use page-number pagination (`page`, `limit`). Vehicle orders is optimized internally for date-ordered traversal of very large vehicles, but the response contract is the same as every other paginated endpoint: page-based with `next` / `previous` URLs and a `count`.

## Response shaping

Vehicles use the shaping pipeline by default. Even without `?shape=` you'll get a curated default response.

**Default LIST shape**: `agency_details, award_date, awardee_count, contract_type, description, fiscal_year, idv_count, last_date_to_order, latest_award_date, naics_code, order_count, organization, program_acronym, psc_code, set_aside, solicitation_date, solicitation_identifier, solicitation_title, total_obligated, type_of_idc, uuid, vehicle_contracts_value, vehicle_obligations, vehicle_type, who_can_use`

**Default DETAIL shape** is a superset: adds `agency_details(*)` (expanded), `agency_id`, `competition_details(*)`, `metrics(*)`, `opportunity_id`, `solicitation_description`.

### Available expansions

| Expansion | Description |
| --- | --- |
| `awardees(...)` | The underlying IDVs that make up the vehicle. Supports `?search=` filtering on detail. |
| `opportunity(...)` | Linked SAM.gov Opportunity (full Opportunity shape — `office`, `attachments`, `meta`, etc.). |
| `competition_details(*)` | Aggregated competition information (12 sub-fields including `extent_competed`, `set_aside`, `solicitation_procedures`, `number_of_offers_received`). |
| `agency_details(*)` | Aggregated awarding/funding office. Returned on detail (`GET /api/vehicles/{uuid}/`); list responses return a null stub for this field. |
| `metrics(*)` | 12 computed vehicle metrics (HHI, competed-rate, top-recipient share, etc.). |
| `organization(*)` | Canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). Both bare `?shape=organization` (leaf) and `?shape=organization(*)` (expand) work — both converge on the same cached lookup. |

Vehicles also support `flat=true|false`, `flat_lists=true|false`, and `joiner=.` (relevant only when `flat=true`). See [Response Shaping](../guides/patterns/response-shaping.md).

### Synthetic GWAC vehicles

Some GWAC vehicles lack a real solicitation number. Tango synthesizes a vehicle for them so the grouping still works; consumers can identify them via:

- `is_synthetic_solicitation` (boolean) — true on synthetic rows
- `program_acronym` — the GWAC's identifier (e.g. `SEWP`)
- `solicitation_identifier` — the user-facing value, with the internal `ACRO:` storage prefix stripped

## SDK examples

> **See also:** Full SDK method reference — [tango-python methods](../sdks/python/api-reference.md) · [tango-node methods](../sdks/node/api-reference.md).

The Python SDK exposes first-class methods for all three vehicle endpoints.

### List vehicles (search + ordering + filters)

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    resp = client.list_vehicles(
        search="SEWP",
        program_acronym="SEWP",
        ordering="-vehicle_obligations",
        page=1,
        limit=10,
    )

    print("count:", resp.count)
    for v in resp.results:
        print(v.uuid, v.solicitation_identifier, v.vehicle_obligations)
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    // Workaround until the Node SDK ships a first-class listVehicles()
    const http = (client as any).http;
    const data = await http.get("/api/vehicles/", {
      search: "SEWP",
      program_acronym: "SEWP",
      ordering: "-vehicle_obligations",
      page: 1,
      limit: 10,
    });

    console.log("count:", data.count);
    ```

### Vehicle detail with metrics and organization

=== "Python (tango-python)"

    ```python
    import os

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    vehicle_uuid = "00000000-0000-0000-0000-000000000000"  # replace
    v = client.get_vehicle(
        vehicle_uuid,
        shape="uuid,solicitation_identifier,vehicle_obligations,metrics(*),organization(*)",
    )
    print(v["uuid"], v["organization"]["agency_name"])
    ```

### Vehicle awardees with search

=== "Python (tango-python)"

    ```python
    resp = client.list_vehicle_awardees(
        "00000000-0000-0000-0000-000000000000",   # vehicle UUID is positional (kwarg name: uuid)
        search="deloitte",
        limit=25,
    )
    for awd in resp.results:
        print(awd.piid, awd.recipient.display_name)
    ```

### Vehicle orders (task orders)

=== "Python (tango-python)"

    ```python
    resp = client.list_vehicle_orders(
        "00000000-0000-0000-0000-000000000000",   # vehicle UUID is positional (kwarg name: uuid)
        ordering="-award_date",
        limit=50,
    )
    print("orders:", resp.count)
    ```

---

<!-- doc: api-reference/version.md -->

# Version

Inspect the deployed Tango API version and build date. Useful for logging, support tickets, and client-side feature detection.

## Endpoint

`GET /api/version/`

No authentication required.

## Response

| Field | Type | Description |
| --- | --- | --- |
| `version` | string | Semantic version string (e.g. `4.4.0`). Bumped on each release per [Keep a Changelog](https://keepachangelog.com/) conventions; see the [public Changelog](../CHANGELOG.md). |
| `date` | string | RFC 2822 build / release date (e.g. `"Mon, 08 May 2026 12:17:42 GMT"`). |

```json
{
  "version": "4.4.0",
  "date": "Mon, 08 May 2026 12:17:42 GMT"
}
```

## Example

```bash
curl -sS https://tango.makegov.com/api/version/
```

## When to use

- **Bug reports**: include the live `version` value alongside the request that failed; it speeds up triage.
- **Client compatibility**: an SDK can refuse to start (or warn) when the API is older than its minimum supported version.
- **Cache invalidation**: a version bump is a good signal to invalidate any client-side cached schemas (e.g. response-shape help retrieved via `?show_shapes=true`).

---

<!-- doc: data-dictionary/agencies.md -->

# Agencies Data Dictionary

This document describes the fields available on the agency data structure as it appears in response shaping expands across multiple endpoints (e.g., contracts, opportunities). For filtering, ordering, and pagination on the standalone endpoint, see the [Agencies API Reference](../api-reference/agencies.md).

For the unified federal hierarchy, see [Organizations](organizations.md).

## Overview

Agencies are federal organizations that sit below departments in the federal hierarchy. When expanded via response shaping (e.g., `agency(code,name,abbreviation)`), agency objects use the fields below.

## Update Frequency

Agency data is updated **monthly** via the organization sync process from the Federal Hierarchy (SAM.gov).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | String | FPDS 4-digit agency code (primary key for the legacy endpoint). | FPDS / USAspending |
| `name` | String | Full agency name. | Federal Hierarchy (SAM.gov) |
| `abbreviation` | String | Common abbreviation (e.g., "NASA"). | Federal Hierarchy (SAM.gov) |

## Expansions

| Expansion | Fields | Description |
| --------- | ------ | ----------- |
| `department(...)` | `code`, `name`, `abbreviation`, `description`, `cgac`, `website`, `congressional_justification` | Parent department. See [Departments data dictionary](departments.md). |

## Usage Notes

- **Use `code` for lookups.** The agency `code` is the FPDS 4-digit identifier used in contract and financial assistance transactions.
- **Prefer Organizations for new integrations.** The agency data structure is a legacy shape. Use the [Organizations API](../api-reference/organizations.md) for the full hierarchy with richer search and obligation data.

## Data Sources

- **Federal Hierarchy (SAM.gov)** — Authoritative source for agency names and hierarchy position.
- **FPDS / USAspending** — Agency codes (FPDS 4-digit) as they appear in contract and financial transactions.

---

<!-- doc: data-dictionary/assistance-listings.md -->

# Assistance Listings (CFDA) Data Dictionary

This document describes the fields available in the Assistance Listings API endpoints (`/api/assistance_listings/`). For filtering, ordering, and pagination, see the [Assistance Listings API Reference](../api-reference/assistance-listings.md).

## Overview

Assistance listings are CFDA (Catalog of Federal Domestic Assistance) program codes used for federal grants and assistance. Each listing has a `number` (e.g., "84.027") and `title`. This is **reference data**, not the assistance transactions endpoint. Data is sourced from USAspending reference files.

**Note**: This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter. On list requests, the default shape is `number,title`. Detail responses use the full serializer when no `?shape=` is provided.

## Update Frequency

Assistance listing reference data is updated when USAspending CFDA reference data is refreshed (periodic).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `number` | String | CFDA program number (primary key; digits and dots, e.g., "84.027"). | USAspending |
| `title` | String | Program title. | USAspending |
| `popular_name` | String | Popular or common name, when available. | USAspending |
| `objectives` | Text | Program objectives. | USAspending |
| `applicant_eligibility` | Text | Applicant eligibility description. | USAspending |
| `benefit_eligibility` | Text | Benefit eligibility description. | USAspending |
| `published_date` | Date | Date published. | USAspending |
| `archived_date` | Date | Date archived, when applicable. | USAspending |

## Data Sources

- **USAspending** – CFDA reference data (e.g., cfda.csv).

## Usage Notes

- **Use `number` for lookups.** Detail at `/api/assistance_listings/{number}/` (number supports digits and `.`). This endpoint is for the **reference list** only; for assistance **transactions** use the assistance/awards endpoints; see [Assistance Listings API Reference](../api-reference/assistance-listings.md).

---

<!-- doc: data-dictionary/business-types.md -->

# Business Types Data Dictionary

This document describes the fields available in the Business Types API endpoints (`/api/business_types/`). For filtering, ordering, and pagination, see the [Business Types API Reference](../api-reference/business-types.md).

## Overview

Business types are SAM.gov codes that classify entities (e.g., small business, 8(a), HUBZone). Each type has a `code` and `name`. Data is sourced from SAM.gov.

**Note**: This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter. Default shape: `name,code`.

## Update Frequency

Business type reference data is updated when SAM.gov reference data is refreshed (periodic).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | String | SAM code for the business type (e.g., 2-character). | SAM.gov |
| `name` | String | Human-readable business type name. | SAM.gov |
| `certifier` | String | Certifier for the business type (e.g., SBA). | SAM.gov |

## Value formats and common values

- **`code`**: 2-character alphanumeric, typically uppercase (examples: `A6`, `JT`, `XX`, `2R`). This is what entities store in `business_types`.
- **`certifier`**: Who “owns” / attests the classification. In practice you’ll see a small set of values:
  - `SBA`: SBA-certified programs (e.g., 8(a), HUBZone, WOSB variants).
  - `AbilityOne`: AbilityOne nonprofit agency classification.
  - `Government`: Government-entity classifications (federal/state/local/tribal/foreign).
  - `Self`: Self-reported classifications in SAM (ownership/demographic/org-type, etc.).

If you need the full canonical list in one place, the `/api/business_types/` endpoint is the source of truth.

## Data Sources

- **SAM.gov** – Business type codes and names.

## Usage Notes

- **Use `code` for lookups.** List and detail at `/api/business_types/` and `/api/business_types/{code}/`; see [Business Types API Reference](../api-reference/business-types.md).

---

<!-- doc: data-dictionary/contracts.md -->

# Contracts Data Dictionary

This document describes the fields available in the Contracts API endpoints (`/api/contracts/`). For filtering, ordering, and pagination, see the [Contracts API Reference](../api-reference/contracts.md).

## Overview

Contracts are definitive contract awards from FPDS (Federal Procurement Data System). Contracts are sourced from **both FPDS and USAspending**, and Tango merges those inputs during award materialization.

## Identifiers

- `key` (canonical): The canonical award identifier derived from FPDS/USAspending award keys. This is what the API uses for lookups: `GET /api/contracts/{key}/`.
- `uuid` (internal): Tango’s deterministic internal UUID used as the database primary key and for stable internal joins/orderings. This is not intended as a public lookup key.

**Note**: The full set of fields is available via the `shape` query parameter. The base response includes a subset of commonly-used fields. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Contract data is refreshed by the Tango ingest pipeline (FPDS updates **twice daily**; USAspending inputs update **daily**).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | String | Canonical award identifier (stable across FPDS and USAspending). Used for detail lookups and cross-referencing. | FPDS / USAspending |
| `piid` | String | Procurement Instrument Identifier. The official contract number. | FPDS / USAspending |
| `award_date` | Date | Date the contract was awarded. | FPDS / USAspending |
| `award_type` | Object | Type of award (e.g., contract type code and description). Use the `award_type` expansion for `code`, `description`. | FPDS / USAspending |
| `naics_code` | Integer | NAICS code on the award (when stored as a single code). | FPDS / USAspending |
| `psc_code` | String | Product/Service Code on the award (when stored as a single code). | FPDS / USAspending |
| `total_contract_value` | Decimal | Total value of the contract (base + options, when reported). Expressed in US dollars. | FPDS / USAspending |
| `base_and_exercised_options_value` | Decimal | Base contract value plus exercised options. Expressed in US dollars. | FPDS / USAspending |
| `fiscal_year` | Integer | Federal fiscal year of the award (Oct–Sep). | FPDS / USAspending |
| `obligated` | Decimal | Total obligated amount (dollars) on the contract. | FPDS / USAspending |
| `description` | String | Contract description or title, when available. | FPDS / USAspending |
| `set_aside` | Object | Set-aside type (e.g., small business, 8(a)). Use the `set_aside` expansion for `code`, `description`. | FPDS / USAspending |
| `transactions` | Array | List of transaction/modification records. Use the `transactions` expansion; each item can include `modification_number`, `transaction_date`, `obligated`, `description`, `action_type`. | FPDS / USAspending |
| `subawards_summary` | Object | Summary of subawards under this contract. Use the `subawards_summary` expansion for `count`, `total_amount`. | USAspending / Tango |

## Expansions (via `shape`)

### `recipient`

Returns the awardee (entity). Fields: `uei`, `display_name`, `legal_business_name`, `cage`, `duns`.

### `place_of_performance`

Returns the place of performance. Fields: `country_code`, `country_name`, `state_code`, `state_name`, `city_name`, `zip_code`.

### `awarding_office` / `funding_office`

Returns the awarding or funding office. Fields: `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`.

### `parent_award`

Returns the parent award (e.g., IDV) when this contract is a child order. Fields: `key`, `piid`.

The default (non-shaped) response includes `parent_award` as a minimal `{key, piid}` reference derived from the award key without a database query. Use `?shape=` with `parent_award(key,piid)` to get the same fields via the shaping pipeline, or expand to the full parent award object.

### `period_of_performance`

Returns period of performance. Fields: `start_date`, `current_end_date`, `ultimate_completion_date`.

### `award_type` / `set_aside`

Returns code and description for award type or set-aside type.

### `transactions`

Returns the list of modifications/transactions. Each item: `modification_number`, `transaction_date`, `obligated`, `description`, `action_type`.

### `competition`

Returns competition-related fields: `contract_type`, `extent_competed`, `number_of_offers_received`, `other_than_full_and_open_competition`, `solicitation_date`, `solicitation_identifier`, `solicitation_procedures`.

### `legislative_mandates`

Returns legislative mandate flags (e.g., Clinger-Cohen, labor standards, employment eligibility).

### `officers`

Returns contracting/officer information (when available).

### `naics` / `psc`

Returns NAICS or PSC as `{code, description}`.

### `subawards_summary`

Returns `count` and `total_amount` for subawards under this contract.

## Common code values

These fields use standardized code systems sourced from FPDS / USAspending and surfaced as `{code, description}` objects in responses.

### `award_type`

| Code | Meaning |
| --- | --- |
| `A` | BPA Call |
| `B` | Purchase Order |
| `C` | Delivery Order |
| `D` | Definitive Contract |

### `competition.contract_type`

| Code | Meaning |
| --- | --- |
| `J` | Firm Fixed Price |
| `U` | Cost Plus Fixed Fee |
| `R` | Cost Plus Award Fee |
| `Y` | Time and Materials |
| `Z` | Labor Hours |
| `1` | Order Dependent |
| `2` | Combination |
| `3` | Other |

(Many additional contract-type codes exist; see `CONTRACT_TYPE_CHOICES`.)

### `competition.extent_competed`

| Code | Meaning |
| --- | --- |
| `A` | Full and Open Competition |
| `C` | Not Competed |
| `D` | Full and Open after exclusion of sources |
| `F` | Competed under SAP |
| `G` | Not Competed under SAP |
| `CDO` | Competitive Delivery Order |
| `NDO` | Non-Competitive Delivery Order |

(See `EXTENT_COMPETED_CHOICES` for the complete mapping.)

### `set_aside`

| Code | Meaning |
| --- | --- |
| `NONE` | No set aside used |
| `SBA` | Small Business Set Aside - Total |
| `8A` | 8(a) Competed |
| `HZC` | HUBZone Set Aside |
| `SDVOSBC` | Service-Disabled Veteran-Owned Small Business Set Aside |
| `WOSB` | Women-Owned Small Business |
| `EDWOSB` | Economically Disadvantaged Women-Owned Small Business |

(See [Set-aside codes](../api-reference/set-aside.md) for the complete mapping.)

## Data Sources

- **FPDS** – Contract actions, PIID, dates, values, NAICS, PSC, set-aside, competition, offices, recipient reference.
- **USAspending** – Supplemental award/transaction inputs used in award materialization and subaward linkage.
- **Tango** – Internal deterministic `uuid` and merged award materialization.

## Usage Notes

- **Use `key` for API references** – `key` is the canonical award identifier used by the Contracts API.
- **Use `piid` for human/FPDS lookups** – PIID is the contract number users see in FPDS and solicitations.
- **Scoped endpoints** – Use `/api/entities/{uei}/contracts/`, `/api/idvs/{key}/awards/`, or `/api/vehicles/{uuid}/orders/` for contract lists scoped to an entity, IDV, or vehicle; see [Contracts API Reference](../api-reference/contracts.md).

---

<!-- doc: data-dictionary/departments.md -->

# Departments Data Dictionary

This document describes the fields available on the department data structure as it appears in response shaping expands across multiple endpoints (e.g., contracts, opportunities). For filtering, ordering, and pagination on the standalone endpoint, see the [Departments API Reference](../api-reference/departments.md).

For the unified federal hierarchy, see [Organizations](organizations.md).

## Overview

Departments are top-level federal organizations. When expanded via response shaping (e.g., `department(code,name,abbreviation)`), department objects use the fields below.

## Update Frequency

Department data is updated **monthly** via the organization sync process from the Federal Hierarchy (SAM.gov).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | String | CGAC 3-character department code (primary key for the legacy endpoint). | USAspending |
| `name` | String | Full department name. | Federal Hierarchy (SAM.gov) |
| `abbreviation` | String | Common abbreviation (e.g., "DOD"). | Federal Hierarchy (SAM.gov) |
| `description` | String | Department description/mission. | Federal Hierarchy (SAM.gov) |
| `cgac` | String | CGAC code (same as `code` for departments). | USAspending |
| `website` | String | Department website URL. | Various |
| `congressional_justification` | String | URL to congressional budget justification. | Various |

## Expansions

None.

## Usage Notes

- **Use `code` for lookups.** The department `code` is the CGAC 3-character identifier used in financial transactions.
- **Prefer Organizations for new integrations.** The department data structure is a legacy shape. Use the [Organizations API](../api-reference/organizations.md) for the full hierarchy with richer search and obligation data.

## Data Sources

- **Federal Hierarchy (SAM.gov)** — Authoritative source for department names, codes, and hierarchy structure.
- **USAspending** — Supplements with CGAC codes used in financial transactions.

---

<!-- doc: data-dictionary/entities.md -->

# Entities Data Dictionary

This document describes the fields available in the Entities API endpoints (`/api/entities/`). For filtering, ordering, and pagination, see the [Entities API Reference](../api-reference/entities.md).

## Overview

Entities represent vendors and recipients in federal contracting and assistance. The canonical identifier is **UEI** (Unique Entity ID). Data is sourced from SAM.gov, with supplemental data from DSBS and Tango-calculated obligation metrics.

## Identifiers

- `uei` (canonical): Public identifier for entities and the API lookup key: `GET /api/entities/{uei}/`.
- `uuid` (internal): Tango's internal UUID primary key for the entity record. This is not intended as a public lookup key; other tables typically reference entities by `uei`.

**Note**: The full set of fields is available via the `shape` query parameter. The base response includes a subset of commonly-used fields. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Entity data is updated from SAM.gov on a regular sync cadence (typically within 20–60 minutes of SAM changes). Federal obligation rollups are derived from award data.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `uei` | String | Unique Entity Identifier. Canonical identifier for the entity; use for API lookups and cross-referencing. | SAM.gov |
| `uuid` | UUID | Internal primary key. Not intended as a public lookup key. | Tango |
| `display_name` | String | Display name for the entity (e.g., legal name or DBA when useful). | Tango (derived) |
| `legal_business_name` | String | Official legal business name as registered in SAM. | SAM.gov |
| `dba_name` | String | "Doing business as" name, when different from legal name. | SAM.gov |
| `description` | String | Entity description, when available (e.g., from DSBS). | DSBS |
| `cage_code` | String | Commercial and Government Entity (CAGE) code. 5-character code used in defense and federal contracting. | SAM.gov |
| `dodaac` | String | Department of Defense Activity Address Code. | SAM.gov |
| `registered` | String | Whether the entity is registered in SAM (e.g., "Y"/"N"). | SAM.gov |
| `registration_status` | String | Current SAM registration status (e.g., "Active"). | SAM.gov |
| `purpose_of_registration_code` | String | Code indicating the primary purpose of SAM registration. | SAM.gov |
| `purpose_of_registration_desc` | String | Description of the purpose of registration. | SAM.gov |
| `sam_registration_date` | Date | Date the entity was registered in SAM. | SAM.gov |
| `sam_expiration_date` | Date | Date the entity's SAM registration expires. | SAM.gov |
| `sam_activation_date` | Date | Date the entity's SAM registration was activated. | SAM.gov |
| `last_update_date` | Date | Date the entity record was last updated. | SAM.gov |
| `email_address` | String | Primary email address on file. | SAM.gov |
| `entity_url` | String | Entity website or profile URL, when available. | SAM.gov / DSBS |
| `physical_address` | Object | Physical address. Expand with `physical_address(*)` for normalized subfields: `address_line1`, `address_line2`, `city`, `state_or_province_code`, `zip_code`, `zip_code_plus4`, `country_code`, `country_name`, `county`, `county_code`, `fips_code`. Keys are always snake_case regardless of data source. | SAM.gov / DSBS |
| `mailing_address` | Object | Mailing address. Same normalized subfields as `physical_address`. | SAM.gov |
| `congressional_district` | String | Congressional district (e.g., for physical location). | SAM.gov |
| `primary_naics` | String | Primary NAICS code (6-digit). | SAM.gov |
| `naics_codes` | Array of Objects | NAICS codes. Expand with `naics_codes(*)` → `[{code, sba_small_business}]`. Plain string arrays from legacy data are auto-normalized. | SAM.gov |
| `psc_codes` | Array of Strings | Product/Service Codes associated with the entity. | SAM.gov |
| `business_types` | Array of Objects | Business type classifications. Expand with `business_types(*)` → `[{code, description}]`. Dict-format data from DSBS is auto-normalized. | SAM.gov / DSBS |
| `sba_business_types` | Array of Objects | SBA certifications. Expand with `sba_business_types(*)` → `[{code, description, entry_date, exit_date}]`. | DSBS / SAM.gov |
| `keywords` | String | Keywords or capability text, when available. | DSBS |
| `capabilities` | String | Capability narrative, when available. | DSBS |
| `current_principals` | String | Current principals/officers on file, when available (e.g., "Jane Smith - CEO; John Doe - VP"). | DSBS |
| `naics_small_codes` | Array of Strings | NAICS codes for which the entity qualifies as a small business (e.g., `["541511", "541512"]`). | DSBS |
| `special_equip_material` | String | Special equipment or materials the entity possesses, when available. | DSBS |
| `capabilities_link` | String | URL to a capabilities statement document, when available. | DSBS |
| `non_fed_govt_certifications` | String | Non-federal government certifications, when available (e.g., "AS9100C; FAA/EASA"). | DSBS |
| `additional_website` | String | Secondary business website URL, when available. | DSBS |
| `county` | String | County name for the entity's physical location, when available. | DSBS |
| `federal_obligations` | Object (expand-only) | Federal contract obligations. Use `federal_obligations(*)` → `{total, active}` or `federal_obligations(total)`. Each sub-object contains `awards_obligated`, `awards_count`, `idv_count`, and (for total) `subawards_obligated`, `subawards_count`. | Tango (derived) |
| `highest_owner` | Object | Highest-level corporate owner. Expand with `highest_owner(*)` → `{cage_code, legal_business_name, uei}`. | SAM.gov |
| `immediate_owner` | Object | Immediate parent entity. Expand with `immediate_owner(*)` → `{cage_code, legal_business_name, uei}`. | SAM.gov |
| `relationships` | Array | Related entities (parent/subsidiary). Expand with `relationships(*)` → `[{relation, type, uei, display_name}]`. | SAM.gov / Tango |
| `evs_source` | String | Entity validation source (e.g., "E&Y"). | SAM.gov |
| `uei_status` | String | UEI status (e.g., "Active"). | SAM.gov |
| `uei_expiration_date` | Date | UEI expiration date, if applicable. | SAM.gov |
| `uei_creation_date` | Date | UEI creation date, if applicable. | SAM.gov |
| `public_display_flag` | String | Whether the entity record is publicly visible ("Y"/"N"). | SAM.gov |
| `exclusion_status_flag` | String | Whether the entity has an active exclusion ("Y"/"N"). | SAM.gov |
| `exclusion_url` | String | URL for exclusion details, if applicable. | SAM.gov |
| `entity_division_name` | String | Entity division name. | SAM.gov |
| `entity_division_number` | String | Entity division number. | SAM.gov |
| `entity_start_date` | String | Date the entity was started/established. | SAM.gov |
| `fiscal_year_end_close_date` | String | Fiscal year end close date (e.g., "12/31"). | SAM.gov |
| `submission_date` | Date | Registration submission date. | SAM.gov |
| `entity_structure` | Object (expand) | Entity structure. Expand with `entity_structure(*)` → `{code, description}`. Description falls back to `ENTITY_STRUCTURE_MAP` when missing on the model. | SAM.gov |
| `entity_type` | Object (expand) | Entity type. Expand with `entity_type(*)` → `{code, description}`. | SAM.gov |
| `profit_structure` | Object (expand) | Profit structure. Expand with `profit_structure(*)` → `{code, description}`. | SAM.gov |
| `organization_structure` | Object (expand) | Organization structure. Expand with `organization_structure(*)` → `{code, description}`. | SAM.gov |
| `state_of_incorporation` | Object (expand) | State of incorporation. Expand with `state_of_incorporation(*)` → `{code, description}`. | SAM.gov |
| `country_of_incorporation` | Object (expand) | Country of incorporation. Expand with `country_of_incorporation(*)` → `{code, description}`. | SAM.gov |
| `purpose_of_registration` | Object (expand) | Purpose of registration. Expand with `purpose_of_registration(*)` → `{code, description}`. Description falls back to `PURPOSE_OF_REGISTRATION_MAP` when missing. | SAM.gov |

## Common code values

### `purpose_of_registration_code` (SAM.gov)

SAM.gov registration-purpose codes:

| Code | Meaning |
| --- | --- |
| `Z1` | Federal Assistance Awards |
| `Z2` | All Awards |
| `Z3` | IGT-Only |
| `Z4` | Federal Assistance Awards & IGT |
| `Z5` | All Awards & IGT |

### `sba_business_types` (DSBS / SAM.gov)

These are SBA program classifications you'll commonly see (non-exhaustive). From `SBA_BUSINESS_TYPE_MAP`:

| Code | Meaning |
| --- | --- |
| `A0` | SBA-Certified Economically Disadvantaged Women-Owned Small Business |
| `A4` | SBA Certified Small Disadvantaged Business |
| `A6` | SBA Certified 8A Program Participant |
| `A9` | SBA Certified Woman Owned Small Business |
| `JT` | SBA Certified 8A Joint Venture |
| `XX` | SBA Certified Hub Zone Firm |

### `business_types` (SAM.gov)

Entities store business types as **2-character codes** (e.g., `A6`, `2R`, `QF`). The canonical list (including `certifier`) is exposed at `/api/business_types/` (see [Business Types Data Dictionary](business-types.md)).

## Expansions (via `shape`)

### `physical_address` / `mailing_address`

Returns the address as an object with normalized snake_case keys:

- `address_line1`, `address_line2`, `city`, `state_or_province_code`, `zip_code`, `zip_code_plus4`, `country_code`, `country_name`, `county`, `county_code`, `fips_code`

Address data from different sources (SAM uses snake_case, DSBS uses camelCase) is automatically normalized to the canonical snake_case format above. The `county`, `county_code`, and `fips_code` subfields are available when provided by DSBS.

### `business_types`

Returns a normalized array of `{code, description}` objects. Raw data may be stored as:

- `[{code, description}]` (SAM format — passed through)
- `{"2X": true, "F": true}` (DSBS format — normalized with description lookups)

### `sba_business_types`

Returns a normalized array of `{code, description, entry_date, exit_date}` objects. Same normalization as `business_types`.

### `naics_codes`

Returns a normalized array of `{code, sba_small_business}` objects. Plain string arrays (e.g., `["541512", "541511"]`) are normalized to `[{code: "541512", sba_small_business: null}, ...]`.

### `federal_obligations`

Expand-only field (not available as a leaf). Returns obligation rollups with child fields `total` and `active`.

```bash
# Both total and active
/api/entities/{uei}/?shape=uei,federal_obligations(*)

# Only total obligations
/api/entities/{uei}/?shape=uei,federal_obligations(total)
```

Each sub-object structure:

- `total`: `{awards_obligated, awards_count, subawards_obligated, subawards_count, idv_count}`
- `active`: `{awards_obligated, awards_count, idv_count}`

### `highest_owner` / `immediate_owner`

Returns the corporate owner as `{cage_code, legal_business_name, uei}`. Supports child field selection.

### `relationships`

Returns the list of related entities. Each item can include:

- `relation`, `type`, `uei`, `display_name`

### Code/description pair expands

The following fields are structured `{code, description}` expands. When the model's description is missing or empty, the system falls back to a lookup map (where available).

- `entity_structure(code, description)` — falls back to `ENTITY_STRUCTURE_MAP`
- `entity_type(code, description)` — falls back to `BUSINESS_TYPE_MAP`
- `profit_structure(code, description)` — falls back to `BUSINESS_TYPE_MAP`
- `organization_structure(code, description)` — falls back to `BUSINESS_TYPE_MAP`
- `state_of_incorporation(code, description)` — uses model description
- `country_of_incorporation(code, description)` — falls back to the GENC standard for country names when SAM.gov doesn't provide one
- `purpose_of_registration(code, description)` — falls back to `PURPOSE_OF_REGISTRATION_MAP`

Returns `null` when the code is `null`.

```bash
# Get entity structure as structured object
/api/entities/{uei}/?shape=uei,entity_structure(*)

# Just the code
/api/entities/{uei}/?shape=uei,entity_structure(code)
```

## Data Sources

- **SAM.gov** – Registration, UEI, legal name, addresses, NAICS/PSC/business types, dates, status, corporate ownership.
- **DSBS** – Capabilities, keywords, SBA business types, current principals, NAICS small codes, special equipment/materials, capabilities link, non-federal certifications, additional website, county/county code/FIPS code (when available).
- **Tango** – `display_name`, `federal_obligations` (derived from award data), `relationships`.

## Usage Notes

- **Use `uei` for lookups** – All entity endpoints accept UEI; it is case-insensitive.
- **Use `shape` for large responses** – Request only the fields you need to reduce payload size.
- **Scoped award endpoints** – Use `/api/entities/{uei}/contracts/`, `.../idvs/`, etc., for awards tied to an entity; see [Entities API Reference](../api-reference/entities.md).
- **Address key normalization** – Address subfields are always returned in snake_case (`address_line1`, `state_or_province_code`, `zip_code`), regardless of how the data was originally stored.
- **Business type normalization** – `business_types(*)` and `sba_business_types(*)` always return `[{code, description, ...}]` arrays, even when the source data uses dict format.

---

<!-- doc: data-dictionary/forecasts.md -->

# Forecasts Data Dictionary

This document describes the fields available in the Forecasts API endpoints (`/api/forecasts/`). For filtering, ordering, and pagination, see the [Forecasts API Reference](../api-reference/forecasts.md).

## Overview

Forecasts are upcoming procurement opportunities from agency feeds (e.g., HHS, DHS). They represent anticipated contract actions before they appear as SAM.gov opportunities. Data is normalized from multiple source systems.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Forecasts we track

Tango ingests procurement forecasts from the following agency sources. Filter by `agency` or `source_system` in the API (e.g. `?agency=HHS`).

- COMMERCE
- DHS
- DOE
- DOI
- DOL
- DOT
- GSA
- HHS
- NRC
- NSF
- TREASURY
- VA

## Update Frequency

Forecast data is refreshed according to each agency feed’s cadence (varies by source).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `id` | Integer | Primary key for the forecast in Tango. | Tango |
| `source_system` | String | Agency or system that provided the forecast (e.g., "HHS", "DHS"). | Tango |
| `external_id` | String | Identifier from the source system. | Agency feed |
| `agency` | String | Agency acronym or name. | Agency feed |
| `title` | String | Forecast title. | Agency feed |
| `description` | String | Forecast description. | Agency feed |
| `anticipated_award_date` | Date | Expected award date. | Agency feed |
| `fiscal_year` | Integer | Federal fiscal year. | Agency feed / Tango |
| `naics_code` | String | NAICS code. | Agency feed |
| `is_active` | Boolean | Whether the forecast is active. | Tango |
| `status` | String | Status (e.g., active, cancelled). | Agency feed |
| `created` | DateTime | When the record was created in Tango. | Tango |
| `modified` | DateTime | When the record was last updated. | Tango |
| `primary_contact` | Object | Primary contact for the forecast (display-derived). | Agency feed |
| `place_of_performance` | String | Place of performance (display-derived). | Agency feed |
| `estimated_period` | String | Estimated period of performance (display-derived). | Agency feed |
| `set_aside` | String | Set-aside designation (display-derived). | Agency feed |
| `contract_vehicle` | String | Contract vehicle type (display-derived). | Agency feed |
| `raw_data` | Object | Raw JSON from the source (expand for full payload). | Agency feed |

## Expansions (via `shape`)

- `display` – Normalized display fields: `title`, `description`, `agency`, `anticipated_award_date`, `fiscal_year`, `naics_code`, `status`, `primary_contact`, `place_of_performance`, `estimated_period`, `set_aside`, `contract_vehicle`
- `raw_data` – Raw source payload (wildcard fields)

## Data Sources

- **Agency feeds** – See [Forecasts we track](#forecasts-we-track) for the full list of source systems (HHS, DHS, GSA, COMMERCE, DOE, TREASURY, DOI, DOL, DOT, VA, NRC, NSF).
- **Tango** – `id`, `source_system`, `is_active`, `created`, `modified`, normalized fields.

## Usage Notes

- **Default shaping** – Forecasts now default to the shaping pipeline. All responses render through shaping even without `?shape=`. The list shape includes core identity fields; the detail shape adds `raw_data` and `display(*)`.
- **Only active forecasts** are returned by default; filter by `agency`, `fiscal_year`, `naics_starts_with`, or `search` as needed; see [Forecasts API Reference](../api-reference/forecasts.md).

---

<!-- doc: data-dictionary/grants.md -->

# Grants Data Dictionary

This document describes the fields available in the Grants API endpoints (`/api/grants/`). For filtering, ordering, and pagination, see the [Grants API Reference](../api-reference/grants.md).

## Overview

Grants are grant opportunities from Grants.gov. Each record is a funding opportunity with `grant_id` as the primary identifier. Data is sourced from Grants.gov.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Grant opportunity data is refreshed from Grants.gov on a regular sync cadence.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `grant_id` | String | Unique identifier for the grant opportunity (Grants.gov). | Grants.gov |
| `opportunity_number` | String | Opportunity number. | Grants.gov |
| `agency_code` | String | Funding agency code. | Grants.gov |
| `status` | Object | Opportunity status. Use expansion for `code`, `description`. | Grants.gov |
| `title` | String | Grant title. | Grants.gov |
| `description` | String | Grant description. | Grants.gov |
| `last_updated` | DateTime | Last updated timestamp. | Grants.gov |
| `applicant_eligibility_description` | String | Who may apply. | Grants.gov |
| `funding_activity_category_description` | String | Funding activity category. | Grants.gov |
| `grantor_contact` | Object | Grantor contact info (expand for subfields). | Grants.gov |
| `forecast` | Object | Forecast data associated with the grant, if any. | Grants.gov |
| `synopsis` | Object | Synopsis data for the grant opportunity. | Grants.gov |
| `opportunity_history` | Array | History of changes to the opportunity. | Grants.gov |

## Common code values

These are the most important “code → meaning” mappings used in grants data.

### `status`

| Code | Meaning |
| --- | --- |
| `P` | Posted |
| `F` | Forecasted |

### `category`

| Code | Meaning |
| --- | --- |
| `A` | Mandatory |
| `B` | Continuation |
| `C` | Earmark |
| `D` | Discretionary |
| `E` | Discretionary Cooperative Agreement |
| `O` | Other |

### `funding_instruments` (examples)

| Code | Meaning |
| --- | --- |
| `G` | Grant |
| `CA` | Cooperative Agreement |
| `PC` | Procurement Contract |
| `L` | Loan Guarantee |
| `D` | Direct Payment |
| `O` | Other |

`applicant_types` and `funding_categories` are also coded lists (Grants.gov standard taxonomies); the canonical enumerations are large and are best treated as reference data rather than memorized.

## Expansions (via `shape`)

- `cfda_numbers` – List of CFDA numbers: `number`, `title`
- `applicant_types` – `code`, `description`
- `category` – `code`, `description`
- `funding_categories` – `code`, `description`
- `funding_instruments` – `code`, `description`
- `important_dates` – `posted_date`, `response_date`, `response_date_description`, `estimated_synopsis_post_date`, `estimated_application_response_date`, `estimated_application_response_date_description`, `estimated_project_start_date` (fields vary by opportunity status)
- `funding_details` – `award_ceiling`, `award_floor`, `estimated_total_funding`, `expected_number_of_awards`
- `grantor_contact` – `name`, `phone`, `email`
- `additional_info` – `link`, `description`
- `attachments` – `mime_type`, `name`, `posted_date`, `resource_id`, `type`, `url`

## Data Sources

- **Grants.gov** – Grant opportunities, CFDA linkage, contacts, attachments.

## Usage Notes

- **Default shaping** – Grants now default to the shaping pipeline. All responses render through shaping even without `?shape=`. The list shape includes core fields; the detail shape adds all expanded objects.
- **Use `grant_id` for lookups.** Filter by `agency`, `status`, or other params; see [Grants API Reference](../api-reference/grants.md).

---

<!-- doc: data-dictionary/gsa-elibrary-contracts.md -->

# GSA eLibrary Contracts Data Dictionary

This document describes the fields available in the GSA eLibrary Contracts API endpoints (`/api/gsa_elibrary_contracts/`). For filtering, ordering, and pagination, see the [GSA eLibrary Contracts API Reference](../api-reference/gsa-elibrary-contracts.md).

## Overview

GSA eLibrary contracts are persisted contract records from the GSA eLibrary system, representing contracts awarded under the MAS (Multiple Award Schedule) program. Tango syncs this data to provide a queryable view of MAS schedule contracts with links to IDVs and recipients.

## Update Frequency

GSA eLibrary data is updated periodically via the sync pipeline.

## Fields

### Core identity

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `uuid` | UUID | Tango internal identifier for this persisted record. | Tango |
| `schedule` | String | Schedule identifier, typically `MAS`. | GSA eLibrary |
| `contract_number` | String | MAS contract number (join key to IDV `piid`, best-effort). | GSA eLibrary |

### Party

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `recipient` | Object \| null | Best-effort cached entity lookup by `uei` (fields: `uei`, `display_name`). | GSA eLibrary / Tango |

### Linked IDV (best-effort)

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `idv` | Object \| null | Only present when the row is linked to an IDV (fields: `key`, `award_date`). | Tango |

### Enrichment

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `sins` | List[String] | Special Item Numbers (SINs). | GSA eLibrary |
| `cooperative_purchasing` | Boolean | Cooperative Purchasing program flag (best-effort). | GSA eLibrary |
| `disaster_recovery_purchasing` | Boolean | Disaster Recovery Purchasing program flag (best-effort). | GSA eLibrary |
| `file_urls` | List[String] | Public document URLs (T&Cs, price list, catalog), best-effort merged. | GSA eLibrary |

## Usage Notes

- **Use `uuid` for API lookups.** The `uuid` is the primary key for detail requests: `GET /api/gsa_elibrary_contracts/{uuid}/`.
- **These are GSA MAS schedule contracts.** Each record represents a contract awarded under the Multiple Award Schedule program.
- **IDV and recipient links are best-effort.** The `idv` expansion is populated when Tango can match the `contract_number` to an IDV PIID. The `recipient` expansion is populated via UEI lookup.
- **SINs connect to the MAS SINs reference table.** Use `sin` filtering to find contracts by Special Item Number; see [MAS SINs Data Dictionary](mas-sins.md).

## Data Sources

- **GSA eLibrary** — Contract metadata from the GSA eLibrary system.

---

<!-- doc: data-dictionary/idvs.md -->

# IDVs Data Dictionary

This document describes the fields available in the IDVs API endpoints (`/api/idvs/`). For filtering, ordering, and pagination, see the [IDVs API Reference](../api-reference/idvs.md).

## Overview

IDVs (Indefinite Delivery Vehicles) are contract vehicles that can have multiple orders/task orders issued against them. IDVs are sourced from **both FPDS and USAspending**, and Tango merges those inputs during award materialization.

## Identifiers

- `key` (canonical): The canonical award identifier derived from FPDS/USAspending award keys. This is what the API uses for lookups: `GET /api/idvs/{key}/`.
- `uuid` (internal): Tango’s deterministic internal UUID used as the database primary key and for stable internal joins/orderings. This is not intended as a public lookup key.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

IDV data is refreshed by the Tango ingest pipeline (FPDS updates **twice daily**; USAspending inputs update **daily**).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | String | Canonical award identifier (stable across FPDS and USAspending). Used for detail lookups and cross-referencing. | FPDS / USAspending |
| `piid` | String | Procurement Instrument Identifier. | FPDS / USAspending |
| `vehicle_uuid` | UUID | UUID of the Tango Vehicle this IDV is grouped under. Use to fetch vehicle detail at `GET /api/vehicles/{uuid}/`. `null` when not linked to a Vehicle. | Tango |
| `award_date` | Date | Date the IDV was awarded. | FPDS / USAspending |
| `naics_code` | Integer | NAICS code on the award. | FPDS / USAspending |
| `psc_code` | String | Product/Service Code on the award. | FPDS / USAspending |
| `total_contract_value` | Decimal | Total value (base + options). | FPDS / USAspending |
| `base_and_exercised_options_value` | Decimal | Base plus exercised options. | FPDS / USAspending |
| `fiscal_year` | Integer | Federal fiscal year of the award. | FPDS / USAspending |
| `obligated` | Decimal | Total obligated amount. | FPDS / USAspending |
| `description` | String | Contract description/title. | FPDS / USAspending |
| `idv_type` | Object | IDV type (e.g., GWAC, IDIQ). Use expansion for `code`, `description`. | FPDS / USAspending |
| `multiple_or_single_award_idv` | Object | Multiple or single award indicator. | FPDS / USAspending |
| `type_of_idc` | Object | Type of IDC (Indefinite Delivery Contract). Use expansion for `code`, `description`. | FPDS / USAspending |
| `set_aside` | Object | Set-aside type. Use expansion for `code`, `description`. | FPDS / USAspending |
| `transactions` | Array | Modification/transaction records. Use expansion for `modification_number`, `transaction_date`, `obligated`, `description`, `action_type`. | FPDS / USAspending |
| `subawards_summary` | Object | Summary of subawards. Use expansion for `count`, `total_amount`. | USAspending / Tango |

## Expansions (via `shape`)

Same pattern as [Contracts](contracts.md): `recipient`, `place_of_performance`, `awarding_office`, `funding_office`, `parent_award`, `period_of_performance` (fields: `start_date`, `last_date_to_order`), `set_aside`, `transactions`, `competition`, `legislative_mandates`, `officers`, `naics`, `psc`, `subawards_summary`, plus:

**Note**: The default (non-shaped) response includes `parent_award` as a minimal `{key, piid}` reference without a database query. Use `?shape=` with `parent_award(...)` for the full parent award object. See [Contracts](contracts.md#parent_award) for details.

- `gsa_elibrary(schedule,contract_number,uei,sins,cooperative_purchasing,disaster_recovery_purchasing,file_urls,extracted_text,external_id,source_data)` – best-effort enrichment row from GSA eLibrary; returns `null` when no match is present.

## Common code values

These fields use standardized code systems sourced from FPDS / USAspending and surfaced as `{code, description}` objects in responses.

### `idv_type`

| Code | Meaning |
| --- | --- |
| `A` | GWAC |
| `B` | IDC |
| `C` | FSS |
| `D` | BOA |
| `E` | BPA |

### `type_of_idc`

| Code | Meaning |
| --- | --- |
| `A` | Indefinite Delivery / Requirements |
| `B` | Indefinite Delivery / Indefinite Quantity |
| `C` | Indefinite Delivery / Definite Quantity |

### `set_aside`

| Code | Meaning |
| --- | --- |
| `NONE` | No set aside used |
| `SBA` | Small Business Set Aside - Total |
| `8A` | 8(a) Competed |
| `HZC` | HUBZone Set Aside |
| `SDVOSBC` | Service-Disabled Veteran-Owned Small Business Set Aside |
| `WOSB` | Women-Owned Small Business |
| `EDWOSB` | Economically Disadvantaged Women-Owned Small Business |

(See [Set-aside codes](../api-reference/set-aside.md) for the complete mapping.)

## Data Sources

- **FPDS** – IDV actions, PIID, dates, values, types, set-aside, competition, offices.
- **USAspending** – Supplemental award/transaction inputs used in award materialization and subaward linkage.
- **Tango** – Internal deterministic `uuid` and merged award materialization.

## Usage Notes

- **Use `key` for API references.** Use `piid` for FPDS/solicitation lookups.
- **Child awards:** Use `/api/idvs/{key}/awards/` for contracts/orders under this IDV; see [IDVs API Reference](../api-reference/idvs.md).

---

<!-- doc: data-dictionary/index.md -->

# Data Dictionary

Tango's **Data Dictionary** documents the fields you can expect on key resources and what they mean.

If you're looking for **how to query/filter/order** endpoints, see the [API Reference](../api-reference/index.md).

## Awards

Definitive contracts, indefinite-delivery vehicles, other transactions, subawards, and solicitation-level groupings.

- [Contracts](contracts.md) — Definitive contract awards (consolidated from FPDS and USAspending inputs). [Response shaping →](../guides/patterns/response-shaping.md#contracts-apicontracts)
- [IDVs](idvs.md) — Indefinite delivery vehicles (GWACs, IDIQs, etc.). [Response shaping →](../guides/patterns/response-shaping.md#idvs-apiidvs)
- [OTAs](otas.md) — Other Transaction Agreements from FPDS. [Response shaping →](../guides/patterns/response-shaping.md#otas-apiotas)
- [OTIDVs](otidvs.md) — Other Transaction IDVs from FPDS. [Response shaping →](../guides/patterns/response-shaping.md#otidvs-apiotidvs)
- [Subawards](subawards.md) — Sub-contracts and sub-grants under prime awards; sourced from USAspending/FSRS. [Response shaping →](../guides/patterns/response-shaping.md#subawards-apisubawards)
- [Vehicles](vehicles.md) — Solicitation-centric grouping of IDVs. [Response shaping →](../guides/patterns/response-shaping.md#vehicles-apivehicles)
- [GSA eLibrary contracts](gsa-elibrary-contracts.md) — Persisted GSA eLibrary contract metadata.

## Entities

- [Entities](entities.md) — Vendors and recipients in federal contracting and assistance; UEI is the canonical identifier (from SAM.gov). [Response shaping →](../guides/patterns/response-shaping.md#entities-apientities)

## Forecasts

- [Forecasts](forecasts.md) — Upcoming procurement opportunities from agency feeds (e.g., HHS, DHS) before they appear as SAM.gov solicitations. [Response shaping →](../guides/patterns/response-shaping.md#forecasts-apiforecasts)

## Grants

- [Grants](grants.md) — Grant opportunities from Grants.gov (funding opportunities, not assistance transactions). [Response shaping →](../guides/patterns/response-shaping.md#grants-apigrants)

## IT Dashboard

- [IT Dashboard](itdashboard.md) — Federal IT investment records from itdashboard.gov, keyed by Unique Investment Identifier (UII). [Response shaping →](../guides/patterns/response-shaping.md#it-dashboard-apiitdashboard)

## Opportunities

SAM.gov contract opportunity and notice data.

- [Opportunities](opportunities.md) — Contract opportunities aggregated by parent. [Response shaping →](../guides/patterns/response-shaping.md#opportunities-apiopportunities)
- [Notices](notices.md) — Individual SAM.gov notice records (amendments, updates) under an opportunity. [Response shaping →](../guides/patterns/response-shaping.md#notices-apinotices)

## Reference data

Lookup tables and codes used across awards and entities.

- [Assistance listings (CFDA)](assistance-listings.md) — CFDA program numbers and titles for federal grants/assistance. [Response shaping →](../guides/patterns/response-shaping.md#assistance-listings-apiassistance_listings)
- [Business types](business-types.md) — SAM.gov business type codes (e.g., small business, 8(a), HUBZone). [Response shaping →](../guides/patterns/response-shaping.md#business-types-apibusiness_types)
- [MAS SINs](mas-sins.md) — MAS Special Item Numbers (SINs) reference data. [Response shaping →](../guides/patterns/response-shaping.md#mas-sins-apimas_sins)
- [NAICS](naics.md) — North American Industry Classification System codes. [Response shaping →](../guides/patterns/response-shaping.md#naics-apinaics)
- [Organizations](organizations.md) — Unified federal hierarchy (departments, agencies, offices in one tree). [Response shaping →](../guides/patterns/response-shaping.md#organizations-apiorganizations)
- [PSC](psc.md) — Product/Service Codes for federal contracting. [Response shaping →](../guides/patterns/response-shaping.md#psc-apipsc)

## Shared

Data structures shared across multiple endpoints via response shaping expands.

- [Set-aside codes](../api-reference/set-aside.md) — Set-aside type codes and descriptions.
- [Agencies](agencies.md) — Agency fields and expansions as they appear in shaped responses. [Response shaping →](../guides/patterns/response-shaping.md#agencies-apiagencies)
- [Departments](departments.md) — Department fields as they appear in shaped responses. [Response shaping →](../guides/patterns/response-shaping.md#departments-apidepartments)
- [Offices](offices.md) — Office fields, flat aliases, and expansions as they appear in shaped responses. [Response shaping →](../guides/patterns/response-shaping.md#offices-apioffices)

---

<!-- doc: data-dictionary/itdashboard.md -->

# IT Dashboard Data Dictionary

This document describes the fields available in the IT Dashboard API endpoint (`/api/itdashboard/`). For filtering, ordering, and pagination, see the [IT Dashboard API Reference](../api-reference/itdashboard.md).

## Overview

IT Dashboard investments are federal IT investments tracked by the Office of Management and Budget (OMB) and published on [itdashboard.gov](https://www.itdashboard.gov/). Each record represents a single investment identified by a Unique Investment Identifier (UII).

Data includes investment metadata, fiscal-year funding breakdowns, CIO risk evaluations, contract details, project information, and performance metrics.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

IT Dashboard data is refreshed daily from itdashboard.gov. The scraper pulls the CSV data feeds and aggregates them with investment-level API data.

## Core fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `uii` | String | Unique Investment Identifier (e.g., `021-488119819`). Primary lookup key. | IT Dashboard |
| `agency_code` | Integer | Numeric agency code (e.g., 21 for Department of Transportation). | IT Dashboard |
| `agency_name` | String | Full agency name. | IT Dashboard |
| `bureau_code` | Integer | Numeric bureau code within the agency. | IT Dashboard |
| `bureau_name` | String | Bureau name. | IT Dashboard |
| `investment_title` | String | Title of the IT investment. | IT Dashboard |
| `type_of_investment` | String | Classification (e.g., "Major IT Investment", "Standard IT Infrastructure"). | IT Dashboard |
| `part_of_it_portfolio` | String | IT portfolio area (e.g., "Mission Delivery", "Infrastructure"). | IT Dashboard |
| `updated_time` | DateTime | Last update timestamp from the source data. | IT Dashboard |
| `url` | String | Canonical URL to the investment on itdashboard.gov. Computed from UII. | Derived |

## Funding expansion (`funding`)

Fiscal-year internal funding and contribution amounts (Micro+ tier).

| Field | Type | Description |
| ----- | ---- | ----------- |
| `fy2020_internal_funding` | Number | FY2020 internal funding amount. |
| `fy2020_contribution` | Number | FY2020 contribution amount. |
| `fy2021_internal_funding` | Number | FY2021 internal funding amount. |
| `fy2021_contribution` | Number | FY2021 contribution amount. |
| `fy2022_internal_funding` | Number | FY2022 internal funding amount. |
| `fy2022_contribution` | Number | FY2022 contribution amount. |
| `fy2023_internal_funding` | Number | FY2023 internal funding amount. |
| `fy2023_contribution` | Number | FY2023 contribution amount. |
| `fy2024_internal_funding` | Number | FY2024 internal funding amount. |
| `fy2024_contribution` | Number | FY2024 contribution amount. |
| `fy2025_internal_funding` | Number | FY2025 internal funding amount. |
| `fy2025_contribution` | Number | FY2025 contribution amount. |

## Details expansion (`details`)

Extended investment metadata (Micro+ tier).

| Field | Type | Description |
| ----- | ---- | ----------- |
| `investment_description` | String | Full text description of the investment. |
| `previous_uii` | String | Previous UII if the investment was re-baselined. |
| `current_uii` | String | Current UII (may differ from `previous_uii`). |
| `change_in_status` | String | Description of any status change. |
| `shared_services_category` | String | Shared services classification. |
| `shared_services_identifier` | String | Shared services identifier. |
| `mission_support_investment_categories` | String | Mission support categories. |
| `mission_delivery_and_management_support_area` | String | Mission delivery area. |
| `national_security_system_identifier` | String | NSS identifier, if applicable. |
| `it_infrastructure_and_management_type` | String | IT infrastructure type. |
| `business_case_url` | String | URL to the business case on IT Dashboard. |
| `public_urls` | String | Public-facing URLs for the investment. |
| `last_updated` | String | Source-reported last-updated date. |

## CIO evaluation expansion (`cio_evaluation`)

CIO risk ratings over time (Medium+ tier). Each entry is a rating snapshot.

| Field | Type | Description |
| ----- | ---- | ----------- |
| `cioRating` | String | Risk rating with label (e.g., "1 - High Risk", "5 - Low Risk"). |
| `comment` | String | CIO evaluation comment. |
| `ratedDate` | String | Date the rating was assigned. |
| `updatedTime` | String | Timestamp of the rating record update. |
| `latestIndicator` | String | "Y" if this is the most recent rating. |

### CIO rating scale

| Rating | Risk level |
| ------ | ---------- |
| 1 | High Risk |
| 2 | Moderately High Risk |
| 3 | Medium Risk |
| 4 | Moderately Low Risk |
| 5 | Low Risk |

## Contracts expansion (`contracts`)

IT contracts associated with the investment (Medium+ tier). Fields vary by contract.

## Projects expansion (`projects`)

Project details under the investment (Medium+ tier). Fields vary by project.

## Performance metrics expansion (`performance_metrics`)

Performance measurement data (Medium+ tier).

| Field | Type | Description |
| ----- | ---- | ----------- |
| `metricDescription` | String | Description of the performance metric. |
| `metTarget` | String | Whether target was met ("MET" or "NOT MET"). |
| `latestActualResult` | Number | Most recent actual measurement. |
| `target2024PY` | Number | Prior-year target value. |
| `target2025CY` | Number | Current-year target value. |
| `unitOfMeasure` | String | Unit (e.g., "Percentage"). |
| `reportingFrequency` | String | How often the metric is reported. |
| `measurementCondition` | String | Target condition (e.g., "Over target"). |
| `dateOfLatestActualResult` | String | Date of the latest actual result. |
| `performanceMeasurementCategory` | String | Category (e.g., "Strategic and Business Results"). |
| `isRetired` | Boolean | Whether the metric has been retired. |

## Other expansions (Medium+ tier)

| Expansion | Description |
| --------- | ----------- |
| `cost_pools_towers(*)` | Cost pool and cost tower breakdowns. |
| `funding_sources(*)` | Detailed funding source information. |
| `performance_actual(*)` | Historical performance actual results (individual data points over time). |
| `operational_analysis(*)` | Operational analysis data. |

## Special fields

| Field | Tier | Description |
| ----- | ---- | ----------- |
| `business_case_html` | Medium+ | Full HTML content of the business case page from IT Dashboard. Requested as a leaf field, not an expansion. |

---

<!-- doc: data-dictionary/lcats.md -->

# CALC Labor Rates Data Dictionary

This document describes the fields available in the CALC labor category rate endpoints (`/api/idvs/{key}/lcats/`, `/api/entities/{uei}/lcats/`). For filtering, ordering, and pagination, see the [CALC Labor Rates API Reference](../api-reference/lcats.md).

## Overview

CALC (Contract Awarded Labor Category) rates are labor category pricing records sourced from the GSA CALC API. Each record represents a single labor category rate on a GSA MAS (Multiple Award Schedule) contract. The dataset contains approximately 238,000 records.

**Primary identifier:** `uuid` (deterministic, derived from the external CALC record ID).

## Update Frequency

CALC labor rate data is refreshed daily from the GSA CALC API.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `uuid` | UUID | Deterministic primary key (derived from external CALC ID). | CALC / Tango |
| `labor_category` | String | Labor category title. | CALC |
| `current_price` | Decimal | Current hourly rate. | CALC |
| `education_level` | String | Required education level. | CALC |
| `min_years_experience` | Integer | Minimum years of experience required. | CALC |
| `experience_range` | String | Experience range description (e.g., "5-7 years"). | CALC |
| `security_clearance` | String | Security clearance requirement. | CALC |
| `worksite` | String | Work location type. | CALC |
| `sin` | String | Special Item Number. | CALC |
| `category` | String | Service category. | CALC |
| `subcategory` | String | Service subcategory. | CALC |
| `schedule` | String | GSA schedule name. | CALC |
| `contract_start` | Date | Contract period start date. | CALC |
| `contract_end` | Date | Contract period end date. | CALC |

## Expansions (via `shape`)

- `idv` -- Linked IDV details: `key`, `piid`, `award_date`
- `recipient` -- Entity details via IDV recipient linkage: `uei`, `display_name`
- `business_size` -- Business size as structured `code`/`description` (e.g., `{"code": "S", "description": "Small Business"}`)

The `idv` and `recipient` expansions require Pro tier access. The `business_size` expansion is available on all tiers.

## Data Sources

- **GSA CALC** -- Contract Awarded Labor Category API. Provides labor category pricing data from GSA MAS contracts.

## Usage Notes

- **Use `uuid` for unique identification.** The UUID is deterministic and stable across data refreshes for the same CALC record.
- **IDV linkage is best-effort.** The `idv` expansion is populated when Tango can match the CALC contract number to an IDV PIID in the awards dataset.
- **Recipient is derived from the IDV.** The `recipient` expansion comes from the linked IDV's recipient entity, not directly from CALC data.
- **These are nested endpoints.** Access rates via `/api/idvs/{key}/lcats/` or `/api/entities/{uei}/lcats/` -- there is no top-level route.
- **Pro tier required.** Both the endpoints and the `idv`/`recipient` expansions require Pro tier access or above.

---

<!-- doc: data-dictionary/mas-sins.md -->

# MAS SINs Data Dictionary

This document describes the fields available in the MAS SINs API endpoints (`/api/mas_sins/`). For filtering, ordering, and pagination, see the [MAS SINs API Reference](../api-reference/mas-sins.md).

## Overview

MAS SINs (Special Item Numbers) are GSA schedule line items used to categorize offerings under the MAS schedule. Tango stores SINs as reference data so other datasets (e.g. GSA eLibrary enrichment) can store only SIN codes and join to this table for details.

**Note**: This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter. Default shape includes all 15 fields.

## Update Frequency

MAS SIN data is refreshed on a regular cadence from the GSA eLibrary MAS Schedule export.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `sin` | String | Special Item Number code. Primary key. | GSA MAS SIN export |
| `large_category_code` | String (1 char) | Large category code (e.g., `A`). | GSA MAS SIN export |
| `large_category_name` | String | Large category name (e.g., `Office Management`). | GSA MAS SIN export |
| `sub_category_code` | String | Subcategory code (e.g., `A01`). | GSA MAS SIN export |
| `sub_category_name` | String | Subcategory name (e.g., `Audio Visual Products`). | GSA MAS SIN export |
| `psc_code` | String \| null | PSC code from source (4 chars, may be alphanumeric). | GSA MAS SIN export |
| `state_local` | Boolean | Whether SIN is eligible for state/local/cooperative purchasing. | GSA MAS SIN export |
| `set_aside_code` | String (1 char) \| null | One of `Y`, `N`, `B`. | GSA MAS SIN export |
| `service_comm_code` | String (1 char) \| null | One of `B`, `C`, `S`, `P`. | GSA MAS SIN export |
| `expiration_date` | Date \| null | Parsed from a 7-digit `YYYYDDD` numeric format when present. | GSA MAS SIN export |
| `tdr` | Boolean | TDR flag (`Y/N` in source). | GSA MAS SIN export |
| `olm` | Boolean | OLM flag (`Y/N` in source). | GSA MAS SIN export |
| `naics_codes` | List[Integer] | NAICS codes associated with the SIN. | GSA MAS SIN export |
| `title` | String | SIN title (normalized to remove leading SIN code prefix). | GSA MAS SIN export |
| `description` | String \| null | SIN description (normalized to remove leading SIN code prefix). | GSA MAS SIN export |

## Category codes and names

The MAS SIN dataset includes a small, stable hierarchy:

- **Large category**: `large_category_code` + `large_category_name`
- **Subcategory**: `sub_category_code` + `sub_category_name`

For convenience, here is the full mapping.

### Large categories

| Code | Name |
| --- | --- |
| `A` | Office Management |
| `B` | Facilities |
| `C` | Furniture and Furnishings |
| `D` | Human Capital |
| `E` | Professional Services |
| `F` | Information Technology |
| `G` | Miscellaneous |
| `H` | Scientific Management and Solutions |
| `I` | Industrial Products and Services |
| `J` | Security and Protection |
| `K` | Transportation and Logistics Services |
| `L` | Travel |

### Subcategories

| Large category code | Subcategory code | Subcategory name |
| --- | --- | --- |
| `A` | `A01` | Audio Visual Products |
| `A` | `A02` | Audio Visual Services |
| `A` | `A03` | Document Services |
| `A` | `A04` | Mail Management |
| `A` | `A05` | Office Management Maintenance and Repair |
| `A` | `A06` | Media Products |
| `A` | `A07` | Media Services |
| `A` | `A08` | Office Services |
| `A` | `A09` | Office Supplies |
| `A` | `A10` | Printing and Photographic Equipment |
| `A` | `A11` | Records Management |
| `B` | `B01` | Facilities Maintenance and Repair |
| `B` | `B02` | Facilities Services |
| `B` | `B03` | Facilities Solutions |
| `B` | `B04` | Facilities Supplies |
| `B` | `B05` | Food Service Equipment |
| `B` | `B06` | Structures |
| `C` | `C01` | Flooring |
| `C` | `C02` | Furniture Services |
| `C` | `C03` | Healthcare Furniture |
| `C` | `C04` | Household Dormitory and Quarters Furniture |
| `C` | `C05` | Miscellaneous Furniture |
| `C` | `C06` | Office Furniture |
| `C` | `C07` | Packaged Furniture |
| `C` | `C08` | Signs |
| `D` | `D01` | Background Investigations |
| `D` | `D02` | Compensation and Benefits |
| `D` | `D03` | Human Resources |
| `D` | `D04` | Social Services |
| `E` | `E01` | Business Administrative Services |
| `E` | `E02` | Environmental Services |
| `E` | `E03` | Financial Services |
| `E` | `E04` | Identity Protection Services |
| `E` | `E05` | Language Services |
| `E` | `E06` | Legal Services |
| `E` | `E07` | Logistical Services |
| `E` | `E08` | Marketing and Public Relations |
| `E` | `E09` | Technical and Engineering Services (non- IT) |
| `E` | `E10` | Training |
| `F` | `F01` | Electronic Commerce |
| `F` | `F02` | IT Hardware |
| `F` | `F03` | IT Services |
| `F` | `F04` | IT Software |
| `F` | `F05` | IT Solutions |
| `F` | `F06` | IT Training |
| `F` | `F07` | Telecommunications |
| `G` | `G01` | Apparel |
| `G` | `G02` | Awards |
| `G` | `G03` | Flags |
| `G` | `G04` | Musical Instruments |
| `G` | `G05` | Personal Hair Care Items |
| `G` | `G06` | Complementary SINs |
| `H` | `H01` | Laboratory Animals |
| `H` | `H02` | Laboratory Equipment |
| `H` | `H03` | Medical Equipment |
| `H` | `H04` | Scientific Services |
| `H` | `H05` | Search and Navigation |
| `H` | `H06` | Testing and Analysis |
| `I` | `I01` | Cleaning Supplies |
| `I` | `I02` | Fire/Rescue/Safety/Environmental Protection Equipment |
| `I` | `I03` | Fuel Management |
| `I` | `I04` | Hardware and Tools |
| `I` | `I05` | Industrial Products |
| `I` | `I06` | Industrial Products and Services Maintenance and Repair |
| `I` | `I07` | Machinery and Components |
| `I` | `I08` | Packaging |
| `I` | `I09` | Test and Measurement Supplies |
| `J` | `J01` | Marine and Harbor |
| `J` | `J02` | Protective Equipment |
| `J` | `J03` | Security Animals and Related Services |
| `J` | `J04` | Security Services |
| `J` | `J05` | Security Systems |
| `J` | `J06` | Testing Equipment |
| `K` | `K01` | Automotive Body Maintenance and Repair |
| `K` | `K02` | Motor Vehicles (non-Combat) |
| `K` | `K03` | Package Delivery |
| `K` | `K04` | Packaging Services |
| `K` | `K05` | Transportation of Things |
| `L` | `L01` | Employee Relocation |
| `L` | `L02` | Lodging |
| `L` | `L03` | Travel Agent and Misc. Services |

## Data sources

- GSA eLibrary MAS Schedule reference export, normalized by Tango.

---

<!-- doc: data-dictionary/naics.md -->

# NAICS Data Dictionary

This document describes the fields available in the NAICS API endpoints (`/api/naics/`). For filtering, ordering, and pagination, see the [NAICS API Reference](../api-reference/naics.md).

## External references

- [Official NAICS information (U.S. Census Bureau)](https://www.census.gov/naics/)
- [NAICS Association (third-party reference)](https://www.naics.com/)

## Overview

NAICS (North American Industry Classification System) codes are industry classification codes used in federal contracting. Each code has a numeric `code` (primary key) and `description`. Detail responses also include SBA size standards and obligation rollups.

**Note**: This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter. On list requests, the default shape is `code,description`. Detail requests and `?show_limits=1` default to `code,description,size_standards(*),federal_obligations(*)`. The legacy `?show_limits=1` parameter is still accepted but shaping is preferred.

## Update Frequency

NAICS reference data is updated when size standards or code lists are refreshed (periodic).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | Integer | NAICS code (primary key). | NAICS / SBA |
| `description` | String | NAICS industry description. | NAICS / SBA |
| `size_standards` | Expansion | SBA size standards for this NAICS. Expand with `size_standards(employee_limit,revenue_limit)` or `size_standards(*)`. `revenue_limit` is in whole dollars. Included in detail/`show_limits` default shape. | SBA |
| `federal_obligations` | Expansion | Obligation rollups for this NAICS. Expand with `federal_obligations(total,active)` or `federal_obligations(*)`. Each sub-object has `awards_obligated` (float) and `awards_count` (int). Included in detail/`show_limits` default shape. | Tango (derived from awards) |

**Metrics:** Time-series obligation metrics are available at `/api/naics/{code}/metrics/<months>/<period_grouping>/`; see [Metrics](../api-reference/metrics.md).

## Data Sources

- **NAICS / SBA** – Code descriptions and size standards.
- **Tango** – `year`, obligation rollups (via metrics endpoint).

## Usage Notes

- **NAICS codes are hierarchical** — 2-digit sector, 3-digit subsector, up to 6-digit national industry codes.
- **Use `search` for text-based lookup; use `code` for exact match.** Filter by `search`, `revenue_limit`, `employee_limit`; see [NAICS API Reference](../api-reference/naics.md).
- **Size standards and federal obligations are available as shaping expansions.** Expand with `size_standards(*)` or `federal_obligations(*)`; see [Response Shaping](../guides/patterns/response-shaping.md).
- **NAICS codes are updated periodically by the Census Bureau.** Tango reflects the current revision.

---

<!-- doc: data-dictionary/notices.md -->

# Notices Data Dictionary

This document describes the fields available in the Notices API endpoints (`/api/notices/`). For filtering, ordering, and pagination, see the [Notices API Reference](../api-reference/notices.md).

## Overview

Notices are individual SAM.gov contract opportunity notices. Each notice has a `notice_id` (UUID). An opportunity can have multiple notices (e.g., amendments); notices are the versioned records. Data is sourced from SAM.gov.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Notice data is refreshed from SAM.gov on a regular sync cadence (typically 20–60 minutes).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `notice_id` | UUID | Primary identifier for the notice in Tango. | Tango / SAM.gov |
| `active` | Boolean | Whether the notice is active (not archived/cancelled/deleted). | Tango (derived) |
| `address` | Object | Notice address. Use expansion for `city`, `state`, `zip`, `country`. | SAM.gov |
| `attachments` | Array | Attachments. Use expansion for `attachment_id`, `mime_type`, `name`, `posted_date`, `resource_id`, `type`, `url` (and optionally `extracted_text` when enabled). | SAM.gov |
| `attachment_count` | Integer | Number of attachments. | Tango (derived) |
| `description` | String | Notice description. | SAM.gov |
| `last_updated` | DateTime | Last modified. | SAM.gov |
| `meta` | Object | `parent_notice_id`, `related_notice_id`, `notice_type`, `link`. Use expansion for subfields; `notice_type` has `code`, `type`. | Tango |
| `naics_code` | String | NAICS code. | SAM.gov |
| `office` | Object | Contracting office. Use expansion for office/agency/department fields. | SAM.gov |
| `opportunity` | Object | Parent opportunity reference. | Tango |
| `opportunity_id` | UUID | The Tango `opportunity_id` of the parent opportunity (when available). | Tango |
| `place_of_performance` | Object | Place of performance. Use expansion for `street_address`, `city`, `state`, `zip`, `country`. | SAM.gov |
| `posted_date` | DateTime | Posted date. | SAM.gov |
| `psc_code` | String | Product/Service Code (classification_code). | SAM.gov |
| `response_deadline` | DateTime | Response deadline. | SAM.gov |
| `sam_url` | String | Canonical SAM.gov notice URL. | Tango (derived) |
| `set_aside` | String | Set-aside description. Use expansion `set_aside(code,description)` for the structured form. | SAM.gov |
| `solicitation_number` | String | Solicitation number. | SAM.gov |
| `title` | String | Notice title. | SAM.gov |

## Expansions (via `shape`)

- `attachments`, `address`, `archive`, `place_of_performance`, `primary_contact` (fields: `title`, `full_name`, `email`, `phone`, `fax`), `secondary_contact`, `meta` (with `notice_type(code, type)`), `office`, `opportunity`, `set_aside(code,description)`

## Notice type codes

Notices classify the notice “type” using a short code. These values come from SAM.gov, with Tango normalizing them to the codes below:

| Code | Meaning |
| --- | --- |
| `a` | Award Notice |
| `g` | Sale of Surplus Property |
| `i` | Consolidate/(Substantially) Bundle |
| `j` | Justification and Approval (J&A) |
| `k` | Combined Synopsis/Solicitation |
| `l` | Fair Opportunity / Limited Sources Justification (deprecated) |
| `m` | Modification/Amendment/Cancel |
| `o` | Solicitation |
| `p` | Pre solicitation |
| `r` | Sources Sought |
| `s` | Special Notice |
| `u` | Justification (J&A) |

## Data Sources

- **SAM.gov** – Notice records, offices, contacts, attachments.
- **Tango** – `notice_id`, `active`, `attachment_count`, `meta`, `sam_url`, `opportunity`.

## Usage Notes

- **Use `notice_id` for notice lookups.** For the parent opportunity use `opportunity` or the [Opportunities API Reference](../api-reference/opportunities.md); see [Opportunities Data Dictionary](opportunities.md) and [Notices API Reference](../api-reference/notices.md).

---

<!-- doc: data-dictionary/offices.md -->

# Offices Data Dictionary

This document describes the fields available on the office data structure as it appears in response shaping expands across multiple endpoints (e.g., contracts, opportunities). For filtering, ordering, and pagination on the standalone endpoint, see the [Offices API Reference](../api-reference/offices.md).

For the unified federal hierarchy, see [Organizations](organizations.md).

## Overview

Offices are sub-agency organizations in the federal hierarchy. When expanded via response shaping (e.g., `awarding_office(office_code,office_name,agency_name)`), office objects use the fields below. The default shape uses flat aliases for backwards compatibility.

## Update Frequency

Office data is updated **monthly** via the organization sync process from the Federal Hierarchy (SAM.gov).

## Direct Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | String | Office code. | Federal Hierarchy (SAM.gov) |
| `name` | String | Office name. | Federal Hierarchy (SAM.gov) |

## Flat Aliases (Legacy Default Shape)

These fields are returned by default when no `?shape=` parameter is provided. They flatten the office, agency, and department into a single level.

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `office_code` | String | For awarding offices, the Activity Address Code (`aac_code`) that matches the first 6 characters of the contract PIID; falls back to `code`. For funding offices, alias for `code`. | Federal Hierarchy |
| `office_name` | String | Alias for `name`. | Federal Hierarchy |
| `agency_code` | String | Parent agency code (resolved from `agency.code`). | FPDS |
| `agency_name` | String | Parent agency name (resolved from `agency.name`). | Federal Hierarchy |
| `department_code` | String | Parent department code (resolved from `agency.department.code`). | USAspending |
| `department_name` | String | Parent department name (resolved from `agency.department.name`). | Federal Hierarchy |

## Expansions

| Expansion | Fields | Description |
| --------- | ------ | ----------- |
| `agency(...)` | `code`, `name`, `abbreviation` | Parent agency. See [Agencies data dictionary](agencies.md). |
| `department(...)` | `code`, `name`, `abbreviation`, `description`, `cgac`, `website`, `congressional_justification` | Parent department. See [Departments data dictionary](departments.md). |

## Usage Notes

- **Default shape uses flat aliases.** Without `?shape=`, offices return `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`.
- **Use nested expansions for structured data.** Pass `?shape=code,name,agency(code,name)` for nested objects instead of flat aliases.
- **Prefer Organizations for new integrations.** The office data structure is a legacy shape. Use the [Organizations API](../api-reference/organizations.md) for the full hierarchy with richer search and obligation data.

## Data Sources

- **Federal Hierarchy (SAM.gov)** — Authoritative source for office names, codes, and parent relationships.
- **FPDS** — Office codes as they appear in contract transactions.

---

<!-- doc: data-dictionary/opportunities.md -->

# Opportunities Data Dictionary

This document describes the fields available in the Opportunities API endpoints (`/api/opportunities/`). For filtering, ordering, and pagination, see the [Opportunities API Reference](../api-reference/opportunities.md).

## Overview

Opportunities represent SAM.gov contract opportunity notices aggregated by parent opportunity (one opportunity can have multiple notices over time). Each opportunity has an `opportunity_id`. Data is sourced from SAM.gov.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Opportunity data is refreshed from SAM.gov on a regular sync cadence (typically 20–60 minutes).

## Fields

| Field | Type | Description | Source |
| --- | --- | --- | --- |
| `opportunity_id` | UUID | Primary identifier for the opportunity in Tango (parent notice). | Tango / SAM.gov |
| `latest_notice_id` | UUID | The Tango `notice_id` of the latest notice associated with this opportunity (when available). | Tango |
| `active` | Boolean | Whether the opportunity is active. | Tango (derived) |
| `archive_date` | DateTime | Archive date for the opportunity (derived from SAM.gov notice archive metadata when present). | Tango / SAM.gov |
| `agency_id` | String | Agency code associated with the opportunity (when available). | Tango / SAM.gov |
| `award_number` | String | Award number when awarded. | SAM.gov |
| `department_id` | String | Department code associated with the opportunity (when available). | Tango / SAM.gov |
| `description` | String | Opportunity description (from latest notice). | SAM.gov |
| `first_notice_date` | DateTime | First notice date. | SAM.gov |
| `last_notice_date` | DateTime | Last notice date. | SAM.gov |
| `meta` | Object | Metadata: `notices_count`, `attachments_count`, `notice_type`. Use expansion for subfields. | Tango |
| `naics_code` | String | NAICS code. | SAM.gov |
| `office` | Object | Contracting office. Use expansion for `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`. | SAM.gov |
| `office_id` | String | Office code associated with the opportunity (when available). | Tango / SAM.gov |
| `place_of_performance` | Object | Place of performance. Use expansion for `street_address`, `city`, `state`, `zip`, `country`. | SAM.gov |
| `primary_contact` | Object | Primary contact (expand for subfields). | SAM.gov |
| `secondary_contact` | Object | Secondary contact. | SAM.gov |
| `psc_code` | String | Product/Service Code. | SAM.gov |
| `response_deadline` | DateTime | Response deadline. | SAM.gov |
| `sam_url` | String | Canonical SAM.gov opportunity URL. | Tango (derived) |
| `set_aside` | String | Set-aside code. Use expansion `set_aside(code,description)` for the structured form. | SAM.gov |
| `solicitation_number` | String | Solicitation number. | SAM.gov |
| `title` | String | Opportunity title (from latest notice). | SAM.gov |

## Expansions (via `shape`)

- `attachments` – `attachment_id`, `mime_type`, `name`, `posted_date`, `resource_id`, `type`, `url` (and optionally `extracted_text` when enabled)
- `office` – office and agency fields
- `agency` – `code`, `name`, `abbreviation`
- `department` – `code`, `name`, `abbreviation`, `description`, `cgac`, `website`, `congressional_justification`
- `latest_notice` – `notice_id`, `link`
- `place_of_performance` – address fields
- `meta` – `notices_count`, `attachments_count`, `notice_type` (with nested `notice_type(code, type)`)

## Notice type codes

Opportunities and notices classify the notice “type” using a short code. These values come from SAM.gov, with Tango normalizing them to the codes below:

| Code | Meaning |
| --- | --- |
| `a` | Award Notice |
| `g` | Sale of Surplus Property |
| `i` | Consolidate/(Substantially) Bundle |
| `j` | Justification and Approval (J&A) |
| `k` | Combined Synopsis/Solicitation |
| `l` | Fair Opportunity / Limited Sources Justification (deprecated) |
| `m` | Modification/Amendment/Cancel |
| `o` | Solicitation |
| `p` | Pre solicitation |
| `r` | Sources Sought |
| `s` | Special Notice |
| `u` | Justification (J&A) |

## Data Sources

- **SAM.gov** – Contract opportunity notices, offices, contacts, attachments.
- **Tango** – `opportunity_id`, aggregation, `sam_url`, `active`.

## Usage Notes

- **Use `opportunity_id` for API references.** Use `sam_url` for linking to SAM.gov.
- **Notices:** For individual notice-level data, use `/api/notices/`; see [Notices API Reference](../api-reference/notices.md) and [Notices Data Dictionary](notices.md).

---

<!-- doc: data-dictionary/organizations.md -->

<!-- markdownlint-disable MD024 -->
# Organizations Data Dictionary

This document describes the fields available in the Organizations API endpoint (`/api/organizations/`). For filtering, ordering, and pagination, see the [Organizations API Reference](../api-reference/organizations.md).

## Overview

The Organizations resource provides a unified view of federal government organizations, consolidating data from multiple authoritative sources into a single hierarchical structure. Organizations include departments, agencies, sub-agencies, and offices.

**Note**: The full set of fields is available via the `shape` query parameter. Without a `?shape=` parameter, a [default shape](#default-shape) is applied. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

All organization data is refreshed **monthly** by the Tango ingest pipeline.

## Default Shape

All responses go through the shaping pipeline. When no `?shape=` parameter is provided, the following fields are returned:

`key, fh_key, name, short_name, type, level, is_active, code, fpds_code, cgac, canonical_code, parent_fh_key, full_parent_path_name`

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | UUID | Tango's internal UUID primary key for the organization record. Returned in shaped responses, but the Organizations API uses `fh_key` for lookups. | Tango |
| `fh_key` | String | Zero-padded 9-character string representation of the Federal Hierarchy `orgKey` (e.g., `"000000001"`). Used for detail lookups: `GET /api/organizations/{fh_key}/`. Accepts both zero-padded and plain integer forms in URLs. Practically unique for "current" org lookups, but **not guaranteed to be globally unique across all historical snapshots** (Tango stores historical org versions separately). | Federal Hierarchy (SAM.gov) |
| `name` | String | The full official name of the organization (e.g., "Federal Emergency Management Agency"). | Federal Hierarchy (SAM.gov) |
| `short_name` | String | The abbreviated or common name of the organization (e.g., "FEMA", "DHS"). | Federal Hierarchy (SAM.gov) |
| `type` | String | The organizational type within the federal hierarchy (e.g., "DEPARTMENT", "AGENCY", "OFFICE"). | Federal Hierarchy (SAM.gov) |
| `level` | Integer | The depth level in the organizational hierarchy. Level 1 represents top-level departments, with higher numbers indicating deeper nesting. | Federal Hierarchy (SAM.gov) |
| `cgac` | String | Common Government-wide Accounting Classification code. A 3-character code used for financial reporting (e.g., "069" for Department of Transportation). | USAspending |
| `fpds_code` | String | Federal Procurement Data System code. A 4-digit agency identifier used in contract transactions. | USAspending |
| `code` | String | Office or agency code. Used to identify specific offices within agencies, particularly in contract and financial assistance transactions. | Federal Hierarchy / USAspending |
| `canonical_code` | String | Level-prefixed canonical code combining hierarchy level and the appropriate code type (e.g., `"L1:097"` for a department, `"L2:5700"` for an agency). | Calculated |
| `fpds_org_id` | String | The FPDS organization identifier, when available. | FPDS |
| `aac_code` | String | Activity Address Code, used in some procurement and financial transactions. | Federal Hierarchy (SAM.gov) |
| `is_active` | Boolean | Indicates whether the organization is currently active. Derived from the `mod_status` field in Federal Hierarchy. | Federal Hierarchy (SAM.gov) |
| `parent_fh_key` | String | Zero-padded 9-character `fh_key` of this organization's parent. Null for top-level departments. | Federal Hierarchy (SAM.gov) |
| `full_parent_path_name` | String | A human-readable path showing the full hierarchy from the top-level department to this organization, separated by " > " (e.g., "Department of Homeland Security > Federal Emergency Management Agency"). | Federal Hierarchy (SAM.gov) |
| `l1_fh_key` .. `l8_fh_key` | String | Zero-padded 9-character `fh_key` of the ancestor at each hierarchy level (L1 through L8). Null when no ancestor exists at that level. | Federal Hierarchy (SAM.gov) |
| `start_date` | Date | The date the organization became active in the Federal Hierarchy. | Federal Hierarchy (SAM.gov) |
| `end_date` | Date | The date the organization was deactivated, if applicable. | Federal Hierarchy (SAM.gov) |
| `logo` | String | URL to the organization's logo, when available. | Various |
| `summary` | String | Short description or mission summary for the organization. | Federal Hierarchy (SAM.gov) |
| `mod_status` | String | The modification status of the organization in the Federal Hierarchy (e.g., "ACTIVE", "INACTIVE"). | Federal Hierarchy (SAM.gov) |
| `description` | String | A description of the organization's mission or purpose, when available. | Federal Hierarchy (SAM.gov) |
| `obligations` | Float | Total federal contract and financial assistance obligations associated with this organization and all its descendants (rolled up from the entire subtree). Alias for `tree_obligations`. Expressed in US dollars. | USAspending |
| `total_obligations` | Float | Direct obligations for this specific organization (not rolled up from descendants). Expressed in US dollars. | USAspending |
| `tree_obligations` | Float | Total obligations for this organization's entire subtree (the organization plus all descendants). Expressed in US dollars. | USAspending |
| `obligation_rank` | Integer | A percentile ranking (1-100) of this organization's obligations relative to other organizations. Higher values indicate larger obligation amounts. | Calculated |

## Usage Notes

- The Organizations API lookup key is `fh_key` (Federal Hierarchy `orgKey`), returned as a zero-padded 9-character string.
- The `fh_key` is intended for "current" lookups, but it is not a stable globally-unique identifier across all historical snapshots/versions.
- If you need to track historical versions, use the internal `key` (UUID) plus timestamps/versions (Tango stores org history separately).

## Organization Types

Organization types define the position of an organization within the federal hierarchy. Each type corresponds to a specific hierarchy level:

| Type | Level | Description |
| ---- | ----- | ----------- |
| `DEPARTMENT` | 1 | Top-level federal departments (e.g., Department of Defense, Department of Health and Human Services) |
| `AGENCY` | 2 | Major sub-components of departments (e.g., Department of the Army, Food and Drug Administration) |
| `SUBTIER AGENCY` | 2 | Alternative designation for level-2 agencies in USAspending data |
| `MAJOR COMMAND` | 2 | Military major command (e.g., Army Materiel Command) |
| `SUB COMMAND` | 3 | Military sub-command within a major command |
| `OFFICE` | 3+ | Sub-agencies, bureaus, and offices at various depths within the hierarchy |

## Expansions

The following expansions are available via the `shape` query parameter to include related data:

### `parent`

Returns the immediate parent organization with the following fields:

- `key`
- `fh_key` (zero-padded)
- `name`
- `short_name`
- `type`
- `level`
- `is_active`
- `code`
- `cgac`

### `ancestors`

Returns a list of all ancestor organizations from the top-level department down to the immediate parent. Each ancestor includes:

- `fh_key` (zero-padded)
- `name`
- `short_name`
- `level`

### `children`

Returns a list of immediate child organizations (prefetched for performance). Each child includes:

- `key`
- `fh_key` (zero-padded)
- `name`
- `short_name`
- `type`
- `level`
- `is_active`
- `code`
- `cgac`

### `department`

Returns the L1 (department-level) ancestor formatted as a legacy-compatible department object. Returns `null` for level 1 organizations (a department doesn't have a department — it is one).

- `code` — the department's CGAC code
- `name` — the department's full name
- `abbreviation` — the department's short name / abbreviation

### `agency`

Returns the L2 (agency-level) ancestor formatted as a legacy-compatible agency object. Returns `null` for level 1 and 2 organizations (no agency above them).

- `code` — the agency's FPDS code
- `name` — the agency's full name
- `abbreviation` — the agency's short name (resolved via cached lookup)

## Data Sources

Organization data is consolidated from multiple authoritative sources with a priority-based system:

1. **Federal Hierarchy (SAM.gov)** - The authoritative source for federal organization structure. Provides the canonical hierarchy, names, and parent-child relationships.

2. **USAspending** - Supplements Federal Hierarchy with operational codes (CGAC, FPDS codes) and obligation data that appear in financial transactions.

3. **Legacy FPDS** - Historical organization data from the Federal Procurement Data System, used to backfill organizations that appear in contract transactions but are missing from current reference sources.

Higher-priority sources are never overwritten by lower-priority ones, ensuring that Federal Hierarchy's authoritative structure is preserved while other sources fill in the gaps.

## Example Response

The following example shows a default response (no `?shape=` parameter):

```json
{
  "key": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "fh_key": "100012345",
  "name": "Federal Emergency Management Agency",
  "short_name": "FEMA",
  "type": "AGENCY",
  "level": 2,
  "is_active": true,
  "code": "7022",
  "fpds_code": "7022",
  "cgac": "070",
  "canonical_code": "L2:7022",
  "parent_fh_key": "100000123",
  "full_parent_path_name": "Department of Homeland Security > Federal Emergency Management Agency"
}
```

The following example shows a shaped response with legacy-compatible expands:

```plaintext
/api/organizations/?shape=fh_key,name,type,department(code,name),agency(code,name,abbreviation)&level=3
```

```json
{
  "fh_key": "047020017",
  "name": "FA8219 AFLCMC EGLIN",
  "type": "OFFICE",
  "department": {
    "code": "097",
    "name": "DEPT OF DEFENSE"
  },
  "agency": {
    "code": "5700",
    "name": "DEPT OF THE AIR FORCE",
    "abbreviation": "USAF"
  }
}
```

## Usage Notes

- **Use `fh_key` for API lookups** — The Organizations API uses `fh_key` in the detail URL: `GET /api/organizations/{fh_key}/`. The `fh_key` is returned as a zero-padded 9-character string (e.g., `"000000001"`).
- **Use `key` as an internal identifier** — `key` is Tango's internal UUID primary key and may be useful for internal joins/debugging.
- **Use code lookups for transaction matching** — When matching contract or financial assistance transactions, use the specific code type (`cgac`, `fpds_code`, `code`) that appears in your source data.
- **Use the `search` parameter for discovery** — The `/api/organizations/?search=` endpoint handles abbreviations, acronyms, typos, and contextual queries (e.g., "Treasury OIG").
- **Level behavior for `department`/`agency` expands** — The `department` expand is defined for level 2+ orgs; the `agency` expand is defined for level 3+ orgs. When these expands are requested on orgs below the applicable level, the corresponding field is included in the response as `null` (no error is raised). Filter by `level` if you want to avoid `null` values in list responses.

---

<!-- doc: data-dictionary/otas.md -->

# OTAs Data Dictionary

This document describes the fields available in the OTAs API endpoints (`/api/otas/`). For filtering, ordering, and pagination, see the [OTAs API Reference](../api-reference/otas.md).

## Overview

OTAs (Other Transaction Agreements) are non-standard procurement instruments. OTAs are sourced from **FPDS only**.

## Identifiers

- `key` (canonical): The canonical award identifier derived from FPDS award keys. This is what the API uses for lookups: `GET /api/otas/{key}/`.
- `uuid` (internal): Tango’s deterministic internal UUID used as the database primary key and for stable internal joins/orderings. This is not intended as a public lookup key.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

OTA data is refreshed from FPDS **twice daily** by the Tango ingest pipeline.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | String | Canonical award identifier (FPDS). Used for detail lookups and cross-referencing. | FPDS |
| `piid` | String | Procurement Instrument Identifier. | FPDS |
| `award_date` | Date | Date the OTA was awarded. | FPDS |
| `psc_code` | String | Product/Service Code on the award. | FPDS |
| `total_contract_value` | Decimal | Total value. | FPDS |
| `base_and_exercised_options_value` | Decimal | Base plus exercised options. | FPDS |
| `fiscal_year` | Integer | Federal fiscal year. | FPDS |
| `obligated` | Decimal | Total obligated amount. | FPDS |
| `description` | String | OTA description/title. | FPDS |
| `award_type` | String | Award type code. Expandable to `{code, description}` via `?shape=`. | FPDS |
| `type_of_ot_agreement` | String | Type of OT agreement code. Expandable to `{code, description}` via `?shape=`. | FPDS |
| `extent_competed` | String | Extent competed code. Expandable to `{code, description}` via `?shape=`. | FPDS |
| `consortia` | String | Consortia indicator. | FPDS |
| `consortia_uei` | String | Consortia UEI. | FPDS |
| `dod_acquisition_program` | String | DoD acquisition program identifier. | FPDS |
| `non_governmental_dollars` | Decimal | Non-governmental dollars. | FPDS |
| `non_traditional_government_contractor_participation` | String | Non-traditional government contractor participation indicator. | FPDS |
| `parent_award_modification_number` | String | Parent award modification number. | FPDS |
| `transactions` | Array | Modification/transaction records. Use expansion for same fields as contracts. | FPDS |

## Expansions (via `shape`)

`recipient`, `place_of_performance`, `parent_award`, `awarding_office`, `funding_office`, `period_of_performance`, `award_type`, `type_of_ot_agreement`, `extent_competed`, `transactions`, `psc`.

## Data Sources

- **FPDS** – OTA actions, PIID, dates, values, types.
- **Tango** – Internal deterministic `uuid` and award materialization.

## Usage Notes

- **Use `key` for API references.** Use `piid` for FPDS lookups.

---

<!-- doc: data-dictionary/otidvs.md -->

# OTIDVs Data Dictionary

This document describes the fields available in the OTIDVs API endpoints (`/api/otidvs/`). For filtering, ordering, and pagination, see the [OTIDVs API Reference](../api-reference/otidvs.md).

## Overview

OTIDVs (Other Transaction IDVs) are indefinite-delivery Other Transaction vehicles. OTIDVs are sourced from **FPDS only**.

## Identifiers

- `key` (canonical): The canonical award identifier derived from FPDS award keys. This is what the API uses for lookups: `GET /api/otidvs/{key}/`.
- `uuid` (internal): Tango’s deterministic internal UUID used as the database primary key and for stable internal joins/orderings. This is not intended as a public lookup key.

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

OTIDV data is refreshed from FPDS **twice daily** by the Tango ingest pipeline.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | String | Canonical award identifier (FPDS). Used for detail lookups and cross-referencing. | FPDS |
| `piid` | String | Procurement Instrument Identifier. | FPDS |
| `award_date` | Date | Date the OTIDV was awarded. | FPDS |
| `psc_code` | String | Product/Service Code on the award. | FPDS |
| `total_contract_value` | Decimal | Total value. | FPDS |
| `base_and_exercised_options_value` | Decimal | Base plus exercised options. | FPDS |
| `fiscal_year` | Integer | Federal fiscal year. | FPDS |
| `obligated` | Decimal | Total obligated amount. | FPDS |
| `description` | String | OTIDV description/title. | FPDS |
| `idv_type` | Object | IDV type. | FPDS |
| `consortia` | String | Consortia indicator. | FPDS |
| `consortia_uei` | String | Consortia UEI. | FPDS |
| `dod_acquisition_program` | String | DoD acquisition program identifier. | FPDS |
| `non_governmental_dollars` | Decimal | Non-governmental dollars. | FPDS |
| `non_traditional_government_contractor_participation` | String | Non-traditional government contractor participation indicator. | FPDS |
| `type_of_ot_agreement` | String | Type of OT agreement code. Expandable to `{code, description}` via `?shape=`. | FPDS |
| `extent_competed` | String | Extent competed code. Expandable to `{code, description}` via `?shape=`. | FPDS |
| `transactions` | Array | Modification/transaction records. Use expansion for same fields as contracts. | FPDS |

## Expansions (via `shape`)

`recipient`, `place_of_performance`, `awarding_office`, `funding_office`, `period_of_performance`, `type_of_ot_agreement`, `extent_competed`, `transactions`, `psc`.

## Data Sources

- **FPDS** – OTIDV actions, PIID, dates, values, types.
- **Tango** – Internal deterministic `uuid` and award materialization.

## Usage Notes

- **Use `key` for API references.** Use `piid` for FPDS lookups.
- **Child awards:** Use `/api/otidvs/{key}/awards/` for orders under this OTIDV; see [OTIDVs API Reference](../api-reference/otidvs.md).

---

<!-- doc: data-dictionary/protests.md -->

# Protests Data Dictionary

This document describes the fields available in the Protests API endpoints (`/api/protests/`). For filtering, ordering, and pagination, see the [Protests API Reference](../api-reference/protests.md).

## Overview

Protests are bid protest records from GAO and COFC (Court of Federal Claims). The API exposes a narrow set of client-facing fields. Source ingest payload details and internal fields (e.g. `external_id`, `data_quality`) are not exposed. Filter by `source_system` (`gao` or `cofc`) to scope results to a single venue.

**Authentication**: Protests endpoints require authentication (API key or OAuth2).

**Date range**: The API only serves protests from **2015 onward**. Pre-2015 records exist in the database but are not available through the API.

**Note**: Both **list** and **detail** endpoints return case-level objects. Each case is identified by `case_id` (a deterministic UUID from `source_system` + `base_case_number`). Detail lookup uses `case_id` in the URL path: `GET /api/protests/{case_id}/`. Use `?shape=...,dockets(...)` to expand nested docket records. The default (unshaped) response does not include dockets.

## Update Frequency

GAO and COFC protest data is refreshed by loader schedule and available source files. COFC data is sourced from CourtListener (dockets and opinions).

## Fields (API response)

Only the following fields are returned in list and detail responses (and are available via `shape`):

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `case_id` | UUID | Deterministic case UUID derived from `source_system` + `base_case_number`. Use for detail lookup: `GET /api/protests/{case_id}/`. | Tango |
| `source_system` | String | Source venue identifier (for example `gao`). | Tango |
| `case_number` | String | Base case for grouping sub-dockets (for example `b-424214` from `B-424214.1`). Model field: `base_case_number`. Use with `decision_date` for decision-level counting. | Derived |
| `docket_number` | String | Source docket identifier (for example `b-424214.1`, `b-424214.2`). Model field: `case_number`. Only available inside the `dockets(...)` expansion. Use to distinguish sub-dockets under the same `case_number`. | Source |
| `title` | String | Protest title. | Source |
| `protester` | String | Protester name. | Source |
| `agency` | String | Protested agency. | Source |
| `solicitation_number` | String | Solicitation number when provided. | Source |
| `case_type` | String | Protest case type. | Source |
| `outcome` | String | Protest outcome, when available. | Source |
| `filed_date` | Date | Date the protest was filed. | Source |
| `posted_date` | Date | Date the protest was posted. | Source |
| `decision_date` | Date | Decision date, when available. | Source |
| `due_date` | Date | Protest due date, when available. | Source |
| `docket_url` | String | Source docket URL. | Source |
| `decision_url` | String | Source decision URL, when available. | Source |

**Opt-in via shape only:** `digest` — when requested with `?shape=...,digest`, the value from `raw_data.digest` (e.g. decision summary text) is returned. Not included in the default list/detail response.

## Resolution Fields (shape expansions)

Protests support entity and organization resolution via the Bayesian resolver. These fields are available as shape expansions and return `null` when no confident match exists.

### `resolved_protester(...)` — Entity Resolution

| Field | Type | Description |
| ----- | ---- | ----------- |
| `uei` | String | Unique Entity Identifier of the matched entity. |
| `name` | String | Display name of the matched entity. |
| `match_confidence` | String | `"confident"` (high confidence, auto-linkable) or `"review"` (medium confidence, needs human review). |
| `rationale` | String | Human-readable explanation (e.g. `"Exact name match"`, `"Similar name match; multiple entities share this name"`). |

### `resolved_agency(...)` — Organization Resolution

| Field | Type | Description |
| ----- | ---- | ----------- |
| `key` | String (UUID) | UUID primary key of the matched organization in Tango. |
| `name` | String | Display name of the matched organization. |
| `match_confidence` | String | `"confident"` or `"review"`. |
| `rationale` | String | Human-readable explanation (e.g. `"Exact name match; parent agency confirmed"`). |

**Confidence mapping**: Internal resolution tiers map to public labels as follows: `high` → `"confident"`, `medium` → `"review"`. Low-confidence and no-match results are excluded (return `null`).

The following are **not** exposed in the API: `external_id`, `data_quality`, `source_last_updated`, `created`, `modified`, `raw_data`, `field_provenance`.

## Data Quality (internal)

Every protest record has an internal `data_quality` tier (not exposed in the API response). Records with `quarantined` or `unknown` quality are **always excluded from API responses**. Quality is recomputed on each ingestion cycle.

## Shaping

Both list and detail return case-level objects. `shape` lets you request a subset of fields. Use the `dockets(...)` expansion to include nested docket records.

Allowed shape fields (case-level):

- `case_id`, `source_system`, `case_number`, `title`, `protester`, `agency`
- `solicitation_number`, `case_type`, `outcome`
- `filed_date`, `posted_date`, `decision_date`, `due_date`
- `docket_url`, `decision_url`
- `digest` (from `raw_data.digest`; opt-in only)
- `dockets` (expand with docket fields, e.g. `dockets(case_number,docket_number,filed_date)`)
- `resolved_protester` (expand: `uei`, `name`, `match_confidence`, `rationale`)
- `resolved_agency` (expand: `key`, `name`, `match_confidence`, `rationale`)

Inside `dockets(...)`, these additional fields are available:

- `docket_number` (specific sub-docket identifier, e.g. `b-424214.1`)

Examples:

```bash
# List: case-level and nested dockets
GET /api/protests/?shape=case_id,case_number,title,dockets(docket_number,filed_date,outcome)

# List: case-level only
GET /api/protests/?shape=case_id,title,outcome,decision_date

# Detail with nested dockets
GET /api/protests/{case_id}/?shape=case_id,title,dockets(docket_number,filed_date)
```

`raw_data` and field-level provenance metadata are internal-only and not exposed in API responses.

## Data Sources

- **GAO** – primary source for protest records.
- **Tango** – `case_id` (deterministic case UUID) and normalized API behavior.

---

<!-- doc: data-dictionary/psc.md -->

# Product and Service Code (PSC) Data Dictionary

This document describes the fields available in the Product and Service Code (PSC) API endpoints (`/api/psc/`). For filtering, ordering, and pagination, see the [PSC API Reference](../api-reference/psc.md).

## External references

- [Product and Service Code Manual](https://www.acquisition.gov/psc-manual)

## Overview

Product and Service Codes (PSC) classify products and services in federal contracting. Each code has a `code` (string) and a current description; historical descriptions are available when requesting a “complete” representation.

**Note**: This endpoint supports [response shaping](../guides/patterns/response-shaping.md) via the `shape` query parameter. Expansions include `current(...)` for the active description and `historical(...)` for all descriptions.

## Update Frequency

PSC reference data is updated when the PSC Manual or Tango loaders are refreshed (periodic).

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `code` | String | PSC code (primary key). | PSC Manual |
| `current` | Object | Full current description record (`name`, `description`, dates, includes/excludes). Present on list/detail responses. | PSC Manual |
| `historical` | Array | Historical descriptions. Present only when `?complete=true`. | PSC Manual |
| `parent` | String | Parent PSC code in hierarchy. | PSC Manual |
| `category` | String | Category (e.g., `S` Service, `P` Product). | PSC Manual |
| `level_1_category` | String | Level 1 category name. | PSC Manual |
| `level_1_category_code` | Integer | Level 1 category code. | PSC Manual |
| `level_2_category` | String | Level 2 category name. | PSC Manual |
| `level_2_category_code` | String | Level 2 category code. | PSC Manual |

**Metrics:** Time-series obligation metrics are available at `/api/psc/{code}/metrics/<months>/<period_grouping>/`; see [Metrics](../api-reference/metrics.md).

## Data Sources

- **PSC Manual (acquisition.gov)** – Code definitions, descriptions, hierarchy.
- **Tango** – Obligation rollups (via metrics endpoint).

## Usage Notes

- **Use `code` for lookups.** Filter by `has_awards` to restrict to PSCs that appear in award data; see [PSC API Reference](../api-reference/psc.md).

---

<!-- doc: data-dictionary/subawards.md -->

# Subawards Data Dictionary

This document describes the fields available in the Subawards API endpoints (`/api/subawards/`). For filtering, ordering, and pagination, see the [Subawards API Reference](../api-reference/subawards.md).

## Overview

Subawards are sub-contracts or sub-grants under a prime award, sourced from USAspending/FSRS. Each subaward has an auto-incrementing integer `key`. Data links the prime award (contract/assistance) to the subrecipient and amounts.

## Identifiers

- `key` (subaward): Tango’s primary key for the subaward record (integer). This is what the API uses for lookups: `GET /api/subawards/{key}/`.
- `award_key` (prime award, canonical): Canonical identifier of the prime award (e.g., contract/IDV) using the same award-key convention as `/api/contracts/{key}/` and `/api/idvs/{key}/`.
- `award_uuid` (prime award, internal): When available, Tango’s internal deterministic UUID for the prime award (used for resilient joins/backfills).

**Note**: The full set of fields is available via the `shape` query parameter. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Subaward data is refreshed from USAspending **daily** by the Tango ingest pipeline.

## Fields

| Field | Type | Description | Source |
| ----- | ---- | ----------- | ------ |
| `key` | Integer | Primary identifier for the subaward record in Tango (auto-increment). | Tango |
| `award_key` | String | Canonical identifier of the prime award (award-key convention). | USAspending / Tango |
| `piid` | String | Prime award PIID (contract) or equivalent. | USAspending |
| `prime_award_total_outlayed_amount` | Decimal | Total outlayed on the prime award. | USAspending |
| `prime_award_amount` | Decimal | Prime award amount. | USAspending |
| `prime_award_base_action_date` | String | Prime award base action date (`YYYY-MM-DD` date string). | USAspending |
| `prime_award_base_action_date_fiscal_year` | Integer | Fiscal year of base action. | Tango (derived) |
| `prime_award_latest_action_date` | String | Latest action date on prime (`YYYY-MM-DD` date string). | USAspending |
| `prime_award_latest_action_date_fiscal_year` | Integer | Fiscal year of latest action. | Tango (derived) |
| `prime_award_base_transaction_description` | String | Prime award transaction description. | USAspending |
| `prime_award_project_title` | String | Prime award project title. | USAspending |
| `prime_award_naics_code` | Integer | NAICS on prime award. | USAspending |
| `prime_award_naics_description` | String | NAICS description. | USAspending |
| `prime_awardee_uei` | String | Prime recipient UEI. | USAspending |
| `prime_awardee_name` | String | Prime recipient name. | USAspending |
| `prime_awardee_parent_uei` | String | Prime recipient parent UEI. | USAspending |
| `prime_awardee_parent_name` | String | Prime recipient parent name. | USAspending |
| `subawardee_uei` | String | Subrecipient UEI. | USAspending |
| `subawardee_duns` | String | Subrecipient DUNS (legacy). | USAspending |
| `subawardee_name` | String | Subrecipient name. | USAspending |
| `subawardee_dba_name` | String | Subrecipient DBA name. | USAspending |
| `subawardee_parent_uei` | String | Subrecipient parent UEI. | USAspending |
| `subawardee_parent_duns` | String | Subrecipient parent DUNS. | USAspending |
| `subawardee_parent_name` | String | Subrecipient parent name. | USAspending |
| `subawardee_business_types` | String | Subrecipient business types (comma-separated codes). | USAspending |
| `usaspending_permalink` | String | Link to USAspending subaward. | USAspending |

## Expansions (via `shape`)

- `subaward_details` – `description`, `type`, `number`, `amount`, `action_date`, `fiscal_year`
- `fsrs_details` – `last_modified_date`, `id`, `year`, `month`
- `place_of_performance` – `city`, `state`, `zip`, `country_code`
- `highly_compensated_officers` – `name`, `amount`
- `prime_recipient` – `uei`, `display_name` (and other recipient fields)
- `subaward_recipient` – `uei`, `display_name`, `duns`
- `awarding_office` / `funding_office` – office fields

## Data Sources

- **USAspending** – Subaward and prime award data, FSRS.
- **Tango** – `key`, `award_key`, `award_uuid`, and fiscal year derivations/joins.

## Usage Notes

- **Use `key` for subaward lookups.** Use `award_key` to link to the prime contract/award record.
- **Scoped lists:** Use `/api/contracts/{key}/subawards/` for subawards under a contract; see [Subawards API Reference](../api-reference/subawards.md).

---

<!-- doc: data-dictionary/vehicles.md -->

# Vehicles Data Dictionary

This document describes the fields available in the Vehicles API endpoints (`/api/vehicles/`). For filtering, ordering, and pagination, see the [Vehicles API Reference](../api-reference/vehicles.md).

## Overview

Vehicles provide a solicitation-centric grouping of multiple IDVs. Each vehicle represents a unique \(`solicitation_identifier`, agency identifier\) pair and can expand into the underlying IDVs via response shaping.

>In federal data, *each IDV award is a vehicle*.
>In practice, people often think of a "vehicle" as the *solicitation that produced many IDV awards* (one per awardee). We model that higher-level grouping explicitly.

**Note**: The full set of fields is available via the `shape` query parameter. The base response includes a subset of commonly-used fields. See [Response Shaping](../guides/patterns/response-shaping.md) for details.

## Update Frequency

Vehicle data is refreshed periodically by the Tango ingest pipeline.

## Fields

| Field | Type | Description | Source |
| ------- | ------ | ------------- | -------- |
| `uuid` | UUID | Stable identifier for the vehicle in Tango. Deterministic from `solicitation_identifier` + agency identifier derived from the IDV `key`. | Tango |
| `solicitation_identifier` | String | Solicitation identifier shared by the underlying IDVs. | FPDS (via awards) |
| `is_synthetic_solicitation` | Boolean | `true` for GWAC-orphan vehicles whose solicitation identifier was recovered via the `ACRO:` prefix convention. | Tango |
| `agency_id` | String | Agency identifier derived from the IDV award `key` suffix (award-key convention). | Tango |
| `program_acronym` | String | Program acronym (e.g., `ALLIANT`, `OASIS`) when present. | Tango |
| `organization_id` | UUID | Best-effort mapping to a canonical Tango Organization, for consistent agency joins/filtering. | Tango (derived) |
| `organization` | Object | Live awarding-org snapshot: `{organization_id, office_code, office_name, agency_code, agency_name, department_code, department_name}`. Resolved at request time via the office cache. Select as a leaf token (`shape=...,organization`); not currently sub-selectable. | Tango |
| `vehicle_type` | Object | Aggregated IDV type for the vehicle, derived from underlying IDVs. Returned as `{code, description}` (e.g., `{code: "A", description: "GWAC"}`). | FPDS (via awards) |
| `who_can_use` | Object | Choice indicating which agencies may order against the vehicle. Returned as `{code, description}`. | FPDS (via awards) |
| `type_of_idc` | Object | Aggregated IDC type for the vehicle. Returned as `{code, description}` (e.g., `{code: "A", description: "Indefinite Delivery / Requirements"}`). | FPDS (via awards) |
| `contract_type` | Object | Aggregated contract type for the vehicle. Returned as `{code, description}` (e.g., `{code: "J", description: "Firm Fixed Price"}`). | FPDS (via awards) |
| `description` | String | Longest common substring across the vehicle's IDV descriptions. | FPDS (via awards) |
| `descriptions` | Array[String] | Set of distinct descriptions found across the underlying IDVs. | FPDS (via awards) |
| `fiscal_year` | Integer | Representative fiscal year for the vehicle (derived from the underlying IDVs). | FPDS (via awards) |
| `award_date` | Date | Earliest IDV award date associated with the vehicle. | FPDS (via awards) |
| `latest_award_date` | Date | Most recent IDV award date associated with the vehicle. | Tango |
| `last_date_to_order` | Date | Latest order-by date across the vehicle's IDVs. | FPDS (via awards) |
| `idv_count` | Integer | Number of IDV awards in the vehicle (precomputed rollup). | Tango |
| `awardee_count` | Integer | Number of unique awardees across the vehicle's IDVs. | Tango |
| `order_count` | Integer | Total number of child awards/orders under the vehicle's IDVs (precomputed rollup). | Tango |
| `total_obligated` | Decimal | Sum of obligations across child awards/orders (precomputed rollup). | Tango |
| `vehicle_obligations` | Decimal | Sum of obligations across child awards/orders under the vehicle's IDVs (computed at query time). | Tango |
| `vehicle_contracts_value` | Decimal | Sum of total contract value across child awards/orders under the vehicle's IDVs (computed at query time). | Tango |
| `solicitation_title` | String | Title of the solicitation from the latest SAM.gov notice (via linked Opportunity). | SAM.gov (via opportunities) |
| `solicitation_description` | String | Description of the solicitation from the latest SAM.gov notice (via linked Opportunity). | SAM.gov (via opportunities) |
| `solicitation_date` | Date | First notice date from the linked SAM.gov Opportunity. | SAM.gov (via opportunities) |
| `opportunity_id` | UUID | Foreign key to the linked SAM.gov Opportunity (if any). | Tango |
| `naics_code` | Integer | NAICS code from the linked SAM.gov Opportunity. | SAM.gov (via opportunities) |
| `psc_code` | String | Product/Service Code from the linked SAM.gov Opportunity. | SAM.gov (via opportunities) |
| `set_aside` | String | Set-aside type from the linked SAM.gov Opportunity. | SAM.gov (via opportunities) |

### Legacy fields

The following fields are kept on the API for backward compatibility — recomputed from the underlying IDVs at request time. They are **not** formally deprecated today (no `Deprecation` response header, no sunset date) because there are no first-class replacements yet for `agency_details.funding_office` or for the unique `competition_details` keys (`extent_competed`, `set_aside`, `solicitation_procedures`, `number_of_offers_received`). Where a replacement exists, it's listed below.

| Field | Type | Notes / partial replacement |
| ----- | ---- | --------------------------- |
| `agency_details` | Object | Awarding side is covered by the live `organization` object. Funding side (`agency_details.funding_office`) has no replacement at the vehicle grain — aggregate from `/api/vehicles/<uuid>/orders/` (each order carries `funding_organization_id`) for funding analysis. |
| `competition_details` | Object | Competition fields are also surfaced under the underlying IDVs (see `awardees(...)`). |
| `opportunity` (expansion) | Object | Top-level `solicitation_*` fields and `opportunity_id` cover the common cases without a join. The full `opportunity(...)` expansion remains supported with the complete Opportunity field/expand surface (restored to pre-cutover parity). |

## Expansions (via `shape`)

### `awardees`

Expands to the underlying IDVs that make up the vehicle. Each IDV in the response carries vehicle-specific rollups: `order_count`, `idv_obligations`, `idv_contracts_value`. Supports nested `orders(...)` to expand each IDV's task orders.

### `metrics`

Bundles 12 computed metrics under a single nested object. Available fields:

| Field | Type | Description |
| ----- | ---- | ----------- |
| `avg_offers_received` | Float | Average number of offers received across the vehicle's IDVs. |
| `award_concentration_hhi` | Float | Herfindahl-Hirschman Index for award concentration across awardees. |
| `order_concentration_hhi` | Float | HHI for task-order concentration across recipients. |
| `competed_rate` | Float | Fraction of obligations that were competed. |
| `using_agency_count` | Integer | Number of distinct agencies that have ordered against the vehicle. |
| `avg_order_value` | Float | Average task-order obligated amount. |
| `max_order_value` | Float | Largest single task-order obligated amount. |
| `top_recipient_share` | Float | Share of obligations going to the largest single recipient. |
| `recent_obligations_24mo` | Float | Sum of obligations in the trailing 24 months. |
| `recent_orders_24mo` | Integer | Count of task orders in the trailing 24 months. |
| `days_since_last_order` | Integer | Days since the most recent task order. |
| `obligation_to_ceiling_ratio` | Float | Obligated-to-ceiling ratio (when ceiling is known). |

Use `?shape=...,metrics(*)` to include all of them, or pick a subset (e.g. `metrics(competed_rate,top_recipient_share)`).

### `organization`

The canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). Both forms work and return the same payload:

- `?shape=...,organization` — bare leaf
- `?shape=...,organization(*)` — expand (also supports field selection, e.g. `organization(agency_name,office_name)`)

Example:

```bash
/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,organization(*),metrics(*),awardees(key,uuid,piid,recipient(display_name,uei))
```

## Sub-endpoints

### `/api/vehicles/<vehicle-uuid>/awardees/`

Returns the IDV awardees for the vehicle, with vehicle-specific rollups (`order_count`, `idv_obligations`, `idv_contracts_value`) appended. Supports the `awardees`/IDV shaping vocabulary.

### `/api/vehicles/<vehicle-uuid>/orders/`

Returns task orders (child contracts) under the vehicle's IDVs. Optimized for fast pagination over very large vehicles.

Supports `?ordering=...` with `award_date` (default), `obligated`, or `total_contract_value`. Prefix with `-` for descending.

## Search

- **Vehicle list search**: `GET /api/vehicles/?search=...` searches across vehicle metadata and aggregated award search terms via full-text search.
- **Awardee-within-vehicle filtering**: `GET /api/vehicles/<vehicle-uuid>/?shape=...,awardees(...)&search=...` filters the expanded awardees (the vehicle itself is not filtered on detail).

## Ordering

The list endpoint accepts `?ordering=` with one of `vehicle_obligations`, `latest_award_date`, `awardee_count`, `vehicle_contracts_value`, `total_obligated`, `award_date`, `last_date_to_order`, `fiscal_year`, `idv_count`, or `order_count`. Prefix with `-` for descending; combine with commas for multi-field sort. See [Vehicles API Reference — Ordering](../api-reference/vehicles.md#ordering) for usage notes.

---

<!-- doc: getting-started/authentication.md -->

# Authentication

The Tango API supports multiple authentication methods to suit different use cases and security requirements.

## Authentication Methods

### 1. API Keys (Recommended)

API keys are the simplest and most secure method for server-to-server integration.

#### Getting an API Key

1. Visit [Tango Web Interface](https://tango.makegov.com)
2. Sign up for an account or log in
3. Navigate to your [account profile](https://tango.makegov.com/accounts/profile/)
4. Copy your API key (keep it secure!)

#### Using API Keys

Include your API key in the `X-API-KEY` header with every request:

```bash
curl -H "X-API-KEY: your-api-key-here" \
  "https://tango.makegov.com/api/contracts/"
```

```python
import httpx

headers = {'X-API-KEY': 'your-api-key-here'}
response = httpx.get(
    'https://tango.makegov.com/api/contracts/',
    headers=headers
)
```

```javascript
const response = await fetch('https://tango.makegov.com/api/contracts/', {
  headers: {
    'X-API-KEY': 'your-api-key-here'
  }
});
```

### 2. OAuth2

OAuth2 is recommended for web applications and user-specific integrations.

#### OAuth2 Flow

1. **Register your application** in the [Tango web interface](https://tango.makegov.com/o/applications/)
2. **Get client credentials** (client ID and secret)
3. **Implement OAuth2 flow** in your application
4. **Use access tokens** for API requests

#### Example OAuth2 Implementation

```python
import requests
from requests_oauthlib import OAuth2Session

# OAuth2 configuration
client_id = 'your-client-id'
client_secret = 'your-client-secret'
authorization_base_url = 'https://tango.makegov.com/o/authorize/'
token_url = 'https://tango.makegov.com/o/token/'

# Create OAuth2 session
oauth = OAuth2Session(client_id)

# Get authorization URL
authorization_url, state = oauth.authorization_url(authorization_base_url)

# Redirect user to authorization_url
print(f"Please go to {authorization_url} and authorize access")

# After authorization, get the authorization response URL
authorization_response = input('Enter the full callback URL: ')

# Fetch the access token
token = oauth.fetch_token(
    token_url,
    authorization_response=authorization_response,
    client_secret=client_secret
)

# Use the token for API requests
response = oauth.get('https://tango.makegov.com/api/contracts/')
```

#### OAuth2 Endpoints

| Endpoint | Path | Purpose |
|----------|------|---------|
| Authorization | `https://tango.makegov.com/o/authorize/` | Authorization Code flow start |
| Token | `https://tango.makegov.com/o/token/` | Exchange code for access token; client credentials |
| Refresh | `https://tango.makegov.com/o/token/` | Refresh an access token (same path; use `grant_type=refresh_token`) |

Supported grant types: `authorization_code`, `client_credentials`, `refresh_token`.

#### OAuth2 Scopes

Available scopes for OAuth2 applications:

- `read` - Read access to all data

## Monitoring Usage

### Response Headers

Check these headers to monitor your API usage:

```bash
curl -I -H "X-API-KEY: your-api-key-here" \
  "https://tango.makegov.com/api/contracts/"
```

Response headers:

```plaintext
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 45
X-Execution-Time: 0.045s
```

### Rate Limit Headers

- `X-RateLimit-Limit`: Total requests allowed for the most restrictive window
- `X-RateLimit-Remaining`: Requests remaining in the most restrictive window
- `X-RateLimit-Reset`: **Seconds until reset** for the most restrictive window
- `X-Execution-Time`: Request execution time

For the full list of per-window headers (daily/burst) and practical retry guidance, see the [Rate limits guide](../guides/patterns/rate-limits.md).

## Error Handling

### Authentication Errors

#### 401 Unauthorized

```json
{
  "detail": "Authentication credentials were not provided."
}
```

**Causes:**

- Missing API key
- Invalid API key
- Expired API key
- Inactive API key

**Solutions:**

- Check that you're including the `X-API-KEY` header
- Verify your API key is correct
- Ensure your API key is active
- Generate a new API key if needed

#### 403 Forbidden

```json
{
  "error": "InsufficientPermissions",
  "message": "You don't have permission to access this resource",
  "code": 403
}
```

**Causes:**

- Insufficient permissions for the requested resource
- Account type doesn't support the requested feature

**Solutions:**

- Check your account type and permissions
- Contact support to upgrade your account if needed

### Rate Limit Errors

#### 429 Too Many Requests

```json
{
  "detail": "Rate limit exceeded for burst. Please try again in 45 seconds.",
  "wait_in_seconds": 45
}
```

**Solutions:**

- Wait for the rate limit window to reset
- Implement exponential backoff in your application
- Consider upgrading your account for higher limits
- Optimize your requests to reduce frequency

See the [Rate limits guide](../guides/patterns/rate-limits.md) for header semantics, examples (curl/Python/JS), and recommended client behavior.

---

<!-- doc: getting-started/pricing.md -->

# Plans & Data Access

All Tango API plans include full access to raw federal procurement data from FPDS, SAM.gov, USAspending, Grants.gov, and agency feeds. Paid plans unlock Tango-computed enrichments and higher rate limits.

For current pricing, see the [pricing page](https://tango.makegov.com/subscriptions/pricing/).

## What's included in every plan

Every plan -- including Free -- provides complete access to:

- **Contracts, IDVs, OTAs, OTIDVs** -- all FPDS award data with full transaction history
- **Vehicles** -- solicitation-centric groupings of IDVs, with metrics, awardees, and task orders
- **Subawards** -- USAspending/FSRS subcontract data
- **Entities** -- SAM.gov vendor/recipient records (UEI, addresses, business types, NAICS codes, ownership)
- **Opportunities & Notices** -- SAM.gov solicitations with attachments, contacts, and history
- **Grants** -- Grants.gov opportunities
- **Forecasts** -- Agency procurement forecasts
- **Protests** -- GAO and COFC bid protest cases with dockets and decisions
- **IT Dashboard** -- Federal IT investment data from itdashboard.gov, including all expansion data (CIO evaluations, contracts, projects, performance metrics, funding sources). Only the full `business_case_html` blob is gated.
- **Agencies, Departments, Offices, Organizations** -- Full government organization hierarchy
- **Response shaping** -- Request only the fields you need via `?shape=`
- **Full-text search** -- Rich filtering across most endpoints (some advanced search filters require a paid plan)

## What paid plans add

Paid plans (Micro and above) unlock **Tango-computed enrichments** -- fields that Tango derives, resolves, or extracts from raw data. These are not available from the underlying public data sources directly.

### Enriched data fields (Micro and above)

| Resource | Gated fields | What they provide |
|----------|-------------|-------------------|
| **Entities** | `past_performance(*)` | Award history aggregated from FPDS -- total contracts, dollar values, agency breakdown |
| **Entities** | `relationships(*)` | Corporate hierarchy resolved from SAM.gov entity graph |
| **Opportunities** | `attachments(extracted_text)` | Full text extracted (OCR) from solicitation attachments |
| **Notices** | `attachments(extracted_text)` | Full text extracted (OCR) from notice attachments |
| **Protests** | `resolved_protester(*)` | Protester name resolved to a SAM.gov entity via Bayesian matching |
| **Protests** | `resolved_agency(*)` | Agency name resolved to a Tango organization record via Bayesian matching |
| **IT Dashboard** | `business_case_html` | Full HTML of the investment's business case page (Business+ plans only — Medium and above) |

### CALC labor rates (Micro and above)

The `/api/lcats/` endpoint provides GSA CALC labor category rate data. This entire endpoint requires a paid plan.

### Identifier validation (Micro and above)

The [`POST /api/validate/`](../api-reference/validate.md) endpoint validates the format of a PIID, solicitation number, or UEI. Available on Pro+ plans (Micro and above); Free-tier callers receive HTTP 403.

### How field gating works

When you request a gated field on a Free plan, the API:

1. Returns a `200 OK` response (not an error)
2. Omits the gated field from the response
3. Includes a `meta.upgrade_hints` object listing which fields were omitted

```json
{
  "results": [
    {
      "uei": "ABC123",
      "display_name": "Example Corp"
    }
  ],
  "meta": {
    "upgrade_hints": {
      "message": "Some requested fields were omitted because they require a higher tier subscription.",
      "fields": [
        {"field": "past_performance", "required_tier": "Micro"}
      ],
      "upgrade_url": "https://tango.makegov.com/docs/pricing/"
    }
  }
}
```

You can always check which fields are available by inspecting the `meta.upgrade_hints` in your response.

## Rate limits by plan

Each plan has both a **daily** quota (resets at UTC midnight) and a **burst** rate (per-minute). The most-restrictive window is what limits a given burst of activity. Headers (`X-RateLimit-Daily-*`, `X-RateLimit-Burst-*`) and retry guidance are documented in [Rate limits](../guides/patterns/rate-limits.md).

| Plan | Daily | Burst |
|------|------:|------:|
| **Free** | 100 / day | 25 / min |
| **Micro** | 250 / day | 25 / min |
| **Small** | 1,500 / day | 100 / min |
| **Medium** | 7,500 / day | 100 / min |
| **Large** | 25,000 / day | 100 / min |
| **Enterprise** | custom | custom |

For Enterprise limits, [contact sales](mailto:sales@makegov.com).

## Webhook subscriptions by plan

| Plan | Webhook subscriptions | Custom cron |
|------|-------------------|-------------|
| **Free** | 1 | -- |
| **Micro** | 3 | Yes |
| **Small** | 5 | Yes |
| **Medium** | 10 | Yes |
| **Large** | 25 | Yes |
| **Enterprise** | custom | Yes |

Webhook subscriptions monitor resources by query filters (agency, NAICS, search terms, etc.) and POST `alerts.*.match` events to your endpoint. See the [Webhooks guide](../webhooks/index.md) for details.

## Upgrading your plan

You can upgrade or downgrade at any time from the [pricing page](https://tango.makegov.com/subscriptions/pricing/). Changes take effect immediately with prorated billing. Annual plans save 20%.

For Enterprise plans, [contact sales](mailto:sales@makegov.com).

---

<!-- doc: getting-started/quick-start.md -->

# Quick Start Guide

Get up and running with the Tango API in 5 minutes or less.

## Prerequisites

- Basic knowledge of HTTP and JSON
- A web browser or API client (like Postman, Insomnia, or curl)
- Optional: Programming language of your choice (Python, JavaScript, etc.)

## Step 1: Get Your API Key

1. Visit [Tango Web Interface](https://tango.makegov.com)
2. Sign up for an account or log in
3. Navigate to your API keys section
4. Create a new API key
5. Copy your API key (you'll need it for all requests)

> **Note**: The API requires authentication for all access. Anonymous access is not supported. You'll need an API key for any usage.

## Step 2: Make Your First API Call

Pull the five most recently awarded federal contracts:

=== "curl"

    ```bash
    curl -H "X-API-KEY: your-api-key-here" \
      "https://tango.makegov.com/api/contracts/?limit=5&ordering=-award_date"
    ```

=== "Python"

    ```python
    import requests

    headers = {"X-API-KEY": "your-api-key-here"}
    response = requests.get(
        "https://tango.makegov.com/api/contracts/",
        params={"limit": 5, "ordering": "-award_date"},
        headers=headers,
    )

    data = response.json()
    for contract in data["results"]:
        print(contract["piid"], contract["recipient"]["display_name"], contract["obligated"])
    ```

=== "Node"

    ```javascript
    const params = new URLSearchParams({ limit: "5", ordering: "-award_date" });
    const response = await fetch(`https://tango.makegov.com/api/contracts/?${params}`, {
      headers: { "X-API-KEY": "your-api-key-here" },
    });

    const data = await response.json();
    for (const contract of data.results) {
      console.log(contract.piid, contract.recipient.display_name, contract.obligated);
    }
    ```

## Step 3: Explore the Response

You'll get a paginated response shaped like this:

```json
{
  "count": 9824315,
  "next": "https://tango.makegov.com/api/contracts/?limit=5&ordering=-award_date&cursor=cD0yMDI2LTA1LTEx",
  "previous": null,
  "results": [
    {
      "key": "CONT_AWD_47QSWA24P0BWF_4732_-NONE-_-NONE-",
      "piid": "47QSWA24P0BWF",
      "recipient": {
        "display_name": "ACME Corporation",
        "uei": "ZMXAHH8M8VL8"
      },
      "award_date": "2026-05-11",
      "obligated": 1500000.00,
      "description": "IT Services Contract"
    }
  ]
}
```

## Step 4: Try a More Complex Query

Now let's find active opportunities in a specific industry and agency:

=== "curl"

    ```bash
    curl -H "X-API-KEY: your-api-key-here" \
      "https://tango.makegov.com/api/opportunities/?naics=541512&active=true&agency=DOD&ordering=response_deadline&limit=5"
    ```

=== "Python"

    ```python
    import requests

    headers = {"X-API-KEY": "your-api-key-here"}
    response = requests.get(
        "https://tango.makegov.com/api/opportunities/",
        params={
            "naics": "541512",
            "active": "true",
            "agency": "DOD",
            "ordering": "response_deadline",
            "limit": 5,
        },
        headers=headers,
    )
    for opp in response.json()["results"]:
        print(opp["response_deadline"], opp["opportunity_id"], opp["title"])
    ```

=== "Node"

    ```javascript
    const params = new URLSearchParams({
      naics: "541512", active: "true", agency: "DOD",
      ordering: "response_deadline", limit: "5",
    });
    const response = await fetch(`https://tango.makegov.com/api/opportunities/?${params}`, {
      headers: { "X-API-KEY": "your-api-key-here" },
    });
    for (const opp of (await response.json()).results) {
      console.log(opp.response_deadline, opp.opportunity_id, opp.title);
    }
    ```

This query:

- Filters by NAICS `541512` (Computer Systems Design Services)
- Restricts to DOD opportunities
- Returns only currently-biddable opportunities (`active=true`)
- Orders by response deadline (soonest first)
- Limits to 5 results

For more on multi-value filters, agency resolution, and webhook subscriptions for new opportunities, see the [Search opportunities by NAICS](../guides/how-to/search-opportunities-naics.md) guide.

## Step 5: Understand the Response Format

Most list endpoints return paginated responses. The `count` may be exact or approximate depending on the endpoint (see [Result counts](../guides/patterns/result-counts.md)) — check the `X-Results-CountType` response header to know which:

```json
{
  "count": 1250,
  "next": "https://tango.makegov.com/api/opportunities/?...&page=2",
  "previous": null,
  "results": [
    {
      "opportunity_id": "75D30126R00012",
      "title": "Cyber Workforce Development Services",
      "naics_code": 541512,
      "psc_code": "D316",
      "set_aside": "SBA",
      "response_deadline": "2026-06-14T17:00:00Z",
      "first_notice_date": "2026-05-08T15:32:11Z",
      "active": true,
      "sam_url": "https://sam.gov/opp/75D30126R00012/view",
      "office": {
        "office_code": "FA7014",
        "office_name": "FA7014  AFDW PK",
        "agency_code": "5700",
        "agency_name": "Department of the Air Force"
      },
      "meta": {
        "notices_count": 5,
        "notice_type": {"code": "o", "type": "Solicitation"}
      }
    }
  ]
}
```

## Step 6: Use Response Shaping to Customize Your Data

Response shaping lets you request only the specific fields you need, reducing data transfer and speeding up your application.

### Basic Shaping Example

Instead of receiving all fields, request just what you need using the `shape` parameter:

```bash
curl -H "X-API-KEY: your-api-key-here" \
  "https://tango.makegov.com/api/contracts/?shape=key,piid,award_date,recipient(display_name,uei)&limit=5"
```

This request returns only:

- Contract key
- PIID (contract identifier)
- Award date
- Recipient name and UEI (nested)

### Why Use Response Shaping?

- **Faster responses**: Less data to transfer means quicker API calls
- **Reduced bandwidth**: Only get the fields you actually need
- **Cleaner code**: Shape the response to match your application's data model
- **Flexible expansions**: Include related data (offices, transactions, recipients) without separate API calls

### Response Example

```json
{
  "count": 1250,
  "results": [
    {
      "key": "CONT_AWD_47QSWA24P0BWF_4732_-NONE-_-NONE-",
      "piid": "47QSWA24P0BWF",
      "award_date": "2024-01-15",
      "recipient": {
        "display_name": "ACME Corporation",
        "uei": "ZMXAHH8M8VL8"
      }
    }
  ]
}
```

Learn more about advanced features like flattening, aliasing, and multiple expansions in the [Response Shaping Guide](../guides/patterns/response-shaping.md).

## Common Issues

### Rate Limit Exceeded

If you see a `429 Too Many Requests` error, you've hit a rate limit. Check `X-RateLimit-Remaining` and `X-RateLimit-Reset` (seconds until reset), and follow the retry guidance in the [Rate limits guide](../guides/patterns/rate-limits.md).

### Authentication Error

If you get a `401 Unauthorized` error, check that:

- Your API key is correct
- You're including the `X-API-KEY` header
- Your API key is active

### Invalid Request

If you get a `400 Bad Request` error, check that:

- Your URL parameters are properly formatted
- Required parameters are included
- Date formats are YYYY-MM-DD

---

<!-- doc: guides/how-to/migrate-usaspending.md -->

# Migrate from USAspending bulk download

You've been pulling USAspending's nightly bulk CSVs, unpacking them, and bundling transactions into your own award records. This guide moves the **FPDS contracts** side of that pipeline to the Tango API.

## Scope

**In scope.** Prime contract awards (FPDS) and their subawards. The FPDS data Tango ingests is the same FPDS feed USAspending republishes — Tango pulls it directly twice daily and bundles transactions into one row per award.

**Out of scope (today).** Financial assistance (FABS) — grants, loans, direct payments, cooperative agreements, insurance. If your pipeline consumes USAspending's `assistance_transactions.csv`, `subaward_*` assistance rows, or any `award_type` in the assistance set (`02`, `03`, `04`, `05`, `06`, `07`, `08`, `09`, `10`, `11`), keep that on USAspending bulk for now. Tango has no first-class FABS endpoint yet — `/api/contracts/` is contract-only.

**Also out of scope.** Outlays on prime contracts (USAspending's `prime_award_total_outlayed_amount` on the prime side). Obligations are the canonical money metric on `/api/contracts/`. Outlays are exposed on subawards only today.

## What you actually gain

- **Twice-daily FPDS ingest vs USAspending's nightly FPDS pull.** Tango is ~12 hours fresher on contract data specifically. For FABS, USAspending is your only option and they're equivalent (both refresh daily).
- **One row per award, not one row per modification.** USAspending ships `contract_award_transactions.csv` with a row per FPDS mod; you have to group by award and reconcile dollar fields yourself. Tango's `/api/contracts/` returns one row per award with all mods rolled up; if you need the per-mod detail, request `?shape=...,transactions(*)` or hit `GET /api/contracts/{key}/transactions/`.
- **Stable canonical `key` per award.** Tango exposes a canonical `key` derived from the FPDS award components (`CONT_AWD_{piid}_{agency}_{parent_piid}_{parent_agency}`). It's stable across re-pulls and is the URL identifier (`/api/contracts/{key}/`). PIID is still available for human display and cross-checks.
- **Filter + shape at request time.** Replace the ZIP-and-grep step with query params (`?awarding_agency=GSA&obligated_gte=1000000`) and a response-shape selector (`?shape=key,piid,obligated`). Cuts payload size 80–95% vs the full bulk schema.
- **Push delivery (optional).** Subscribe to filter alerts via webhooks instead of diffing CSVs nightly. See [Polling vs webhooks](#change-capture-polling-vs-webhooks) below.

## Step-by-step

### 1. Inventory the USAspending columns you actually consume

Most pipelines touch 20–40 columns out of USAspending's ~280-column contract schema. List the columns your downstream code reads (parquet writer, BI tool, vendor enrichment, etc.) — that's the only set you need to map.

### 2. Map each column to a Tango field

Use the [Field mapping](#field-mapping) table below. Two things to know up front:

- **Agency codes.** USAspending uses two codes per office: a 3-digit CGAC for the **top-tier** (e.g. `047` = GSA) and a 4-digit FPDS code for the **sub-tier** (e.g. `4740` = GSA Public Buildings Service). Tango exposes both on the `awarding_office` / `funding_office` expansions as `department_code` (CGAC) and `agency_code` (FPDS sub-tier). Don't swap them — the names look transposable but the codes won't match.
- **Code/description objects.** Several USAspending columns are split across `<thing>_code` and `<thing>_description`. Tango returns them as `{code, description}` objects via expansion. To get the bare scalar, request `?shape=naics_code` (scalar) instead of `?shape=naics(code,description)` (object).

### 3. Replace the CSV download with a paginated GET

`/api/contracts/` uses **cursor pagination** — there is no `page=` parameter. Each response is `{count, next, previous, cursor, results}`. Follow `next` until it's null, or pass the value from the `cursor` field on the response into the next request.

`/api/subawards/` uses **page-number pagination** (`page=N&limit=M`). Different surface, same idea.

Both endpoints accept the same filter syntax as the API reference: range filters like `obligated_gte`, multi-value filters using `|` (e.g. `naics=541511|541512`), and full-text via `search=`.

### 4. Shape the response

Pass `?shape=` listing only the fields you actually use. Without `shape`, the API returns a sensible default but it's not the full FPDS record — and it's still much bigger than what most ETLs need.

### 5. Decide your dedup key

Use `key` for upserts (canonical, stable, URL-safe). PIID alone is not unique — same PIID can recur across agencies and across fiscal years. The `key` field encodes the full FPDS award tuple.

## Field mapping

This covers the most-used contract and subaward columns. For everything else, request `?show_shapes=true` on `/api/contracts/` to see the full available field set, or check the [Contracts data dictionary](../../data-dictionary/contracts.md).

### Contracts (`/api/contracts/`)

#### Identifiers

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `contract_award_unique_key` / `award_id` | `key` | Use `key` for all API operations. Format: `CONT_AWD_{piid}_{agency}_{parent_piid}_{parent_agency}`. |
| `award_id_piid` | `piid` | Same string. PIID alone is not unique across agencies — prefer `key` for joins. |
| `parent_award_id_piid` | `parent_award.piid` | `parent_award` is a `{key, piid}` reference. |
| `parent_award_id` | `parent_award.key` | |

#### Dollar amounts

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `total_obligated_amount` | `obligated` | Total dollars obligated across all mods. |
| `current_total_value_of_award` | `base_and_exercised_options_value` | |
| `potential_total_value_of_award` | `total_contract_value` | Includes unexercised options. |
| `federal_action_obligation` (transaction-level) | `transactions[].obligated` | One per FPDS mod. Request via `?shape=transactions(*)`. |

#### Dates

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `action_date` (initial transaction) | `award_date` | Date the award was first signed. |
| `action_date` (per modification) | `transactions[].transaction_date` | One per mod, when expanded. |
| `period_of_performance_start_date` | `period_of_performance.start_date` | |
| `period_of_performance_current_end_date` | `period_of_performance.current_end_date` | |
| `period_of_performance_potential_end_date` | `period_of_performance.ultimate_completion_date` | |
| `fiscal_year` | `fiscal_year` | Federal FY (Oct–Sep). Same semantics as USAspending. |

#### Awarding / funding agency

USAspending splits agency identity across four code/name pairs per side (top-tier + sub-tier + office). Tango folds them into a single `awarding_office` / `funding_office` object with three levels. **Watch the code semantics — `department_code` is CGAC, `agency_code` is FPDS sub-tier.**

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `awarding_agency_code` | `awarding_office.department_code` | CGAC top-tier code (3-digit, e.g. `047` for GSA). |
| `awarding_agency_name` | `awarding_office.department_name` | |
| `awarding_sub_agency_code` | `awarding_office.agency_code` | FPDS sub-tier code (4-digit, e.g. `4740` for GSA PBS). |
| `awarding_sub_agency_name` | `awarding_office.agency_name` | |
| `awarding_office_code` | `awarding_office.office_code` | AAC (Activity Address Code), e.g. `47PF52`. |
| `awarding_office_name` | `awarding_office.office_name` | |
| `funding_agency_code` | `funding_office.department_code` | Same CGAC/FPDS distinction as awarding. |
| `funding_agency_name` | `funding_office.department_name` | |
| `funding_sub_agency_code` | `funding_office.agency_code` | |
| `funding_sub_agency_name` | `funding_office.agency_name` | |
| `funding_office_code` | `funding_office.office_code` | |
| `funding_office_name` | `funding_office.office_name` | |

To filter by agency, use `?awarding_agency=` or `?funding_agency=` — these accept the CGAC code, the FPDS code, the name, or the abbreviation (best-effort matching). For reproducible jobs, use the CGAC code.

#### Recipient

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `recipient_uei` | `recipient.uei` | UEI is canonical post-SAM transition. |
| `recipient_name` | `recipient.display_name` | DBA / common name. `recipient.legal_business_name` is the SAM legal name. |
| `recipient_duns` | (not exposed on `/api/contracts/`) | DUNS is no longer the primary entity key. Use UEI. The legacy DUNS is still on subawards as `recipient_duns`. |
| `cage_code` | `recipient.cage_code` | |
| `recipient_parent_uei` | `recipient.parent_uei` (via `?shape=recipient(parent_uei)`) | |

#### Place of performance

USAspending's `primary_place_of_performance_*` columns map to the `place_of_performance` expansion:

| USAspending column | Tango field |
| --- | --- |
| `primary_place_of_performance_country_code` | `place_of_performance.country_code` |
| `primary_place_of_performance_country_name` | `place_of_performance.country_name` |
| `primary_place_of_performance_state_code` | `place_of_performance.state_code` |
| `primary_place_of_performance_state_name` | `place_of_performance.state_name` |
| `primary_place_of_performance_city_name` | `place_of_performance.city_name` |
| `primary_place_of_performance_zip_4` | `place_of_performance.zip_code` |

#### NAICS / PSC / set-aside

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `naics_code` | `naics_code` (scalar) or `naics.code` (via `?shape=naics(code,description)`) | |
| `naics_description` | `naics.description` | |
| `product_or_service_code` | `psc_code` (scalar) or `psc.code` | |
| `product_or_service_code_description` | `psc.description` | |
| `type_of_set_aside` | `set_aside.code` (via `?shape=set_aside(code,description)`) | |
| `type_of_set_aside_description` | `set_aside.description` | |

#### Competition

USAspending flattens competition fields onto the row; Tango groups them under `competition`. Request via `?shape=competition(*)`.

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `type_of_contract_pricing` | `competition.contract_type.code` | Returned as `{code, description}` — use `.code` for direct equivalence. |
| `type_of_contract_pricing_description` | `competition.contract_type.description` | |
| `extent_competed` | `competition.extent_competed.code` | |
| `extent_competed_description` | `competition.extent_competed.description` | |
| `solicitation_identifier` | `competition.solicitation_identifier` (also exposed as top-level `solicitation_identifier`) | |
| `solicitation_procedures` | `competition.solicitation_procedures.code` | |
| `number_of_offers_received` | `competition.number_of_offers_received` | |
| `other_than_full_and_open_competition` | `competition.other_than_full_and_open_competition.code` | |

#### Description and award type

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `award_description` / `prime_award_base_transaction_description` | `description` | |
| `award_type_code` | `award_type.code` (via `?shape=award_type(code,description)`) | |
| `award_type` | `award_type.description` | |

### Subawards (`/api/subawards/`)

Subawards are looser — most columns map 1:1 by name. The default shape already returns most of what FSRS-based pipelines need.

| USAspending column | Tango field | Notes |
| --- | --- | --- |
| `prime_award_unique_key` | `award_key` | Joins to `/api/contracts/{award_key}/`. |
| `prime_award_piid` | `piid` | |
| `prime_awardee_uei` | `prime_awardee_uei` | Also `prime_recipient.uei` if you request `prime_recipient(*)`. |
| `prime_awardee_name` | `prime_awardee_name` | |
| `subawardee_uei` / `recipient_uei` | `recipient_uei` | Also `subaward_recipient.uei`. |
| `subawardee_name` / `recipient_name` | `recipient_name` | |
| `subawardee_duns` / `recipient_duns` | `recipient_duns` | DUNS is retained on subawards as a legacy column. |
| `subawardee_parent_uei` | `recipient_parent_uei` | |
| `subawardee_parent_duns` | `recipient_parent_duns` | |
| `subaward_number` | `subaward_details.number` | |
| `subaward_action_date` | `subaward_details.action_date` | |
| `subaward_amount` | `subaward_details.amount` | |
| `subaward_description` | `subaward_details.description` | |
| `usaspending_permalink` | `usaspending_permalink` | Passed through verbatim. |
| `highly_compensated_officer_*_name` / `_amount` | `highly_compensated_officers[].name` / `.amount` | List of `{name, amount}` objects. |
| `subaward_fsrs_report_last_modified_date` | `fsrs_details.last_modified_date` | Closest thing to a change-detection timestamp on subawards. |

### Reference data

| USAspending file | Tango endpoint | Notes |
| --- | --- | --- |
| Recipient profiles ZIP | `GET /api/entities/`, `GET /api/entities/{uei}/` | UEI-keyed. |
| Agency reference CSV | `GET /api/organizations/` | Unified Federal Hierarchy with CGAC, FPDS, and FH keys. See [Federal Agency Hierarchy](../../api-reference/concepts/federal-agency-hierarchy.md). |
| NAICS reference CSV | `GET /api/naics/` | `code` + `description`, full-text `search=`. |
| PSC reference CSV | `GET /api/psc/` | Same shape as NAICS. |

## Worked example: replace one CSV job

The classic: "GSA contracts awarded in FY2026 over $1M, with recipient and place of performance." On USAspending this is a bulk-download request + ZIP unpack + filter pass. On Tango it's a paginated loop with a shape selector.

=== "curl"

    ```bash
    # GSA: pass the name, "047" (CGAC), or the abbreviation — best-effort matching.
    # Use CGAC for reproducible jobs.
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/contracts/?\
    awarding_agency=047&\
    fiscal_year=2026&\
    obligated_gte=1000000&\
    ordering=-award_date&\
    limit=100&\
    shape=key,piid,award_date,obligated,total_contract_value,\
    recipient(uei,display_name),naics(code,description),\
    place_of_performance(state_code,city_name)"

    # Then follow the `next` URL on each response until it's null.
    ```

=== "Python (tango-python)"

    ```python
    import os
    from urllib.parse import parse_qs, urlparse

    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    SHAPE = (
        "key,piid,award_date,obligated,total_contract_value,"
        "recipient(uei,display_name),naics(code,description),"
        "place_of_performance(state_code,city_name)"
    )

    cursor: str | None = None
    rows: list = []

    while True:
        resp = client.list_contracts(
            awarding_agency="047",   # GSA CGAC
            fiscal_year=2026,
            obligated_gte="1000000",
            sort="award_date",
            order="desc",
            limit=100,
            cursor=cursor,
            shape=SHAPE,
        )
        rows.extend(resp.results)
        if not resp.next:
            break
        cursor = parse_qs(urlparse(resp.next).query).get("cursor", [None])[0]

    print(f"Pulled {len(rows)} contracts")
    ```

=== "Node (tango-node)"

    ```ts
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const SHAPE = [
      "key",
      "piid",
      "award_date",
      "obligated",
      "total_contract_value",
      "recipient(uei,display_name)",
      "naics(code,description)",
      "place_of_performance(state_code,city_name)",
    ].join(",");

    let cursor: string | null | undefined = undefined;
    const rows: unknown[] = [];

    while (true) {
      const resp = await client.listContracts({
        awarding_agency: "047", // GSA CGAC
        fiscal_year: 2026,
        obligated_gte: "1000000",
        sort: "award_date",
        order: "desc",
        limit: 100,
        cursor,
        shape: SHAPE,
      });
      rows.push(...resp.results);
      if (!resp.next) break;
      cursor = new URL(resp.next).searchParams.get("cursor");
    }

    console.log(`Pulled ${rows.length} contracts`);
    ```

The subaward equivalent uses `client.list_subawards(...)` / `client.listSubawards(...)` with `page=N` instead of `cursor`.

## Change capture: polling vs webhooks

Two patterns. Pick the one that matches your tolerance for latency and ops complexity.

### Polling (simpler default)

Run a scheduled job that pulls awards newer than your last cursor. The change-capture filter is `award_date_gte`:

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/contracts/?award_date_gte=2026-05-01&ordering=-award_date&limit=100&shape=..."
```

What this catches:

- **New awards** since the last poll, ordered by `award_date`. ✅
- **Mods to existing awards** — only if the mod is significant enough that Tango re-materialized the award. The `/api/contracts/` API does not expose a `updated_at` / `modified_after` filter today; `award_date` is the only date-window filter. If you need per-mod change detection, pull the transactions endpoint for the awards you care about (next section) or use webhooks.

The simple pattern most teams need:

1. Persist the latest `award_date` you've seen.
2. On each run, request `award_date_gte=<that date>` and follow `next` until exhausted.
3. Upsert by `key`.

This is enough for most BI / dashboards / vendor-watch use cases.

### Webhooks (push, lower latency)

For "tell me when a new contract matches this filter, within minutes of FPDS landing," subscribe to a filter alert and consume `alerts.contract.match` events. The webhook payload ships matched IDs only — your receiver pulls the full record from `/api/contracts/{key}/`.

Full walkthrough including signature verification, deduping, and the receiver loop: [Stream contract awards](./stream-awards.md). The full subscription / payload / retry / circuit-breaker reference: [Webhooks user guide](../../webhooks/index.md).

One thing to know before you `POST /api/webhooks/alerts/`: if your account has more than one webhook endpoint, you **must** include `"endpoint": "<endpoint_uuid>"` in the request body. Single-endpoint accounts auto-resolve; multi-endpoint accounts get a 400 without it. The Python SDK's `client.create_webhook_alert(..., endpoint="...")` exposes the same kwarg.

## Historical backfill

For seeding history (years of contracts at cutover), use a date-windowed cursor crawl. The pattern:

1. Pick a window — fiscal-year-sized windows (`fiscal_year=2018`, then `2019`, ...) parallelize well because they're independent and roughly balanced (FPDS volumes per FY are within an order of magnitude).
2. For each window, cursor-paginate `/api/contracts/?fiscal_year=<YYYY>&ordering=-award_date&limit=100` and write batches as you go.
3. Checkpoint the `cursor` value after each successful batch so you can resume without re-pulling.
4. Subawards: `?fiscal_year_gte=<YYYY>&fiscal_year_lte=<YYYY>` with page-number pagination.

For reference, FY2026 contracts on production are ~1.8M rows; a window-per-FY crawl at `limit=100` is on the order of ~20K requests per fiscal year. Use `shape=` aggressively to keep payloads small.

If you're cutting over from USAspending, validate by spot-checking 50–100 random `key` lookups against your last USAspending CSV pull for the same agency/FY and confirm `obligated`, `recipient.uei`, and `awarding_office.agency_code` agree.

## Transactions completeness

`/api/contracts/{key}/transactions/` returns the FPDS modifications Tango has for that award. Each item: `modification_number`, `transaction_date`, `obligated`, `description`, `action_type`. Transactions are loaded from FPDS at award materialization — for an award that exists in `/api/contracts/`, the transaction list reflects what FPDS has published for that award up to the most recent ingest cycle.

If your downstream wants USAspending's per-transaction row shape (one row per mod, denormalized), don't try to recreate it from the bundled award. Hit the transactions detail endpoint per `key`, or request `?shape=...,transactions(*)` on the list call to get the mods inline.

## NULL semantics

USAspending bulk CSVs and Tango JSON disagree on how absent values look. Plan for both:

- **USAspending CSVs** emit empty strings (`""`), and in some columns sentinel strings like `"None"` or `"NULL"`. Your ingest probably already has cleanup for this.
- **Tango JSON** uses JSON `null` for absent values and omits keys from shaped responses when the field isn't requested or isn't present. A shaped response with `?shape=key,piid,recipient(uei)` will not include `recipient` at all if the recipient is unresolved — vs an empty object — so check key presence, not just truthiness, when transforming.
- **Numeric fields** (`obligated`, `total_contract_value`) come back as JSON numbers, not strings. USAspending's CSVs are strings — your existing parser needs to handle both or you'll write strings to a numeric column.
- **DUNS on contracts** is not exposed — see the field-mapping note above. Don't try to map `recipient_duns` through `/api/contracts/`; it'll be missing every time. DUNS lives on subawards (`recipient_duns`) for legacy joins.
- **`awarding_office` codes can be `null` independently.** An award can have a known department (`department_code = "047"`) but a missing sub-tier or office. Don't assume the three levels are co-populated.

## Pitfalls

- **Don't filter agency by name unless you have to.** `awarding_agency=047` (CGAC code) is exact for GSA. `awarding_agency=GSA` does best-effort name resolution — great for ad-hoc, can match more or fewer orgs than you expect on a production job. Use CGAC for reproducibility. The FPDS sub-tier code (e.g. `4700`) is stored on org records but **isn't a reliable filter** on `/api/contracts/` — use CGAC.
- **`fiscal_year` is federal FY (Oct–Sep).** Same as USAspending; just don't accidentally pass a calendar year.
- **`obligated_gte` / `obligated_lte` are exact USD.** Pass `1000000`, not `"1M"`.
- **Multi-value filters use `|`.** `naics=541511|541512` matches either. Same for `psc` and other scalar filters.
- **Cursor pagination doesn't accept `page=`.** `/api/contracts/` uses keyset cursors — follow `next` or pass `cursor=<token>`. `/api/subawards/` is the opposite — page-number pagination with `page=N`. They are not interchangeable.
- **Default response shape is opinionated.** It's a sensible middle ground, not the full FPDS record. Pass `?shape=` explicitly for production jobs; `?show_shapes=true` lists the available fields.
- **PIID is not unique.** Use `key` for upserts and joins. PIID alone repeats across agencies.

## What's out of scope today

- **FABS / assistance data.** No grants, loans, direct payments, cooperative agreements. Keep that on USAspending bulk. See the [Grants API reference](../../api-reference/grants.md) for what Tango does ship on the grants side — it's Grants.gov data, not FABS-derived.
- **Prime-contract outlays.** Obligations only on `/api/contracts/`. `prime_award_total_outlayed_amount` is exposed on subawards.
- **Per-mod change feed across all contracts.** No global "give me every FPDS mod since timestamp X" endpoint. Polling on `award_date_gte` catches new awards; webhook filter alerts cover filtered slices in near-real-time; per-award transaction history is on `/api/contracts/{key}/transactions/`.

## Related

- [Contracts API reference](../../api-reference/contracts.md)
- [Contracts data dictionary](../../data-dictionary/contracts.md)
- [Subawards API reference](../../api-reference/subawards.md)
- [Subawards data dictionary](../../data-dictionary/subawards.md)
- [Federal Agency Hierarchy](../../api-reference/concepts/federal-agency-hierarchy.md)
- [Stream contract awards](./stream-awards.md) — webhook receiver pattern for `alerts.contract.match`
- [Webhooks user guide](../../webhooks/index.md)

---

<!-- doc: guides/how-to/opportunity-updates-in-slack.md -->

# Opportunity updates in Slack

Get a Slack message every time a new SAM.gov opportunity matches a saved filter — e.g., "new HHS solicitations in NAICS 541512." Tango POSTs an `alerts.opportunity.match` event; your receiver maps it to a Slack Block Kit message and forwards to a Slack [Incoming Webhook](https://api.slack.com/messaging/webhooks).

## The 1-line answer

Stand up a tiny HTTP receiver that:

1. verifies the Tango signature on the inbound POST,
2. iterates `events[].matches.new[]` — each entry is a summary object with the fields you need (title, solicitation number, NAICS, response deadline, opportunity ID),
3. converts each match to Slack Block Kit blocks and POSTs to your Slack Incoming Webhook URL.

Then register your receiver URL as a Tango webhook endpoint and attach an opportunity alert.

!!! info "The payload already has what you need"
    Tango's webhook payload includes a `matches.new[]` array of summary objects — `opportunity_id`, `title`, `solicitation_number`, `naics_code`, `response_deadline`. No fan-out fetch required for a Slack notification. See the [Webhooks payload format](../../webhooks/index.md#6-payload-format) for the full shape.

## Prerequisites

- Tango API key (`TANGO_API_KEY`).
- A Slack workspace where you can install an app — you need permission to create an Incoming Webhook.
- Somewhere to host the receiver. Anywhere that can serve a public HTTPS endpoint works: a Cloudflare Worker, an AWS Lambda + API Gateway, a Fly.io / Render / Railway service, a Vercel / Netlify Function, or your own server.

## Step 1 — Create the Slack Incoming Webhook

Follow [Slack's setup guide](https://api.slack.com/messaging/webhooks) — it's three clicks: create a Slack app, enable "Incoming Webhooks," and "Add New Webhook to Workspace." Pick the channel you want messages to land in.

Slack hands you a URL that looks like:

```text
https://hooks.slack.com/services/T0XXXX/B0XXXX/abcDEFghi123…
```

Treat this URL as a secret — anyone with it can post to your channel.

Smoke-test it before wiring up Tango:

```bash
curl -X POST -H "Content-Type: application/json" \
  --data '{"text": "Hello from Tango setup test."}' \
  "$SLACK_WEBHOOK_URL"
```

You should see a plain message land in the channel within a second.

## Step 2 — Stand up the receiver

The receiver is one HTTP handler. Four things it must do:

1. **Verify the `X-Tango-Signature` header.** Reject mismatches with 401.
2. **Dedupe on `delivery_id`** if you might receive the same delivery twice (Tango retries on 5xx). The `delivery_id` is a UUID on the top-level payload, stable across retries.
3. **Iterate `event.matches.new[]`.** Each entry is a summary object — `opportunity_id`, `title`, `solicitation_number`, `naics_code`, `response_deadline`. No additional fetch required.
4. **Convert each match to a Slack message and POST it.**

Pick whichever runtime fits your stack. Below are minimal examples in Python and Node; both use the helpers shipped in the official SDKs.

=== "Python (FastAPI)"

    ```python
    # pip install tango-python[webhooks] fastapi uvicorn httpx
    import os
    import httpx
    from fastapi import FastAPI, Request, HTTPException
    from tango.webhooks.signing import verify_signature, SIGNATURE_HEADER

    SLACK_URL = os.environ["SLACK_WEBHOOK_URL"]
    SECRET = os.environ["TANGO_WEBHOOK_SECRET"]

    app = FastAPI()
    seen_deliveries: set[str] = set()  # use Redis/DB in production

    @app.post("/tango/webhooks")
    async def receive(req: Request):
        body = await req.body()
        if not verify_signature(body, SECRET, req.headers.get(SIGNATURE_HEADER)):
            raise HTTPException(401, "invalid signature")

        payload = await req.json()

        # Dedupe on the top-level delivery_id (stable across retries).
        delivery_id = payload.get("delivery_id")
        if delivery_id and delivery_id in seen_deliveries:
            return {"ok": True}
        if delivery_id:
            seen_deliveries.add(delivery_id)

        async with httpx.AsyncClient(timeout=5.0) as http:
            for event in payload["events"]:
                if event["event_type"] != "alerts.opportunity.match":
                    continue
                alert_id = event.get("alert_id", "")
                # The payload carries summary objects — no fetch needed.
                for match in event.get("matches", {}).get("new", []):
                    blocks = opportunity_to_blocks(match, alert_id)
                    await http.post(SLACK_URL, json={"blocks": blocks})
        return {"ok": True}

    def opportunity_to_blocks(match: dict, alert_id: str) -> list[dict]:
        title = match.get("title") or "(untitled opportunity)"
        sol = match.get("solicitation_number") or "—"
        deadline = match.get("response_deadline") or "—"
        naics = match.get("naics_code") or "—"
        url = f"https://sam.gov/opp/{match['opportunity_id']}/view"
        return [
            {"type": "header", "text": {"type": "plain_text", "text": f"New: {title[:140]}"}},
            {"type": "section", "fields": [
                {"type": "mrkdwn", "text": f"*Solicitation*\n{sol}"},
                {"type": "mrkdwn", "text": f"*Response by*\n{deadline}"},
                {"type": "mrkdwn", "text": f"*NAICS*\n{naics}"},
                {"type": "mrkdwn", "text": f"*Alert*\n{alert_id[:8]}…"},
            ]},
            {"type": "actions", "elements": [
                {"type": "button", "text": {"type": "plain_text", "text": "View on SAM.gov"}, "url": url},
            ]},
        ]
    ```

=== "Node (Hono / Bun / Node 18+)"

    ```typescript
    // npm i @makegov/tango-node hono
    import { Hono } from "hono";
    import { verifySignature, SIGNATURE_HEADER } from "@makegov/tango-node";

    const SLACK_URL = process.env.SLACK_WEBHOOK_URL!;
    const SECRET = process.env.TANGO_WEBHOOK_SECRET!;
    const seenDeliveries = new Set<string>(); // use a real store in production

    const app = new Hono();

    app.post("/tango/webhooks", async (c) => {
      const body = await c.req.text();
      if (!verifySignature(body, SECRET, c.req.header(SIGNATURE_HEADER))) {
        return c.json({ error: "invalid signature" }, 401);
      }

      const payload = JSON.parse(body);

      // Dedupe on the top-level delivery_id (stable across retries).
      const deliveryId: string | undefined = payload.delivery_id;
      if (deliveryId && seenDeliveries.has(deliveryId)) {
        return c.json({ ok: true });
      }
      if (deliveryId) seenDeliveries.add(deliveryId);

      for (const event of payload.events) {
        if (event.event_type !== "alerts.opportunity.match") continue;
        const alertId: string = event.alert_id ?? "";
        // The payload carries summary objects — no fetch needed.
        for (const match of (event.matches?.new ?? []) as any[]) {
          await fetch(SLACK_URL, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ blocks: opportunityToBlocks(match, alertId) }),
          });
        }
      }
      return c.json({ ok: true });
    });

    function opportunityToBlocks(match: any, alertId: string) {
      const title = (match.title ?? "(untitled opportunity)").slice(0, 140);
      const sol = match.solicitation_number ?? "—";
      const deadline = match.response_deadline ?? "—";
      const naics = match.naics_code ?? "—";
      const url = `https://sam.gov/opp/${match.opportunity_id}/view`;
      return [
        { type: "header", text: { type: "plain_text", text: `New: ${title}` } },
        { type: "section", fields: [
          { type: "mrkdwn", text: `*Solicitation*\n${sol}` },
          { type: "mrkdwn", text: `*Response by*\n${deadline}` },
          { type: "mrkdwn", text: `*NAICS*\n${naics}` },
          { type: "mrkdwn", text: `*Alert*\n${alertId.slice(0, 8)}…` },
        ]},
        { type: "actions", elements: [
          { type: "button", text: { type: "plain_text", text: "View on SAM.gov" }, url },
        ]},
      ];
    }

    export default app;
    ```

Both examples assume `TANGO_WEBHOOK_SECRET` matches the secret you set when registering the endpoint with Tango (next step). For full receiver patterns — async deduplication, retries, structured logging — see the [Webhooks user guide](../../webhooks/index.md#8-best-practices-on-your-side).

## Step 3 — Register the receiver with Tango and create the alert

Two API calls: one to register the endpoint, one to create the alert.

=== "curl"

    ```bash
    # 1. Register the receiver URL as a webhook endpoint.
    ENDPOINT_ID=$(curl -sS -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/endpoints/" \
      -d '{
        "name": "Slack — HHS NAICS 541512",
        "callback_url": "https://your-receiver.example.com/tango/webhooks",
        "is_active": true
      }' | jq -r '.endpoint_id')

    # The response also includes a `secret`. Save it as TANGO_WEBHOOK_SECRET
    # on the receiver — that's the shared secret Tango signs with.

    # 2. Create the opportunity alert targeting this endpoint.
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d "{
        \"name\": \"HHS NAICS 541512 opportunities\",
        \"query_type\": \"opportunity\",
        \"filters\": {\"agency\": \"7500\", \"naics\": \"541512\", \"active\": true},
        \"frequency\": \"realtime\",
        \"endpoint\": \"$ENDPOINT_ID\"
      }"
    ```

=== "Python"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    endpoint = client.create_webhook_endpoint(
        name="Slack — HHS NAICS 541512",
        callback_url="https://your-receiver.example.com/tango/webhooks",
        is_active=True,
    )
    print("Save this as TANGO_WEBHOOK_SECRET:", endpoint["secret"])

    alert = client.create_webhook_alert(
        name="HHS NAICS 541512 opportunities",
        query_type="opportunity",
        filters={"agency": "7500", "naics": "541512", "active": True},
        frequency="realtime",
        endpoint=endpoint["endpoint_id"],
    )
    ```

=== "Node"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    const endpoint = await client.createWebhookEndpoint({
      name: "Slack — HHS NAICS 541512",
      callback_url: "https://your-receiver.example.com/tango/webhooks",
      is_active: true,
    });
    console.log("Save this as TANGO_WEBHOOK_SECRET:", (endpoint as any).secret);

    const alert = await client.createWebhookAlert({
      name: "HHS NAICS 541512 opportunities",
      query_type: "opportunity",
      filters: { agency: "7500", naics: "541512", active: true },
      frequency: "realtime",
      endpoint: (endpoint as any).endpoint_id,
    });
    ```

!!! info "Save the endpoint secret"
    The endpoint-create response includes a one-time `secret`. Tango signs every delivery with this secret; your receiver verifies with the same value. Store it as `TANGO_WEBHOOK_SECRET` in your receiver's environment. Lose it and you'll have to rotate by recreating the endpoint.

## Step 4 — Test before you wait for a real match

You don't want to discover the receiver is broken when the first real match shows up. Use `client.test_webhook_delivery()` (or `client.testWebhookEndpoint()` in Node) to fire a synthetic delivery at your endpoint immediately:

=== "Python"

    ```python
    client.test_webhook_delivery(endpoint_id=endpoint["endpoint_id"])
    ```

=== "Node"

    ```typescript
    await client.testWebhookEndpoint(endpoint.endpoint_id);
    ```

You should see a Slack message arrive within a few seconds. If you don't:

- Check the endpoint logs — does the request reach the receiver?
- Did `verifySignature` return `false`? Double-check `TANGO_WEBHOOK_SECRET` matches the endpoint secret.
- Did `payload["events"]` exist? The sample-payload shape is documented in the [Webhooks user guide §6](../../webhooks/index.md#6-payload-format).

For deeper local debugging, the SDKs ship a `WebhookReceiver` that records deliveries to an in-process queue and a `simulate.deliver()` helper that signs a payload without involving the live Tango API at all. See the [Python](../../sdks/python/webhooks.md#programmatic-use) / [Node](../../sdks/node/webhooks.md#programmatic-use) reference.

## Tuning the filter

Too many alerts is worse than too few — Slack channels die from noise. Two levers:

- **Narrow the filter.** Add `set_aside`, `agency`, or `place_of_performance` to cut volume. The same filter parameters available on `GET /api/opportunities/` work in the alert `filters` object.
- **Switch frequency.** `realtime` fires on every ingestion cycle (multiple times per day). For lower-priority feeds, `daily` rolls all new matches from the past 24h into a single delivery — fewer Slack messages, batched.

To see how many matches your current filter is producing before going live:

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/opportunities/?agency=7500&naics=541512&active=true&first_notice_date_after=2026-04-01" | jq '.count'
```

If `count` is in the thousands, narrow the filter — that's roughly how many alerts your channel will see when matches start landing.

## Updating or pausing the alert

```bash
# Pause without deleting
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/$ALERT_ID/" \
  -d '{"is_active": false}'

# Resume
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/$ALERT_ID/" \
  -d '{"is_active": true}'
```

`filters` and `query_type` are immutable post-create — to change either, delete and recreate.

## What about contracts, grants, forecasts?

Same pattern. Swap `query_type` for `contract` / `grant` / `forecast`, point `filters` at the same fields you'd pass to the equivalent list endpoint, and adapt the Block Kit mapping to the relevant entity (PIID + recipient + obligation for contracts, opportunity number + funding categories for grants, etc.). The signature verification, dedup, and Slack-post pieces are identical.

---

<!-- doc: guides/how-to/resolve-agency-names.md -->

# Resolve free-text agency names

You have a column of agency strings — `"Department of Energy"`, `"DOE"`, `"Energy"`, `"Lab @ Argonne"` — and you need canonical Tango identifiers (a CGAC code, an FPDS code, an `Organization` UUID) before you can join, filter, or load. This guide shows you the smallest loop that gets you there.

## The 1-line answer

=== "curl"

    ```bash
    curl -X POST https://tango.makegov.com/api/resolve/ \
      -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"name": "Department of Energy", "target_type": "organization"}'
    ```

=== "Python"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    result = client.resolve(name="Department of Energy", target_type="organization")
    for c in result.candidates:
        print(c.identifier, c.display_name, c.match_tier)
    ```

=== "Node"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const result = await client.resolve({
      name: "Department of Energy",
      target_type: "organization",
    });
    for (const c of result.candidates) {
      console.log(c.identifier, c.display_name, c.match_tier);
    }
    ```

`target_type` is `"organization"` for any government org (department, agency, sub-agency, office). `"entity"` is for vendors/recipients.

## What you get back

```json
{
  "candidates": [
    {
      "identifier": "362361bf-4dd3-5254-85a3-4649884108d6",
      "display_name": "ENERGY, DEPARTMENT OF",
      "match_tier": "high"
    }
  ],
  "count": 1
}
```

- `identifier` — the UUID `key` of the matching `Organization`. Use it directly against [`/api/organizations/{identifier}/`](../../api-reference/organizations.md). To use in an agency filter on list endpoints, prefer the `fh_key` or CGAC code (obtainable by hydrating the org record first, or by using the org's `short_name`/abbreviation directly in the filter). See [Agency search pattern guide](../patterns/agency-search.md) for full filter semantics.
- `display_name` — the canonical name on file.
- `match_tier` — `"high"` / `"medium"` / `"low"`. **Free tier callers do not get this field**; they see only `identifier` and `display_name` and at most 3 candidates. Pro+ gets up to 5 candidates with `match_tier`. See the [resolve reference](../../api-reference/resolve.md) for the full contract.

The endpoint does **not** return hierarchy context (parent department, level, etc.) inline. If you need that, hydrate via `/api/organizations/{identifier}/` once you have a winner.

!!! info "Free-text only"
    `/api/resolve/` is for *free-text strings*. If your input is already a numeric code (3-digit CGAC, 4-digit FPDS), you don't need to resolve — skip to [Numeric codes](#numeric-codes-skip-the-resolver) below.

## Boost quality with context

The resolver weights signals beyond the bare name. If your spreadsheet has city, state, or any descriptive context, pass it:

=== "curl"

    ```bash
    curl -X POST https://tango.makegov.com/api/resolve/ \
      -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Lab @ Argonne",
        "target_type": "organization",
        "state": "IL",
        "context": "national laboratory, basic science research, DOE"
      }'
    ```

=== "Python"

    ```python
    result = client.resolve(
        name="Lab @ Argonne",
        target_type="organization",
        state="IL",
        context="national laboratory, basic science research, DOE",
    )
    ```

=== "Node"

    ```typescript
    const result = await client.resolve({
      name: "Lab @ Argonne",
      target_type: "organization",
      state: "IL",
      context: "national laboratory, basic science research, DOE",
    });
    ```

Useful context fields: industry references (NAICS/PSC), parent department name, location signals, mission text from the source row. More context generally narrows the candidate set.

## Handling low-confidence matches

Treat `match_tier` as a **heuristic, not a verdict.** Empirically the tiers are noisy in both directions: a `high`-tier hit on an abbreviation collision can still be the wrong org, and a `low`-tier hit on a partial string (`"Environmental Protection"` → EPA at `low`) can still be the right one.

| Tier | Reasonable default | Caveat |
| --- | --- | --- |
| `high` | Promising. Safe enough for human-reviewed surfaces (search dropdowns, autocomplete). | A `high` tier can still be wrong on abbreviation collisions — don't auto-join into a downstream system without spot-checking. |
| `medium` | Inspect `candidates[1:]` before accepting. | Often the second candidate is what you actually wanted. |
| `low` | Treat as a suggestion. | Sometimes correct (e.g., a clipped partial name) — don't discard before reviewing. |
| `count: 0` | Queue for human review. | No match at all — the string didn't hit anything. |

On the free tier you don't get `match_tier`, so use ordinal position as a weak proxy (first candidate is best) and lean harder on validating downstream — or upgrade if this is core to your workflow.

!!! warning "Don't assume the top hit is right"
    The resolver returns *ranked candidates*, not a single answer. For ambiguous strings ("Energy" could be the Department, an EERE office, an EIA program) the second candidate is sometimes what you wanted. If your input might be ambiguous, log all candidates, not just `[0]`.

!!! danger "Anti-pattern: tiered auto-accept into automated joins"
    Wiring `if tier == "high": auto_accept()` straight into an ETL pipeline that writes to a downstream system is a great way to ship silently-wrong joins. Strings like `"Dept of Energy"` (no period, abbreviated "Dept") have been observed to resolve to unrelated logistics orgs at `high` tier in some snapshots — the resolver is doing its best on noisy input, but it can't tell you when it's wrong. **Rule of thumb:** if a wrong answer would corrupt a downstream join, send anything below explicit, reviewed confidence through a human-in-the-loop step regardless of tier.

## Numeric codes: skip the resolver

If your input is already a federal code, the resolve endpoint is the wrong tool — it's tuned for free-text names. Use the [`Organizations` API](../../api-reference/organizations.md) or any list endpoint's agency filter, which understand numeric codes directly:

| Input shape | Means | Resolves to |
| --- | --- | --- |
| 3-digit (`069`, `075`, `089`) | CGAC code | L1 department |
| 4-digit (`2100`, `7530`) | FPDS sub-tier code | L2 agency under the matching department |
| 8-9 digit (`100011980`) | `fh_key` | Exact `Organization` row |
| Letters + digits (`15JCRM`) | Office / AAC code | Office-level org |

=== "curl"

    ```bash
    # CGAC — direct list filter
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/organizations/?cgac=089"

    # FPDS code — multi-stage search picks it up via aliases
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/organizations/?search=2100"

    # fh_key — direct path lookup (DOE fh_key = 100011980)
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/organizations/100011980/"
    ```

=== "Python"

    ```python
    # CGAC — direct list filter
    resp = client.list_organizations(cgac="089")

    # FPDS code — multi-stage search picks it up via aliases
    resp = client.list_organizations(search="2100")

    # fh_key — direct path lookup
    org = client.get_organization("100011980")
    ```

=== "Node"

    ```typescript
    // CGAC — direct list filter
    let resp = await client.listOrganizations({ cgac: "089" });

    // FPDS code — multi-stage search picks it up via aliases
    resp = await client.listOrganizations({ search: "2100" });

    // fh_key — direct path lookup
    const org = await client.getOrganization("100011980");
    ```

For filter-time use ("give me contracts under DOE"), the agency filter on every list endpoint accepts any of these forms — see the [Agency search pattern guide](../patterns/agency-search.md) for full semantics.

## Batch loop: clean a CSV

The endpoint is per-call, not bulk. To clean a CSV column, loop with light concurrency.

!!! note "Illustrative, not production-ready"
    The scripts below are teaching examples — sequential, no de-duplication, no retries, no concurrency. For real 1000-row ETL jobs, dedupe inputs first (`"DOE"` appears 400 times → resolve once, fan out), wrap calls in retry-with-backoff, and pace yourself off the SDK's exposed rate-limit headers: `client.rate_limit_info` (Python) / `client.rateLimitInfo` (Node). Both expose `remaining`, `reset`, and `retryAfter` after each request.

=== "Python"

    ```python
    import csv
    import os
    import time
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])


    def resolve_agency(name: str, *, state: str | None = None, context: str | None = None) -> dict:
        """Return the top candidate plus the full ranked list for review."""
        result = client.resolve(
            name=name,
            target_type="organization",
            state=state,
            context=context,
        )
        candidates = result.candidates
        return {
            "input": name,
            "match_count": result.count,
            "top_identifier": candidates[0].identifier if candidates else None,
            "top_display_name": candidates[0].display_name if candidates else None,
            "top_tier": candidates[0].match_tier if candidates else None,  # None on free tier
            "all_candidates": candidates,
        }


    with open("agencies_in.csv") as fin, open("agencies_resolved.csv", "w") as fout:
        reader = csv.DictReader(fin)
        writer = csv.DictWriter(
            fout,
            fieldnames=["input", "identifier", "display_name", "tier", "needs_review"],
        )
        writer.writeheader()

        for row in reader:
            result = resolve_agency(row["agency"], state=row.get("state"))
            tier = result["top_tier"]
            writer.writerow(
                {
                    "input": result["input"],
                    "identifier": result["top_identifier"] or "",
                    "display_name": result["top_display_name"] or "",
                    "tier": tier or "",
                    "needs_review": tier in (None, "low") or result["match_count"] == 0,
                }
            )
            time.sleep(0.05)  # be polite; standard rate limits apply
    ```

=== "Node"

    ```typescript
    import { TangoClient, type ResolveCandidate } from "@makegov/tango-node";
    import { createReadStream, createWriteStream } from "node:fs";
    import { setTimeout as sleep } from "node:timers/promises";
    import { parse } from "csv-parse";
    import { stringify } from "csv-stringify";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

    async function resolveAgency(
      name: string,
      opts: { state?: string; context?: string } = {},
    ) {
      const result = await client.resolve({
        name,
        target_type: "organization",
        state: opts.state,
        context: opts.context,
      });
      const candidates: ResolveCandidate[] = result.candidates ?? [];
      const top = candidates[0];
      return {
        input: name,
        matchCount: result.count ?? 0,
        topIdentifier: top?.identifier ?? null,
        topDisplayName: top?.display_name ?? null,
        topTier: top?.match_tier ?? null, // null on free tier
        allCandidates: candidates,
      };
    }

    const parser = createReadStream("agencies_in.csv").pipe(
      parse({ columns: true }),
    );
    const writer = stringify({
      header: true,
      columns: ["input", "identifier", "display_name", "tier", "needs_review"],
    });
    writer.pipe(createWriteStream("agencies_resolved.csv"));

    for await (const row of parser) {
      const result = await resolveAgency(row.agency, { state: row.state });
      const tier = result.topTier;
      writer.write({
        input: result.input,
        identifier: result.topIdentifier ?? "",
        display_name: result.topDisplayName ?? "",
        tier: tier ?? "",
        needs_review:
          tier === null || tier === "low" || result.matchCount === 0,
      });
      await sleep(50); // be polite; standard rate limits apply
    }
    writer.end();
    ```

Either script writes `identifier` (the UUID `key` returned by the resolver) into the output column. That UUID is `Organization.key` — it's what `/api/organizations/{identifier}/` accepts. It is **not** an `fh_key` (the 8–9 digit numeric Federal Hierarchy code); if you need the `fh_key` for downstream filter use, hydrate the org record first.

## Pitfalls

- **`target_type` is `"organization"`, not `"agency"`.** The endpoint has no `"agency"` choice; all government orgs (department, agency, sub-agency, office) live under `"organization"`.
- **GET won't work.** This endpoint is POST-only with a JSON body. Drop the `?q=` pattern you may have seen in older snippets.
- **"Department of X" vs codes.** The resolver handles both well in isolation, but mixed input shapes ("DOE", `089`, "Energy") in one column means you should branch: route numeric-looking values through `/api/organizations/?search=` and free text through `/api/resolve/`.
- **Sub-offices bubble up.** A string like `"NNSA Office of Defense Programs"` may resolve to NNSA (L2) rather than the specific program office (L3). Pass the office name in `context` to nudge the resolver, or hydrate the result and walk down via `/api/organizations/?parent_fh_key=...`.
- **Deprecated / reorganized agencies.** Tango preserves demoted Federal Hierarchy records by `fh_key` so historical IDs still resolve, but the `display_name` you get back may be the *current* canonical name, not the historical one. If you're matching contracts from 2010, expect the modern descendant org back — that's intentional. See [Federal agency hierarchy](../../api-reference/concepts/federal-agency-hierarchy.md) for the provenance story.
- **Empty `candidates` array, HTTP 200.** No match is not an error. Always check `count` before indexing `candidates[0]`.
- **`identifier` is a UUID, not an `fh_key`.** The resolver returns the org's UUID `key`. If your downstream system expects the 8–9 digit `fh_key`, hydrate the org record first — don't pipe the UUID into an `?fh_key=...` filter.

## See also

- [Resolve endpoint reference](../../api-reference/resolve.md) — full request/response contract, tier limits, error codes.
- [Organizations API](../../api-reference/organizations.md) — direct lookups by `fh_key`, CGAC filter, search.
- [Federal agency hierarchy](../../api-reference/concepts/federal-agency-hierarchy.md) — what CGAC, FPDS, L1/L2/L3 mean and where the data comes from.
- [Agency search pattern](../patterns/agency-search.md) — how the agency filter on every list endpoint resolves the same shapes of input at query time.

---

<!-- doc: guides/how-to/search-opportunities-naics.md -->

# Search opportunities by NAICS

Find live SAM.gov opportunities matching one or more NAICS codes, sorted by response deadline.

## The 1-line answer

`GET /api/opportunities/` with `naics=<code>` + `active=true` + `ordering=response_deadline`.

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/?naics=541512&active=true&ordering=response_deadline"
    ```

=== "Python"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    resp = client.list_opportunities(
        naics="541512",
        active=True,
        ordering="response_deadline",
        limit=50,
    )
    for opp in resp.results:
        print(opp.response_deadline, opp.opportunity_id, opp.title)
    ```

=== "Node"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const resp = await client.listOpportunities({
      naics: "541512",
      active: true,
      ordering: "response_deadline",
      limit: 50,
    });
    for (const opp of resp.results) {
      console.log(opp.response_deadline, opp.opportunity_id, opp.title);
    }
    ```

That's it. The rest of this guide is refinements: looking up codes you don't know, getting `naics_code` back in the payload, layering narrowing filters, drilling into a single opportunity, paginating, and staying fresh with webhooks.

!!! warning "Filter name gotcha"
    The filter is `naics`, not `naics_code`. And it's `active=true`, not `status=active` — there is no `status` filter on this endpoint.

## Step 1 — Find the NAICS codes you care about

Most users don't have `541512` memorized. Browse `/api/naics/` with `search=` first. It matches both code prefix and description text:

=== "curl"

    ```bash
    # All NAICS in the 5415 (Computer Systems Design) family
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/naics/?search=5415&limit=25"

    # Free-text on description (returns 9 hits today)
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/naics/?search=computer&limit=25"
    ```

=== "Python"

    ```python
    # All NAICS in the 5415 family
    family = client.list_naics(search="5415", limit=25)
    for r in family.results:
        # list_naics() returns plain dicts, not shaped objects — use dict access.
        print(r["code"], "-", r["description"])

    # Free-text on description
    computer = client.list_naics(search="computer", limit=25)
    ```

=== "Node"

    ```typescript
    const family = await client.listNaics({ search: "5415", limit: 25 });
    for (const r of family.results) {
      console.log(r.code, "-", r.description);
    }

    const computer = await client.listNaics({ search: "computer", limit: 25 });
    ```

!!! tip "Search terms that miss"
    The NAICS table uses Census Bureau wording. Industry jargon often misses — `cybersecurity`, `cyber`, and `cloud` all return zero hits because those words don't appear in any NAICS description. Try the underlying activity: `computer`, `security`, `programming`, `data processing`, `software`.

Once you've picked your codes, `|`-OR them together for the opportunities call (see [Multi-NAICS search](#multi-naics-search) below). Full reference: [NAICS API](../../api-reference/naics.md).

## Step 2 — Get `naics_code` back in the response

The default opportunity shape for `list_opportunities()` is minimal (`opportunity_id, title, solicitation_number, response_deadline, active`) and does **not** include `naics_code`. If you're searching by NAICS you almost certainly want to see which code each result matched. Ask for it explicitly via `shape=`:

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/?naics=541511|541512&active=true&ordering=response_deadline&shape=opportunity_id,title,response_deadline,naics_code,office(*),set_aside"
    ```

=== "Python"

    ```python
    resp = client.list_opportunities(
        naics="541511|541512",
        active=True,
        ordering="response_deadline",
        shape="opportunity_id,title,response_deadline,naics_code,office(*),set_aside",
        limit=50,
    )
    for opp in resp.results:
        print(opp.naics_code, opp.response_deadline, opp.title)
    ```

=== "Node"

    ```typescript
    const resp = await client.listOpportunities({
      naics: "541511|541512",
      active: true,
      ordering: "response_deadline",
      shape: "opportunity_id,title,response_deadline,naics_code,office(*),set_aside",
      limit: 50,
    });
    for (const opp of resp.results) {
      console.log(opp.naics_code, opp.response_deadline, opp.title);
    }
    ```

The list-detail default shape is documented in [Opportunities reference → Response Shaping](../../api-reference/opportunities.md#response-shaping). For the syntax (nested expansions, wildcards, flattening), see the [Response Shaping pattern guide](../patterns/response-shaping.md).

## Step 3 — Add the usual narrowing filters

In practice you almost never search by NAICS alone. Layer on whatever applies. Keep it to two or three narrowing filters at a time — five at once usually reduces the result set to zero on any single day:

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/?\
    naics=541512&\
    active=true&\
    agency=DOD&\
    response_deadline_after=2026-05-15&\
    ordering=response_deadline"
    ```

=== "Python"

    ```python
    resp = client.list_opportunities(
        naics="541512",
        active=True,
        agency="DOD",
        response_deadline_after="2026-05-15",
        ordering="response_deadline",
        limit=50,
    )
    ```

=== "Node"

    ```typescript
    const resp = await client.listOpportunities({
      naics: "541512",
      active: true,
      agency: "DOD",
      response_deadline_after: "2026-05-15",
      ordering: "response_deadline",
      limit: 50,
    });
    ```

| Filter | What you'd use it for |
| --- | --- |
| `set_aside` | Small-business / 8(a) / WOSB / SDVOSB carve-outs. |
| `agency` | Limit to a department or sub-agency (vector-backed; `"DOD"`, `"Department of Energy"`, `"NAVSEA"` all work). |
| `notice_type` | `Solicitation`, `Combined Synopsis/Solicitation`, `Sources Sought`, `Presolicitation`, etc. Validated server-side. |
| `response_deadline_after` | Drop anything already closed or closing too soon to bid. |
| `response_deadline_before` | Cap how far out you look. |
| `place_of_performance` | Free-text-ish match on PoP. |
| `search` | Full-text search on the opportunity itself. |

See the full list in the [Opportunities API reference](../../api-reference/opportunities.md).

## Multi-NAICS search

Most vendors care about a portfolio of NAICS codes, not just one. Use `|` to pass multiple:

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/?naics=541511|541512|541519&active=true&ordering=response_deadline"
    ```

=== "Python"

    ```python
    resp = client.list_opportunities(
        naics="541511|541512|541519",
        active=True,
        ordering="response_deadline",
        limit=50,
    )
    ```

=== "Node"

    ```typescript
    const resp = await client.listOpportunities({
      naics: "541511|541512|541519",
      active: true,
      ordering: "response_deadline",
      limit: 50,
    });
    ```

The same `|` syntax works for `psc` and other filters that accept multiple values.

## Paginating through results

A single NAICS code can easily return hundreds of opportunities (`541512` alone returns ~500). At the default `limit=50` that's ten pages. The endpoint is page-based — there is no cursor for opportunities — and the response includes `next`/`previous` URLs.

=== "curl"

    ```bash
    # Page 2, 3, ... until response.next is null
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/?naics=541512&active=true&ordering=response_deadline&limit=50&page=2"
    ```

=== "Python"

    ```python
    # tango-python doesn't ship an iterator — drive it yourself.
    page = 1
    while True:
        resp = client.list_opportunities(
            naics="541512",
            active=True,
            ordering="response_deadline",
            limit=50,
            page=page,
        )
        for opp in resp.results:
            print(opp.response_deadline, opp.opportunity_id)
        if not resp.next:
            break
        page += 1
    ```

=== "Node"

    ```typescript
    // tango-node ships an async iterator that walks pages for you.
    for await (const opp of client.iterateOpportunities({
      naics: "541512",
      active: true,
      ordering: "response_deadline",
      limit: 50,
    })) {
      console.log(opp.response_deadline, opp.opportunity_id);
    }
    ```

Always pass an `ordering=` when paginating. Without one, page boundaries are undefined and you can see duplicates or skipped rows across pages.

## Drilling into a single opportunity

The list endpoint gives you summary fields. To pull the full record — description, attachments, notice history — hit the detail endpoint:

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/opportunities/<opportunity_id>/?shape=opportunity_id,title,naics_code,response_deadline,description,attachments(name,mime_type,url)"
    ```

=== "Python"

    ```python
    # No public get_opportunity() yet — use the lower-level _get(). See note below.
    opp = client._get(
        f"/api/opportunities/{opp_id}/",
        params={"shape": "opportunity_id,title,naics_code,response_deadline,"
                         "description,attachments(name,mime_type,url)"},
    )
    print(opp["title"], "-", len(opp.get("attachments") or []), "attachment(s)")
    for a in (opp.get("attachments") or []):
        print(" -", a["name"], a["mime_type"], a["url"])
    ```

=== "Node"

    ```typescript
    // No public getOpportunity() yet — use fetch directly. See note below.
    const r = await fetch(
      `https://tango.makegov.com/api/opportunities/${oppId}/?shape=opportunity_id,title,naics_code,response_deadline,description,attachments(name,mime_type,url)`,
      { headers: { "X-API-KEY": process.env.TANGO_API_KEY! } },
    );
    const opp = await r.json();
    console.log(opp.title, "-", (opp.attachments ?? []).length, "attachment(s)");
    ```

!!! info "No `get_opportunity()` convenience method yet"
    Neither SDK currently ships a typed `get_opportunity()` / `getOpportunity()`. The examples above use `client._get(...)` (Python) and `fetch` (Node). A typed wrapper is on the roadmap; the URL shape will not change.

!!! warning "`attachments(extracted_text)` is Pro+ tier"
    The default `attachments(...)` expansion gives you `attachment_id, file_size, mime_type, name, posted_date, resource_id, type, url`. The `extracted_text` sub-field (full OCR'd text of each attachment) is **Pro+ tier only**. Free-tier callers who request it get the response without it plus a `meta.upgrade_hints` entry. See [pricing](../../getting-started/pricing.md).

## Stay fresh with webhooks

Polling `/api/opportunities/` every few minutes works, but it's wasteful and you'll miss the window on fast-turn solicitations. Use a **filter subscription** instead. The full webhooks model — endpoint setup, signing, retries — lives in the [Webhooks user guide](../../webhooks/index.md); this is the abbreviated version for "watch a NAICS portfolio."

!!! info "`endpoint` is required for multi-endpoint accounts"
    If your account has more than one webhook endpoint configured, the alert-create call **must** include the `endpoint` UUID (the server returns 400 otherwise so deliveries can't go to the wrong receiver). Single-endpoint accounts may omit it. List your endpoints first with `GET /api/webhooks/endpoints/` or `client.list_webhook_endpoints()`.

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "NAICS 5415xx opportunities (SBA)",
        "query_type": "opportunity",
        "filters": {
          "naics": "541511|541512|541519",
          "active": "true",
          "set_aside": "SBA"
        },
        "frequency": "realtime",
        "endpoint": "<your-endpoint-id>"
      }'
    ```

=== "Python"

    ```python
    # Single-endpoint account — `endpoint=` may be omitted.
    # Multi-endpoint account — pass the UUID explicitly.
    alert = client.create_webhook_alert(
        name="NAICS 5415xx opportunities (SBA)",
        query_type="opportunity",
        filters={
            "naics": "541511|541512|541519",
            "active": "true",
            "set_aside": "SBA",
        },
        frequency="realtime",
        endpoint="<your-endpoint-id>",  # required if you have >1 endpoint
    )
    print(alert.alert_id, alert.status)
    ```

=== "Node"

    ```typescript
    const alert = await client.createWebhookAlert({
      name: "NAICS 5415xx opportunities (SBA)",
      query_type: "opportunity",
      filters: {
        naics: "541511|541512|541519",
        active: "true",
        set_aside: "SBA",
      },
      frequency: "realtime",
      endpoint: "<your-endpoint-id>", // required if you have >1 endpoint
    });
    console.log(alert.alert_id, alert.status);
    ```

Tango re-evaluates the filter on its schedule and POSTs matching opportunities to your endpoint as they appear. Deliveries carry IDs only — fetch the full record with the [drill-in pattern](#drilling-into-a-single-opportunity) above. Tier limits and cron-vs-realtime semantics in the [Webhooks user guide](../../webhooks/index.md).

## Combining with agency search

NAICS + agency together is the most common shape, and `agency` is vector-backed — you don't have to know the exact CGAC/FPDS code. `agency=DOD`, `agency=Navy`, `agency=NAVSEA` all do something sensible.

If you need precise agency-tree semantics (e.g. *the whole Department* vs. *just this sub-agency*), see the [agency search pattern guide](../patterns/agency-search.md).

## Common pitfalls

- **`active=true` vs `notice_type`.** `active=true` filters by Tango's archive flag — i.e., the opportunity isn't archived/expired. It does **not** filter by phase. If you want only currently-biddable solicitations, combine `active=true` with `notice_type=Solicitation` (or `Combined Synopsis/Solicitation`) **and** `response_deadline_after=<today>`.
- **NAICS code format.** Six-digit string, no hyphens, no leading zeros stripped. `"541512"` is correct; `541512` (int) works in JSON but URL-encode it as a string. Five-digit and four-digit prefixes are **not** matched — use the [NAICS browse path](#step-1-find-the-naics-codes-you-care-about) to find the exact six-digit codes you want, then `|`-OR them together.
- **Default ordering.** Without `ordering=`, list responses come back in an undefined order. If you care about "soonest deadline first," always pass `ordering=response_deadline`. This matters double when paginating.
- **`naics_code` isn't in the default shape.** If `opp.naics_code` raises `AttributeError`, you forgot the `shape=` override from [Step 2](#step-2-get-naics_code-back-in-the-response).

## Related

- [Opportunities API reference](../../api-reference/opportunities.md)
- [NAICS API reference](../../api-reference/naics.md)
- [Response Shaping pattern](../patterns/response-shaping.md)
- [Webhooks user guide](../../webhooks/index.md)
- [Agency search patterns](../patterns/agency-search.md)

---

<!-- doc: guides/how-to/stream-awards.md -->

# Get notified when new awards match your filter

You want a heads-up when a new contract award matches a saved filter — say, "DOD awards over $1M in NAICS 541512." There are two ways to do it. **Polling is the default for most teams.** Webhooks are the upgrade if you already run that kind of infrastructure.

## The 1-line answer

- **Polling:** `GET /api/contracts/?<your filters>&modified_after=<last_run>` on a 15-30 min cron. Dedupe on `key`. Done.
- **Webhooks:** create a filter alert (`POST /api/webhooks/alerts/`) — and if your account has more than one webhook endpoint, you **must** pass `endpoint=<uuid>` (see [Step 1: Create the filter alert](#step-1-create-the-filter-alert) under Option B).

## How fast is "new"?

Whichever option you pick, latency is bounded by upstream data sources, not by your transport:

| Source | Cadence |
| --- | --- |
| FPDS publishes new contract rows | Irregular; weekday business hours, large catch-ups overnight |
| Tango ingests from FPDS | Twice daily |
| Tango's `realtime` alert evaluator | `*/5 * * * *` (every 5 min) |

So the gap between a contract being signed and you finding out is typically **hours, sometimes a day** — not seconds. The 5-min webhook cron is *downstream* of the twice-daily FPDS pull; it does not make the upstream feed any fresher. If you need true second-level streaming, neither option will give it to you, because FPDS itself doesn't.

## When to pick which

| | Polling | Webhooks |
| --- | --- | --- |
| Latency from FPDS ingest | 0-30 min (your poll interval) | ≤ 5 min |
| Latency from FPDS *publish* | Hours (FPDS cadence) | Hours (same) |
| Needs public HTTPS endpoint | No | Yes |
| Needs HMAC verifier | No | Yes |
| Needs a job queue | No | Strongly recommended |
| Retry / dedupe logic | Free (idempotent by construction) | Required (at-least-once delivery) |
| Backpressure if your end is slow | None | Tango opens the circuit breaker on you |

Webhook only wins when you're already running a webhook stack (Stripe, GitHub, Slack, etc.). For a single BD / analytics team, a polling job has half the moving parts and the same effective freshness.

---

## Option A — Polling (the default)

Run a cron that hits `/api/contracts/` with your filters plus `modified_after=<last successful run>`. Track the last-run timestamp in your DB or a state file; dedupe on each result's `key`.

### Sketch

```text
every 15-30 min:
  since   = read last_successful_run_ts()      # ISO 8601
  results = GET /api/contracts/?<filters>&modified_after=<since>&ordering=-award_date&limit=100
  for row in results.paginate():
      if row.key not in already_seen_table:
          handle(row)
          already_seen_table.insert(row.key)
  write last_successful_run_ts(now_before_request)
```

A few rules to keep this boring:

- Record `now_before_request` *before* you send the call, not after. Otherwise you'll lose records that landed during your fetch window.
- Always paginate (`next` cursor). The default page size is small; a busy filter will exceed it.
- Dedupe on `key` (the contract's stable primary id), not on `(piid, agency)`. PIIDs are not globally unique.
- `modified_after` covers both new and modified rows — that's normally what you want. If you only want net-new awards, also filter on `award_date_gte=<some recent floor>`.
- Allowed `ordering` values are `award_date`, `obligated`, `total_contract_value` (and the same with a `-` prefix). `key` is not an allowed ordering.

### Three-language smoke test

=== "curl"

    ```bash
    curl -H "X-API-KEY: $TANGO_API_KEY" \
      "https://tango.makegov.com/api/contracts/?\
    funding_agency=DOD&naics=541512&obligated_gte=1000000\
    &modified_after=2026-05-12T00:00:00Z&ordering=-award_date&limit=20"
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    # The Python SDK takes `naics_code=` and maps it to the API's `naics=`.
    # Same story for `psc_code` → `psc`.
    #
    # `modified_after` isn't exposed as a top-level kwarg on list_contracts() yet,
    # so pass it via `filters={...}` — values there get forwarded verbatim.
    contracts = client.list_contracts(
        funding_agency="DOD",
        naics_code="541512",
        obligated_gte="1000000",
        ordering="-award_date",
        limit=20,
        filters={"modified_after": "2026-05-12T00:00:00Z"},
    )
    for c in contracts.results:
        handle(c)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    // The Node SDK accepts BOTH `naics` (raw API name) and `naics_code` (alias).
    const page = await client.listContracts({
      funding_agency: "DOD",
      naics: "541512",
      obligated_gte: "1000000",
      modified_after: "2026-05-12T00:00:00Z",
      ordering: "-award_date",
      limit: 20,
    });

    for (const c of page.results) {
      handle(c);
    }
    ```

That's the whole pattern. Half of all "stream awards" use cases stop here. If yours is one of them, you're done.

---

## Option B — Webhooks (advanced)

Pick webhooks if you already run a public HTTPS endpoint with HMAC verification and a job queue, and want push delivery instead of polling. Otherwise stick with Option A.

### Step 1: Create the filter alert

`POST /api/webhooks/alerts/` is the entrypoint. Returns **201 Created** with the new alert, or **200 OK** with the existing one if a subscription with the same `query_type` + `filters` already exists (dedup by SHA-256 hash of the canonical filter).

!!! warning "Multi-endpoint accounts must specify `endpoint`"
    If your account has more than one webhook endpoint, the API rejects `POST /api/webhooks/alerts/` without an explicit `endpoint=<uuid>` field — it will not guess which one to use. Single-endpoint accounts get auto-resolve for free. List your endpoints with `GET /api/webhooks/endpoints/` and copy the `id` of the one you want.

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "DOD IT services > $1M",
        "query_type": "contract",
        "filters": {
          "funding_agency": "DOD",
          "naics": "541512",
          "obligated_gte": "1000000"
        },
        "frequency": "realtime",
        "endpoint": "<endpoint-uuid-from-/api/webhooks/endpoints/>"
      }'
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    # Filter keys here match the RAW API param names (`naics`, `psc`), not the
    # `list_contracts()` kwarg aliases (`naics_code`, `psc_code`).
    alert = client.create_webhook_alert(
        name="DOD IT services > $1M",
        query_type="contract",
        filters={
            "funding_agency": "DOD",
            "naics": "541512",
            "obligated_gte": "1000000",
        },
        frequency="realtime",
        endpoint="<endpoint-uuid>",  # required if you have >1 endpoint
    )
    print(alert.alert_id, alert.status)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const alert = await client.createWebhookAlert({
      name: "DOD IT services > $1M",
      query_type: "contract",
      filters: {
        funding_agency: "DOD",
        naics: "541512",
        obligated_gte: "1000000",
      },
      frequency: "realtime",
      endpoint: "<endpoint-uuid>",  // required if you have >1 endpoint
    });
    console.log(alert.alert_id, alert.status);
    ```

#### Frequency choices

| Frequency | When it runs | Tier |
| --- | --- | --- |
| `realtime` | Every 5 minutes (`*/5 * * * *`); independent of FPDS ingest | All tiers |
| `daily` | Once per day | All tiers |
| `weekly` | Once per week | All tiers |
| `custom` | 5-field cron in `cron_expression` | Micro and above |

"Realtime" is the most-frequent option, not a true streaming pipe. The evaluator polls every 5 min regardless of whether anything new landed upstream.

### Step 2: Build the receiver

Tango POSTs a signed JSON batch to your endpoint. The receiver has three jobs: **verify the signature, dedupe on `delivery_id`, return 2xx fast.**

#### The payload shape (real production deliveries)

```json
{
  "timestamp": "2026-05-12T18:20:14Z",
  "delivery_id": "8c5e3f6a-1234-4abc-9def-9b21abcde000",
  "events": [
    {
      "event_type": "alerts.contract.match",
      "alert_id": "e4c4aaaa-bbbb-cccc-dddd-eeeeffff0000",
      "query_type": "contract",
      "filters": {"funding_agency": "DOD", "naics": "541512", "obligated_gte": "1000000"},
      "matches": {
        "new_count": 2,
        "modified_count": 0,
        "new": [
          {"id": "CONT_AWD_47QFWA26F0009_4732_47QTCK18D0004_4732", "piid": "47QFWA26F0009", "obligated": 5364614.65, "recipient_uei": "ABC123DEF456"},
          {"id": "CONT_AWD_FA8773_24_C_0099_5700_GS35F0119Y_4732", "piid": "FA877324C0099",   "obligated": 1450000.00, "recipient_uei": "DEF456GHI789"}
        ],
        "modified": []
      },
      "checked_at": "2026-05-12T18:20:12Z"
    }
  ]
}
```

See [Webhooks user guide §6 — Payload format](../../webhooks/index.md#6-payload-format) for the field-by-field reference. The summary objects in `matches.new[]` / `matches.modified[]` carry just enough to route on (`id`, `piid`, `obligated`, `recipient_uei`); pull the full record from `GET /api/contracts/{id}/` if you need more.

!!! info "Sample-payload preview vs production"
    `GET /api/webhooks/endpoints/sample-payload/?event_type=alerts.contract.match` returns a **simplified preview** (just `new_ids` / `modified_ids`, no `delivery_id`, no `matches.new[]`). Real deliveries from the dispatcher carry the richer shape above. Build against this guide and the webhooks user guide §6 — not against the sample-payload response.

#### Python receiver (Flask)

```python
import hashlib
import hmac
import json
import os

import flask

SECRET = os.environ["TANGO_WEBHOOK_SECRET"].encode()
app = flask.Flask(__name__)

# Replace with Redis / your DB. Must persist across restarts and be fast.
_seen_deliveries: set[str] = set()


@app.post("/tango/webhooks")
def recv():
    body = flask.request.get_data()  # raw bytes, NOT request.json
    sig_header = flask.request.headers.get("X-Tango-Signature", "")
    expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
    received = sig_header.removeprefix("sha256=")

    if not hmac.compare_digest(expected, received):
        return "Invalid signature", 401

    payload = json.loads(body)

    # 1. Idempotency: Tango guarantees at-least-once delivery.
    delivery_id = payload.get("delivery_id")
    if delivery_id and delivery_id in _seen_deliveries:
        return "ok", 200
    if delivery_id:
        _seen_deliveries.add(delivery_id)

    # 2. Enqueue and return 2xx fast. Don't process inline — slow handlers
    #    trigger retries and eventually trip the circuit breaker.
    for event in payload.get("events", []):
        if event.get("event_type") != "alerts.contract.match":
            continue  # ignore other event types in the same batch
        matches = event.get("matches", {})
        for match in matches.get("new", []) + matches.get("modified", []):
            enqueue_match(event["alert_id"], match)  # match has id, piid, obligated, recipient_uei

    return "ok", 200


def enqueue_match(alert_id: str, match: dict):
    # Push to Celery / RQ / SQS / whatever. Your worker fetches the full
    # award details from /api/contracts/{id}/ as needed.
    ...
```

#### Node receiver (Express)

```typescript
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.TANGO_WEBHOOK_SECRET!;
const app = express();

// Replace with Redis / your DB. Must persist across restarts.
const seenDeliveries = new Set<string>();

// IMPORTANT: capture the raw body for HMAC. JSON-parse from the buffer, NOT via express.json().
app.post(
  "/tango/webhooks",
  express.raw({ type: "*/*" }),
  (req, res) => {
    const body = req.body as Buffer; // raw bytes

    const sigHeader = (req.header("x-tango-signature") || "").replace(/^sha256=/, "");
    const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");

    const a = Buffer.from(expected, "utf8");
    const b = Buffer.from(sigHeader, "utf8");
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(body.toString("utf8"));

    // 1. Idempotency
    const deliveryId: string | undefined = payload.delivery_id;
    if (deliveryId && seenDeliveries.has(deliveryId)) {
      return res.status(200).send("ok");
    }
    if (deliveryId) seenDeliveries.add(deliveryId);

    // 2. Enqueue + ack
    for (const event of payload.events ?? []) {
      if (event.event_type !== "alerts.contract.match") continue;
      const matches = event.matches ?? {};
      for (const m of [...(matches.new ?? []), ...(matches.modified ?? [])]) {
        enqueueMatch(event.alert_id, m);
      }
    }

    res.status(200).send("ok");
  },
);

function enqueueMatch(alertId: string, match: unknown) {
  // Push to BullMQ / SQS / whatever. Pull full record from
  // GET /api/contracts/{id}/ in the worker.
}
```

Key rules:

- **Sign the raw body bytes**, not a re-serialized JSON. Any whitespace change breaks the HMAC.
- **Constant-time compare** (`hmac.compare_digest` / `crypto.timingSafeEqual`) — never `==`.
- **Return 2xx within a few seconds.** Synchronous work trips retries and eventually opens the circuit breaker.
- **Dedupe on `delivery_id`** — same `delivery_id` may arrive more than once on retry. It is the canonical idempotency key.
- **Ignore unknown event types** in the batch (don't 4xx) — a single delivery may contain multiple event types for the same endpoint.

### Step 3: Test end-to-end with the webhook lab

The Tango repo ships a [standalone webhook lab](https://github.com/makegov/tango/tree/main/tools/webhook_lab) that receives, verifies, and displays signed deliveries. Use it during integration before you have a production endpoint.

```bash
# In your tango checkout:
just webhook-lab                                       # starts on localhost:8011
cloudflared tunnel --url http://localhost:8011        # public URL
just webhook-lab-subscribe --url "https://<tunnel>/tango/webhooks" --clear
```

Then trigger a test delivery:

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/endpoints/test-delivery/" \
  -d '{"endpoint": "<endpoint-uuid>"}'   # required for multi-endpoint accounts
```

### Managing the alert

```bash
# List your filter alerts
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/"

# Pause without deleting
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/" \
  -d '{"is_active": false}'

# Delete
curl -X DELETE -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/"
```

`query_type` and `filters` are **immutable** after creation — delete and recreate to change them. You can update `name`, `frequency`, `cron_expression`, and `is_active`.

---

## Common pitfalls

- **Expecting sub-minute latency.** Don't. FPDS ingests twice daily; effective end-to-end latency from "vendor signs contract" to "your code sees it" is measured in hours, not seconds — regardless of whether you poll or use webhooks.
- **Forgetting `endpoint=<uuid>` on multi-endpoint accounts.** Any `POST /api/webhooks/alerts/` (and any `test-delivery`) returns 400 with `"You have multiple webhook endpoints. Please specify which endpoint to use via the 'endpoint' field."` Pass the UUID explicitly.
- **Python SDK kwarg surprise.** `client.list_contracts(naics=...)` raises — the kwarg is `naics_code` (the SDK maps it to the API's `naics`). Same for `psc_code` → `psc`. The raw HTTP filter, and the `filters={}` dict you pass to `create_webhook_alert()`, both use the API name (`naics`, `psc`).
- **Filter doesn't match `/api/contracts/`.** Test the filter against the API first. If `/api/contracts/?<filters>` returns nothing, the alert will never fire and your poll loop will never see anything.
- **HMAC mismatch.** You're hashing a re-serialized body, or you stripped a trailing newline. Hash the **raw request bytes** as received. Signature is `sha256=<64 hex chars>` over `body` using your endpoint's secret.
- **Slow receiver.** If your handler takes more than a few seconds, Tango retries with exponential backoff (up to 10 min between attempts) and eventually opens the circuit breaker. Always enqueue + return 2xx.
- **At-least-once duplicates.** The same `delivery_id` can land more than once on retry. If your downstream isn't idempotent, you'll double-write. Dedupe on `delivery_id`.
- **Polling without `modified_after`.** A raw `award_date_gte` poll will miss modifications to existing records. `modified_after` covers both new and updated rows. Use both if you want strict "new awards only".
- **Tier caps.** Free tier gets 1 filter alert; Micro 3; Small 5; Medium 10; Large 25. Hitting the cap returns 400.

## Related

- [Contracts API reference](../../api-reference/contracts.md)
- [Webhooks user guide](../../webhooks/index.md) — full subscription / payload / retry / circuit-breaker reference (see [§6 Payload format](../../webhooks/index.md#6-payload-format))
- [Vendor watchlist recipe](../../webhooks/recipes/vendor-watchlist.md) — N-alerts pattern for tracking specific UEIs
- [Awards by NAICS recipe](../../webhooks/recipes/awards-by-naics.md) — single alert across multiple NAICS codes

---

<!-- doc: guides/index.md -->

# Guides

Task-oriented walkthroughs for working with the Tango API. If you know what you want to *do*, start here. If you know what you want to *look up*, jump to the [API reference](../api-reference/index.md).

## Getting Started

New to Tango? These get you to a working request fast.

- [Quick start](../getting-started/quick-start.md) — your first API call in under five minutes
- [Authentication](../getting-started/authentication.md) — API keys and OAuth2
- [Plans & pricing](../getting-started/pricing.md) — what each tier unlocks

## Patterns

Cross-cutting techniques you'll use on every endpoint.

- [Response shaping](patterns/response-shaping.md) — pick exactly the fields you want
- [Pagination & result counts](patterns/result-counts.md) — cursor pagination, totals, large result sets
- [Rate limits & retries](patterns/rate-limits.md) — what the headers tell you and how to back off
- [Vehicles explained](patterns/vehicles.md) — IDVs, BPAs, GWACs, schedule contracts

## How-to recipes

Specific jobs, end-to-end.

- [Search opportunities by NAICS](how-to/search-opportunities-naics.md)
- [Resolve free-text agency names](how-to/resolve-agency-names.md)
- [Stream contract awards in real time](how-to/stream-awards.md)
- [Migrate from USAspending bulk download](how-to/migrate-usaspending.md)

## Webhook recipes

Filter-based webhook alerts (`/api/webhooks/alerts/`) — see also the [Webhooks user guide](../webhooks/index.md).

- [Vendor watchlist](../webhooks/recipes/vendor-watchlist.md)
- [Awards by NAICS](../webhooks/recipes/awards-by-naics.md)
- [Grants by agency](../webhooks/recipes/grants-by-agency.md)
- [Forecast pipeline](../webhooks/recipes/forecast-pipeline.md)

---

Looking for SDK-specific examples? See the [Python](../sdks/python/index.md) or [Node](../sdks/node/index.md) SDK docs.

---

<!-- doc: guides/patterns/agency-search.md -->

# Agency search

The `agency` filter (and its variants — `awarding_agency`, `funding_agency`) on every endpoint that exposes one is a **resolver**, not a substring search. You hand it any agency identifier — a name, abbreviation, code — and Tango figures out which `Organization` (and its hierarchy subtree) you meant.

## What it accepts

A single agency identifier value, in any of these forms:

| You pass | Example | Resolves to |
| --- | --- | --- |
| Abbreviation | `HHS`, `DOD`, `FDA` | The agency whose canonical short name matches |
| Name (full or partial) | `Health and Human Services`, `Defense` | The closest matching agency by full-text / fuzzy name |
| CGAC code (3-digit) | `075` | The L1 department |
| FPDS sub-agency code (4-digit) | `7530` | The L2 agency under the matching department |
| Federal Hierarchy key (fh_key) | `100004222` | The exact org row in `agencies_organization` |
| AAC code | `87FCAB` | The org row whose AAC matches |

You don't pick a "type". You just hand over the identifier you have, and Tango ranks candidates across multiple signal types (abbreviation > acronym > alternate name > hierarchy) plus direct field matches (CGAC, FPDS code, fh_key, name trigram).

## Multi-value OR (`|`)

Use `|` to filter by multiple agencies at once. Each token is resolved **independently**, then results are the **union** of every token's subtree.

```http
GET /api/contracts/?awarding_agency=HHS|DOD
```

Returns contracts awarded by anything under HHS *or* anything under DOD, as a flat list in the endpoint's default order.

What `|` does **not** do:

- It does **not** group results by token.
- It does **not** preserve token order.
- It is **not** "search by N inputs and merge with deduplication" — each token is an independent agency selection.

If you need bucketed-by-agency results (e.g. `5 HHS contracts + 5 DOD contracts`), make N separate calls. A lopsided union (huge agency + tiny agency) buries the small one in pagination.

## Pagination

The response shape is identical to a single-token query. Standard pagination fields (`count`, `next`, `previous`, `results`) apply to the union.

## Examples

### Single agency by abbreviation

```http
GET /api/contracts/?awarding_agency=HHS&limit=5
```

### Single agency by FPDS code

```http
GET /api/contracts/?awarding_agency=7530
```

### Two agencies (OR)

```http
GET /api/contracts/?awarding_agency=HHS|DOD
```

### Forecast filter with fh_key

```http
GET /api/forecasts/?agency=100004222
```

### Mixed inputs

You can mix forms across `|`-separated tokens — abbreviation in one, fh_key in the next, name in the third:

```http
GET /api/opportunities/?agency=HHS|100000700|Treasury
```

Each token is resolved independently.

## Endpoints

The same resolver is used wherever you see an agency filter:

- `/api/contracts/` — `awarding_agency`, `funding_agency`
- `/api/idvs/` — `awarding_agency`, `funding_agency`
- `/api/otas/` — `awarding_agency`, `funding_agency`
- `/api/otidvs/` — `awarding_agency`, `funding_agency`
- `/api/subawards/` — `awarding_agency`, `funding_agency`
- `/api/vehicles/` — `agency`
- `/api/opportunities/` — `agency`
- `/api/notices/` — `agency`
- `/api/forecasts/` — `agency`
- `/api/grants/` — `agency`
- `/api/protests/` — `agency`

For the per-endpoint filter list, see the relevant API reference page (e.g. [Contracts](../../api-reference/contracts.md)).

## See also

- [Federal agency hierarchy](../../api-reference/concepts/federal-agency-hierarchy.md) — how Tango models L1 / L2 / L3 / sub-office relationships.
- [Organizations API](../../api-reference/organizations.md) — direct access to the underlying `Organization` records.

---

<!-- doc: guides/patterns/rate-limits.md -->

# Rate limits

Tango enforces rate limits to keep the API fast and reliable for everyone. Limits apply at the **account level**: requests made via **API keys** and **OAuth2 tokens** for the same account all draw from the same quotas.

## Where to see your limits and usage

- **Your current plan limits + near-real-time usage**: [Account profile](https://tango.makegov.com/accounts/profile/)
- **Upgrade pricing / higher limits**: [Pricing](https://tango.makegov.com/subscriptions/pricing/)

## How rate limits work (burst + daily)

Most plans have **more than one** rate limit window:

- **Burst**: short window (e.g. per minute) that protects the API from sudden spikes
- **Daily**: fixed window that resets at midnight UTC, caps total volume per day

You may be “fine” on daily usage but still hit burst limits (or vice versa).

## Rate limit headers

Every `/api/*` response includes rate limit headers.

### Overall headers (most restrictive window)

These headers summarize the **most restrictive** window (the one you’re closest to hitting):

- `X-RateLimit-Limit`: total requests allowed for that window
- `X-RateLimit-Remaining`: requests remaining in that window
- `X-RateLimit-Reset`: **seconds until reset** for that window

### Per-window headers (daily, burst, etc.)

For each configured window (commonly `Daily` and `Burst`), you’ll also see:

- `X-RateLimit-Daily-Limit`, `X-RateLimit-Daily-Remaining`, `X-RateLimit-Daily-Reset`
- `X-RateLimit-Burst-Limit`, `X-RateLimit-Burst-Remaining`, `X-RateLimit-Burst-Reset`

Each `*-Reset` value is **seconds until that specific window resets**.

### Quick header check (curl)

```bash
curl -s -D - -o /dev/null \
  -H "X-API-KEY: your-api-key-here" \
  "https://tango.makegov.com/api/contracts/?limit=1"
```

Example response headers:

```http
X-RateLimit-Daily-Limit: 2400
X-RateLimit-Daily-Remaining: 2350
X-RateLimit-Daily-Reset: 86400
X-RateLimit-Burst-Limit: 100
X-RateLimit-Burst-Remaining: 95
X-RateLimit-Burst-Reset: 45
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 45
X-Execution-Time: 0.045s
```

## What happens when you exceed a limit (HTTP 429)

When you hit a rate limit, Tango responds with **HTTP 429** and a JSON body like:

```json
{
  "detail": "Rate limit exceeded for burst. Please try again in 45 seconds.",
  "wait_in_seconds": 45
}
```

### Recommended client behavior

- **Stop retrying immediately** after a 429.
- Sleep for **at least** `wait_in_seconds` (preferred) or `X-RateLimit-Reset`.
- Then retry with **exponential backoff + jitter** to avoid a thundering herd.

#### Python example

```python
import random
import time
import httpx

url = "https://tango.makegov.com/api/contracts/?limit=1"
headers = {"X-API-KEY": "your-api-key-here"}

backoff = 1.0
for _ in range(10):
    r = httpx.get(url, headers=headers)
    if r.status_code != 429:
        r.raise_for_status()
        break

    body = r.json()
    wait = body.get("wait_in_seconds")
    if wait is not None:
        time.sleep(float(wait))
        continue

    reset = r.headers.get("X-RateLimit-Reset")
    if reset is not None:
        time.sleep(float(reset))
        continue

    time.sleep(backoff + random.random())
    backoff = min(backoff * 2, 60)
```

#### JavaScript example

This example assumes `fetch` is available (modern browsers or Node 18+).

```javascript
(async () => {
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  const url = "https://tango.makegov.com/api/contracts/?limit=1";
  const headers = { "X-API-KEY": "your-api-key-here" };

  let backoffMs = 1000;
  for (let i = 0; i < 10; i++) {
    const r = await fetch(url, { headers });
    if (r.status !== 429) {
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      break;
    }

    const body = await r.json();
    if (body.wait_in_seconds != null) {
      await sleep(body.wait_in_seconds * 1000);
      continue;
    }

    const reset = r.headers.get("X-RateLimit-Reset");
    if (reset != null) {
      await sleep(Number(reset) * 1000);
      continue;
    }

    await sleep(backoffMs + Math.random() * 250);
    backoffMs = Math.min(backoffMs * 2, 60_000);
  }
})().catch((err) => {
  console.error("Request failed:", err);
});
```

## Reduce calls (and avoid limits) in practice

- Use **response shaping** (`shape=`) to avoid extra “follow-up” requests. See the [Response Shaping Guide](response-shaping.md).
- Paginate responsibly; avoid re-fetching the same pages repeatedly.
- Cache hot lookups on your side when appropriate (e.g. “entity by UEI”).
- Prefer webhooks for event-driven updates instead of polling where possible.

---

<!-- doc: guides/patterns/response-shaping.md -->

# Response Shaping (Custom Fields)

Tango’s API lets you request only the fields you want by passing a `shape` query parameter. Shaped responses are built directly from your selection for speed and consistency. If you omit `shape`, the endpoint returns the default serializer output.

## Endpoints with shaping

- Agencies (legacy lookups):
  - Departments: `/api/departments/`
  - Agencies: `/api/agencies/`
  - Offices: `/api/offices/`
- Reference data:
  - Business Types: `/api/business_types/`
  - NAICS: `/api/naics/`
  - PSC: `/api/psc/`
  - Assistance Listings: `/api/assistance_listings/`
  - MAS SINs: `/api/mas_sins/`
- Awards (canonical routes):
  - Contracts: `/api/contracts/`
  - IDVs: `/api/idvs/`
  - OTAs: `/api/otas/`
  - OTIDVs: `/api/otidvs/`
  - Subawards: `/api/subawards/`
  - Vehicles: `/api/vehicles/`
  - GSA eLibrary Contracts: `/api/gsa_elibrary_contracts/`
  - CALC Rates (nested): `/api/idvs/{key}/lcats/`, `/api/entities/{uei}/lcats/`
- Organizations: `/api/organizations/`
- Entities: `/api/entities/`
- Forecasts: `/api/forecasts/`
- Grants: `/api/grants/`
- IT Dashboard: `/api/itdashboard/`
- Opportunities (canonical routes):
  - Opportunities: `/api/opportunities/`
  - Opportunity Notices: `/api/notices/`
- Protests: `/api/protests/`

All standard filters, ordering, pagination, throttling, and rate‑limit headers still apply.

## Shape syntax

- Root leaves: `key,piid,award_date`
- Expansions (related objects): `recipient(display_name,uei)`
- Multiple items: `key,piid,recipient(display_name),awarding_office(office_code,office_name)`
- Star in child selections: `recipient(*)` returns all allowed fields of `recipient`
- Aliasing: `field::alias` or `child(field::alias)`
- Bracket parameters: `expand[key=value](children)` — pass configuration to an expansion

### Bracket parameters

Some expansions accept parameters via bracket syntax. Parameters go between the expansion name and the child selection:

```bash
# Top 10 agencies in past_performance
/api/entities/?shape=uei,past_performance[top=10](*)

# Multiple params (comma-separated)
/api/entities/?shape=uei,past_performance[top=3](summary,top_agencies)
```

Parameters are validated server-side with type checking and range enforcement. Unknown or out-of-range parameters return a 400 error with details.

### Leaves vs expansions

In the shape DSL there are two different kinds of selections:

- **Leaf**: `field_name`
- **Expansion**: `expand_name(child1,child2,...)`

Some endpoints expose the **same name** as both a leaf and an expansion. In those cases, the API will keep the selection as a **leaf** unless the shaping runtime determines that treating the token as a leaf would be unsafe (see “Bare expansions” below).

Example (Opportunities set-aside):

```bash
# Leaf (code string)
/api/opportunities/?shape=opportunity_id,title,set_aside

# Expansion (structured form)
/api/opportunities/?shape=opportunity_id,title,set_aside(code,description)
```

### Bare expansions (shorthand)

If you write an expansion name **without** parentheses, e.g. `office` instead of `office(*)`, Tango will sometimes treat it as shorthand for `office(*)`.

This shorthand is applied **only when needed for safety**:

- when the expansion is a relation fetched via a database join, or
- when the expansion is a “virtual” expansion that reads its values from the parent object, **unless** the value is field-map-backed.

Examples (Notices):

```bash
# Bare expansions: office == office(*), opportunity == opportunity(*)
/api/notices/?shape=notice_id,opportunity_id,office,opportunity&limit=1
```

Example response (shape excerpt):

```json
[
  {
    "notice_id": "d2e3f9bf-9c2e-4c58-84f1-2b2b8a1d3b4d",
    "opportunity_id": "ff12a2d0-3b8a-4a1d-8d7f-7d8c2b6c0a11",
    "office": {
      "organization_id": "f3ab8e7c-6c2d-4e8e-9f1a-2b3c4d5e6f70",
      "office_code": "W91QF5",
      "office_name": "ACC-APG",
      "agency_code": "9700",
      "agency_name": "DEPT OF THE ARMY",
      "department_code": "97",
      "department_name": "DEPARTMENT OF DEFENSE"
    },
    "opportunity": {
      "opportunity_id": "ff12a2d0-3b8a-4a1d-8d7f-7d8c2b6c0a11",
      "link": "/api/opportunities/ff12a2d0-3b8a-4a1d-8d7f-7d8c2b6c0a11/"
    }
  }
]
```

Examples:

```bash
# Basic
/api/contracts/?shape=key,piid,recipient(display_name)

# Aliasing
/api/contracts/?shape=key::id,recipient(display_name::name)

# Star in nested expansion
/api/contracts/?shape=key,recipient(*),transactions(*)
```

Child field aliasing inside an expand:

```bash
/api/contracts/?shape=key,awarding_office(agency_name::foo)
```

Response:

```json
[
  {
    "key": "CONT_AWD_...",
    "awarding_office": { "foo": "General Services Administration" }
  }
]
```

Notes:

- `*` is only valid inside a child selection (e.g., `recipient(*)`). Root‑level `*` is not supported.
- Shaping requests that include unknown fields/expands, malformed shapes, or `shape` on non-shaping endpoints return a 400 with a structured error payload.

### Flattening

Add `flat=true` to flatten nested objects with a joiner (default `.`). Works with both paginated and single‑object responses.

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/contracts/?shape=key,recipient(display_name)&flat=true&joiner=."
```

Response:

```json
[
  {
    "key": "CONT_AWD_...",
    "recipient.display_name": "ACME CORP"
  }
]
```

Notes:

- Flattening applies to nested objects (dicts). Lists remain arrays for readability and stability by default. Expansions like `relationships` (entities) and `attachments` (opportunities/notices) will remain arrays of objects even when `flat=true`.
- Optional: `flat_lists=true` will additionally flatten lists of objects into indexed keys (e.g., `relationships.0.uei`). This is opt‑in and defaults to `false`.

Examples:

```bash
# Flatten dicts only (default list behavior)
/api/entities/?shape=uei,relationships(uei,display_name)&flat=true

# Also flatten list elements into indexed keys
/api/entities/?shape=uei,relationships(uei,display_name)&flat=true&flat_lists=true&joiner=.
```

## Validation & limits

- Unknown fields or expansions return a 400 with structured issues.
- Malformed shapes (syntax errors) return a 400 with a `parse_error` issue.
- Passing `shape` to an endpoint that does not support shaping returns a 400 (instead of ignoring `shape`).
- Default limits (subject to change):
  - `SHAPE_MAX_DEPTH` = 2
  - `SHAPE_MAX_FIELDS` = 64 (for user-supplied `?shape=` params; server-controlled default shapes may exceed this)

Example errors:

```json
{
  "error": "Invalid shape",
  "issues": [{"path": "foo", "reason": "unknown_field"}],
  "available_fields": {
    "fields": ["award_date", "key", "piid", "recipient"],
    "expands": {"recipient": {"fields": ["uei", "display_name"], "expands": {}, "wildcard_fields": false}},
    "wildcard_fields": false
  }
}
```

```json
{
  "error": "Invalid shape",
  "issues": [{"path": "$", "reason": "parse_error (Unclosed '(')"}],
  "available_fields": {
    "fields": ["award_date", "key", "piid", "recipient"],
    "expands": {"recipient": {"fields": ["uei", "display_name"], "expands": {}, "wildcard_fields": false}},
    "wildcard_fields": false
  }
}
```

```json
{
  "error": "Invalid shape value(s): foo. Shaping is not available for /api/example/.",
  "issues": [{"path": "$", "reason": "shape_not_supported"}]
}
```

## Discovering available fields

Every shaped endpoint supports `?show_shapes=true` for schema discovery. Truthy values: `true`, `1`, `yes`, `on`.

It works in two modes:

### Schema-only (no `?shape=`)

Pass `?show_shapes=true` without a `?shape=` parameter to get just the schema — no data is fetched, no DB query is made:

```bash
/api/contracts/?show_shapes=true
```

```json
{
  "available_fields": {
    "fields": ["award_date", "awarding_office", "description", "key", "naics_code", "piid", "recipient", "transactions"],
    "expands": {
      "awarding_office": {"fields": ["organization_id", "agency_code", "agency_name", "office_code", "office_name", "department_code", "department_name"], "expands": {}, "wildcard_fields": false},
      "recipient": {"fields": ["cage_code", "display_name", "legal_business_name", "uei"], "expands": {}, "wildcard_fields": false},
      "transactions": {"fields": ["action_type", "description", "modification_number", "obligated", "transaction_date"], "expands": {}, "wildcard_fields": false}
    },
    "wildcard_fields": false
  }
}
```

### Schema appended to data (with `?shape=`)

Pass both `?shape=...` and `?show_shapes=true` to get the normal shaped response with a `_shaping` block appended:

```bash
/api/contracts/?shape=key,piid&show_shapes=true
```

```json
{
  "count": 1234,
  "next": "...",
  "previous": null,
  "results": [
    {"key": "CONT_AWD_...", "piid": "GS00Q14OADS134"}
  ],
  "_shaping": {
    "available_fields": {
      "fields": ["award_date", "awarding_office", "description", "key", "naics_code", "piid", "recipient", "transactions"],
      "expands": {
        "awarding_office": {"fields": ["organization_id", "agency_code", "agency_name", "office_code", "office_name", "department_code", "department_name"], "expands": {}, "wildcard_fields": false},
        "recipient": {"fields": ["cage_code", "display_name", "legal_business_name", "uei"], "expands": {}, "wildcard_fields": false},
        "transactions": {"fields": ["action_type", "description", "modification_number", "obligated", "transaction_date"], "expands": {}, "wildcard_fields": false}
      },
      "wildcard_fields": false
    }
  }
}
```

### Schema structure

Each level of the schema contains:

| Key | Type | Meaning |
| --- | --- | --- |
| `fields` | array | All valid shape keys at this level, including both leaf fields and expand names. |
| `expands` | object | Expandable relations, each containing its own nested `{fields, expands, wildcard_fields}` schema. |
| `wildcard_fields` | boolean | When `true`, the expansion accepts any field name (i.e., `expand(*)` returns all keys from the underlying data, and you can also request arbitrary keys by name). When `false`, only the fields listed in `fields` are valid. Most endpoints return `false`; `wildcard_fields: true` appears on expansions backed by freeform JSON (e.g., `raw_data(*)` on forecasts). |

Notes:

- Schema-only mode (`?show_shapes=true` alone) makes no DB queries — safe to call frequently.
- `_shaping` is appended after `results` in paginated responses and after all other keys in detail responses, so existing clients that ignore unknown keys are unaffected.
- All 400 error responses for invalid shapes also include `available_fields` at the top level (no `?show_shapes` needed).

## Date/time formatting

All datetimes in shaped responses are formatted using a consistent ISO‑8601 format.

## Identity fallback for missing relations

When an expanded relation is missing (e.g., a join yields no row), the API returns a minimal identity object using the relation's key value instead of `null`:

```json
{
  "parent_award": {"key": "PARENT-123"}
}
```

## Field reference by endpoint (excerpt)

Below is a concise reference of commonly used fields and expansions per endpoint. Use `(*)` inside an expansion to request all allowed fields for that relation.

### Departments `/api/departments/`

- Leaves: `code`, `name`, `abbreviation`, `description`, `cgac`, `website`, `congressional_justification`
- No expansions.

Default response (no `?shape=`): `code,name,abbreviation`

```bash
# Select specific fields
/api/departments/?shape=code,name

# Extended department info
/api/departments/?shape=code,name,description,cgac,website,congressional_justification
```

### Agencies `/api/agencies/`

- Leaves: `code`, `name`, `abbreviation`
- Expansions:
  - `department(code,name,abbreviation,description,cgac,website,congressional_justification)`

Default response (no `?shape=`): `code,name,abbreviation,department(name,code)`

```bash
# Just codes and names
/api/agencies/?shape=code,name

# Include department info
/api/agencies/?shape=code,name,department(name,code)

# Department with extended fields
/api/agencies/?shape=code,department(name,description,cgac,website)

# All department fields
/api/agencies/?shape=code,department(*)
```

### Offices `/api/offices/`

- Leaves: `code`, `name`, plus flat aliases `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`
- Expansions:
  - `agency(code,name,abbreviation)`
  - `department(code,name,abbreviation,description,cgac,website,congressional_justification)`

Default response (no `?shape=`): `office_code,office_name,agency_code,agency_name,department_code,department_name` (flat aliases)

```bash
# Default flat format
/api/offices/?shape=office_code,office_name,agency_name

# Direct leaves with nested expansions
/api/offices/?shape=code,name,agency(code,name,abbreviation)

# Office with department details
/api/offices/?shape=code,name,department(name,description,website)

# Both expansions
/api/offices/?shape=code,name,agency(code,name),department(code,name)
```

### Business Types `/api/business_types/`

- Leaves: `name`, `code`
- No expansions.

Default response (no `?shape=`): `name,code`

```bash
# Select a single field
/api/business_types/?shape=name
```

### NAICS `/api/naics/`

- Leaves: `code`, `description`
- Expansions:
  - `size_standards(employee_limit,revenue_limit)` — SBA size standards for this NAICS code. `revenue_limit` is in whole dollars (e.g., `2250000`).
  - `federal_obligations(total,active)` — obligation rollups. Each sub-object contains `awards_obligated` (float) and `awards_count` (int).

Default response (list, no `?shape=`): `code,description`

Default response (detail or `show_limits=1`): `code,description,size_standards(*),federal_obligations(*)`

**Note:** The legacy `?show_limits=1` parameter is still supported — it triggers the same response as `?shape=code,description,size_standards(*),federal_obligations(*)`. Prefer using `?shape=` directly.

```bash
# List with just code and description (default)
/api/naics/?shape=code,description

# Include size standards and obligations (equivalent to ?show_limits=1)
/api/naics/?shape=code,description,size_standards(*),federal_obligations(*)

# Size standards only
/api/naics/541330/?shape=code,description,size_standards(employee_limit,revenue_limit)

# Obligations total only
/api/naics/541330/?shape=code,federal_obligations(total)
```

### PSC `/api/psc/`

- Leaves: `code`, `parent`, `category`, `level_1_category`, `level_1_category_code`, `level_2_category`, `level_2_category_code`
- Expansions:
  - `current(name,active,start_date,end_date,description,includes,excludes)` — active or latest description
  - `historical(name,active,start_date,end_date,description,includes,excludes)` — all descriptions

Default response (no `?shape=`): `code,current(*),parent,category,level_1_category,level_1_category_code,level_2_category,level_2_category_code`

```bash
# Just code and current description name
/api/psc/?shape=code,current(name)

# Code with current and historical descriptions
/api/psc/AD12/?shape=code,current(name,active),historical(name,active)
```

### Assistance Listings `/api/assistance_listings/`

- Leaves: `number`, `title`, `published_date`, `archived_date`, `popular_name`, `objectives`, `applicant_eligibility`, `benefit_eligibility`
- No expansions.

Default response (list, no `?shape=`): `number,title`

**Note:** Detail responses (`/api/assistance_listings/{number}/`) use the full serializer (includes all fields) when no `?shape=` param is provided.

```bash
# Include extra fields
/api/assistance_listings/?shape=number,title,objectives

# Detail with specific fields
/api/assistance_listings/10.001/?shape=number,title,objectives,popular_name
```

### MAS SINs `/api/mas_sins/`

- Leaves: `sin`, `large_category_code`, `large_category_name`, `sub_category_code`, `sub_category_name`, `psc_code`, `state_local`, `set_aside_code`, `service_comm_code`, `expiration_date`, `tdr`, `olm`, `naics_codes`, `title`, `description`
- No expansions.

Default response (no `?shape=`): all 15 fields above.

```bash
# Select a subset of fields
/api/mas_sins/?shape=sin,title,description
```

### Organizations `/api/organizations/`

Organizations uses a default shape — all responses go through the shaping pipeline even without `?shape=`.

- Default shape: `key,fh_key,name,short_name,type,level,is_active,code,fpds_code,cgac,canonical_code,parent_fh_key,full_parent_path_name`
- Additional leaves: `fpds_org_id`, `aac_code`, `start_date`, `end_date`, `logo`, `summary`, `l1_fh_key` through `l8_fh_key`, `total_obligations`, `tree_obligations`, `mod_status`, `description`, `obligations`, `obligation_rank`
- Expansions:
  - `parent(key,fh_key,name,short_name,type,level,is_active,code,cgac)`
  - `ancestors(fh_key,name,short_name,level)`
  - `children(key,fh_key,name,short_name,type,level,is_active,code,cgac)`
  - `department(code,name,abbreviation)`
  - `agency(code,name,abbreviation)`

```bash
# Default response (all responses are shaped)
/api/organizations/

# Select specific fields
/api/organizations/?shape=fh_key,name,type,level

# Include department and agency expands
/api/organizations/?shape=fh_key,name,department(code,name),agency(code,name,abbreviation)
```

### Contracts `/api/contracts/`

- Leaves: `key`, `piid`, `award_date`, `naics_code`, `psc_code`, `total_contract_value`, `description`, `base_and_exercised_options_value`, `fiscal_year`, `obligated`, `set_aside`, `award_type`, `transactions`, `subawards_summary`
- Expansions:
  - `recipient(uei,display_name,legal_business_name,cage,cage_code,...)`
  - `awarding_office(organization_id,office_code,office_name,agency_code,agency_name,department_code,department_name)`
  - `funding_office(...)` — same 7-key shape as `awarding_office`
  - `place_of_performance(country_code,country_name,state_code,state_name,city_name,zip_code)`
  - `parent_award(key,piid)`
  - `period_of_performance(start_date,current_end_date,ultimate_completion_date)`
  - `transactions(modification_number,transaction_date,obligated,description,action_type)`
  - `subawards_summary(count,total_amount)`
  - `competition(contract_type,extent_competed,number_of_offers_received,other_than_full_and_open_competition,solicitation_date,solicitation_identifier,solicitation_procedures)`
  - `officers(highly_compensated_officer_1_name,highly_compensated_officer_1_amount,highly_compensated_officer_2_name,highly_compensated_officer_2_amount,highly_compensated_officer_3_name,highly_compensated_officer_3_amount,highly_compensated_officer_4_name,highly_compensated_officer_4_amount,highly_compensated_officer_5_name,highly_compensated_officer_5_amount)` – returns `null` if no officers are on record. Not included in default serializers; available only via shaping.

Examples:

```bash
# Minimal contract fields
/api/contracts/?shape=key,piid,award_date

# Recipient and awarding office
/api/contracts/?shape=key,recipient(display_name),awarding_office(office_code,office_name)

# Period and transactions
/api/contracts/?shape=key,period_of_performance(start_date,current_end_date),transactions(modification_number,obligated)

# Officers (subset)
/api/contracts/?shape=key,officers(highly_compensated_officer_1_name,highly_compensated_officer_1_amount)

# Officers (all allowed fields)
/api/contracts/?shape=key,officers(*)
```

### IDVs `/api/idvs/`

- Leaves: `key`, `piid`, `vehicle_uuid`, `solicitation_identifier`, `award_date`, `naics_code`, `psc_code`, `total_contract_value`, `description`, `fiscal_year`, `obligated`, `idv_type`, `multiple_or_single_award_idv`, `type_of_idc`, `set_aside`, `transactions`, `subawards_summary`
- Detail-only leaves: `commercial_item_acquisition_procedures`, `consolidated_contract`, `contingency_humanitarian_or_peacekeeping_operation`, `contract_bundling`, `contract_financing`, `cost_accounting_standards_clause`, `cost_or_pricing_data`, `dod_acquisition_program`, `dod_transaction_number`, `domestic_or_foreign_entity`, `email_address`, `epa_designated_product`, `evaluated_preference`, `fair_opportunity_limited_sources`, `fed_biz_opps`, `fee_range_lower_value`, `fee_range_upper_value`, `fixed_fee_value`, `foreign_funding`, `government_furnished_property`, `idv_website`, `inherently_governmental_functions`, `local_area_set_aside`, `major_program`, `number_of_actions`, `number_of_offers_source`, `ordering_procedure`, `performance_based_service_acquisition`, `program_acronym`, `recovered_materials_sustainability`, `research`, `sam_exception`, `simplified_procedures_for_certain_commercial_items`, `small_business_competitiveness_demonstration_program`, `subcontracting_plan`, `total_estimated_order_value`, `tradeoff_process`, `type_of_fee_for_use_of_service`, `undefinitized_action`, `who_can_use`
- Expansions:
  - `recipient(uei,display_name,legal_business_name,cage,cage_code,...)`
  - `awarding_office(organization_id,office_code,office_name,agency_code,agency_name,department_code,department_name)`
  - `funding_office(...)` — same 7-key shape as `awarding_office`
  - `place_of_performance(country_code,country_name,state_code,state_name,city_name,zip_code)`
  - `parent_award(key,piid)`
  - `period_of_performance(start_date,last_date_to_order)` — note: uses `last_date_to_order` instead of contracts' `current_end_date`/`ultimate_completion_date`
  - `transactions(modification_number,transaction_date,obligated,description,action_type)`
  - `subawards_summary(count,total_amount)`
  - `idv_type(code,description)` — IDV type as structured code/description
  - `multiple_or_single_award_idv(code,description)` — single/multiple award designation
  - `type_of_idc(code,description)` — IDC type as structured code/description
  - `set_aside(code,description)` — set-aside type
  - `naics(code,description)` — resolved NAICS lookup
  - `psc(code,description)` — resolved PSC lookup
  - `competition(contract_type,extent_competed,number_of_offers_received,other_than_full_and_open_competition,solicitation_date,solicitation_identifier,solicitation_procedures)`
  - `officers(...)` — same fields as contracts; `null` when absent; shaping-only
  - `awards(key,piid,award_date,naics_code,psc_code,total_contract_value,description,base_and_exercised_options_value,fiscal_year,obligated,transactions)` — child awards under this IDV
  - `gsa_elibrary(schedule,contract_number,uei,sins,cooperative_purchasing,disaster_recovery_purchasing,file_urls,extracted_text,external_id,source_data)` — GSA eLibrary enrichment (`null` when no match)
  - `legislative_mandates(...)` — legislative mandate flags

Default response (list, no `?shape=`): `award_date,awarding_office(*),description,fiscal_year,funding_office(*),gsa_elibrary(*),idv_type(*),key,legislative_mandates(*),multiple_or_single_award_idv(*),naics_code,obligated,parent_award(*),period_of_performance(*),piid,place_of_performance(*),psc_code,recipient(*),set_aside,solicitation_identifier,subawards_summary(*),total_contract_value,type_of_idc(*),vehicle_uuid`

Default response (detail): adds all detail-only leaves plus `competition(*)` and `officers(*)`

```bash
# Minimal IDV fields
/api/idvs/?shape=key,piid,award_date

# Expand idv_type to get code + description
/api/idvs/?shape=key,piid,idv_type(code,description)

# IDV with recipient and awarding office
/api/idvs/?shape=key,piid,recipient(display_name,uei),awarding_office(office_name,agency_name)

# Period of performance with last_date_to_order
/api/idvs/?shape=key,piid,period_of_performance(start_date,last_date_to_order)

# GSA eLibrary enrichment
/api/idvs/?piid=GS-35F-0001X&shape=key,piid,gsa_elibrary(schedule,contract_number,sins,file_urls)

# Child awards under an IDV
/api/idvs/{key}/?shape=key,piid,awards(key,piid,total_contract_value)
```

### Vehicles `/api/vehicles/`

- Leaves: `uuid`, `solicitation_identifier`, `agency_id`, `organization_id`, `vehicle_type`, `fiscal_year`, `agency_details`, `descriptions`, `competition_details`, `type_of_idc`, `contract_type`, `awardee_count`, `order_count`, `vehicle_obligations`, `vehicle_contracts_value`, `solicitation_title`, `solicitation_description`, `solicitation_date`, `naics_code`, `psc_code`, `set_aside`
- Expansions:
  - `awardees(...)`: expands to the underlying IDVs that make up the vehicle
  - `opportunity(...)`: expands to the linked SAM.gov Opportunity (supports all Opportunity fields and expansions like `office`, `attachments`, `meta`)
  - `competition_details(...)`: expands the `competition_details` JSON object (fields: `extent_competed`, `number_of_offers_received`, `set_aside`, `solicitation_procedures`, etc., or `*` for all)

Default response (list, no `?shape=`): `agency_details,award_date,awardee_count,competition_details(*),contract_type,description,fiscal_year,last_date_to_order,latest_award_date,naics_code,order_count,psc_code,set_aside,solicitation_date,solicitation_identifier,solicitation_title,type_of_idc,uuid,vehicle_contracts_value,vehicle_obligations,vehicle_type,who_can_use`

Default response (detail): adds `agency_id`, `organization_id`, `solicitation_description`

Notes:

- `GET /api/vehicles/?search=...` performs **vehicle-level** full-text search.
- On `GET /api/vehicles/{uuid}/`, `?search=` is reserved for filtering expanded `awardees(...)` (it does not filter the vehicle itself).
- Opportunity-derived fields (`solicitation_title`, `solicitation_description`, `solicitation_date`, `naics_code`, `psc_code`, `set_aside`) are populated from the linked SAM.gov Opportunity's latest notice when available.

Examples:

```bash
# Find vehicles by term
/api/vehicles/?search=schedule

# Vehicle detail + shaped award expansion
/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,awardees(key,uuid,piid,award_date,recipient(display_name,uei))

# Include opportunity-derived fields
/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,solicitation_title,solicitation_date,naics_code,psc_code

# Expand linked Opportunity with office details
/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,opportunity(opportunity_id,title,first_notice_date,office(office_name,agency_name))

# Expand JSON detail fields to select specific sub-fields
/api/vehicles/<vehicle-uuid>/?shape=uuid,competition_details(extent_competed,number_of_offers_received),type_of_idc,contract_type

# Filter expanded awards within a vehicle
/api/vehicles/<vehicle-uuid>/?shape=uuid,awardees(key,uuid,recipient(display_name))&search=acme
```

### OTAs `/api/otas/`

- Leaves: `key`, `piid`, `award_date`, `psc_code`, `total_contract_value`, `description`, `base_and_exercised_options_value`, `fiscal_year`, `obligated`, `award_type`, `type_of_ot_agreement`, `extent_competed`, `consortia`, `consortia_uei`, `dod_acquisition_program`, `non_governmental_dollars`, `non_traditional_government_contractor_participation`, `parent_award_modification_number`, `transactions`
- Expansions:
  - `recipient(uei,display_name,legal_business_name,cage,cage_code,...)`
  - `awarding_office(organization_id,office_code,office_name,agency_code,agency_name,department_code,department_name)`
  - `funding_office(...)` — same 7-key shape as `awarding_office`
  - `place_of_performance(country_code,country_name,state_code,state_name,city_name,zip_code)`
  - `parent_award(key,piid)`
  - `period_of_performance(start_date,current_end_date,ultimate_completion_date)`
  - `transactions(modification_number,transaction_date,obligated,description,action_type)`
  - `psc(code,description)`
  - `award_type(code,description)` — OTA-specific choices ("O" = Other Transaction Non-Research, "R" = Other Transaction for Research)
  - `type_of_ot_agreement(code,description)`
  - `extent_competed(code,description)`

Default response (no `?shape=`): `award_date,award_type,awarding_office(*),base_and_exercised_options_value,consortia,consortia_uei,dod_acquisition_program,extent_competed,fiscal_year,funding_office(*),key,non_governmental_dollars,non_traditional_government_contractor_participation,parent_award(key,piid),parent_award_modification_number,period_of_performance(start_date,current_end_date,ultimate_completion_date),piid,place_of_performance(*),psc_code,recipient(*),type_of_ot_agreement`

**Note:** OTAs have no `naics_code` field and no `naics` expand.

```bash
# Minimal OTA fields
/api/otas/?shape=key,piid,award_date

# Expand award_type to get OTA-specific code + description
/api/otas/?shape=key,piid,award_type(code,description)

# Expand extent competed and type of OT agreement
/api/otas/?shape=key,piid,extent_competed(code,description),type_of_ot_agreement(code,description)

# OTA with recipient and awarding office
/api/otas/?shape=key,piid,recipient(display_name,uei),awarding_office(office_name,agency_name)
```

### OTIDVs `/api/otidvs/`

- Leaves: `key`, `piid`, `award_date`, `psc_code`, `total_contract_value`, `description`, `base_and_exercised_options_value`, `fiscal_year`, `obligated`, `idv_type`, `type_of_ot_agreement`, `extent_competed`, `consortia`, `consortia_uei`, `dod_acquisition_program`, `non_governmental_dollars`, `non_traditional_government_contractor_participation`, `transactions`
- Expansions:
  - `recipient(uei,display_name,legal_business_name,cage,cage_code,...)`
  - `awarding_office(organization_id,office_code,office_name,agency_code,agency_name,department_code,department_name)`
  - `funding_office(...)` — same 7-key shape as `awarding_office`
  - `place_of_performance(country_code,country_name,state_code,state_name,city_name,zip_code)`
  - `period_of_performance(start_date,current_end_date,ultimate_completion_date)`
  - `transactions(modification_number,transaction_date,obligated,description,action_type)`
  - `psc(code,description)`
  - `type_of_ot_agreement(code,description)`
  - `extent_competed(code,description)`

Default response (no `?shape=`): `award_date,awarding_office(*),base_and_exercised_options_value,consortia,consortia_uei,dod_acquisition_program,extent_competed,fiscal_year,funding_office(*),idv_type,key,non_governmental_dollars,non_traditional_government_contractor_participation,period_of_performance(start_date,current_end_date,ultimate_completion_date),piid,place_of_performance(*),psc_code,recipient(*),type_of_ot_agreement`

**Note:** OTIDVs have no `naics_code` field, no `naics` expand, and no `parent_award` expand.

```bash
# Minimal OTIDV fields
/api/otidvs/?shape=key,piid,award_date

# Expand extent competed and type of OT agreement
/api/otidvs/?shape=key,piid,extent_competed(code,description),type_of_ot_agreement(code,description)

# OTIDV with recipient
/api/otidvs/?shape=key,piid,recipient(display_name,uei),awarding_office(office_name,agency_name)
```

### Subawards `/api/subawards/`

Subawards use a default shape — all responses go through the shaping pipeline even without `?shape=`.

- Default response (no `?shape=`): `award_key,awarding_office(*),fsrs_details(*),funding_office(*),key,piid,place_of_performance(*),prime_recipient(uei,display_name),subaward_details(*),subaward_recipient(uei,display_name)`
- Leaves: identity + prime award metadata (`piid`, `prime_award_total_outlayed_amount`, dates/NAICS/description, prime/subawardee identities, business types, and `usaspending_permalink`)
- Expansions:
  - `subaward_details(description,type,number,amount,action_date,fiscal_year)`
  - `fsrs_details(last_modified_date,id,year,month)`
  - `place_of_performance(city,state,zip,country_code)`
  - `highly_compensated_officers(name,amount)`
  - `prime_recipient(uei,display_name)`, `subaward_recipient(uei,display_name,duns)`
  - `awarding_office(*)`, `funding_office(*)`

```bash
# Default response (shaped automatically)
/api/subawards/

# Select specific fields
/api/subawards/?shape=key,piid,subaward_details(*)

# Recipient and office info
/api/subawards/?shape=key,prime_recipient(uei,display_name),awarding_office(*)
```

### GSA eLibrary Contracts `/api/gsa_elibrary_contracts/`

- Leaves: `uuid`, `schedule`, `contract_number`, `sins`, `cooperative_purchasing`, `disaster_recovery_purchasing`, `file_urls`
- Expansions:
  - `recipient(uei,display_name)`
  - `idv(key,award_date)`

```bash
# Basic fields
/api/gsa_elibrary_contracts/?shape=uuid,schedule,contract_number

# With recipient info
/api/gsa_elibrary_contracts/?shape=uuid,contract_number,recipient(uei,display_name)
```

### Opportunities `/api/opportunities/`

- Leaves: `opportunity_id`, `latest_notice_id`, `archive_date`, `title`, `description`, `naics_code`, `psc_code`, `response_deadline`, `first_notice_date`, `last_notice_date`, `active`, `set_aside`, `award_number`, `solicitation_number`, `snippet`, `sam_url`, plus relation id leaves: `agency_id`, `department_id`, `office_id`
- Expansions:
  - `attachments(attachment_id,extracted_text,mime_type,name,posted_date,resource_id,type,url)` (shorthand: `attachments` → `attachments(*)`)
  - `office(office_code,office_name,agency_code,agency_name,department_code,department_name)` (shorthand: `office` → `office(*)`)
  - `agency(*)` (shorthand: `agency` → `agency(*)`)
  - `department(*)` (shorthand: `department` → `department(*)`)
  - `latest_notice(notice_id,link)` (shorthand: `latest_notice` → `latest_notice(*)`)
  - `place_of_performance(street_address,city,state,zip,country)`
  - `meta(notices_count,attachments_count,notice_type(code,type))`
  - `set_aside(code,description)`
  - `notice_history(index,notice_id,latest,title,deleted,posted_date,notice_type_code,solicitation_number,parent_notice_id,related_notice_id)`
  - `primary_contact(title,full_name,email,phone,fax)`
  - `secondary_contact(title,full_name,email,phone,fax)`

Default response (list, no `?shape=`): `active,award_number,first_notice_date,last_notice_date,meta(*),naics_code,office(*),opportunity_id,place_of_performance(*),psc_code,response_deadline,sam_url,set_aside,solicitation_number,title`

Default response (detail): adds `attachments(*)`, `description`, `notice_history(*)`, `primary_contact(*)`

```bash
# Expand set_aside to get code + description
/api/opportunities/?shape=opportunity_id,title,set_aside(code,description)

# Office + attachments using bare expand shorthand
/api/opportunities/?shape=opportunity_id,title,office,attachments&limit=1

# Detail with notice history
/api/opportunities/{id}/?shape=opportunity_id,title,notice_history(*)

# Contacts
/api/opportunities/{id}/?shape=opportunity_id,primary_contact(*),secondary_contact(*)
```

### Notices `/api/notices/`

- Leaves: `notice_id`, `opportunity_id`, `title`, `description`, `naics_code`, `psc_code`, `posted_date`, `response_deadline`, `last_updated`, `active`, `set_aside`, `sam_url`, `attachment_count`, `award_number`, `solicitation_number`
- Expansions:
  - `attachments(attachment_id,extracted_text,mime_type,name,posted_date,resource_id,type,url)` (shorthand: `attachments` → `attachments(*)`)
  - `address(city,state,zip,country)`
  - `archive(date,type)`
  - `place_of_performance(street_address,city,state,zip,country)`
  - `primary_contact(title,full_name,email,phone,fax)`
  - `secondary_contact(title,full_name,email,phone,fax)`
  - `meta(parent_notice_id,related_notice_id,notice_type(code,type),link)`
  - `office(office_code,office_name,agency_code,agency_name,department_code,department_name)` (shorthand: `office` → `office(*)`)
  - `opportunity(opportunity_id,link)` (shorthand: `opportunity` → `opportunity(*)`)
  - `set_aside(code,description)`

Default response (list, no `?shape=`): `active,address(*),attachment_count,award_number,description,last_updated,meta(*),naics_code,notice_id,office(*),opportunity(*),place_of_performance(*),posted_date,psc_code,response_deadline,sam_url,set_aside,solicitation_number,title`

Default response (detail): adds `archive(*)`, `primary_contact(*)`, `secondary_contact(*)`

```bash
# Expand set_aside to get code + description
/api/notices/?shape=notice_id,title,set_aside(code,description)

# Include archive and contacts on detail
/api/notices/{id}/?shape=notice_id,title,archive(*),primary_contact(*)

# Get linked opportunity reference
/api/notices/?shape=notice_id,opportunity(opportunity_id,link)
```

### Entities `/api/entities/`

Entities default to the shaping pipeline. Default responses include core identity fields (list) or the full detail set (retrieve) with normalized field formats. Use `?shape=` to customize.

- Leaves: `uei`, `uuid`, `display_name`, `legal_business_name`, `dba_name`, `cage_code`, `dodaac`, `registered`, `registration_status`, `primary_naics`, `psc_codes`, `email_address`, `entity_url`, `description`, `capabilities`, `keywords`, `sam_activation_date`, `sam_registration_date`, `sam_expiration_date`, `last_update_date`, `congressional_district`, `evs_source`, `uei_status`, `uei_expiration_date`, `uei_creation_date`, `public_display_flag`, `exclusion_status_flag`, `exclusion_url`, `entity_division_name`, `entity_division_number`, `entity_start_date`, `fiscal_year_end_close_date`, `submission_date`
- Expansions:
  - `physical_address(address_line1,address_line2,city,state_or_province_code,zip_code,zip_code_plus4,country_code,country_name)` — normalized snake_case keys
  - `mailing_address(...)` — same fields as `physical_address`
  - `business_types(code,description)` — normalized `[{code, description}]`; auto-normalizes dict-format DSBS data
  - `sba_business_types(code,description,entry_date,exit_date)` — normalized SBA certifications
  - `naics_codes(code,sba_small_business)` — normalized; converts plain string arrays automatically
  - `federal_obligations(total,active)` — expand-only; `total` and `active` sub-objects with obligation metrics
  - `highest_owner(cage_code,legal_business_name,uei)` — highest corporate owner
  - `immediate_owner(cage_code,legal_business_name,uei)` — immediate parent entity
  - `relationships(relation,type,uei,display_name)`
  - `entity_structure(code,description)` — falls back to ENTITY_STRUCTURE_MAP for description
  - `entity_type(code,description)` — falls back to BUSINESS_TYPE_MAP for description
  - `profit_structure(code,description)` — falls back to BUSINESS_TYPE_MAP for description
  - `organization_structure(code,description)` — falls back to BUSINESS_TYPE_MAP for description
  - `state_of_incorporation(code,description)` — uses model description
  - `country_of_incorporation(code,description)` — falls back to the GENC standard for country names when SAM.gov doesn't provide one
  - `purpose_of_registration(code,description)` — falls back to PURPOSE_OF_REGISTRATION_MAP for description
  - `past_performance[top=N](summary,top_agencies)` — aggregated contract history; `summary` has `total_obligated`, `total_awards`, `agency_count`, `naics_count`, `psc_count`; `top_agencies` lists top N agencies (default 5, max 100) with `agency_code`, `agency_name`, `department_code`, `department_name`, `obligations`, `awards_count`, `top_naics`, `top_psc`. Accepts bracket param `[top=N]`.

```bash
# List with default shape (core identity fields)
/api/entities/

# Detail with all fields + normalized expands
/api/entities/ZQGGHJH74DW7/

# Custom shape: just UEI and business types
/api/entities/?shape=uei,business_types(code,description)

# Federal obligations for a specific entity
/api/entities/ZQGGHJH74DW7/?shape=uei,federal_obligations(total)

# Structured code/description fields
/api/entities/ZQGGHJH74DW7/?shape=uei,entity_structure(*),purpose_of_registration(*)

# Owner info
/api/entities/ZQGGHJH74DW7/?shape=uei,highest_owner(*),immediate_owner(*)

# Past performance (default top 5 agencies)
/api/entities/ZQGGHJH74DW7/?shape=uei,past_performance(*)

# Past performance with top 10 agencies
/api/entities/ZQGGHJH74DW7/?shape=uei,past_performance[top=10](summary,top_agencies)
```

### Forecasts `/api/forecasts/`

- Leaves: `id`, `source_system`, `external_id`, `agency`, `title`, `description`, `anticipated_award_date`, `fiscal_year`, `naics_code`, `is_active`, `status`, `created`, `modified`, `primary_contact`, `place_of_performance`, `estimated_period`, `set_aside`, `contract_vehicle`, `raw_data`
- Expansions:
  - `display(title,description,agency,anticipated_award_date,fiscal_year,naics_code,status,primary_contact,place_of_performance,estimated_period,set_aside,contract_vehicle)` — normalized view of the forecast's display data
  - `raw_data(*)` — the raw JSON data from the source system; accepts wildcard fields

```bash
# Basic forecast fields
/api/forecasts/?shape=id,title,agency,anticipated_award_date,status

# With display expansion for normalized view
/api/forecasts/?shape=id,title,display(agency,anticipated_award_date,primary_contact,place_of_performance)

# Include raw source data
/api/forecasts/?shape=id,title,raw_data(*)
```

### Grants `/api/grants/`

- Leaves: `agency_code`, `applicant_eligibility_description`, `description`, `forecast`, `funding_activity_category_description`, `grant_id`, `grantor_contact`, `last_updated`, `opportunity_history`, `opportunity_number`, `status`, `synopsis`, `title`
- Expansions:
  - `cfda_numbers(number,title)` — associated CFDA/Assistance Listing numbers
  - `applicant_types(code,description)`
  - `category(code,description)` — opportunity category
  - `funding_categories(code,description)`
  - `funding_instruments(code,description)`
  - `status(code,description)` — expanded status with code and label (use this when you want the structured form; `status` alone is treated as a leaf unless the runtime must expand it for safety)
  - `important_dates(posted_date,response_date,response_date_description,estimated_synopsis_post_date,estimated_application_response_date,estimated_application_response_date_description,estimated_project_start_date)` — key dates (fields vary by status: posted opportunities return `posted_date`/`response_date`; forecasted ones return `estimated_*` fields)
  - `funding_details(award_ceiling,award_floor,estimated_total_funding,expected_number_of_awards)`
  - `grantor_contact(name,phone,email)`
  - `additional_info(link,description)`
  - `attachments(mime_type,name,posted_date,resource_id,type,url)`

```bash
# Basic grant fields
/api/grants/?shape=grant_id,title,status,agency_code

# With CFDA numbers and applicant types
/api/grants/?shape=grant_id,title,cfda_numbers(number,title),applicant_types(code,description)

# Expanded status and funding instruments
/api/grants/?shape=grant_id,title,status(code,description),funding_instruments(code,description)
```

### IT Dashboard `/api/itdashboard/`

IT Dashboard exposes **tiered shaping**: Free gets basic investment metadata, Micro+ adds the `funding` and `details` expansions, and Medium+ adds nested sub-tables (contracts, projects, CIO evaluations, performance metrics, etc.) plus the `business_case_html` leaf. Fields or expansions above the caller's tier are silently stripped from the response, and removed nodes are listed in `meta.upgrade_hints`. For the full tier/field matrix, see the [IT Dashboard API Reference](../../api-reference/itdashboard.md#response-shaping).

- Leaves (all tiers): `uii`, `agency_code`, `agency_name`, `bureau_code`, `bureau_name`, `investment_title`, `type_of_investment`, `part_of_it_portfolio`, `updated_time`, `url`
- Medium+ leaf: `business_case_html`
- Expansions (Micro+):
  - `funding(*)` — fiscal-year internal funding and contributions (FY2020–FY2025)
  - `details(*)` — extended metadata: description, previous/current UII, classification, business case URL, public URLs
- Expansions (Medium+): `cio_evaluation(*)`, `contracts(*)`, `projects(*)`, `cost_pools_towers(*)`, `funding_sources(*)`, `performance_metrics(*)`, `performance_actual(*)`, `operational_analysis(*)`

Default response (no `?shape=`):

- List: `uii,agency_name,bureau_name,investment_title,type_of_investment,part_of_it_portfolio,updated_time,url`
- Detail: adds `agency_code,bureau_code`

```bash
# Free tier: basic metadata
/api/itdashboard/?shape=uii,investment_title,agency_name,url

# Micro+ tier: funding and details
/api/itdashboard/?shape=uii,investment_title,funding(*),details(*)

# Medium+ tier: nested sub-tables + business case HTML
/api/itdashboard/{uii}/?shape=uii,investment_title,cio_evaluation(*),performance_metrics(*),business_case_html
```

### Protests `/api/protests/`

Both list and detail return case-level objects identified by `case_id`. Dockets are available via the `dockets(...)` expansion.

- Leaves: `case_id`, `source_system`, `case_number`, `title`, `protester`, `agency`, `solicitation_number`, `case_type`, `outcome`, `filed_date`, `posted_date`, `decision_date`, `due_date`, `docket_url`, `decision_url`, `digest`
- Expansions:
  - `dockets(source_system,case_number,docket_number,title,protester,agency,solicitation_number,case_type,outcome,filed_date,posted_date,decision_date,due_date,docket_url,decision_url,digest)`

Default response (no `?shape=`): `case_id,source_system,case_number,title,protester,agency,solicitation_number,case_type,outcome,filed_date,posted_date,decision_date,due_date,docket_url,decision_url` (no nested dockets)

Notes:

- `case_number` is the base B-number (e.g. `b-424214`) identifying the case.
- `docket_number` (e.g. `b-424214.1`) identifies a specific sub-docket; only available inside `dockets(...)`.
- `digest` is opt-in only (decision summary text from `raw_data`).

```bash
# Case-level fields with nested dockets
/api/protests/?shape=case_id,case_number,title,dockets(docket_number,filed_date,outcome)

# Case-level only
/api/protests/?shape=case_id,title,outcome,decision_date

# Detail with dockets
/api/protests/{case_id}/?shape=case_id,title,dockets(docket_number,filed_date)
```

---

<!-- doc: guides/patterns/result-counts.md -->

# Result counts

Every paginated response includes a `count` field with the total number of matching results. The `X-Results-CountType` response header indicates whether that count is exact or approximate.

## Which endpoints use approximate counts?

Most endpoints always return **exact counts**. These high-volume endpoints use approximate counting by default:

- `/api/contracts/`
- `/api/idvs/`
- `/api/opportunities/`
- `/api/notices/`
- `/api/entities/`

Other endpoints (grants, subawards, vehicles, protests, etc.) always return exact counts. The `?exact=true` parameter has no effect on those — they're already exact. Inspect the `X-Results-CountType` response header (`exact` or `approximate`) on any request to confirm.

## How approximate counts work

Approximate counts use database query-planner estimates for large result sets and fall back to exact counts for small ones (typically under 1,000 rows). This means even the endpoints listed above often return exact counts when the result set is small enough.

## Requesting exact counts

For the endpoints that use approximate counting, add `?exact=true` to force an exact count:

```bash
GET /api/contracts/?exact=true
GET /api/opportunities/?exact=true&search=software
```

**Performance note:** Exact counts run a full `COUNT(*)` query, which can be slow on large, unfiltered result sets (e.g., all contracts). Use this when accuracy matters more than speed.

## Knowing whether a count is exact

Check the `X-Results-CountType` response header:

```plaintext
X-Results-CountType: exact
X-Results-CountType: approximate
```

This header is present on all paginated **list** responses. It is not included on detail (single-resource) endpoints.

## Examples

### Contracts (approximate by default)

```bash
curl -sI -H "X-API-KEY: your-key" \
  "https://tango.makegov.com/api/contracts/" | grep X-Results
```

```plaintext
X-Results-CountType: approximate
```

```json
{
  "count": 2541837,
  "next": "...",
  "results": [...]
}
```

### Contracts with exact count

```bash
curl -sI -H "X-API-KEY: your-key" \
  "https://tango.makegov.com/api/contracts/?exact=true" | grep X-Results
```

```plaintext
X-Results-CountType: exact
```

```json
{
  "count": 2541293,
  "next": "...",
  "results": [...]
}
```

### Entities (always exact)

```bash
curl -sI -H "X-API-KEY: your-key" \
  "https://tango.makegov.com/api/entities/" | grep X-Results
```

```plaintext
X-Results-CountType: exact
```

```json
{
  "count": 487293,
  "next": "...",
  "results": [...]
}
```

---

<!-- doc: guides/patterns/vehicles.md -->

# Vehicles

Vehicles provide a solicitation-centric way to discover groups of related IDVs and then expand into the underlying awards.

## What is a Vehicle?

In Tango, a **Vehicle** is a grouping of multiple IDVs that share the same `solicitation_identifier` and the same awarding-agency identifier derived from the IDV award `key`.

>In federal data, *each IDV award is a vehicle*.
>In practice, people often think of a “vehicle” as the *solicitation that produced many IDV awards* (one per awardee). We model that higher-level grouping explicitly.

Vehicles are useful when you want to:

- Find “the contract vehicle” behind a set of IDVs (e.g., the solicitation that produced a schedule/IDIQ vehicle)
- Search across a vehicle’s underlying IDVs
- Pull a shaped list of the IDVs that make up a vehicle

## Endpoints

- **List**: `GET /api/vehicles/`
- **Detail**: `GET /api/vehicles/{uuid}/`
- **Awardees** (the underlying IDVs): `GET /api/vehicles/{uuid}/awardees/` — supports `?search=`
- **Task Orders**: `GET /api/vehicles/{uuid}/orders/`

## Search

### Vehicle-level search (list)

Use `?search=` on the list endpoint to search vehicles:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/?search=GSA%20schedule&page=1"
```

### Award-within-vehicle search (detail, when expanding awards)

On the detail endpoint, `?search=` is used to filter **expanded awardees** (IDVs) when your `shape` includes `awardees(...)`.

## Filtering

Vehicles support a rich filter set across enums, reference codes, org hierarchy, numeric ranges, and dates. Multi-value enum filters use `|` for OR semantics. See the full list and details on the [Vehicles API Reference](../../api-reference/vehicles.md#filtering).

Quick examples:

```bash
# All SEWP-program vehicles awarded in FY 2024 or later
GET /api/vehicles/?program_acronym=SEWP&fiscal_year=2024

# IDIQ vehicles where DOD or DHS is the awarding org
GET /api/vehicles/?type_of_idc=A&agency=DOD|DHS

# Vehicles with at least 100 task orders and $100M+ obligated
GET /api/vehicles/?order_count_min=100&total_obligated_min=100000000
```

## Ordering

The vehicles list endpoint supports ordering by activity metrics and statistics using the `?ordering=` query parameter.

### Available ordering fields (10 total)

- `vehicle_obligations` - Total obligations across all IDVs in the vehicle (annotated)
- `latest_award_date` - Most recent IDV award date in the vehicle (annotated)
- `awardee_count` - Number of IDVs in the vehicle (annotated)
- `vehicle_contracts_value` - Sum of total contract value across child orders (annotated)
- `total_obligated` - Direct column rollup
- `award_date` - Earliest IDV award date
- `last_date_to_order` - Latest "last date to order" across IDVs
- `fiscal_year` - Vehicle fiscal year
- `idv_count` - Denormalized IDV count
- `order_count` - Denormalized task-order count

### Ordering syntax

- Ascending: `?ordering=vehicle_obligations`
- Descending: `?ordering=-vehicle_obligations` (prefix with `-`)
- Multiple fields: `?ordering=-vehicle_obligations,-latest_award_date`

### Examples

Sort by total obligations (highest first):

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/?ordering=-vehicle_obligations&page=1"
```

Sort by most recent award activity:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/?ordering=-latest_award_date&page=1"
```

Sort by obligations, then by latest award date:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/?ordering=-vehicle_obligations,-latest_award_date&page=1"
```

**Note**: When no `?ordering=` parameter is provided, vehicles are ordered by `solicitation_identifier`, `agency_id`, and `uuid` (default ordering).

## Task Orders

The `GET /api/vehicles/{uuid}/orders/` endpoint returns all task orders (contracts) issued against all IDVs within a vehicle. This endpoint supports pagination, response shaping, filtering, search, and transaction expansion, similar to the `/api/contracts/` endpoint.

### Basic usage

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?page=1"
```

### Filtering

Filter task orders using the same filters available on `/api/contracts/`:

```bash
# Filter by date range
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?award_date_gte=2024-01-01&award_date_lte=2024-12-31"

# Filter by recipient
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?recipient=ACME"
```

### Response shaping

Use the `shape` parameter to customize fields and expand related objects:

```bash
# Include recipient and transaction details
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?shape=key,piid,award_date,recipient(display_name,uei),transactions(description,transaction_date,modification_number)"
```

### Search

Search task order descriptions:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?search=software"
```

### Ordering

The orders endpoint supports ordering by contract fields using the `?ordering=` parameter (the same allowlist as `/api/contracts/`):

- `award_date` - Contract award date (default: descending, newest first)
- `obligated` - Obligated amount
- `total_contract_value` - Total contract value (base + all options)

**Default ordering**: When no `?ordering=` parameter is provided, task orders are ordered by `-award_date, -uuid` (newest first).

**Examples**:

```bash
# Default ordering (newest first)
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?page=1"

# Order by obligated amount (highest first)
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?ordering=-obligated&page=1"

# Order by total contract value
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/orders/?ordering=-total_contract_value&page=1"
```

The response includes vehicle context in the metadata (vehicle UUID and solicitation identifier).

## Vehicle metrics and organization

Vehicle responses can include two enrichment expansions:

- `metrics(*)` — 12 computed vehicle metrics (HHI, competed-rate, top-recipient share, etc.). Included in the default detail shape; opt-in via `?shape=...,metrics(*)` on the list endpoint.
- `organization(*)` — canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). Both bare `?shape=organization` (leaf) and `?shape=organization(*)` (expand) work and return the same payload.

```bash
# List vehicles with their metrics bundle and resolved org
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/?shape=uuid,solicitation_identifier,metrics(*),organization(*)&limit=5"
```

## Synthetic GWAC vehicles

Some GWAC vehicles lack a real solicitation number; Tango synthesizes a vehicle so the grouping still works. Identify these via:

- `is_synthetic_solicitation` — boolean, `true` for synthetic rows
- `program_acronym` — the GWAC's identifier (e.g. `SEWP`)
- `solicitation_identifier` — the user-facing value, with the internal `ACRO:` storage prefix stripped automatically

## Response shaping

Vehicles support the `shape` parameter, including the `awardees(...)` expansion.

Example: fetch a vehicle with a shaped list of its IDVs:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,organization_id,awardee_count,order_count,vehicle_obligations,vehicle_contracts_value,awardees(key,uuid,piid,award_date,recipient(display_name,uei))"
```

Example: include opportunity-derived fields and expand the linked Opportunity:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,solicitation_title,solicitation_date,naics_code,opportunity(opportunity_id,title,first_notice_date,office(office_name,agency_name))"
```

Example: expand JSON detail fields to select specific sub-fields:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/?shape=uuid,solicitation_identifier,competition_details(extent_competed,number_of_offers_received),type_of_idc,contract_type"
```

Example: filter expanded awards within a vehicle:

```bash
curl -H "X-API-KEY: <key>" \
  "https://tango.makegov.com/api/vehicles/<vehicle-uuid>/?shape=uuid,awardees(key,uuid,recipient(display_name))&search=acme"
```

See [Response Shaping](response-shaping.md) for full syntax and flattening options.

---

<!-- doc: mcp/index.md -->

# Tango MCP (AI agents)

The **Tango MCP server** gives AI agents access to federal procurement competitive intelligence via the [Tango API](https://tango.makegov.com). The server is **hosted at `https://govcon.dev/mcp`** and uses **HTTP transport** — you connect your MCP client to the remote endpoint; no local install required. Use it from Claude Desktop, Cursor, the OpenAI Responses API, or any MCP-compatible client.

## What it does

The server exposes 5 tools that let an AI agent research government contracting data on your behalf:

- **Discover what's in the data** — Use `resolve` to find entities, agencies, vehicles, NAICS/PSC codes, contracts, opportunities, OTAs, OTIDVs, subawards, organizations, protests, and IT Dashboard investments by name or keyword; get identifiers and previews to use with other tools.
- **Search awards and related data** — Use `search` to query contracts, IDVs, vehicles, OTAs, OTIDVs, subawards, organizations, GSA eLibrary contracts, CALC labor rates (LCATs), bid protests, and IT Dashboard investments. Filter by vendor, agency, NAICS, PSC, dates, and many other criteria; optionally get aggregate statistics (obligated amounts, set-aside breakdown, etc.).
- **Search active opportunities** — Use `search_opportunities` to find SAM.gov opportunities and agency forecasts by agency, NAICS, set-aside type, response deadline, and keyword.
- **Get full details for any item** — Use `get_details` to retrieve detailed information for a specific entity, contract, IDV, vehicle, opportunity, OTA, OTIDV, organization, subaward, protest, MAS SIN, GSA eLibrary contract, or IT Dashboard investment by ID, with optional related-data enrichment.
- **Read curated API documentation** — Use `fetch_api_docs` to pull short, LLM-friendly documentation for any major Tango resource (contracts, opportunities, vehicles, IT Dashboard, etc.).

## Quick start

### Prerequisites

- A **Tango API key** (from the [Tango web interface](https://tango.makegov.com)), or for OAuth clients: access tokens via `Authorization: Bearer <access_token>`
- An MCP-compatible client (e.g. Cursor, Claude Desktop)

### Connect via HTTP

The server is hosted at **[https://govcon.dev](https://govcon.dev)**. The MCP endpoint is **`https://govcon.dev/mcp`**. Configure your client to connect to this URL over HTTP and send your API key (see [Using with MCP clients](#using-with-mcp-clients) below).

## Using with MCP clients

### Remote MCP (HTTP) — recommended

Connect your client to the hosted server over HTTP using [mcp-remote](https://github.com/geelen/mcp-remote). The server expects your Tango API key in the `X-Tango-API-Key` header (or an OAuth bearer token in `Authorization: Bearer`).

**Cursor / Claude with remote URL (API key):**

```json
{
  "mcpServers": {
    "tango-remote": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://govcon.dev/mcp",
        "--header",
        "X-Tango-API-Key: your_api_key_here"
      ]
    }
  }
}
```

**Cursor / Claude with remote URL (OAuth bearer token):**

```json
{
  "mcpServers": {
    "tango-remote-oauth": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://govcon.dev/mcp",
        "--header",
        "Authorization: Bearer your_oauth_token_here"
      ]
    }
  }
}
```

Use a fresh access token from your Tango OAuth flow; refresh the token when it expires.

**Streamable HTTP (JSON mode)** — The server uses HTTP transport and returns JSON responses rather than Server-Sent Events (SSE). If you see errors like "Unexpected content type" or "Failed to open SSE stream", ensure the client uses JSON mode.

**API key security** — Prefer setting the API key via environment variable or a secrets manager. When using mcp-remote with `--header X-Tango-API-Key: ...`, avoid committing the key to config; use a local env var or secret so the key is not logged or stored in plain text.

### Claude Desktop

If you have the **Tango MCP Desktop Extension** (`.mcpb`), install it in Claude Desktop (Settings → Extensions), set the Server URL to **`https://govcon.dev/mcp`**, and enter your Tango API key when prompted. The extension connects to the hosted server over HTTP — no local server required.

### OAuth flow

When connecting over HTTP, you can authenticate with a **bearer token** instead of an API key. This is useful for clients that support OAuth (e.g. ChatGPT MCP) or when you obtain access tokens from Tango's OAuth server.

1. **Obtain an access token** from the Tango OAuth server at [tango.makegov.com](https://tango.makegov.com). Use the authorization code or client credentials flow; supported grant types include `authorization_code`, `client_credentials`, and `refresh_token`. Required scope is typically `read`.

2. **Send the token on each request** via the `Authorization` header: `Authorization: Bearer <your_access_token>`.

3. **Auth precedence** — The server resolves auth in this order: (1) `Authorization: Bearer` if present, (2) `X-Tango-API-Key` if present, (3) `TANGO_API_KEY` from the environment (stdio/local only).

**OAuth discovery** — The server exposes standard discovery endpoints so clients can find the Tango authorization server and required scopes:

- Authorization server metadata: `GET /.well-known/oauth-authorization-server` and `GET /mcp/.well-known/oauth-authorization-server`
- Protected resource metadata: `GET /.well-known/oauth-protected-resource`

These return JSON (issuer, `authorization_endpoint`, `token_endpoint`, `scopes_supported`, etc.). The default issuer is [https://tango.makegov.com](https://tango.makegov.com).

### OpenAI Responses API

OpenAI's MCP integration requires a publicly accessible server. Use the hosted MCP endpoint `https://govcon.dev/mcp` in your `tools` configuration. See the [OpenAI MCP documentation](https://platform.openai.com/docs/guides/tools-remote-mcp) for details.

## Available tools

| Tool | Description |
| ---- | ----------- |
| `resolve` | Find entities, vehicles, organizations, contracts, opportunities, OTAs, OTIDVs, subawards, protests, IT Dashboard investments, NAICS / PSC / SIN codes by name or keyword. Use first when you have a string and need to discover what's in the data; returns identifiers and short previews for use with other tools. |
| `search` | Search across 13 record types: `contract` (default), `idv`, `vehicle`, `ota`, `otidv`, `subaward`, `organization`, `protest`, `gsa_elibrary`, `lcat`, `itdashboard`, and `all`. Filter by vendor, agency, NAICS, PSC, dates, keyword, and many other criteria. Set `include_summary=true` for aggregate statistics across matching records. |
| `search_opportunities` | Search open opportunities and agency forecasts. Filter by agency, NAICS, PSC, set-aside type, response deadline, notice type, keyword, and other criteria. Type defaults to `opportunity`; pass `type="forecast"` for forecast data. |
| `get_details` | Get detailed information for a single item by ID and type. Supported types: `entity`, `contract`, `idv`, `vehicle`, `opportunity`, `ota`, `otidv`, `organization`, `subaward`, `protest`, `sin`, `gsa_elibrary_contract`, `itdashboard`. Use after `search` or `resolve` to drill into a specific record. Optional `include_related=True` enriches the response (see "Enrichment matrix" below). IT Dashboard `business_case_html` content requires Business+. |
| `fetch_api_docs` | Fetch curated, LLM-friendly markdown documentation for a single Tango resource. Sections: `contracts`, `entities`, `forecasts`, `gsa-elibrary`, `idvs`, `itdashboard`, `lcats`, `metrics`, `opportunities`, `otas`, `otidvs`, `protests`, `resolve`, `response-shaping`, `subawards`, `vehicles`. |

### Enrichment matrix (`get_details(..., include_related=True)`)

| Type | Adds when `include_related=True` |
| --- | --- |
| `entity` | Socioeconomic status + 10 most recent contracts |
| `contract` | Full detail + subawards |
| `idv` | Full detail + child awards + transactions + CALC labor rates |
| `vehicle` | Full detail + awardees + task orders |
| `opportunity` | Full detail (attachments, notice history, primary contact) |
| `ota` / `otidv` | Full detail + child awards / OTAs + transactions |
| `organization` | Detail + child organizations |
| `protest` | Detail + dockets + related opportunities / contracts |
| `sin` | Detail + GSA eLibrary contracts (Schedule holders) |
| `gsa_elibrary_contract` | Detail (no further enrichment) |
| `itdashboard` | Detail + nested CIO evaluation, contracts, projects, funding, performance metrics. The `business_case_html` HTML blob requires Business+ tier. |

Enrichment failures degrade gracefully — if a related-data fetch fails, the main object is still returned with empty related blocks.

## Resources

The server also exposes static MCP resources. Clients that support resource fetching can pull curated reference content without a tool call:

| URI | Content |
| --- | --- |
| `tango://guides/federal-procurement` | Overview guide: how federal procurement data fits together. |
| `tango://guides/api-reference` | Quick reference: enums, sorting fields, response shaping, recipes. |
| `tango://reference/set-asides` | Set-aside type codes and meanings. |
| `tango://reference/protests` | Background on bid protests (GAO / COFC). |
| `tango://reference/naics/{code}` | NAICS code lookup. |
| `tango://reference/psc/{code}` | PSC (Product / Service Code) lookup. |
| `tango://reference/sin/{code}` | GSA MAS SIN lookup. |

## Configuration

- **API key vs OAuth:** Send your Tango API key in the `X-Tango-API-Key` header, or use `Authorization: Bearer <token>` with an OAuth access token from [tango.makegov.com](https://tango.makegov.com).
- **Endpoint:** The hosted server is at [https://govcon.dev](https://govcon.dev); the MCP endpoint is **`https://govcon.dev/mcp`** (HTTP transport).
- **Response format:** The hosted server returns responses in **TOON** (Token-Oriented Object Notation) by default — a compact serialization that is roughly 30–60% smaller than JSON for typical responses. To force standard JSON, set `TANGO_RESPONSE_FORMAT=json` (relevant when self-hosting; the hosted server returns TOON).

### Self-hosting / local environment variables

If you run the server locally (e.g. via stdio for development), the following environment variables are recognized:

| Variable | Purpose |
| --- | --- |
| `TANGO_API_KEY` | Default API key (used when no `X-Tango-API-Key` / `Authorization` header is supplied; stdio mode only). |
| `TANGO_RESPONSE_FORMAT` | `json` to force JSON output. Default is TOON. |
| `TRANSPORT` | `http` to switch from stdio to streamable-HTTP transport. |
| `TANGO_TELEMETRY` | `1` to log per-tool duration / token counts / status to stderr as JSON. |
| `TANGO_LOG_USER_FINGERPRINT` | `1` to log a stable hash of the API key (not the key itself) for correlation. |
| `SENTRY_ENABLED` | `1` to enable optional Sentry error monitoring. |

## Notable behaviors

The server includes several robustness features worth knowing about when integrating:

- **Strict argument validation.** Unknown tool arguments are rejected with a structured error that lists the valid arg names — no silent drops. Pass `type` not `record_type`, etc.
- **Zero-result hints.** When a search returns no results, the response includes a `_hint` with the active filters and broaden-query guidance — helpful for LLM agents that would otherwise guess randomly.
- **Ordering-alias rewrite.** LLM-friendly sort field names like `expiring`, `date`, `value`, and `amount` are rewritten to API-valid values (`award_date`, `obligated`, `total_contract_value`).
- **Parameter coercion.** Bare strings for array fields (`naics_codes`, `psc_codes`, `set_aside_types`) are wrapped automatically. `active` accepts `"true"` / `"false"`. `notice_type` accepts both code letters (`p`, `k`, `o`, `a`, `m`, `r`, `s`, `u`, `i`, `g`) and full names (`"Solicitation"`, `"Sources Sought"`, etc.).
- **Circuit breaker.** After 3 transient upstream failures (502 / 503 / 504 / timeout) within 60 seconds, the server short-circuits with `UpstreamUnavailableError`. A probe is allowed every 10 seconds during the cooldown.
- **Tier-aware errors.** Tier-gated endpoints (e.g. CALC labor rates) return a typed error with an upgrade hint rather than a raw HTTP exception.

## Troubleshooting

**424 error from OpenAI** — OpenAI can only reach publicly accessible servers. Use the hosted endpoint `https://govcon.dev/mcp`.

**Empty results** — Verify your API key is valid. Check that the UEI, NAICS code, or other identifiers you are using exist in the Tango database.

**502 Bad Gateway / "Failed to open SSE stream"** — The server uses HTTP transport and JSON responses, not SSE. If the client expects SSE, use a client that supports JSON mode.

**403 when calling the server URL** — A proxy or firewall may be blocking the request to `https://govcon.dev/mcp`. Ensure your client sends the API key or Bearer token as described above.

**"Invalid API key or authentication required"** — When connecting to `https://govcon.dev/mcp`, the server must receive either your Tango API key in the `X-Tango-API-Key` header or an OAuth bearer token in the `Authorization: Bearer <token>` header. Ensure your MCP client is configured to send one of these.

## Get help

For API keys, account issues, or MCP connection problems:

- **Email:** [support@makegov.com](mailto:support@makegov.com)
- **Tango web interface:** [tango.makegov.com](https://tango.makegov.com) — sign in to manage your account and API keys.

## Privacy

Data handling and privacy are described in our [Privacy policy](https://tango.makegov.com/privacy).

## References

- [govcon.dev](https://govcon.dev) — Hosted Tango MCP server (HTTP transport)
- [Model Context Protocol Specification](https://modelcontextprotocol.io/specification/2025-11-25)
- [MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) — Interactive testing and debugging for MCP servers
- [OpenAI MCP Documentation](https://platform.openai.com/docs/guides/tools-remote-mcp)
- [Tango API](https://tango.makegov.com)

---

<!-- doc: sdks/index.md -->

# SDKs

Idiomatic clients for the Tango API in your language of choice.

| SDK | Status | Docs |
| --- | ------ | ---- |
| **Python** | Stable | [tango-python](python/index.md) |
| **Node / TypeScript** | Stable | [tango-node](node/index.md) |

Both SDKs cover the full API surface, handle pagination automatically, and share the same response-shaping conventions as the underlying HTTP API. They wrap retries, rate-limit backoff, and webhook signature verification so you don't have to.

## Which one should I use?

- **Python** if you're doing data work, scripting, or building backend services in Python.
- **Node / TypeScript** if you're building a web app, serverless function, or anything in the JS/TS ecosystem.

Both stay in feature parity. If something lands in one, it lands in the other within a release cycle.

## More languages?

Not on the roadmap yet. The OpenAPI schema at [tango.makegov.com/api/](https://tango.makegov.com/api/) is suitable for generating clients in other languages — open an issue if you want one we should support directly.

---

<!-- doc: sdks/node/api-reference.md -->

# Tango Node SDK – API Reference

This document provides the full API reference for the **Node.js / TypeScript**
version of the Tango SDK. It is a translation of the Python SDK documentation,
rewritten for JavaScript runtime semantics, async/await, and the TypeScript
type system.

## Importing

```ts
import { TangoClient, ShapeConfig } from "@makegov/tango-node";
// Models (optional)
import type { Contract } from "@makegov/tango-node/models";
```

All methods are async and return Promises.

---

## Agencies

### `listAgencies(options?)`

List federal departments and subagencies.

```ts
const resp = await client.listAgencies({ page: 1, limit: 25 });
```

#### Parameters

| Name    | Type     | Description                                 |
| ------- | -------- | ------------------------------------------- |
| `page`  | `number` | Page number (default 1).                    |
| `limit` | `number` | Max results per page (default 25, max 100). |

#### Returns (Agencies)

`PaginatedResponse<AgencyLike>`

---

### `getAgency(code)`

Fetch a single agency by its code.

```ts
const agency = await client.getAgency("2000");
```

Returns a shaped Agency object. Responses are materialized via the dynamic model pipeline (dates parsed, nested objects built).

---

## Contracts

### `listContracts(options)`

Search and list contract records.

```ts
const resp = await client.listContracts({
  keyword: "cloud",
  naics_code: "541511",
  shape: ShapeConfig.CONTRACTS_MINIMAL,
  flat: true,
});
```

#### Search / Filter Parameters

These mirror the Python SDK:

| Filter           | Maps to API param |
| ---------------- | ----------------- |
| `keyword`        | `search`          |
| `naics_code`     | `naics`           |
| `psc_code`       | `psc`             |
| `recipient_name` | `recipient`       |
| `recipient_uei`  | `uei`             |
| `set_aside_type` | `set_aside`       |

Sorting:

```ts
sort: "award_date",
order: "desc"   // -> ordering="-award_date"
```

Pagination + shaping options:

```ts
shape: string,
flat: boolean,
flatLists: boolean,
page: number,
limit: number,
cursor: string,  // mutually exclusive with `page` — if provided, `page` is ignored
```

Contracts support both **page-based** and **cursor-based** pagination. Use `cursor` for deep pagination (faster and more stable on large result sets); use `page` for small offsets or when you need to jump to a specific page. `page` and `cursor` are mutually exclusive — if you pass `cursor`, the SDK ignores `page`.

#### Returns (Contracts)

`PaginatedResponse<Contract>` materialized according to the requested shape. Date/datetime fields are parsed, decimals normalized to strings, nested recipients, agencies, and locations are objects.

---

## Vehicles

Vehicles provide a solicitation-centric grouping of related IDVs.

### `listVehicles(options)`

```ts
const resp = await client.listVehicles({
  search: "GSA schedule",
  shape: ShapeConfig.VEHICLES_MINIMAL,
  page: 1,
  limit: 25,
});
```

Supported parameters:

- `search` (vehicle-level full-text search)
- `page`, `limit` (max 100)
- `shape`, `flat`, `flatLists`

### `getVehicle(uuid, options?)`

```ts
const vehicle = await client.getVehicle("00000000-0000-0000-0000-000000000001", {
  shape: ShapeConfig.VEHICLES_COMPREHENSIVE,
});
```

Notes:

- On vehicle detail, `search` filters expanded `awardees(...)` when included in your `shape` (it does not filter the vehicle itself).
- When using `flat: true`, you can override the joiner with `joiner` (default `"."`).

### `listVehicleAwardees(uuid, options?)`

```ts
const awardees = await client.listVehicleAwardees("00000000-0000-0000-0000-000000000001", {
  shape: ShapeConfig.VEHICLE_AWARDEES_MINIMAL,
});
```

---

## IDVs

IDVs (indefinite delivery vehicles) are the parent “vehicle award” records that can have child awards/orders under them.

### `listIdvs(options)`

```ts
const idvs = await client.listIdvs({
  limit: 25,
  cursor: null,
  shape: ShapeConfig.IDVS_MINIMAL,
  awarding_agency: "4700",
});
```

Notes:

- This endpoint uses **keyset pagination** (`cursor` + `limit`) rather than `page`.

### `getIdv(key, options?)`

```ts
const idv = await client.getIdv("SOME_IDV_KEY", {
  shape: ShapeConfig.IDVS_COMPREHENSIVE,
});
```

### `listIdvAwards(key, options?)`

Lists child awards (contracts) under an IDV.

```ts
const awards = await client.listIdvAwards("SOME_IDV_KEY", { limit: 25 });
```

### `listIdvChildIdvs({ key, ...options })`

```ts
const children = await client.listIdvChildIdvs({ key: "SOME_IDV_KEY", limit: 25 });
```

### `listIdvTransactions(key, options?)`

```ts
const tx = await client.listIdvTransactions("SOME_IDV_KEY", { limit: 100 });
```

### `getIdvSummary(identifier)` / `listIdvSummaryAwards(identifier, options?)`

> **Deprecated.** These methods wrap the `/api/idvs/{identifier}/summary/` and `/api/idvs/{identifier}/summary/awards/` routes, which were removed server-side and now return **404**. The methods will be removed from the SDK in a future release. For solicitation-grouped views, query `/api/vehicles/` instead (see [Vehicles](#vehicles)).

```ts
const summary = await client.getIdvSummary("SOLICITATION_IDENTIFIER");
const awards = await client.listIdvSummaryAwards("SOLICITATION_IDENTIFIER", { limit: 25 });
```

---

## Entities

### `listEntities(options)`

```ts
const resp = await client.listEntities({
  search: "Acme",
  shape: ShapeConfig.ENTITIES_MINIMAL,
});
```

Filters:

- `search`
- any field names supported by the API

### `getEntity(uei, options?)`

Fetch a single entity by UEI or CAGE.

Returns a shaped entity object with nested addresses/fields based on the shape.

---

## Forecasts

### `listForecasts(options)`

Forecast search, with optional shaping.

---

## Opportunities

### `listOpportunities(options)`

Search SAM.gov opportunities with shaping.

---

## Notices

### `listNotices(options)`

---

## Grants

### `listGrants(options)`

---

## Organizations / Offices / Departments

### `listOrganizations(options?)`

The canonical agency/department/office hierarchy. `level` filters by hierarchy depth: `1` = department, `2` = agency, `3` = sub-agency, and so on.

```ts
const orgs = await client.listOrganizations({
  level: 1,                // 1 = department, 2 = agency, 3 = sub-agency, …
  include_inactive: false,
  search: "Defense",
  limit: 25,
});
```

### `getOrganization(identifier)`

```ts
const org = await client.getOrganization("ORG_KEY");
```

### `listOffices(options?)`

```ts
const offices = await client.listOffices({ search: "acquisitions" });
```

### `getOffice(code)`

```ts
const office = await client.getOffice("4732XX");
```

### `listDepartments(options?)`

> **Deprecated.** Use `listOrganizations({ level: 1 })` instead. The standalone departments endpoint is retained for backward compatibility and will be removed in a future API version.

```ts
const depts = await client.listDepartments({ page: 1, limit: 25 });
```

### `getDepartment(code)`

```ts
const dept = await client.getDepartment("097");
```

---

## OTAs

Other Transaction Agreements — non-FAR-based awards.

### `listOtas(options?)`

Uses **keyset pagination** (`cursor` + `limit`).

```ts
const otas = await client.listOtas({ limit: 25, awarding_agency: "4700" });
```

### `getOta(key)`

```ts
const ota = await client.getOta("OTA_KEY");
```

---

## OTIDVs

Other Transaction IDVs — umbrella OT agreements with child awards.

### `listOtidvs(options?)`

Uses **keyset pagination** (`cursor` + `limit`).

```ts
const otidvs = await client.listOtidvs({ limit: 25 });
```

### `getOtidv(key)`

```ts
const otidv = await client.getOtidv("OTIDV_KEY");
```

### `listOtidvAwards(key, options?)`

```ts
const awards = await client.listOtidvAwards("OTIDV_KEY", { limit: 25 });
```

---

## Subawards

### `listSubawards(options?)`

```ts
const subs = await client.listSubawards({ prime_uei: "ABC123DEF456", limit: 25 });
```

---

## GSA eLibrary Contracts

### `listGsaElibraryContracts(options?)`

```ts
const contracts = await client.listGsaElibraryContracts({ schedule: "MAS", limit: 25 });
```

---

## Protests

### `listProtests(options?)`

```ts
const protests = await client.listProtests({ source_system: "gao", limit: 25 });
```

### `getProtest(caseNumber)`

```ts
const protest = await client.getProtest("CASE_UUID");
```

---

## IT Dashboard

### `listItDashboard(options?)`

```ts
const investments = await client.listItDashboard({ search: "cloud", limit: 25 });
```

### `getItDashboard(uii)`

```ts
const investment = await client.getItDashboard("023-000001234");
```

---

## LCATs

### `listLcats(options)`

Requires either `{ uei }` (entity LCATs) or `{ idvKey }` (IDV LCATs) — throws `TangoValidationError` if neither is provided.

```ts
const lcats = await client.listLcats({ uei: "ABCDEF123456" });
// or:
const lcats = await client.listLcats({ idvKey: "GS-00F-XXXX" });
```

### `listIdvLcats(key, options?)`

Labor Categories (`/api/idvs/{key}/lcats/`) attached to an IDV.

```ts
const lcats = await client.listIdvLcats("GS-00F-XXXX", { limit: 25 });
```

---

## Metrics

### `listMetrics(options)`

List metrics for a NAICS code, PSC code, or entity. `ownerType`, `ownerId`, `months`, and `periodGrouping` are all required.

```ts
const metrics = await client.listMetrics({
  ownerType: "naics",
  ownerId: "541511",
  months: 12,
  periodGrouping: "month",
});
```

### `getNaicsMetrics(code, months, periodGrouping)`

```ts
const m = await client.getNaicsMetrics("541511", 12, "month");
```

### `getPscMetrics(code, months, periodGrouping)`

```ts
const m = await client.getPscMetrics("D302", 12, "month");
```

### `getEntityMetrics(uei, months, periodGrouping)`

```ts
const m = await client.getEntityMetrics("ABCDEF123456", 12, "month");
```

---

## Reference Lookups

### `listNaics(options?)` / `getNaics(code)`

```ts
const naics = await client.listNaics({ search: "software" });
const code = await client.getNaics("541511");
```

### `listPsc(options?)` / `getPsc(code)`

```ts
const psc = await client.listPsc();
const code = await client.getPsc("D302");
```

### `listMasSins(options?)` / `getMasSin(sin)`

```ts
const sins = await client.listMasSins();
const sin = await client.getMasSin("54151S");
```

### `listAssistanceListings(options?)` / `getAssistanceListing(number)`

```ts
const listings = await client.listAssistanceListings();
const listing = await client.getAssistanceListing("10.310");
```

### `listBusinessTypes(options?)` / `getBusinessType(code)`

```ts
const types = await client.listBusinessTypes();
const bt = await client.getBusinessType("A6");
```

---

## Resolve / Validate

### `resolve(input)`

Resolve a free-text name to ranked entity or organization candidates.

```ts
const result = await client.resolve({ name: "Lockheed Martin", target_type: "entity" });
// result.candidates[0].display_name, result.count
```

Required fields: `name`, `target_type` (`"entity"` | `"organization"`).

### `validate(input)`

Validate the format of a PIID, solicitation number, or UEI.

```ts
const result = await client.validate({ type: "uei", value: "ABCDEF123456" });
```

Required fields: `type` (`"piid"` | `"solicitation"` | `"uei"`), `value`.

---

## Entity Sub-resources

### `listEntityContracts(uei, options?)`

```ts
const contracts = await client.listEntityContracts("ABCDEF123456", { limit: 25 });
```

### `listEntityIdvs(uei, options?)` / `listEntityOtas(uei, options?)` / `listEntityOtidvs(uei, options?)`

```ts
const idvs = await client.listEntityIdvs("ABCDEF123456");
```

### `listEntitySubawards(uei, options?)` / `listEntityLcats(uei, options?)`

```ts
const subawards = await client.listEntitySubawards("ABCDEF123456");
```

---

## Agency Sub-resources

### `listAgencyAwardingContracts(code, options?)`

```ts
const contracts = await client.listAgencyAwardingContracts("4700", { limit: 25 });
```

### `listAgencyFundingContracts(code, options?)`

```ts
const contracts = await client.listAgencyFundingContracts("4700", { limit: 25 });
```

---

## Opportunities (attachments)

### `searchOpportunityAttachments(options)`

Semantic search over opportunity attachments. `q` is required.

```ts
const results = await client.searchOpportunityAttachments({
  q: "cybersecurity",
  topK: 10, // max results (optional)
  includeExtractedText: false, // include raw extracted text (optional)
});
```

| Name                   | Type      | Description                            |
| ---------------------- | --------- | -------------------------------------- |
| `q`                    | `string`  | **Required.** Search query.            |
| `topK`                 | `number`  | Maximum number of results to return.   |
| `includeExtractedText` | `boolean` | Whether to include raw extracted text. |

---

## Async Iteration

All list methods can be iterated page-by-page via the generic `iterate()` helper or the named convenience wrappers.

### `iterate(method, options?)`

```ts
for await (const contract of client.iterate("listContracts", { awarding_agency: "9700" })) {
  console.log(contract.piid);
}
```

Named wrappers: `iterateContracts`, `iterateEntities`, `iterateOpportunities`, `iterateNotices`, `iterateGrants`, `iterateForecasts`, `iterateIdvs`, `iterateVehicles`.

---

## Utility

### `getVersion()`

```ts
const v = await client.getVersion();
```

### `listApiKeys()`

```ts
const keys = await client.listApiKeys();
```

---

## Webhooks (v2)

Webhook APIs let **Large / Enterprise** users manage subscription filters for outbound Tango webhooks.

### `listWebhookEventTypes()`

Discover supported `event_type` values.

```ts
const info = await client.listWebhookEventTypes();
```

### Webhook endpoints

In production, MakeGov provisions the initial endpoint for you. These methods are most useful for dev/self-service.

```ts
const endpoints = await client.listWebhookEndpoints({ page: 1, limit: 25 });
const endpoint = await client.getWebhookEndpoint("ENDPOINT_UUID");
```

`createWebhookEndpoint` accepts the canonical snake_case shape (`callback_url`, `is_active`, `name`) or the legacy camelCase aliases (`callbackUrl`, `isActive`). If `name` is not provided, the SDK falls back to the URL host.

```ts
// Create (canonical snake_case)
const created = await client.createWebhookEndpoint({
  name: "Prod receiver",
  callback_url: "https://example.com/tango/webhooks",
  // is_active defaults to true on create
});

// Legacy camelCase still works:
const created2 = await client.createWebhookEndpoint({
  callbackUrl: "https://example.com/tango/webhooks",
  isActive: true,
});

// Update
await client.updateWebhookEndpoint(created.id, { is_active: false });

// Delete
await client.deleteWebhookEndpoint(created.id);
```

### `testWebhookEndpoint(endpointId)`

Send an immediate test webhook to a specific endpoint. `endpointId` is required. The SDK sends `{ endpoint: <id> }` in the request body (canonical post-tango#2252 cleanup; the API also accepts `endpoint_id` as a deprecated alias).

```ts
const result = await client.testWebhookEndpoint("ENDPOINT_UUID");
console.log(result.success, result.status_code);
```

### `testWebhookDelivery(options?)` _(legacy alias)_

Legacy wrapper around `testWebhookEndpoint`. `endpointId` may be omitted, in which case the API auto-resolves the user's only endpoint (404 if 0, 400 if >1). Prefer `testWebhookEndpoint` for new code.

```ts
const result = await client.testWebhookDelivery({ endpointId: "ENDPOINT_UUID" });
```

### `getWebhookSamplePayload(options?)`

Fetch Tango-shaped sample deliveries.

```ts
const sample = await client.getWebhookSamplePayload({ eventType: "alerts.contract.match" });
```

### Webhook Alerts

The Alerts API is a filter-subscription convenience layer on top of subscriptions. The SDK uses cleaner field names than the underlying API: `name` (vs `subscription_name`), `filters` (vs `filter_definition`), and singular `query_type` values.

```ts
// Create
const alert = await client.createWebhookAlert({
  name: "New IT cloud contracts",                  // vs subscription_name on the wire
  query_type: "contract",                          // SINGULAR — not "contracts"
  filters: { naics: "541511" },                    // vs filter_definition on the wire
  frequency: "realtime",                           // realtime | daily | weekly | custom
  cron_expression: undefined,                      // required if frequency === "custom"
});

// List
const alerts = await client.listWebhookAlerts({ page: 1, pageSize: 25 });

// Get / Update / Delete
const got = await client.getWebhookAlert("ALERT_UUID");
await client.updateWebhookAlert("ALERT_UUID", { name: "Updated name" });
await client.deleteWebhookAlert("ALERT_UUID");
```

Notes:

- `name` and `query_type` are required on create. `query_type` is **singular** (e.g. `"contract"`, not `"contracts"`).
- Only `name`, `frequency`, `cronExpression`, and `isActive` are writable via `updateWebhookAlert` — `query_type` and `filters` are read-only after creation.

### Deliveries / redelivery

The API does not currently expose a public `/api/webhooks/deliveries/` or redelivery endpoint. Use:

- `testWebhookEndpoint(endpointId)` for connectivity checks
- `getWebhookSamplePayload()` for building handlers + alert payloads

### Receiving webhooks (signature verification)

Every delivery includes an HMAC signature header:

- `X-Tango-Signature: sha256=<hex digest>`

Use the SDK's `verifySignature` helper — **do not hand-roll HMAC**. Verify against the **raw request body bytes** (not a re-serialized parsed body). Arg order is `(body, header, secret)`.

```ts
import { verifySignature } from "@makegov/tango-node";

// Express — use express.raw() to get the body as a Buffer before JSON parsing
app.post("/tango/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body; // Buffer
  const signatureHeader = req.headers["x-tango-signature"];

  if (!verifySignature(rawBody, signatureHeader, process.env.TANGO_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "invalid_signature" });
  }

  const payload = JSON.parse(rawBody.toString("utf8"));
  // ... handle payload.events ...
  res.json({ ok: true });
});
```

`verifySignature` signature:

```ts
function verifySignature(body: string | Buffer, header: string | null | undefined, secret: string): boolean;
```

Returns `false` for missing, malformed, or mismatched headers — never throws on mismatch. Uses `timingSafeEqual` internally. See [`WEBHOOKS.md` § Signature verification](webhooks.md#signature-verification-in-your-handler) for Fastify and framework-agnostic examples.

---

## Error Types

All thrown by async methods:

- `TangoAPIError`
- `TangoAuthError`
- `TangoNotFoundError`
- `TangoRateLimitError`
- `TangoValidationError`
- `ShapeError`
- `ShapeParseError`
- `ShapeValidationError`
- `TypeGenerationError`
- `ModelInstantiationError`

---

## Pagination

All list endpoints return:

```ts
interface PaginatedResponse<T> {
  count: number;
  next: string | null;
  previous: string | null;
  pageMetadata: Record<string, unknown> | null;
  results: T[];
}
```

You can follow `next` / `previous` manually or use your own wrapper.

---

<!-- doc: sdks/node/changelog.md -->

<!-- markdownlint-disable MD024, MD013 -->
# Changelog

All notable changes to `@makegov/tango-node` will be documented in this file.

This project follows [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [1.0.0] - 2026-05-13

First stable release. `tango-node` is now at **full feature parity** with both the Tango API and `tango-python` for the surface that remains after the subject-based webhook removal (see "Removed" below). Every read method and every endpoint/alert/signing helper available on `tango_python.TangoClient` has an idiomatic camelCase counterpart on `TangoClient`, the SDK's docs are auto-published to `docs.makegov.com/sdks/node/` via the composer pipeline (makegov/docs#15 / makegov/docs#16), and from `1.x` on we'll only ship breaking changes on a major bump.

### Changed (breaking)

- **`createWebhookEndpoint({ name, ... })`** — `name` is now required. Previously the SDK silently fell back to the callback URL's host when `name` was omitted, which masked the server's `unique(user, name)` constraint until the second duplicate endpoint. Raising client-side gives a clearer error and matches `tango-python` 1.0.0's behavior.
- **`createWebhookAlert({ filters, ... })`** — `filters` is now validated as a non-empty plain object. Previously `{}` and arrays passed the check; the error message claimed "non-empty object" but didn't enforce it. Matches the server-side validation.

### Fixed

- `tests/unit/client.iterate.test.ts` — corrected the comment above the first `page` assertion; previous text claimed the first call should NOT carry a page, but the assertion (correctly) expects `page=1` since `listContracts` defaults to it.
- `docs/DEVELOPERS.md` — `listContracts({ offset: 25 })` example replaced with `{ page: 2 }` and "manual offset tracking" → "manual page/cursor tracking" (the method has never accepted `offset`).
- `CHANGELOG.md` — corrected "4 new unit test files" to "5"; the parenthesized list always contained 5 paths.
- Lint cleanup ahead of the npm publish — 44 redundant `as AnyRecord` / `as <T>` type-assertion casts dropped across `src/` (`@typescript-eslint/no-unnecessary-type-assertion` fires under the newer plugin minor that CI resolves). `src/webhooks/receiver.ts`: dropped the unused `AddressInfo` import, simplified `Delivery.bodyJson: unknown | null` → `unknown` (the latter already includes `null`), and restructured `WebhookReceiver.run()` to avoid `const receiver = this` (replaced with arrow-function closures over `getUrl`/`getDeliveries`/`stop`). No behavior change — tests still pass 220/220.
- `eslint.config.js` — disabled the core `no-undef` rule for TS files. TypeScript itself does undefined-symbol checking with knowledge of TS-only types (`AsyncDisposable`, `Disposable`, etc.); the core rule double-fires and false-flags those. Matches the typescript-eslint upstream guidance.

### Internal

- Pinned `devDependencies` to exact versions (dropped `^` from `package.json`). The previous unpinned ranges + gitignored `package-lock.json` meant CI re-resolved deps on every run and could pick up minor versions with stricter behavior that local installs hadn't seen yet — that's how the lint mismatch above slipped past local checks.

### Docs

- **README** updated for the docs-review sweep:
  - Added `TangoTimeoutError` to the documented error class list (it has been exported from `src/errors.ts` since v0.4 but the README omitted it).
  - Replaced the "_(Coming Soon!)_" marker on the docs link with the live `https://docs.makegov.com/sdks/node/` URL.
  - Rewrote the "Comprehensive API Coverage" feature bullet — the old enumeration listed fewer than half of the actually-implemented domains. New bullet points at the canonical "API Methods" section for the full surface.
- New `docs/CLIENT.md` — `TangoClient` constructor reference, environment variables, full retry/backoff semantics (including `Retry-After` handling), error-handling patterns, `fetchImpl` injection, and staging/local targeting. Ported from `docs.makegov.com/sdks/node/client.md` ahead of the docs-site auto-pull cutover (makegov/docs#15 / makegov/docs#16).
- `docs/API_REFERENCE.md` enriched with notes from the docs-site `methods.md` that hadn't been folded in yet: `listContracts` page/cursor mutual exclusion, `getIdvSummary` / `listIdvSummaryAwards` deprecation (server returns 404), `listIdvLcats` clarification, `listOrganizations` `level` semantics, `createWebhookEndpoint` snake_case canonical vs camelCase legacy aliases (`name` is required either way per the 1.0.0 change above), `testWebhookEndpoint` post-#2252 cleanup (`{ endpoint: <id> }` is canonical), and `createWebhookAlert` field-rename notes (`name` vs `subscription_name`, `filters` vs `filter_definition`, singular `query_type`, update-writable field list).

### CI

- New `.github/workflows/docs-dispatch.yml` — fires on push to `main` when `docs/**`, `README.md`, or `CHANGELOG.md` changes and dispatches `external_updated` at `makegov/docs` so the public docs site rebuilds with the latest SDK content. Required for the makegov/docs#15 auto-pull pipeline.

### Parity closure — all previously-tracked gaps addressed

Every gap surfaced in the May 2026 parity audit is now closed:

#### Typed filter `Options` interfaces

- `listForecasts`, `listOpportunities`, `listNotices`, `listGrants` — previously `ListOptionsBase & Record<string, unknown>` with zero typed filters; now ship full `Options` interfaces (`ListForecastsOptions`, `ListOpportunitiesOptions`, `ListNoticesOptions`, `ListGrantsOptions`) enumerating every filter kwarg from the Python signatures. `ListNoticesOptions` deliberately omits `ordering` (server rejects it).
- `ListContractsOptions` expanded to enumerate all 27+ Python kwargs (`award_date*`, `awarding_agency`, `funding_agency`, `obligated_gte/lte`, `pop_*`, `expiring_*`, `keyword`/`recipient_name`/`recipient_uei`/`set_aside_type`/`naics_code`/`psc_code` aliases, `sort`+`order`→`ordering`, etc.).
- `ListEntitiesOptions` expanded with the 12 Python kwargs (`cage_code`, `naics`, `name`, `psc`, `purpose_of_registration_code`, `socioeconomic`, `state`, `total_awards_obligated_gte/lte`, `uei`, `zip_code`).
- `ListVehiclesOptions` expanded with all 21 filter kwargs (`vehicle_type`, `type_of_idc`, `contract_type`, `who_can_use`, `total_obligated_min/max`, etc.).
- `ListIdvsOptions` expanded with the full IDV filter surface.
- `ListOtasOptions` / `ListOtidvsOptions` expanded with the missing `_gte`/`_lte` ranges (`award_date_gte/lte`, `fiscal_year_gte/lte`, `expiring_gte/lte`, `pop_start_date_gte/lte`, `pop_end_date_gte/lte`).
- `ListAgenciesOptions` added (was inline `{ page?, limit? }`).

All `Options` interfaces keep a `[key: string]: unknown` index signature for forward-compatibility with new server-side filters that haven't been ported yet — typed fields give autocomplete and typo-protection on known filters; unknown fields still pass through.

#### `ShapeConfig` preset parity

Added: `PROTESTS_MINIMAL`, `OTAS_MINIMAL`, `OTIDVS_MINIMAL`, `SUBAWARDS_MINIMAL`, `GSA_ELIBRARY_CONTRACTS_MINIMAL`, `ORGANIZATIONS_MINIMAL`, `VEHICLE_ORDERS_MINIMAL`, `ITDASHBOARD_INVESTMENTS_MINIMAL`, `ITDASHBOARD_INVESTMENTS_COMPREHENSIVE`.

Updated to match Python field lists: `ENTITIES_COMPREHENSIVE` (now uses `federal_obligations(*)` expansion), `VEHICLES_MINIMAL` (extended to mirror Python's larger field list), `VEHICLES_COMPREHENSIVE` (dropped `competition_details(*)` per SDK v0.6.0; added `program_acronym`, `is_synthetic_solicitation`, `metrics(*)`, organization expansion).

#### Explicit schemas

Added 11 schemas + registry entries: `ORGANIZATION_OFFICE_SCHEMA`, `VEHICLE_METRICS_SCHEMA`, `ORGANIZATION_SCHEMA`, `OTA_SCHEMA`, `OTIDV_SCHEMA`, `SUBAWARD_SCHEMA`, `PROTEST_DOCKET_SCHEMA`, `PROTEST_SCHEMA`, `GSA_ELIBRARY_IDV_REF_SCHEMA`, `GSA_ELIBRARY_CONTRACT_SCHEMA`, `ITDASHBOARD_INVESTMENT_SCHEMA`. Wired into the `EXPLICIT_SCHEMAS` registry under canonical model names.

Also extended `VEHICLE_SCHEMA` with the fields the new `VEHICLES_*` presets reference: `is_synthetic_solicitation`, `program_acronym`, `organization` (expandable to `OrganizationOffice`), `metrics` (expandable to `VehicleMetrics`), `idv_count`, `total_obligated`, `latest_award_date`, `opportunity_id`, `description`.

#### Typed return models

- `client.resolve(input)` → `Promise<ResolveResult>` with typed `ResolveCandidate[]` (was `Promise<{ candidates: AnyRecord[]; count: number }>`).
- `client.validate(input)` → `Promise<ValidateResult>` (was `Promise<AnyRecord>`).
- `client.getAgency(code)` → `Promise<AgencyRecord>` (was `Promise<AnyRecord>`).
- `client.getProtest(caseNumber)` → `Promise<ProtestRecord>` with typed `docket`, `resolved_*`, etc.

All new types exported from package root.

#### Observability — `rateLimitInfo` + `lastResponseHeaders`

Two new instance properties on `TangoClient` mirroring Python's `rate_limit_info` / `last_response_headers`:

```typescript
await client.listContracts({ limit: 5 });
console.log(client.rateLimitInfo);
// { remaining: 98, limit: 100, resetIn: 60, retryAfter: null, limitType: "per_minute" }
console.log(client.lastResponseHeaders?.["x-request-id"]);
```

Both `null` until the first request completes; populated after every successful request. `HttpClient` parses `X-RateLimit-{Remaining,Limit,Reset,Type}` + `Retry-After` headers into a `RateLimitInfo` snapshot.

#### `listContracts` cursor pagination (non-breaking)

`ListContractsOptions` now accepts `cursor` alongside `page`. When `cursor` is supplied, the request omits `page` and uses keyset pagination (Python parity); otherwise falls back to page-based. `PaginatedResponse` gained a `cursor` field auto-extracted from `next` so callers can `client.listContracts({ cursor: resp.cursor })` for the next page without parsing the URL themselves.

#### `WebhookReceiver` framework

New `src/webhooks/receiver.ts`. `WebhookReceiver` dataclass-style class with `start()`, `stop()`, `url`, `deliveries`, `onDelivery` callback, optional `forwardTo` mirror, history cap, signature verification (via existing `verifySignature`). Two run patterns shipped:

- `await using rx = await new WebhookReceiver(opts).run();` (modern, `Symbol.asyncDispose`).
- `await WebhookReceiver.withRunning(opts, async (rx) => { ... });` (portable callback scope).

Pure `node:http` + native `fetch` — no new dependencies.

#### Webhook simulator

New `src/webhooks/simulate.ts`. `sign(payload, secret)` returns a `SignedRequest` (body bytes, signature header value, content-type). `deliver({ targetUrl, payload, secret, ... })` signs and POSTs via native `fetch` with `AbortSignal.timeout()`. Includes a `stableStringify` helper that matches Python's `json.dumps(sort_keys=True, separators=(",", ":"))` byte-for-byte so signatures are reproducible across languages.

#### Webhook CLI — `tango-node webhooks`

New `bin/tango-node` (entry in `src/bin/tango-node.ts`) backed by `commander`. Subcommands: `listen`, `simulate`, `trigger`, `fetch-sample`, `list-event-types`, `endpoints {list, get, create, delete}` — mirroring the Python `tango webhooks` CLI. New runtime dep: `commander@^12.1.0`.

#### Conformance gate

New `scripts/check-filter-shape-conformance.ts` + `npm run check-conformance` script. Walks `src/client.ts` with the TypeScript compiler AST, extracts each `list*` method's `Options` interface, and validates against the canonical manifest at `tango/contracts/filter_shape_contract.json`. JSON output + exit codes match the Python conformance script. Current state: **0 errors, 0 warnings** — parity verified.

#### Test coverage

Net **+74 tests** across the new work (220 total now passing, up from 146 pre-audit):

- ShapeConfig preset parity (`config.shapes.parity`)
- Explicit schema parity (`shapes.schema.parity`, 12 tests)
- WebhookReceiver lifecycle, signature paths, forwarding, history cap, `withRunning` + `Symbol.asyncDispose` (`webhooks/receiver`, 21 tests)
- Webhook simulator stable-stringify, sign-verify round-trip, deliver against a real `http.createServer`, timeout (`webhooks/simulate`, 17 tests)
- CLI subcommand wiring (`webhooks/cli`, 7 tests)
- Conformance script (`scripts/conformance`, 5 tests)
- Observability + cursor pagination + typed return models (`client.observability`, 11 tests)

### Internal

- `tsx` added as a devDependency to run the new conformance script.
- `HttpClient` constructor body unchanged; just adds two readonly fields (`lastResponseHeaders`, `rateLimitInfo`) populated on every successful response.

### Removed

- **Subject-based webhook subscriptions** are gone. The Tango API is dropping the `/api/webhooks/subscriptions/` surface for subject delivery (see [makegov/tango#2267](https://github.com/makegov/tango/issues/2267)); `tango-node` mirrors that here. Removed methods: `listWebhookSubscriptions`, `getWebhookSubscription`, `createWebhookSubscription`, `updateWebhookSubscription`, `deleteWebhookSubscription`. Removed types: `WebhookSubscription`, `WebhookSubscriptionCreateInput`, `WebhookSubscriptionUpdateInput`, `WebhookSubscriptionPayload`, `WebhookSubscriptionPayloadRecord`, `WebhookSubjectTypeDefinition`, `WebhookSampleSubject`, `ListWebhookSubscriptionsOptions`. `WebhookEventTypesResponse` no longer carries `subject_types` / `subject_type_definitions`; `WebhookEventType` no longer carries `default_subject_type`; sample-payload responses no longer carry `sample_subjects` / `sample_subscription_requests`. Use `createWebhookAlert` (filter-based delivery via `/api/webhooks/alerts/`) — that's the only remaining subscription path.

SemVer-major (`0.3.0` → `0.4.0`).

### Added

#### API parity — read methods

- **Lookups**: `listNaics`, `getNaics`, `listPsc`, `getPsc`, `listMasSins`, `getMasSin`, `listAssistanceListings`, `getAssistanceListing`, `listOrganizations`, `getOrganization`, `listOffices`, `getOffice`, `listDepartments` (`@deprecated` JSDoc), `getDepartment`, `getBusinessType`.
- **Awards completeness**: `listOtas`, `getOta`, `listOtidvs`, `getOtidv`, `listOtidvAwards`, `listSubawards`, `listGsaElibraryContracts`, `listLcats` (accepts `{ uei }` or `{ idvKey }`).
- **Other resources**: `listProtests`, `getProtest`, `listItDashboard`, `getItDashboard`, `listMetrics` (parameterized over `ownerType` since the API exposes metrics only under owner-scoped paths).
- **Utility endpoints**: `resolve(input)` (POST `/api/resolve/` — returns `{ candidates, count }`), `validate(input)` (POST `/api/validate/`).

#### API parity — typed wrappers for Python's `get_*_metrics` helpers

- `getEntityMetrics(uei, months, periodGrouping)`
- `getNaicsMetrics(code, months, periodGrouping)`
- `getPscMetrics(code, months, periodGrouping)`

#### API parity — entity, IDV, and agency sub-resources

- `listEntityContracts`, `listEntityIdvs`, `listEntityOtas`, `listEntityOtidvs`, `listEntitySubawards`, `listEntityLcats`
- `listIdvLcats(key, options?)` — typed sibling of the generic `listLcats({ idvKey })`
- `listAgencyAwardingContracts`, `listAgencyFundingContracts`

#### Webhook write API

- Endpoints: `createWebhookEndpoint` (now `name` is first-class; defaults to URL host if omitted), `updateWebhookEndpoint`, `deleteWebhookEndpoint`. `testWebhookEndpoint(endpointId)` is the canonical method; `testWebhookDelivery` is kept as an auto-resolving variant (omit `endpointId` to let the API pick the sole endpoint).
- Alerts (filter-subscription API): `listWebhookAlerts`, `getWebhookAlert`, `createWebhookAlert`, `updateWebhookAlert`, `deleteWebhookAlert`. `WebhookAlertCreateInput` now has an optional `endpoint` field — required for multi-endpoint accounts, optional for single-endpoint accounts (the API auto-resolves). Server support landed in [makegov/tango#2256](https://github.com/makegov/tango/issues/2256).

New typed input interfaces exported from the package root: `WebhookEndpointCreateInput`, `WebhookEndpointUpdateInput`, `WebhookAlertCreateInput`, `WebhookAlert`, plus options types for the new sub-resources.

#### Webhook signature helpers (parity with `tango_python.webhooks.signing`)

- `verifySignature(body, header, secret)` — constant-time HMAC-SHA256 verification. Accepts `"sha256=<hex>"` and bare-hex forms. Returns `boolean`, never throws.
- `generateSignature(body, secret)` — emits `"sha256=<hex>"` matching the dispatcher format.
- `parseSignatureHeader(header)` — returns `{ algorithm, signature } | null` for cleaner branching in receivers.

All exported from the package root; receivers don't need to instantiate `TangoClient`.

#### Async iterator pagination

For convenience, list methods now have async-iterator wrappers that handle `next` / `cursor` for you:

```typescript
for await (const contract of client.iterateContracts({ awarding_agency: "9700" })) {
  console.log(contract.piid, contract.total_contract_value);
}
```

Typed iterators: `iterateContracts`, `iterateEntities`, `iterateOpportunities`, `iterateNotices`, `iterateGrants`, `iterateForecasts`, `iterateIdvs`, `iterateVehicles`. Iteration is sequential (no concurrent requests) to respect API rate limits.

#### Retry with exponential backoff

`HttpClient` now automatically retries failed requests:

- Retries on 5xx, 408 (Request Timeout), 429 (Too Many Requests), network errors, and client-side timeouts.
- Does **not** retry on other 4xx — those surface as the appropriate `Tango*` error immediately.
- Exponential backoff: base `retryBackoffMs` (default 250ms), doubled per attempt, capped at 10s.
- Honors `Retry-After` headers (delta-seconds and HTTP-date) on 429/503.

#### Constructor surface

- `retries` (default `3`) and `retryBackoffMs` (default `250`) options on `TangoClientOptions`. Set `retries: 0` to disable.
- `timeout` accepted as a shorthand alias for `timeoutMs` (both in ms; `timeoutMs` wins if both are supplied).

#### Environment variable fallback

- `TANGO_BASE_URL` env var is now read when `baseUrl` is not passed to the constructor — parity with `TANGO_API_KEY`.

#### Misc

- `searchOpportunityAttachments`, `getVersion`, `listApiKeys` round out parity with the Python SDK's introspection / search surface.
- Shape generator now accepts `naics(code,description)` / `psc(code,description)` as canonical expands on Contract, Opportunity, Notice, Forecast, and Vehicle (IDV already had them). Mirrors `makegov/tango#2259`. (refs `makegov/tango#2265`)

### Changed

- `createWebhookEndpoint` and related write methods accept the canonical Tango API payload shape in addition to the previous camelCase wrappers — see the new typed input interfaces.
- `testWebhookEndpoint` / `testWebhookDelivery` now send the canonical `{ endpoint }` body key instead of the deprecated `{ endpoint_id }` (server still accepts both as aliases). Tracks [makegov/tango#2252](https://github.com/makegov/tango/issues/2252).
- `ListSubawardsOptions.ordering` narrowed from `string` to the literal union `"last_modified_date" | "-last_modified_date"`, matching the server-side enum (no other values are accepted; others 400). Tracks [makegov/tango#2254](https://github.com/makegov/tango/issues/2254).
- Shape generator rewrites legacy `naics_code(...)` / `psc_code(...)` expand spellings to canonical `naics(...)` / `psc(...)` before validation, matching the server's `_EXPAND_ALIASES` map. Scalar `naics_code` / `psc_code` (no parens) is untouched and still returns the raw column value. (refs `makegov/tango#2265`, `makegov/tango#2259`)

### Fixed

- `ShapeConfig.IDVS_COMPREHENSIVE` no longer includes `base_and_exercised_options_value`, which is not a valid IDV shape field — the API was returning `400 Invalid shape` on this preset. Now aligned with `tango_python.IDVS_COMPREHENSIVE`. Also reconciled `recipient.cage_code` → `recipient.cage` to match the Python preset exactly.
- `createWebhookAlert` now plumbs an explicit `endpoint` UUID through to the API. Multi-endpoint accounts can now create alerts directly instead of relying on the server's single-endpoint auto-resolution. Tracks [makegov/tango#2256](https://github.com/makegov/tango/issues/2256).
- **`Subaward` schema matches the server's `SubawardSerializer`.** The previous `SUBAWARD_SCHEMA` (ported from the broken Python schema) declared two fields the server has never exposed (`id`, `amount`) and was missing every real field — including `piid`, `key`, `awarding_office` / `funding_office` / `place_of_performance` / `subaward_details` / `fsrs_details` / `highly_compensated_officers` / `usaspending_permalink`, and the denormalized `prime_awardee_*` / `recipient_*` lookup columns. Shape strings that referenced any real field (e.g. `shape: "piid"`) would fail client-side validation with `unknown_field`. `SUBAWARD_SCHEMA` is now derived directly from `awards.serializers.subawards.SubawardSerializer` and the resource's runtime `available_fields`. New nested schemas `SubawardDetails`, `FsrsDetails`, `SubawardPlaceOfPerformance`, and `HighlyCompensatedOfficer` are registered so the corresponding shape expansions validate end-to-end.

### Internal

- Live smoke harnesses at `scripts/smoke-{reads,writes,extras,parity}.ts` exercise every new method against a running Tango instance. All four require `TANGO_API_KEY` in the environment (hard-fail if unset — no fallback).
- 5 new unit test files (`tests/unit/{client.parity,client.iterate,client.baseurl,webhooks.signing,config.shapes}.test.ts`) added; total suite is now 16 files / 111 tests / 82% line coverage.
- ESM build (`tsc -p tsconfig.json`) clean.

## [0.3.0] - 2026-02-09

### Added

- Vehicles endpoints: `listVehicles`, `getVehicle`, and `listVehicleAwardees` (supports shaping + flattening). (refs `makegov/tango#1327`)
- IDV endpoints: `listIdvs`, `getIdv`, `listIdvAwards`, `listIdvChildIdvs`, `listIdvTransactions`, `getIdvSummary`, `listIdvSummaryAwards`. (refs `makegov/tango#1327`)
- Webhooks v2 client support: event type discovery, subscription CRUD, endpoint management, test delivery, and sample payload helpers. (refs `makegov/tango#1275`)

### Changed

- HTTP client now supports PATCH/PUT/DELETE for non-GET endpoints.
- `joiner` is now respected when unflattening `flat=true` responses on supported endpoints.

## [0.1.0] - 2025-11-21

- Initial Node.js port of the Tango Python SDK.
- Basic project scaffolding for client, models, and shapes.
- ESM + TypeScript build configuration.

## [0.1.4] - 2025-11-21

- Added tests and cleaned up formatting and structure of SDK.

---

<!-- doc: sdks/node/client.md -->

# Client Configuration

`TangoClient` is the entry point for every API call. This guide covers the constructor, environment variables, retry semantics, error handling, and how to plug in a custom `fetch`.

For per-method signatures, see [`API_REFERENCE.md`](api-reference.md). For webhook receivers, see [`WEBHOOKS.md`](webhooks.md). For response shaping, see [`SHAPES.md`](shapes.md).

## Constructor

```typescript
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({
  apiKey: process.env.TANGO_API_KEY,   // optional if TANGO_API_KEY is set in env
  baseUrl: "https://tango.makegov.com", // default
  timeoutMs: 30_000,                    // default
  retries: 3,                           // default
  retryBackoffMs: 250,                  // default
});
```

### Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `apiKey` | `string` | reads `TANGO_API_KEY` env | Tango API key. Sent as `X-API-KEY` header. |
| `baseUrl` | `string` | `https://tango.makegov.com` | API base URL. Override for staging/local. |
| `timeoutMs` | `number` | `30000` | Per-request timeout in milliseconds. Aborts with `TangoTimeoutError`. |
| `timeout` | `number` | — | Ergonomic shorthand for `timeoutMs` (also in ms). If both are supplied, `timeoutMs` wins. |
| `retries` | `number` | `3` | Number of retry attempts on retryable failures. Set to `0` to disable. Total attempts = `retries + 1`. |
| `retryBackoffMs` | `number` | `250` | Initial backoff between retries (ms). Doubles each retry, capped at 10s. |
| `fetchImpl` | `typeof fetch` | global `fetch` | Custom fetch implementation. Useful for proxies, instrumentation, or runtimes without a global fetch. |

### Environment variables

| Env var | Purpose |
| --- | --- |
| `TANGO_API_KEY` | Default API key when `apiKey` is not passed to the constructor. |
| `TANGO_BASE_URL` | Default base URL when `baseUrl` is not passed. Falls through to `https://tango.makegov.com` if neither is set. |

## Retry semantics

The client retries failed requests automatically. A request is retried when:

- The HTTP status is `5xx` (any server error),
- The status is `408` (Request Timeout) or `429` (Too Many Requests),
- Or the request fails at the network layer (DNS, connection refused, fetch network error, abort due to client-side timeout).

Other `4xx` statuses (`400`, `401`, `403`, `404`, …) are **not** retried — they're surfaced as the appropriate `Tango*` error immediately.

### Backoff math

- First retry: `retryBackoffMs` (default 250ms)
- Second retry: `retryBackoffMs * 2`
- Third retry: `retryBackoffMs * 4`
- Each wait is capped at **10 seconds**.

If the response includes a `Retry-After` header (typical on `429` and `503`), the client honors that value instead of computing its own backoff:

- A delta-seconds value (`Retry-After: 5`) → waits 5 seconds.
- An HTTP-date value (`Retry-After: Wed, 21 Oct 2026 07:28:00 GMT`) → waits until that time.
- The honored wait is still capped at 10 seconds.

To disable retries entirely (e.g. for smoke tests or one-shot scripts where you'd rather see the raw failure):

```typescript
const client = new TangoClient({
  apiKey: process.env.TANGO_API_KEY,
  retries: 0,
});
```

## Error handling

All Tango errors extend `TangoAPIError`. Import them from the package root:

```typescript
import {
  TangoClient,
  TangoAPIError,
  TangoAuthError,
  TangoNotFoundError,
  TangoValidationError,
  TangoRateLimitError,
  TangoTimeoutError,
} from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

try {
  const entity = await client.getEntity("ABCDEF123456");
  console.log(entity);
} catch (err) {
  if (err instanceof TangoNotFoundError) {
    console.warn("No such entity");
  } else if (err instanceof TangoValidationError) {
    console.error("Bad request:", err.message, err.responseData);
  } else if (err instanceof TangoAuthError) {
    console.error("Check your API key");
  } else if (err instanceof TangoRateLimitError) {
    // The client already retried `retries` times before giving up.
    console.error("Rate limited; back off harder");
  } else if (err instanceof TangoTimeoutError) {
    console.error("Request timed out; consider raising timeoutMs");
  } else if (err instanceof TangoAPIError) {
    console.error("API error:", err.statusCode, err.message);
  } else {
    throw err;
  }
}
```

Every `TangoAPIError` carries:

- `message: string` — human-readable; for 400s, the SDK extracts the first `detail` / `message` / `error` / field-error from the response body when present.
- `statusCode?: number` — HTTP status (or `408` for client-side timeouts).
- `responseData?: unknown` — parsed JSON body of the error response (when the server returned JSON).

The client also throws `TangoAPIError` when an HTTP 200 response body contains `{ "error": "..." }` — the Tango API occasionally signals errors in 200 payloads.

## Custom `fetch`

Pass a custom `fetchImpl` to instrument requests, route through a proxy, or run in a non-Node runtime:

```typescript
import { TangoClient } from "@makegov/tango-node";

const tracedFetch: typeof fetch = async (input, init) => {
  const start = Date.now();
  const res = await fetch(input, init);
  console.log(`${init?.method ?? "GET"} ${input} → ${res.status} (${Date.now() - start}ms)`);
  return res;
};

const client = new TangoClient({
  apiKey: process.env.TANGO_API_KEY,
  fetchImpl: tracedFetch,
});
```

## Targeting staging or local

Point `baseUrl` at the host you want:

```typescript
const client = new TangoClient({
  apiKey: process.env.TANGO_API_KEY,
  baseUrl: "http://localhost:8000",
});
```

The trailing slash on `baseUrl` is optional; the client normalizes it.

---

<!-- doc: sdks/node/dynamic-models.md -->

# Tango Node SDK – Dynamic Models Guide

This document explains how the **Node.js dynamic shaping system** works.
It is a full translation of the Python `DEVELOPERS.md` shaping guide.

---

## Overview

Tango’s dynamic modeling allows you to:

- Request _exactly the fields you want_
- Validate the shape string against Tango’s schemas
- Generate a typed model descriptor at runtime
- Materialize shaped objects using correct:
  - date parsing
  - datetime parsing
  - decimal handling
  - list vs scalar logic
  - nested structure

---

## Components

### ShapeParser

Parses shape strings into a `ShapeSpec`.

```ts
import { ShapeParser } from "@makegov/tango-node/shapes";

const parser = new ShapeParser();
const spec = parser.parse("key,piid,recipient(display_name)");
```

### SchemaRegistry

Holds the field schemas for all models.

```ts
import { SchemaRegistry } from "@makegov/tango-node/shapes";

const registry = new SchemaRegistry();
registry.getField("Contract", "award_date");
```

### TypeGenerator

Builds a `GeneratedModel` descriptor from `(baseModel, shapeSpec)`.

```ts
import { TypeGenerator } from "@makegov/tango-node/shapes";

const gen = new TypeGenerator();
const model = gen.generateModelDescriptor("Contract", spec);
```

### ModelFactory

Takes a descriptor + raw API JSON and produces typed shaped objects. The TangoClient now uses this pipeline automatically after fetching data.

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
const contracts = await client.listContracts({
  shape: "key,award_date,recipient(display_name)",
});

// contracts.results are materialized via ModelFactory:
// - date/datetime parsed to Date
// - decimals normalized to string
// - nested structures enforced
```

---

## Example: Full Shaping Pipeline (manual)

```ts
const parser = new ShapeParser();
const spec = parser.parse("key,award_date,recipient(display_name)");

const gen = new TypeGenerator();
const descriptor = gen.generateModelDescriptor("Contract", spec);

const factory = new ModelFactory();
const shaped = factory.createOne("Contract", spec, {
  key: "C-1",
  award_date: "2024-01-15",
  recipient: { display_name: "Acme" },
});
```

`shaped` becomes:

```ts
{
  key: "C-1",
  award_date: Date("2024-01-15"),
  recipient: { display_name: "Acme" }
}
```

---

## Type Safety

Node SDK enforces shape correctness at runtime and guarantees nested structures. The client materializes responses through ModelFactory, so the shape schema is applied automatically. TypeScript interfaces are not codegenerated per shape at build time; the SDK exports lightweight model interfaces in `@makegov/tango-node/models` for convenience.

---

## Caching

TypeGenerator caches descriptors with FIFO eviction.

ShapeParser also caches parse results.

---

## Nested Models

If a field is nested in the schema (e.g. `"recipient"` → `RecipientProfile`),
the generator recursively builds the nested descriptor.

---

---

<!-- doc: sdks/node/index.md -->

# Tango Node.js SDK

[![npm](https://img.shields.io/npm/v/@makegov/tango-node.svg)](https://www.npmjs.com/package/@makegov/tango-node)
[![Node.js Version](https://img.shields.io/node/v/@makegov/tango-node.svg)](https://www.npmjs.com/package/@makegov/tango-node)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

A modern Node.js SDK for the [Tango API](https://tango.makegov.com), featuring dynamic response shaping, strong TypeScript types, and full coverage of the core Tango endpoints.

> This is the Node.js/TypeScript port of the official Tango Python SDK.

## Features

- **Dynamic Response Shaping** – Ask Tango for exactly the fields you want using a simple shape syntax.
- **Type-Safe by Design** – Shape strings are validated against Tango schemas and mapped to generated TypeScript types.
- **Full Tango API surface** – Awards (contracts, IDVs, OTAs, OTIDVs, subawards, vehicles, GSA eLibrary), opportunities + notices, forecasts, grants, protests, IT Dashboard, entities (with sub-resources), agencies/organizations/offices/departments, lookups (NAICS, PSC, MAS SINs, assistance listings, business types), metrics, resolve/validate, webhooks. See `## API Methods` below for the full list.
- **Flexible Data Access** – Plain JavaScript objects backed by runtime validation and parsing, materialized via the dynamic model pipeline.
- **Modern Node.js** – Built for Node.js 18+ with native `fetch` and ESM-first design.
- **Tested Against the Real API** – Integration tests (mirroring the Python SDK) keep behavior aligned.

## Installation

**Requirements:** Node.js 18 or higher.

```bash
npm install @makegov/tango-node
# or
yarn add @makegov/tango-node
# or
pnpm add @makegov/tango-node
```

## Quick Start

### Initialize the client

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({
  apiKey: process.env.TANGO_API_KEY,
  // baseUrl: "https://tango.makegov.com", // default
});
```

### List agencies

```ts
const agencies = await client.listAgencies();

for (const agency of agencies.results) {
  console.log(agency.code, agency.name);
}
```

### Get a specific agency

```ts
const treasury = await client.getAgency("2000"); // Treasury
console.log(treasury.name, treasury.department?.name);
```

### Search contracts with a minimal shape

```ts
import { TangoClient, ShapeConfig } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

const contracts = await client.listContracts({
  shape: ShapeConfig.CONTRACTS_MINIMAL,
  keyword: "cloud services",
  awarding_agency: "4700",
  fiscal_year: 2024,
  limit: 10,
});

// Each contract is shaped according to CONTRACTS_MINIMAL
for (const c of contracts.results) {
  console.log(c.piid, c.award_date, c.recipient.display_name);
}
```

### Get a fully-shaped entity

```ts
import { TangoClient, ShapeConfig } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

const entity = await client.getEntity("ABC123DEF456", {
  shape: ShapeConfig.ENTITIES_COMPREHENSIVE,
});

console.log(entity.uei, entity.legal_business_name, entity.primary_naics);
```

## Authentication

The Node.js SDK uses the same model as the Python one: you can either pass the API key directly or read it from `TANGO_API_KEY`.

### With API key

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({
  apiKey: "your-api-key-here",
});
```

### From environment variable (`TANGO_API_KEY`)

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient();
// If apiKey is omitted, the client will look for process.env.TANGO_API_KEY
```

## Core Concepts

### Dynamic Response Shaping

Response shaping is the core feature of Tango. Instead of always receiving huge objects with every field, you describe the fields you want with a compact shape string:

```ts
const contracts = await client.listContracts({
  shape: "key,piid,award_date,recipient(display_name),total_contract_value",
  keyword: "software",
  limit: 5,
});
```

**Shapes:**

- Reduce payload size (often massively).
- Keep responses focused on what your app actually uses.
- Drive type safety – the SDK maps the shape to a TypeScript type.

**The Node.js SDK includes:**

- A **shape parser** that validates shape strings.
- A **schema registry** that knows what fields exist on each resource.
- A **type generator** and **model factory** that convert raw API JSON into strongly-typed objects.

### Flat vs nested responses

By default, nested fields are returned as nested objects:

```ts
// shape:
"key,piid,recipient(display_name,uei)";

//
contract.recipient.display_name;
contract.recipient.uei;
```

You can request a "flat" representation that uses dotted keys and then unflattens into nested objects on the client:

```ts
const contracts = await client.listContracts({
  shape: ShapeConfig.CONTRACTS_MINIMAL,
  flat: true,
});
```

The Node.js SDK mirrors the Python client's behavior for `shape`, `flat`, and `flat_lists`.

## API Methods

The Node.js client mirrors the Python SDK's high-level API. Selected highlights:

**Agencies / Offices / Organizations / Departments**

- `listAgencies(options)` / `getAgency(code)`
- `listOffices(options)` / `getOffice(code)`
- `listOrganizations(options)` / `getOrganization(identifier)`
- `listDepartments(options)` / `getDepartment(code)`

**Contracts / IDVs / OTAs / OTIDVs / Subawards**

- `listContracts(options)` / `listIdvs(options)` / `getIdv(key, options)`
- `listIdvAwards(key, options)` / `listIdvChildIdvs({key, ...options})` / `listIdvTransactions(key, options)`
- `getIdvSummary(identifier)` / `listIdvSummaryAwards(identifier, options)`
- `listOtas(options)` / `getOta(key)` / `listOtidvs(options)` / `getOtidv(key)` / `listOtidvAwards(key, options)`
- `listSubawards(options)`

**Vehicles**

- `listVehicles(options)` / `getVehicle(uuid, options)` / `listVehicleAwardees(uuid, options)`

**Entities**

- `listEntities(options)` / `getEntity(ueiOrCage, options)`
- `listEntityContracts(uei, options)` / `listEntityIdvs(uei, options)` / `listEntityOtas(uei, options)`
- `listEntityOtidvs(uei, options)` / `listEntitySubawards(uei, options)` / `listEntityLcats(uei, options)`
- `getEntityMetrics(uei, months, periodGrouping)`

**Forecasts / Opportunities / Notices / Grants**

- `listForecasts(options)` / `listOpportunities(options)` / `listNotices(options)` / `listGrants(options)`
- `searchOpportunityAttachments(options)`

**GSA eLibrary / Protests / IT Dashboard / Subawards / LCATs**

- `listGsaElibraryContracts(options)` / `listProtests(options)` / `getProtest(caseNumber)`
- `listItDashboard(options)` / `getItDashboard(uii)`
- `listLcats(options)` / `listIdvLcats(key, options)`

**Reference / Lookups**

- `listBusinessTypes(options)` / `getBusinessType(code)`
- `listNaics(options)` / `getNaics(code)` / `getNaicsMetrics(code, months, periodGrouping)`
- `listPsc(options)` / `getPsc(code)` / `getPscMetrics(code, months, periodGrouping)`
- `listMasSins(options)` / `getMasSin(sin)`
- `listAssistanceListings(options)` / `getAssistanceListing(number)`
- `listMetrics(options)` / `listAgencyAwardingContracts(code, options)` / `listAgencyFundingContracts(code, options)`

**Resolve / Validate**

- `resolve(input)` — resolve a free-text name to ranked entity/org candidates
- `validate(input)` — validate a PIID, solicitation number, or UEI

**Webhooks**

- `listWebhookEventTypes()`
- `listWebhookEndpoints(options)` / `getWebhookEndpoint(id)`
- `createWebhookEndpoint(...)` / `updateWebhookEndpoint(id, patch)` / `deleteWebhookEndpoint(id)`
- `testWebhookEndpoint(endpointId)` (preferred) / `testWebhookDelivery(options?)` (legacy alias)
- `getWebhookSamplePayload(options?)`
- `listWebhookAlerts(options)` / `getWebhookAlert(id)` / `createWebhookAlert(input)`
- `updateWebhookAlert(id, patch)` / `deleteWebhookAlert(id)`

**Async iteration helpers**

- `iterate(method, options)` — generic async iterator over any supported list method
- `iterateContracts` / `iterateEntities` / `iterateOpportunities` / `iterateNotices`
- `iterateGrants` / `iterateForecasts` / `iterateIdvs` / `iterateVehicles`

**Utility**

- `getVersion()` / `listApiKeys()`

See [docs/API_REFERENCE.md](api-reference.md) for full signatures and parameters.

All list methods return a paginated response:

```ts
interface PaginatedResponse<T> {
  count: number;
  next: string | null;
  previous: string | null;
  pageMetadata: Record<string, unknown> | null;
  results: T[];
}
```

## Error Handling

Errors are surfaced as typed exceptions, aligned with the Python SDK:

- `TangoAPIError` – Base error for unexpected issues.
- `TangoAuthError` – Authentication problems (e.g., invalid API key, 401).
- `TangoNotFoundError` – Resource not found (404).
- `TangoValidationError` – Invalid request parameters (400).
- `TangoRateLimitError` – Rate limit exceeded (429).
- `TangoTimeoutError` – Request exceeded the configured `timeoutMs`.

Shape-related errors:

- `ShapeError`
- `ShapeValidationError`
- `ShapeParseError`
- `TypeGenerationError`
- `ModelInstantiationError`

Use them in your code:

```ts
import { TangoClient, TangoAPIError, TangoValidationError } from "@makegov/tango-node";

try {
  const resp = await client.listContracts({ keyword: "cloud", limit: 5 });
} catch (err) {
  if (err instanceof TangoValidationError) {
    console.error("Bad request:", err.message);
  } else if (err instanceof TangoAPIError) {
    console.error("Tango API error:", err.message);
  } else {
    console.error("Unexpected error:", err);
  }
}
```

## Project Structure

```text
tango-node/
├── src/                         # Source TypeScript
│   ├── client.ts                # TangoClient implementation
│   ├── config.ts                # Default base URL + shape presets
│   ├── errors.ts                # Error classes (API, auth, validation, etc.)
│   ├── index.ts                 # Public API exports
│   ├── types.ts                 # Shared types (options, PaginatedResponse)
│   ├── models/                  # Lightweight model interfaces (Contract, Entity, etc.)
│   ├── shapes/                  # Shape system (parser, generator, factory)
│   │   ├── explicitSchemas.ts   # Predefined schemas for resources
│   │   ├── factory.ts           # Instantiate typed models from data
│   │   ├── generator.ts         # Type generation from shape specs
│   │   ├── index.ts             # Shapes exports
│   │   ├── parser.ts            # Shape string parser
│   │   ├── schema.ts            # Schema registry + validation
│   │   ├── schemaTypes.ts       # Schema data structures
│   │   └── types.ts             # Shape spec types
│   └── utils/                   # Helpers
│       ├── dates.ts             # Date/time parsing utilities
│       ├── http.ts              # HTTP client wrapper
│       ├── number.ts            # Numeric parsing/formatting
│       └── unflatten.ts         # Unflatten dotted-key responses
├── docs/                        # Documentation
│   ├── API_REFERENCE.md
│   ├── DYNAMIC_MODELS.md
│   └── SHAPES.md
├── tests/                       # Test suite (Vitest)
│   └── unit/
│       ├── client.test.ts
│       ├── errors.test.ts
│       ├── shapes.factory.test.ts
│       ├── shapes.generator.test.ts
│       ├── shapes.parser.test.ts
│       ├── shapes.schema.test.ts
│       ├── utils.dates.test.ts
│       ├── utils.http.test.ts
│       ├── utils.number.test.ts
│       └── utils.unflatten.test.ts
├── dist/                        # Build output (compiled JS + d.ts) from `npm run build`
├── eslint.config.js             # ESLint flat config
├── .prettierrc                  # Prettier config
├── package.json                 # Package metadata/scripts
├── tsconfig.json                # TypeScript config
├── README.md                    # Usage docs
├── CHANGELOG.md                 # Version history
└── LICENSE                      # MIT license
```

## Development

After cloning the repo:

```bash
npm install
npm run build
npm test
```

Useful scripts:

- `npm run build` – Compile TypeScript to `dist/`.
- `npm test` – Run unit and integration tests.
- `npm run coverage` – Get test coverage report.
- `npm run lint` – Run ESLint.
- `npm run format` – Run Prettier.
- `npm run typecheck` – TS type checking without emit.

## Requirements

- Node.js 18 or higher.
- A valid [Tango API key](https://tango.makegov.com/).

## Documentation

- [API Reference](api-reference.md) - Detailed API documentation
- [Shape System Guide](shapes.md) - Comprehensive guide to response shaping
- [Dynamic Models Guide](dynamic-models.md) - How the dynamic shaping system works.

## License

MIT License - see [LICENSE](https://github.com/makegov/tango-node/blob/main/LICENSE) for details.

## Support

For questions, issues, or feature requests:

- **Email**: [tango@makegov.com](mailto:tango@makegov.com)
- **Issues**: [GitHub Issues](https://github.com/makegov/tango-node/issues)
- **Documentation**: [https://docs.makegov.com/sdks/node/](https://docs.makegov.com/sdks/node/)

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Run tests (`npm run test`)
4. Commit your changes (`git commit -m 'Add amazing feature'`)
5. Push to the branch (`git push origin feature/amazing-feature`)
6. Open a Pull Request

---

<!-- doc: sdks/node/shapes.md -->

# Tango Node SDK – Shaping Guide

A complete translation of the Python SHAPES.md document for Node.

---

## Why Shapes?

Tango resources can have hundreds of fields. Shapes let you request:

- Only what you need
- In nested form
- With aliases
- With wildcards
- With flattening options

---

## Shape Grammar

```
shape       := field_list
field_list  := field ("," field)*
field       := field_name [alias] [nested]
field_name  := identifier | "*"
alias       := "::" identifier
nested      := "(" field_list ")"
identifier  := [a-zA-Z_][a-zA-Z0-9_]*
```

---

## Examples

### Simple

```ts
shape: "key,piid,award_date";
```

### Nested

```ts
shape: "recipient(display_name,uei)";
```

### Aliases

```ts
shape: "recipient::vendor(display_name)";
```

### Wildcard

```ts
shape: "*";
```

### Wildcard nested

```ts
shape: "recipient(*)";
```

---

## ShapeConfig Presets

The SDK ships with a `ShapeConfig` object of ready-made shape strings for common patterns. Import from the main entry point:

```ts
import { TangoClient, ShapeConfig } from "@makegov/tango-node";
```

| Constant                       | Intended use                    |
| ------------------------------ | ------------------------------- |
| `ShapeConfig.CONTRACTS_MINIMAL`        | `listContracts()`        |
| `ShapeConfig.ENTITIES_MINIMAL`         | `listEntities()`         |
| `ShapeConfig.ENTITIES_COMPREHENSIVE`   | `getEntity()`            |
| `ShapeConfig.FORECASTS_MINIMAL`        | `listForecasts()`        |
| `ShapeConfig.OPPORTUNITIES_MINIMAL`    | `listOpportunities()`    |
| `ShapeConfig.NOTICES_MINIMAL`          | `listNotices()`          |
| `ShapeConfig.GRANTS_MINIMAL`           | `listGrants()`           |
| `ShapeConfig.IDVS_MINIMAL`             | `listIdvs()`             |
| `ShapeConfig.IDVS_COMPREHENSIVE`       | `getIdv()`               |
| `ShapeConfig.VEHICLES_MINIMAL`         | `listVehicles()`         |
| `ShapeConfig.VEHICLES_COMPREHENSIVE`   | `getVehicle()`           |
| `ShapeConfig.VEHICLE_AWARDEES_MINIMAL` | `listVehicleAwardees()`  |

These are plain strings — you can use them directly or as a starting point:

```ts
const contracts = await client.listContracts({
  shape: ShapeConfig.CONTRACTS_MINIMAL,
  limit: 10,
});
```

---

## Flat Responses

```ts
shape: ShapeConfig.CONTRACTS_MINIMAL,
flat: true
```

When `flat: true` is passed, the Tango API returns dotted key names instead of nested objects. The SDK automatically unflattens them back into nested objects on the client side:

```ts
// API returns:       { "recipient.display_name": "Acme" }
// SDK unflattens to: { recipient: { display_name: "Acme" } }
```

You can override the separator character (default `"."`) with the `joiner` option.

---

## Validation

ShapeParser enforces syntax.

TypeGenerator enforces semantic model rules (existence of fields, nested models).

---

## Performance Tips

- Use minimal shapes in production.
- Avoid full-wildcard unless you need all fields.
- Prefer shallow nested shapes for large nested structures.

---

<!-- doc: sdks/node/webhooks.md -->

# Webhooks Guide

This guide covers everything `@makegov/tango-node` provides for **building, testing, and operating webhook integrations against the Tango API**: signing helpers, signature verification, and management commands for endpoints and alerts.

If you only need the SDK method signatures, see [`API_REFERENCE.md` § Webhooks](api-reference.md#webhooks-v2). For the API-level contract (signing scheme, event taxonomy, retry behavior), see the [Tango Webhooks Partner Guide](https://docs.makegov.com/webhooks-user-guide/).

> **Breaking change in v0.4.0**: subject-based webhook subscriptions have been removed. Use the [Alerts API](#alerts-filter-subscription-api) for filter-based delivery. Mirrors [makegov/tango#2267](https://github.com/makegov/tango/issues/2267).

---

## Contents

- [Install](#install)
- [Concepts in 60 seconds](#concepts-in-60-seconds)
- [Quickstart: zero to receiving](#quickstart-zero-to-receiving)
- [Programmatic use](#programmatic-use)
  - [Signature verification in your handler](#signature-verification-in-your-handler)
  - [Generating signatures (for testing)](#generating-signatures-for-testing)
  - [Parsing the signature header](#parsing-the-signature-header)
- [Webhook write API](#webhook-write-api)
  - [Endpoints](#endpoints)
  - [Alerts (filter-subscription API)](#alerts-filter-subscription-api)
  - [Event types and sample payloads](#event-types-and-sample-payloads)
  - [Test delivery](#test-delivery)
- [Common workflows](#common-workflows)
- [Troubleshooting](#troubleshooting)

---

## Install

```bash
npm install @makegov/tango-node
# or
yarn add @makegov/tango-node
# or
pnpm add @makegov/tango-node
```

The signing helpers and full webhook write API are included in the default install — no extras needed.

```ts
import { TangoClient, generateSignature, verifySignature, parseSignatureHeader, SIGNATURE_HEADER, SIGNATURE_PREFIX } from "@makegov/tango-node";
```

---

## Concepts in 60 seconds

Tango webhooks have three pieces of state:

| Concept      | What it is                                                              | Tango term           |
| ------------ | ----------------------------------------------------------------------- | -------------------- |
| **Endpoint** | The URL Tango POSTs to, plus a generated signing secret                 | `WebhookEndpoint`    |
| **Alert**    | A saved query-filter that fires deliveries when matching records appear | `WebhookAlert`       |
| **Delivery** | A single signed POST Tango makes when a matching event fires            | (the request itself) |

A typical setup:

1. **Create an endpoint** with the public URL of your handler. Tango returns a `secret` — save it; it's used to sign every delivery.
2. **Create one or more alerts** describing the records your handler cares about (e.g. new IT-services contracts).
3. **Tango POSTs** to your endpoint when matching records appear. The body is JSON; the header `X-Tango-Signature: sha256=<hex>` is the HMAC-SHA256 of the raw body bytes keyed by your endpoint's secret.
4. **Your handler verifies the signature**, parses the body, and acts on it.

---

## Quickstart: zero to receiving

Assumes you have a `TANGO_API_KEY` and want to receive webhooks for new IT-services contracts.

### 1. See what you can subscribe to

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
const info = await client.listWebhookEventTypes();
console.log(info.event_types);
// [{ event_type: "alerts.contract.match", description: "...", schema_version: 1 }, ...]
```

### 2. See what a payload looks like

```ts
const sample = await client.getWebhookSamplePayload({ eventType: "alerts.contract.match" });
console.log(JSON.stringify(sample, null, 2));
```

Fetches the canonical JSON shape Tango will deliver for that event type. No alert needed.

### 3. Register your endpoint and alert

When you're ready for end-to-end testing, expose your local handler via a tunnel (`ngrok http 3000`, `cloudflared tunnel`, etc.) and register that public URL with Tango:

```ts
// Endpoint names are unique per user. Tango returns a secret — save it.
const endpoint = await client.createWebhookEndpoint({
  callbackUrl: "https://<your-tunnel>.ngrok.io/tango/webhooks",
  name: "dev",
});
console.log("Secret:", endpoint.secret); // save this!

// Create an alert — fires when matching records appear
const alert = await client.createWebhookAlert({
  name: "New IT cloud contracts",
  query_type: "contract", // singular — required
  filters: { naics: "541511" }, // any /api/contracts/ filter
});
```

### 4. Force a test delivery

```ts
const result = await client.testWebhookEndpoint(endpoint.id);
console.log(result.success, result.status_code);
```

You should see a signed delivery hit your handler with the `X-Tango-Signature` header generated by Tango.

---

## Programmatic use

### Signature verification in your handler

`verifySignature` is the only function you need in production. It takes `(body, header, secret)` — note the arg order.

Call it on the **raw request body bytes** — not on a re-serialized parsed body. The HMAC is computed over the exact bytes Tango sent; reformatting or reordering keys breaks it.

```ts
import { verifySignature } from "@makegov/tango-node";

// Express example
app.post("/tango/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body; // Buffer — express.raw() gives you bytes
  const signatureHeader = req.headers["x-tango-signature"];

  if (!verifySignature(rawBody, signatureHeader, process.env.TANGO_WEBHOOK_SECRET)) {
    return res.status(401).json({ error: "invalid_signature" });
  }

  const payload = JSON.parse(rawBody.toString("utf8"));
  // ... act on payload.events ...
  res.json({ ok: true });
});
```

`verifySignature` signature:

```ts
function verifySignature(body: string | Buffer, header: string | null | undefined, secret: string): boolean;
```

- Returns `false` for missing, empty, malformed, or mismatched headers — never throws on mismatch.
- Uses `timingSafeEqual` from `node:crypto` internally.
- Accepts both the canonical `sha256=<hex>` header form and a bare hex string (legacy compatibility).

**Fastify example:**

```ts
import { verifySignature } from "@makegov/tango-node";

fastify.addContentTypeParser("application/json", { parseAs: "buffer" }, (req, body, done) => {
  done(null, body);
});

fastify.post("/tango/webhooks", async (request, reply) => {
  const rawBody = request.body as Buffer;
  const signatureHeader = request.headers["x-tango-signature"] as string | undefined;

  if (!verifySignature(rawBody, signatureHeader ?? null, process.env.TANGO_WEBHOOK_SECRET!)) {
    reply.code(401).send({ error: "invalid_signature" });
    return;
  }

  const payload = JSON.parse(rawBody.toString("utf8"));
  // ... handle payload ...
  reply.send({ ok: true });
});
```

### Generating signatures (for testing)

`generateSignature` produces the exact header value Tango sends. Use it in tests to sign synthetic payloads before POSTing to your handler.

```ts
import { generateSignature, SIGNATURE_HEADER } from "@makegov/tango-node";

const body = Buffer.from(
  JSON.stringify({
    event_type: "alerts.entity.match",
    alert_id: "alert_123",
    query_type: "entity",
    filters: { uei: "ABC123" },
    matches: { new: [], modified: [], new_count: 0, modified_count: 0 },
    checked_at: "2024-01-01T00:00:00Z",
  }),
);

const signatureHeader = generateSignature(body, "test_secret");
// → "sha256=<lowercase hex>"

// Drive your handler directly (e.g. in a test):
const response = await fetch("http://localhost:3000/tango/webhooks", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    [SIGNATURE_HEADER]: signatureHeader,
  },
  body,
});
```

`generateSignature` signature:

```ts
function generateSignature(body: string | Buffer, secret: string): string;
// Returns: "sha256=<lowercase hex HMAC-SHA256>"
```

The return value is always the prefixed form `sha256=<hex>` — pass it directly as the `X-Tango-Signature` header value.

### Parsing the signature header

`parseSignatureHeader` breaks a raw `X-Tango-Signature` header value into its component parts. Mainly useful for debugging or building custom verification logic.

```ts
import { parseSignatureHeader } from "@makegov/tango-node";

const parsed = parseSignatureHeader("sha256=abc123def456");
// → { algorithm: "sha256", signature: "abc123def456" }

const bad = parseSignatureHeader("sha256=");
// → null  (empty digest)

const missing = parseSignatureHeader(null);
// → null
```

Returns `null` for absent, empty, or malformed values (non-hex digest, empty digest). Never throws.

---

## Webhook write API

All methods are on `TangoClient`. They're async and return Promises. Webhook APIs require **Large / Enterprise** tier access.

### Endpoints

An endpoint is the URL Tango POSTs to, paired with a signing secret.

```ts
const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

// List
const list = await client.listWebhookEndpoints({ page: 1, limit: 25 });

// Get one
const endpoint = await client.getWebhookEndpoint("ENDPOINT_UUID");

// Create — `name` is required and must be unique per user; save the returned `secret`
const created = await client.createWebhookEndpoint({
  callbackUrl: "https://example.com/tango/webhooks",
  name: "Production handler",
  isActive: true, // default true
});
console.log("Secret:", created.secret); // only returned on create — save it

// Update
await client.updateWebhookEndpoint(created.id, { isActive: false });

// Delete
await client.deleteWebhookEndpoint(created.id);
```

`createWebhookEndpoint` also accepts the snake_case canonical API form directly (`callback_url`, `is_active`). CamelCase is preferred for new code.

**`WebhookEndpoint` shape:**

```ts
interface WebhookEndpoint {
  id: string;
  name: string;
  callback_url: string;
  secret?: string; // present on create response only
  is_active: boolean;
  created_at: string;
  updated_at: string;
}
```

### Alerts (filter-subscription API)

Alerts are the SDK's interface for telling Tango "deliver me records matching this filter." Subject-based subscriptions (match by event type + specific subject IDs) were removed in v0.4.0 — alerts are now the only way to subscribe.

```ts
// Create
const alert = await client.createWebhookAlert({
  name: "New IT cloud contracts",
  query_type: "contract", // singular — required
  filters: { naics: "541511" }, // required, non-empty
  frequency: "daily", // optional
});

// List
const alerts = await client.listWebhookAlerts({ page: 1, pageSize: 25 });

// Get
const alert = await client.getWebhookAlert("ALERT_UUID");

// Update
await client.updateWebhookAlert("ALERT_UUID", { name: "Updated name" });

// Delete
await client.deleteWebhookAlert("ALERT_UUID");
```

**`WebhookAlert` shape:**

```ts
interface WebhookAlert {
  alert_id: string;
  name: string;
  query_type: string;
  filters: Record<string, unknown>;
  frequency: string;
  cron_expression: string | null;
  status: "active" | "paused";
  created_at: string;
  last_checked_at: string | null;
  match_count: number;
}
```

### Event types and sample payloads

```ts
// List all supported event types
const info = await client.listWebhookEventTypes();
// info.event_types → [{ event_type, description, schema_version }]

// Fetch a canonical sample payload for one event type
const sample = await client.getWebhookSamplePayload({ eventType: "alerts.contract.match" });

// Or fetch sample payloads for all event types at once
const all = await client.getWebhookSamplePayload();
```

`getWebhookSamplePayload` wraps `GET /api/webhooks/endpoints/sample-payload/`. When `eventType` is omitted, returns all event types. The response includes a `signature_header` field showing what the `X-Tango-Signature` header will look like — useful for understanding the wire format.

### Test delivery

Force Tango to POST a real test delivery to a registered endpoint:

```ts
const result = await client.testWebhookEndpoint("ENDPOINT_UUID");
console.log(result.success); // boolean
console.log(result.status_code); // HTTP code Tango got from your endpoint
console.log(result.response_time_ms);
console.log(result.message);
console.log(result.error); // set on failure
```

**`WebhookTestDeliveryResult` shape:**

```ts
interface WebhookTestDeliveryResult {
  success: boolean;
  status_code?: number;
  response_time_ms?: number;
  endpoint_url?: string;
  message?: string;
  error?: string;
  response_body?: string;
  test_payload?: Record<string, unknown>;
}
```

`testWebhookDelivery(options?)` is a legacy alias that accepts `{ endpointId?: string }`. If `endpointId` is omitted, the API auto-resolves the caller's only endpoint (404 if none, 400 if multiple). Prefer `testWebhookEndpoint` for new code.

---

## Common workflows

### "Set me up to receive contract-match alerts from scratch"

```ts
import { TangoClient } from "@makegov/tango-node";

const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });

// 1. Confirm available event types
const { event_types } = await client.listWebhookEventTypes();
console.log(event_types.map((e) => e.event_type));

// 2. Create endpoint — expose your handler via ngrok/cloudflared first
const endpoint = await client.createWebhookEndpoint({
  callbackUrl: "https://<id>.ngrok.io/tango/webhooks",
});
// Save endpoint.secret — you need it to verify incoming deliveries
process.env.TANGO_WEBHOOK_SECRET = endpoint.secret!;

// 3. Create an alert — fires when matching records appear
await client.createWebhookAlert({
  name: "New IT cloud contracts",
  query_type: "contract",
  filters: { naics: "541511" },
});

// 4. Force a test delivery to verify your handler is reachable
const result = await client.testWebhookEndpoint(endpoint.id);
console.log("Delivery success:", result.success, "Status:", result.status_code);
```

### "Verify a Tango delivery in any HTTP framework"

```ts
import { verifySignature } from "@makegov/tango-node";

// The pattern is the same in Express, Fastify, Hono, Next.js API routes, etc.:
// 1. Get raw body bytes BEFORE any JSON parsing middleware
// 2. Get the X-Tango-Signature header
// 3. Call verifySignature(rawBody, header, secret)

function handleTangoWebhook(rawBody: Buffer, signatureHeader: string | undefined) {
  if (!verifySignature(rawBody, signatureHeader ?? null, process.env.TANGO_WEBHOOK_SECRET!)) {
    throw new Error("invalid_signature");
  }
  return JSON.parse(rawBody.toString("utf8"));
}
```

### "Test my handler in a unit test (no network)"

Use `generateSignature` to sign synthetic payloads and POST them directly to your handler — no Tango account or live endpoint needed:

```ts
import { generateSignature, SIGNATURE_HEADER } from "@makegov/tango-node";

const secret = "test_secret";
const payload = {
  timestamp: "2024-01-01T00:00:00Z",
  events: [{ event_type: "alerts.contract.match", record: { piid: "ABC123" } }],
};
const rawBody = Buffer.from(JSON.stringify(payload));
const sig = generateSignature(rawBody, secret);

// Drive your handler directly — e.g. with supertest or a test fetch:
const res = await request(app).post("/tango/webhooks").set("Content-Type", "application/json").set(SIGNATURE_HEADER, sig).send(rawBody);

expect(res.status).toBe(200);
```

### "Inspect what bytes Tango actually sends"

```ts
const sample = await client.getWebhookSamplePayload({ eventType: "alerts.contract.match" });
// sample.signature_header shows the X-Tango-Signature format
// sample.sample_delivery shows the exact JSON body shape
console.log(JSON.stringify(sample.sample_delivery, null, 2));
```

---

## Troubleshooting

**Signature always fails.** Verify on raw bytes, not on a re-serialized parsed body. The HMAC is over exact bytes; reformatting whitespace or reordering keys breaks it. Most frameworks expose the raw body separately from a parsed JSON shortcut — use the raw one. In Express, use `express.raw({ type: "application/json" })` before your route handler.

**`verifySignature` returns `false` even with the right secret.** Check argument order: it's `(body, header, secret)` — the header is second, secret is third. This differs from some other webhook libraries.

**`createWebhookEndpoint` returns 400 or "endpoint already exists".** Endpoint names are unique per user — if you've already created one with that `name`, either pick a different name or use `listWebhookEndpoints()` to find the existing one and reuse its ID.

**`createWebhookAlert` throws `TangoValidationError: query_type is required`.** The `query_type` field is singular — `"contract"`, not `"contracts"`.

**`testWebhookEndpoint` returns `success: false`.** Tango reached your endpoint but got a non-2xx response. Check `result.status_code` and `result.response_body` in the result, then look at your handler's logs.

**`getWebhookSamplePayload` throws with 401.** Set `TANGO_API_KEY` (or pass `apiKey` to `TangoClient`). This endpoint requires authentication.

**`listWebhookAlerts` returns an empty array unexpectedly.** Check your API key — alerts are scoped to the authenticated user. Also confirm the endpoint UUID associated with your alerts matches your current endpoint (alerts pointing at a deleted endpoint aren't automatically cleaned up).

---

<!-- doc: sdks/python/api-reference.md -->

# API Reference

Complete reference for all Tango Python SDK methods and functionality.

## Table of Contents

- [Client Initialization](#client-initialization)
- [Agencies](#agencies)
- [Offices](#offices)
- [Organizations](#organizations)
- [Contracts](#contracts)
- [IDVs](#idvs)
- [OTAs](#otas)
- [OTIDVs](#otidvs)
- [Subawards](#subawards)
- [Vehicles](#vehicles)
- [Entities](#entities)
- [Forecasts](#forecasts)
- [Opportunities](#opportunities)
- [Notices](#notices)
- [Grants](#grants)
- [GSA eLibrary Contracts](#gsa-elibrary-contracts)
- [Protests](#protests)
- [Business Types](#business-types)
- [NAICS](#naics)
- [Webhooks](#webhooks)
- [Response Objects](#response-objects)
- [ShapeConfig (predefined shapes)](#shapeconfig-predefined-shapes)
- [Error Handling](#error-handling)

## Client Initialization

### TangoClient

Initialize the Tango API client.

```python
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.

```python
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](#paginatedresponse) with `Agency` dataclass objects

**Example:**
```python
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.

```python
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](https://docs.makegov.com/api-reference/concepts/federal-agency-hierarchy/) for code semantics.

**Returns:** `Agency` dataclass with agency details

**Example:**
```python
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.

```python
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](#paginatedresponse) with office dictionaries

### get_office()

Get a specific office by code.

```python
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.

```python
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](#paginatedresponse) with organization dictionaries

### get_organization()

Get a specific organization by fh_key.

```python
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.

```python
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](shapes.md))
- `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](#paginatedresponse) with contract dictionaries

**Examples:**

```python
# 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.

```python
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](#paginatedresponse) with OTA dictionaries

### get_ota()

```python
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.

```python
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](#paginatedresponse) with OTIDV dictionaries

### get_otidv()

```python
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.

```python
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](#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.

```python
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](#paginatedresponse) with vehicle dictionaries

### get_vehicle()

Get a single vehicle by UUID.

```python
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.

```python
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.

```python
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](#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`, `awardee_count`, `order_count` | int \| None | Denormalized rollups. |
| `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 nested `orders(...)`.
- `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 in `ShapeConfig.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 and `agency_details(*)` expansion)
- `competition_details` (top-level field and `competition_details(*)` expansion)
- `opportunity(*)` expansion (use the new top-level `solicitation_*` and `opportunity_id` fields 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()

```python
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()

```python
idv = client.get_idv("SOME_IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)
```

### list_idv_awards()

Lists child awards (contracts) under an IDV.

```python
awards = client.list_idv_awards("SOME_IDV_KEY", limit=25)
```

### list_idv_child_idvs()

Lists child IDVs under an IDV.

```python
children = client.list_idv_child_idvs("SOME_IDV_KEY", limit=25)
```

### list_idv_transactions()

```python
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).

```python
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
- `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](#paginatedresponse) with entity dictionaries

**Example:**
```python
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.

```python
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:**
```python
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.

```python
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](#paginatedresponse) with forecast dictionaries

**Example:**
```python
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.

```python
opportunities = client.list_opportunities(
    page=1,
    limit=25,
    shape=None,
    flat=False,
    flat_lists=False,
    # Filter parameters (all optional)
    active=None,
    agency=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
- `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](#paginatedresponse) with opportunity dictionaries

**Example:**
```python
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

---

## Notices

Contract award notices and modifications.

### list_notices()

List contract notices.

```python
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](#paginatedresponse) with notice dictionaries

**Example:**
```python
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.

```python
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](#paginatedresponse) with grant dictionaries

**Example:**
```python
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:**
```python
# 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.

```python
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](#paginatedresponse) with GSA eLibrary contract dictionaries

### get_gsa_elibrary_contract()

Get a single GSA eLibrary contract by UUID.

```python
contract = client.get_gsa_elibrary_contract("UUID_HERE")
```

---

## Protests

Bid protest records (GAO, COFC, etc.).

### list_protests()

List bid protests with filtering and shaping.

```python
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,
    search=None,
)
```

**Filter Parameters:**
- `source_system` - Filter by source system (e.g., `"gao"`)
- `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 case number (e.g., `"b-423274"`)
- `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
- `search` - Full-text search

**Returns:** [PaginatedResponse](#paginatedresponse) with protest dictionaries

**Example:**
```python
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).

```python
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.

---

## Business Types

Business type classifications.

### list_business_types()

List available business type codes.

```python
business_types = client.list_business_types(page=1, limit=25)
```

**Parameters:**
- `page` (int): Page number
- `limit` (int): Results per page

**Returns:** [PaginatedResponse](#paginatedresponse) with business type dictionaries

**Example:**
```python
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.

```python
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](#paginatedresponse) with NAICS dictionaries

**Example:**
```python
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.

```python
naics = client.get_naics("541511")
```

**Returns:** Dictionary with NAICS code details.

### get_naics_metrics()

Get computed metrics for a NAICS code.

```python
metrics = client.get_naics_metrics(code="541511", months=12, period_grouping="month")
```

---

## PSC

Product and Service Codes.

### list_psc()

```python
psc = client.list_psc(page=1, limit=25)
```

### get_psc()

```python
psc = client.get_psc("D302")
```

### get_psc_metrics()

```python
metrics = client.get_psc_metrics(code="D302", months=12, period_grouping="month")
```

---

## MAS SINs

GSA Multiple Award Schedule Special Item Numbers.

### list_mas_sins()

```python
sins = client.list_mas_sins(page=1, limit=25)
```

### get_mas_sin()

```python
sin = client.get_mas_sin("54151S")
```

---

## Assistance Listings (CFDA)

Catalog of Federal Domestic Assistance listings.

### list_assistance_listings()

```python
listings = client.list_assistance_listings(page=1, limit=25)
```

### get_assistance_listing()

```python
listing = client.get_assistance_listing("10.310")
```

---

## Departments

### list_departments()

```python
depts = client.list_departments(page=1, limit=25)
```

### get_department()

```python
dept = client.get_department("097")
```

---

## Business Types (by code)

### get_business_type()

Get a single business type by code.

```python
bt = client.get_business_type("A6")
```

---

## IT Dashboard

Federal IT investments from the OMB IT Dashboard.

### list_itdashboard_investments()

```python
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()

```python
investment = client.get_itdashboard_investment("023-000001234")
```

---

## Entity Sub-resources

### list_entity_contracts()

```python
contracts = client.list_entity_contracts("ABCDEF123456", limit=25)
```

### list_entity_idvs()

```python
idvs = client.list_entity_idvs("ABCDEF123456", limit=25)
```

### list_entity_otas() / list_entity_otidvs()

```python
otas = client.list_entity_otas("ABCDEF123456", limit=25)
otidvs = client.list_entity_otidvs("ABCDEF123456", limit=25)
```

### list_entity_subawards()

```python
subawards = client.list_entity_subawards("ABCDEF123456", limit=25)
```

### list_entity_lcats()

```python
lcats = client.list_entity_lcats("ABCDEF123456", limit=25)
```

### get_entity_metrics()

```python
metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")
```

---

## IDV LCATs

### list_idv_lcats()

```python
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.

```python
contracts = client.list_agency_awarding_contracts("4700", limit=25)
```

### list_agency_funding_contracts()

List contracts where the agency is the funding agency.

```python
contracts = client.list_agency_funding_contracts("4700", limit=25)
```

---

## Resolve / Validate

### resolve()

Resolve a free-text name to ranked entity or organization candidates.

```python
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.

```python
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.

```python
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()

```python
alerts = client.list_webhook_alerts(page=1, page_size=25)
```

### get_webhook_alert()

```python
alert = client.get_webhook_alert("ALERT_UUID")
```

### create_webhook_alert()

```python
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=`:

```python
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()

```python
alert = client.update_webhook_alert("ALERT_UUID", name="Updated name")
```

### delete_webhook_alert()

```python
client.delete_webhook_alert("ALERT_UUID")
```

---

## Utility

### get_version()

```python
version = client.get_version()
```

### list_api_keys()

```python
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](#webhook-alerts) section above.

> **For testing, signing, and a CLI tool**, see [`docs/WEBHOOKS.md`](webhooks.md). This section covers SDK method signatures only.

### list_webhook_event_types()

Discover supported `event_type` values.

```python
info = client.list_webhook_event_types()
print(info.event_types[0].event_type)
```

### list_webhook_endpoints()

List your webhook endpoint(s).

```python
endpoints = client.list_webhook_endpoints(page=1, limit=25)
```

### get_webhook_endpoint()

```python
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.

```python
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.

```python
result = client.test_webhook_delivery()
print(result.success, result.status_code)
```

### get_webhook_sample_payload()

Fetch Tango-shaped sample deliveries.

```python
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 checks
- `get_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):

```python
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`](webhooks.md) for usage guides; this section is the import-level reference.

### Signing (default install)

```python
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.

```python
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`

```python
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](webhooks.md#cli-reference) for the full command list.

---

## Response Objects

### PaginatedResponse

All list methods return a `PaginatedResponse` object with the following attributes:

```python
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:**
```python
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):**
```python
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.

```python
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, 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 |
| `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 |

All predefined shapes are validated at SDK release time (see [Developer Guide](https://github.com/makegov/tango-python/blob/main/docs/DEVELOPERS.md#sdk-conformance-maintainers)). For custom shapes, see the [Shaping Guide](shapes.md).

---

## Error Handling

The SDK provides specific exception types for different error scenarios.

### Exception Types

```python
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

```python
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:

```python
# ❌ 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](shapes.md) for details.

### 2. Handle Pagination Properly

Don't fetch all results at once - paginate responsibly:

```python
# ✅ 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:

```python
# ❌ 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:

```python
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:

```python
# ❌ 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](shapes.md) - Response shaping syntax, examples, and field reference
- [Developer Guide](https://github.com/makegov/tango-python/blob/main/docs/DEVELOPERS.md) - Dynamic models, predefined shapes, and SDK conformance (maintainers)
- [Quick Start](https://github.com/makegov/tango-python/blob/main/docs/quick_start.ipynb) - Interactive notebook with examples
- [GitHub Repository](https://github.com/makegov/tango-python) - Source code and examples
- [Tango API Documentation](https://tango.makegov.com/docs) - Full API documentation

---

<!-- doc: sdks/python/changelog.md -->

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2026-05-13

> First stable release. `tango-python` is now at full API parity with the
> Tango HTTP surface, the legacy subject-based webhook subscription
> mechanism has been removed in favor of filter alerts, the shape parser
> agrees byte-for-byte with the server's expand-alias handling, and the
> SDK's docs are auto-published to `docs.makegov.com/sdks/python/` via the
> composer pipeline (makegov/docs#15 / makegov/docs#16). From `1.x` on,
> we'll only do breaking changes on a major bump.
>
> Originally tracked as: API parity (PR #25), subject-based webhook
> removal (PR #27 / issue #2275), shape-validator alias support (PR #28 /
> issue #2266), and the docs-only content port (makegov/docs#16).

### Added
- `ordering` parameter on `list_forecasts`, `list_grants`, `list_subawards`, `list_gsa_elibrary_contracts`, and `list_opportunities`. Prefix with `-` for descending. Closes a parity gap with the API surface (these endpoints all accept `?ordering=` server-side).
- `create_webhook_endpoint` accepts `name=` (keyword-only) and now **requires** it. The Tango API enforces unique `(user, name)` on endpoints; omitting `name` returns a 400 server-side, so the SDK raises `TangoValidationError` client-side instead of round-tripping. (0.7.0 — never publicly released — emitted a `DeprecationWarning` instead.)
- `update_webhook_endpoint` accepts `name=` for renaming an endpoint.
- Webhook alerts (filter subscriptions): `list_webhook_alerts`, `get_webhook_alert`, `create_webhook_alert`, `update_webhook_alert`, `delete_webhook_alert` — the canonical write surface over `/api/webhooks/alerts/`. New `WebhookAlert` dataclass exported from the top-level package.
- `resolve(name, target_type, ...)` — POST `/api/resolve/` to rank entity / organization candidates from a free-text name. Returns `ResolveResult` with `ResolveCandidate` entries (both exported).
- `validate(identifier_type, value)` — POST `/api/validate/` to validate the format of a PIID, solicitation number, or UEI. Returns `ValidateResult` (exported).
- Reference data: `list_departments`, `get_department`, `list_psc`, `get_psc`, `get_psc_metrics`, `get_naics`, `get_naics_metrics`, `get_business_type`, `list_assistance_listings`, `get_assistance_listing`, `list_mas_sins`, `get_mas_sin`.
- Entity sub-resources: `list_entity_contracts`, `list_entity_idvs`, `list_entity_otas`, `list_entity_otidvs`, `list_entity_subawards`, `list_entity_lcats`, `get_entity_metrics`. All shape-aware where the underlying endpoint supports shaping.
- IDV sub-resources: `list_idv_lcats`.
- Agency sub-resources: `list_agency_awarding_contracts`, `list_agency_funding_contracts`.
- Misc: `search_opportunity_attachments(q, top_k, include_extracted_text)` for `/api/opportunities/attachment-search/`; `get_version()` for `/api/version/`; `list_api_keys()` for `/api/api-keys/`.

### Changed
- `create_webhook_alert` accepts `endpoint=` (keyword-only). Required for accounts with multiple webhook endpoints; auto-resolves for single-endpoint accounts. Closes the multi-endpoint smoke-test gap (tango#2256).
- `test_webhook_delivery` now sends the canonical `endpoint` body key instead of the deprecated `endpoint_id` alias (tango#2252). The Python kwarg name stays `endpoint_id=` for backwards compatibility; the wire payload is what changed.
- **`generate_signature(body, secret)` now returns the full wire form `"sha256=<hex>"`** instead of bare hex. Callers can assign the return value directly to the `X-Tango-Signature` header without wrapping in a format string. This is a breaking change for code that relied on the bare-hex return; pass it through `parse_signature_header()` to recover the previous form. `verify_signature` accepts both prefixed and bare-hex inputs (unchanged), so receivers continue to work either way.

### Removed
- **Subject-based webhook subscription surface** (tango#2275). Migrate to `create_webhook_alert(...)` and the alerts API.
  - Methods: `list_webhook_subscriptions`, `get_webhook_subscription`, `create_webhook_subscription`, `update_webhook_subscription`, `delete_webhook_subscription`.
  - Dataclasses: `WebhookSubscription`, `WebhookSubjectTypeDefinition`. Both are no longer exported from the top-level `tango` package — importing them raises `ImportError`.
  - Fields: `default_subject_type` removed from `WebhookEventType`; `subject_types` and `subject_type_definitions` removed from `WebhookEventTypesResponse`. The server's `/api/webhooks/event-types/` response no longer carries these.
  - CLI: the entire `tango webhooks subscriptions` Click subgroup (`list` / `get` / `create` / `delete`). Use the SDK's `client.create_webhook_alert(...)` etc. directly — there is no CLI subgroup for alerts.
- `ordering` kwarg from `list_notices` and `list_protests`. The notices and protests viewsets reject every `?ordering=` value at runtime (tango#2254); the kwarg silently sent unsupported values. Other five list methods retain `ordering`.

### Fixed
- `TangoClient._post()` and `_patch()` accept both `json_data=` (positional) and `json=` (keyword) for backward compatibility. Internal callers and docs examples that use `json=` no longer fail with `TypeError`. Passing **both** now raises `TangoValidationError` rather than silently preferring one — that ambiguity would hide caller bugs.
- `get_psc_metrics` / `get_naics_metrics` / `get_entity_metrics` docstrings — `period_grouping` values are `"month"` / `"quarter"` / `"year"` (the path-segment values the API accepts), not `"monthly"` / `"quarterly"`.
- `docs/API_REFERENCE.md#get_agency` — example uses `client.get_agency("GSA")` consistently and notes the parameter accepts CGAC / FPDS / short code / abbreviation / canonical name.
- `README.md` Quick Start — `get_agency()` returns an `Agency` dataclass, so the example uses attribute access (`agency.name`) instead of `agency['name']` which would `TypeError`.
- `scripts/smoke_api_parity.py` — `list_business_types(limit=1)` is now wrapped in the `run(...)` helper so a failure on that call records FAIL instead of aborting the smoke run.
- `tango webhooks endpoints create` CLI now accepts and requires `--name` (passed through to `create_webhook_endpoint(name=...)`). Previously the option was absent, meaning the CLI could never set a custom endpoint name and every call would 400 server-side (the server enforces `unique(user, name)`).
- `WebhookAlert.query_type` and `WebhookAlert.filters` tightened from `Optional` to non-optional (`str` and `dict[str, Any]` respectively). Legacy nullable rows were purged by the tango#2275 migration; the server model and serializer guarantee non-null values for all current data. `WebhookAlert.status` narrowed from `str` to `Literal["active", "paused"]` — the server serializer produces exactly those two values.
- **Shape validator agrees with server on `naics(...)` / `psc(...)` expansions.** The client-side `ShapeParser.validate()` previously rejected the canonical `shape=naics(code,description)` form (which the server has always accepted) and also rejected the alias `shape=naics_code(code,description)`. The parser now mirrors the server's `_EXPAND_ALIASES` (introduced in Tango PR makegov/tango#2259) and rewrites `naics_code(...)` / `psc_code(...)` to their canonical `naics(...)` / `psc(...)` form at parse time. Bare scalar leaves (`shape=naics_code` / `shape=psc_code`) are left untouched and still return the raw column value, matching the server. Schemas for `Contract`, `Forecast`, `Opportunity`, `Notice`, and `Vehicle` gained explicit `naics` / `psc` expand entries backed by the existing `CodeDescription` nested model. Fixes makegov/tango#2266.
- **`Subaward` schema matches the server's `SubawardSerializer`.** The previous `SUBAWARD_SCHEMA` declared two fields the server has never exposed (`id`, `amount`) and was missing every real field on the resource — including `piid`, `key`, `awarding_office` / `funding_office` / `place_of_performance` / `subaward_details` / `fsrs_details` / `highly_compensated_officers` / `usaspending_permalink`, and the denormalized `prime_awardee_*` / `recipient_*` lookup columns. Shape strings that referenced any real field (e.g. `shape="piid"`) would fail client-side validation with `unknown_field`, and conversely the SDK happily passed `shape="id"` / `shape="amount"` through to the server, where they were rejected. `SUBAWARD_SCHEMA` is now derived directly from `awards.serializers.subawards.SubawardSerializer` and the resource's runtime `available_fields`. The `Subaward` dataclass in `tango/models.py` was updated to match. New nested schemas `SubawardDetails`, `FsrsDetails`, `SubawardPlaceOfPerformance`, and `HighlyCompensatedOfficer` are registered so the corresponding shape expansions validate end-to-end.

### Documentation
- New `docs/ERRORS.md` — full exception hierarchy, recovery patterns, and the shape-error classes (`ShapeValidationError`, `ShapeParseError`, `TypeGenerationError`, `ModelInstantiationError`). Ported from `docs.makegov.com/sdks/python/errors.md` ahead of the docs-site auto-pull cutover (makegov/docs#15 / makegov/docs#16).
- New `docs/PAGINATION.md` — page-based vs cursor-based strategies, iteration patterns, and the `PaginatedResponse` field reference. Ported from `docs.makegov.com/sdks/python/pagination.md`.
- New `docs/CLIENT.md` — `TangoClient` constructor reference, `rate_limit_info` / `last_response_headers` properties, and retry-semantics note (the SDK has no built-in retry). Ported from `docs.makegov.com/sdks/python/client.md`.

### CI
- New `.github/workflows/docs-dispatch.yml` — fires on push to `main` when `docs/**`, `README.md`, or `CHANGELOG.md` changes and dispatches `external_updated` at `makegov/docs` so the public docs site rebuilds with the latest SDK content. Required for the makegov/docs#15 auto-pull pipeline.

## [0.6.0] - 2026-05-07

### Added
- Vehicles: new top-level fields `program_acronym`, `idv_count`, `total_obligated`, `is_synthetic_solicitation`, `latest_award_date`, `description`, `opportunity_id`.
- Vehicles: new `metrics(*)` shape expansion bundling 12 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`. Backed by a new `VehicleMetrics` schema.
- `list_vehicle_orders(uuid, ...)` for the new `/api/vehicles/{uuid}/orders/` endpoint, returning task orders under the vehicle's IDVs with two-phase pagination.
- `list_vehicles` gained 21 explicit filter parameters per API 4.3.0: `vehicle_type`, `type_of_idc`, `contract_type`, `set_aside` (multi-value via `|`), `who_can_use`, `naics_code`, `psc_code`, `program_acronym`, `agency`, `organization_id`, `total_obligated_min`/`max`, `idv_count_min`/`max`, `order_count_min`/`max`, `fiscal_year`, `award_date_after`/`before`, `last_date_to_order_after`/`before`.
- `list_vehicle_awardees` gained a `search` parameter for entity-aware full-text search across IDV fields and recipient entity details (API 4.3.0).
- `ordering` parameter on `list_vehicles` (whitelist: `vehicle_obligations`, `latest_award_date`, `total_obligated`, `award_date`, `last_date_to_order`, `fiscal_year`, `idv_count`, `order_count`) and on `list_vehicle_orders` (whitelist: `award_date`, `obligated`, `total_contract_value`). Prefix with `-` for descending.
- `ShapeConfig.VEHICLE_ORDERS_MINIMAL` default for the new orders endpoint.
- Shaping: New `organization(*)` expand on `Vehicle`, `Forecast`, `Grant`, `ITDashboardInvestment`, and `Protest` schemas — returns the canonical 7-key office payload (`organization_id`, `office_code`, `office_name`, `agency_code`, `agency_name`, `department_code`, `department_name`). Selectable as the bare leaf (`shape=...,organization`) or as a sub-selectable expansion (`shape=...,organization(office_code,...)`).
- Shaping: New `vehicle(*)` expand on `Contract` — request the parent vehicle inline from `/api/contracts/` (API 4.2.0).
- `Vehicle` and `VehicleMetrics` are now exported from the top-level `tango` package.
- `tango.webhooks` subpackage with HMAC-SHA256 signing helpers (`verify_signature`, `generate_signature`, `parse_signature_header`) that mirror the canonical Tango server scheme byte-for-byte. Importable from a default `pip install tango-python` (pure stdlib).
- `WebhookReceiver`: a stdlib-based local HTTP listener for development and integration tests. Verifies signatures, optionally forwards each delivery to a downstream URL, and records deliveries in memory for inspection. Usable as a context manager (`with WebhookReceiver(secret=...).run() as rx: ...`).
- `tango.webhooks.simulate.deliver(...)`: locally sign and POST a payload to any URL — no Tango involvement. Useful for offline iteration on receiver code.
- New `tango[webhooks]` extra (adds `click`) ships a `tango` console script covering the full webhook lifecycle for developer integrations:
  - `listen` — local receiver
  - `simulate` — sign a payload locally; with `--to`, also POST it
  - `trigger` — ask Tango to send a real test delivery
  - `fetch-sample` — print the canonical payload Tango emits for an event type
  - `list-event-types` — discover what's subscribable
  - `endpoints list|get|create|delete` — manage delivery endpoints
  - `subscriptions list|get|create|delete` — manage what events you receive
  Together these let a developer go from zero to receiving real Tango webhooks without leaving the shell or dropping into Python.

### Changed
- `ShapeConfig.VEHICLES_MINIMAL` and `VEHICLES_COMPREHENSIVE` now include the new top-level fields and the `organization` expansion. `VEHICLES_COMPREHENSIVE` defaults to `metrics(*)` and no longer pulls the deprecated `competition_details(*)` blob.

### Deprecated
- Vehicles shape fields `agency_details`, `competition_details`, and the `opportunity` expansion. The upstream API now sends a `Deprecation: true` header for these and recomputes them at request time. Explicit use in `shape=...` emits a Python `DeprecationWarning`. Sunset timeline TBD upstream.

### Notes
- Console script name `tango` may be revisited in a future release if it conflicts with sibling tooling (`tango-scripts` reuses the bare name).

### Documentation
- New `docs/WEBHOOKS.md` — comprehensive guide covering install, concepts, a zero-to-receiving quickstart, full CLI reference, and programmatic patterns for `WebhookReceiver` / `simulate.sign` / `simulate.deliver` in pytest fixtures.
- `docs/API_REFERENCE.md`: filled in `get_webhook_subscription`, replaced the hand-rolled signature-verification snippet with a pointer to `tango.webhooks.verify_signature`, and added a new "Webhook tooling (`tango.webhooks`)" section that documents every importable from the new subpackage.
- `README.md`: new "Webhook Tooling" section under Advanced Features, plus the new guide is linked from the Documentation index.

## [0.5.0] - 2026-04-08

### Added
- IT Dashboard investments: `list_itdashboard_investments`, `get_itdashboard_investment` (`/api/itdashboard/`) with shaping and filter params (`search`, `agency_code`, `agency_name`, `type_of_investment`, `updated_time_after`, `updated_time_before`, `cio_rating`, `cio_rating_max`, `performance_risk`). Tier-gated by the API: free tier gets `search`, pro adds structured filters, business+ adds CIO/performance analytics. New `ITDashboardInvestment` model and `ShapeConfig.ITDASHBOARD_INVESTMENTS_MINIMAL` / `ITDASHBOARD_INVESTMENTS_COMPREHENSIVE` defaults.

## [0.4.4] - 2026-03-25

### Added
- `parent_piid` filter parameter on `list_contracts` for filtering orders under a specific parent IDV PIID.
- `user_agent` and `extra_headers` parameters on `TangoClient` for custom request headers.
- `TangoClient.last_response_headers` property for accessing full HTTP headers from the most recent API response.

## [0.4.3] - 2026-03-21

### Added
- `TangoRateLimitError` now exposes `wait_in_seconds`, `detail`, and `limit_type` properties parsed from the API's 429 response body.
- `RateLimitInfo` dataclass for structured access to rate limit headers (`X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, and per-window daily/burst variants).
- `TangoClient.rate_limit_info` property returns rate limit info from the most recent API response.

### Changed
- `_request` now passes the full 429 response body to `TangoRateLimitError` (previously discarded), enabling callers to access `wait_in_seconds` and the specific limit type that was exceeded.

## [0.4.2] - 2026-03-04

### Added
- Protests endpoints: `list_protests`, `get_protest` with shaping and filter params (`source_system`, `outcome`, `case_type`, `agency`, `case_number`, `solicitation_number`, `protester`, `filed_date_after`, `filed_date_before`, `decision_date_after`, `decision_date_before`, `search`).

### Changed
- Lint CI workflow disabled for push/PR (runs only on manual trigger) until the private `makegov/tango` repo is accessible to the workflow.
- Updated documents to reflect changes since v0.4.0
- Entities: `ENTITIES_COMPREHENSIVE` now uses `federal_obligations(*)` expansion; the API treats federal obligations as an expansion rather than a plain shape field.
- Docs: `SHAPES.md` documents `federal_obligations(*)` as an expansion for entity shaping.
- Integration tests: `test_parsing_nested_objects_with_missing_data` accepts award office fields (`office_code`, `agency_code`, `department_code`) and empty nested objects when the API returns partial data.

### Removed
- Assistance: `list_assistance` endpoint and all related tests, docs, and references.
- IDV summaries: `get_idv_summary` and `list_idv_summary_awards` endpoints and related integration tests, cassettes, and API reference section.

## [0.4.1] - 2026-03-03

### Added
- GSA eLibrary contracts: `list_gsa_elibrary_contracts`, `get_gsa_elibrary_contract` with shaping and filter params (`contract_number`, `key`, `piid`, `schedule`, `search`, `sin`, `uei`).

### Changed
- Conformance: replaced `**kwargs`/`**filters` with explicit filter parameters on `list_contracts`, `list_idvs`, `list_entities`, `list_forecasts`, `list_grants`, `list_notices`, `list_opportunities` for full filter/shape conformance. Backward compatibility preserved for `list_contracts(filters=SearchFilters(...))`.

## [0.4.0] - 2026-02-24

### Added
- Offices, Organizations, OTAs, OTIDVs, Subawards, NAICS, and Assistance endpoints.
- Filter/shape conformance tooling and documentation.

### Changed
- CI lint workflow runs filter/shape conformance when the manifest is available.

## [0.3.0] - 2026-02-09

### Added
- Vehicles endpoints: `list_vehicles`, `get_vehicle`, and `list_vehicle_awardees` (supports shaping + flattening). (refs `makegov/tango#1328`)
- IDV endpoints: `list_idvs`, `get_idv`, `list_idv_awards`, `list_idv_child_idvs`, `list_idv_transactions`, `get_idv_summary`, `list_idv_summary_awards`. (refs `makegov/tango#1328`)
- Webhooks v2 client support: event type discovery, subscription CRUD, endpoint management, test delivery, and sample payload helpers. (refs `makegov/tango#1274`)

### Changed
- Expanded explicit schemas to support common IDV shaping expansions (award offices, officers, period of performance, etc.).
- HTTP client now supports PATCH/DELETE helpers for webhook management endpoints.

## [0.2.0] - 2025-11-16

- Entirely refactored SDK

---

<!-- doc: sdks/python/client.md -->

# Client Configuration

`TangoClient` is the entry point for every API call. This guide covers the constructor, the rate-limit and response-inspection properties, and how the client handles authentication and transport.

For per-method signatures, see [`API_REFERENCE.md`](api-reference.md). For error handling, see [`ERRORS.md`](errors.md). For shaping responses, see [`SHAPES.md`](shapes.md).

## Constructor

```python
from tango import TangoClient

client = TangoClient(
    api_key="your-api-key",           # or set TANGO_API_KEY env var
    base_url="https://tango.makegov.com",  # default
    user_agent="my-app/1.0",          # optional custom User-Agent
    extra_headers={"X-Custom": "val"},  # optional additional headers
)
```

| Parameter | Type | Default | Description |
|---|---|---|---|
| `api_key` | `str \| None` | `None` | API key. Falls back to `TANGO_API_KEY` environment variable. |
| `base_url` | `str` | `"https://tango.makegov.com"` | Base URL for the Tango API. |
| `user_agent` | `str \| None` | `None` | Custom User-Agent string appended to the default. |
| `extra_headers` | `dict[str, str] \| None` | `None` | Additional HTTP headers sent with every request. |

The client uses [httpx](https://www.python-httpx.org/) under the hood with a 30-second timeout. The API key is sent as an `X-API-KEY` header on every request.

## Properties

### `rate_limit_info`

Returns rate limit information from the most recent API response.

```python
resp = client.list_contracts(limit=5)
info = client.rate_limit_info

if info:
    print(f"Remaining: {info.remaining}/{info.limit}")
    print(f"Resets in: {info.reset}s")
    print(f"Daily remaining: {info.daily_remaining}/{info.daily_limit}")
```

The `RateLimitInfo` object exposes:

| Field | Type | Description |
|---|---|---|
| `limit` | `int \| None` | Request limit for the current window |
| `remaining` | `int \| None` | Requests remaining in the current window |
| `reset` | `int \| None` | Seconds until the window resets |
| `daily_limit` | `int \| None` | Daily request limit |
| `daily_remaining` | `int \| None` | Daily requests remaining |
| `daily_reset` | `int \| None` | Seconds until the daily limit resets |
| `burst_limit` | `int \| None` | Burst request limit |
| `burst_remaining` | `int \| None` | Burst requests remaining |
| `burst_reset` | `int \| None` | Seconds until the burst limit resets |

### `last_response_headers`

Returns the full HTTP headers from the most recent API response, as an `httpx.Headers` object.

```python
resp = client.list_contracts(limit=5)
headers = client.last_response_headers
print(headers["content-type"])
```

## Retry Semantics

The SDK does **not** include built-in retry or backoff. Each method call maps to exactly one HTTP request. If you need retry-on-429 or retry-on-transient-error behavior, wrap your calls (or catch [`TangoRateLimitError`](errors.md#tangoratelimiterror-429) and use `wait_in_seconds`).

See the [Rate Limits guide](https://docs.makegov.com/guides/patterns/rate-limits/) for recommended strategies.

---

<!-- doc: sdks/python/dynamic-models.md -->

# Tango Python SDK – Dynamic Models Guide

This document explains how the **Python dynamic shaping system** works.
It mirrors the Node.js `DYNAMIC_MODELS.md` guide for the Python SDK.

---

## Overview

Tango's dynamic modeling allows you to:

- Request _exactly the fields you want_
- Validate the shape string against Tango's schemas
- Generate a typed model descriptor at runtime
- Materialize shaped objects using correct:
  - date parsing
  - datetime parsing
  - decimal handling
  - list vs scalar logic
  - nested structure

---

## Components

### ShapeParser

Parses shape strings into a `ShapeSpec`.

```python
from tango.shapes import ShapeParser

parser = ShapeParser()
spec = parser.parse("key,piid,recipient(display_name)")
```

### SchemaRegistry

Holds the field schemas for all models.

```python
from tango.shapes import SchemaRegistry
from tango.models import Contract

registry = SchemaRegistry()
schema = registry.get_schema(Contract)
award_date_field = schema["award_date"]
# FieldSchema(name='award_date', type=date | None)
```

### TypeGenerator

Builds a dynamic `TypedDict`-backed type from `(shape_spec, base_model)`.

```python
from tango.shapes import ShapeParser, TypeGenerator
from tango.models import Contract

parser = ShapeParser()
spec = parser.parse("key,piid,recipient(display_name)")

gen = TypeGenerator()
dynamic_type = gen.generate_type(
    shape_spec=spec,
    base_model=Contract,
    type_name="ContractShaped",
)
```

### ModelFactory

Takes a dynamic type + raw API JSON and produces typed `ShapedModel` instances.
The `TangoClient` uses this pipeline automatically after fetching data.

```python
from tango import TangoClient

client = TangoClient(api_key="your-api-key")
contracts = client.list_contracts(
    shape="key,award_date,recipient(display_name)",
)

# contracts.results are ShapedModel instances materialized by ModelFactory:
# - date/datetime strings parsed to date/datetime objects
# - decimals normalized via Decimal
# - nested structures are themselves ShapedModel instances
```

---

## Example: Full Shaping Pipeline (manual)

```python
from tango.shapes import ShapeParser, TypeGenerator, ModelFactory, create_default_parser_registry
from tango.models import Contract

parser = ShapeParser()
spec = parser.parse("key,award_date,recipient(display_name)")

gen = TypeGenerator()
dynamic_type = gen.generate_type(
    shape_spec=spec,
    base_model=Contract,
    type_name="ContractShaped",
)

parsers = create_default_parser_registry()
factory = ModelFactory(gen, parsers)

shaped = factory.create_instance(
    data={
        "key": "C-1",
        "award_date": "2024-01-15",
        "recipient": {"display_name": "Acme"},
    },
    shape_spec=spec,
    base_model=Contract,
    dynamic_type=dynamic_type,
)
```

`shaped` becomes:

```python
ContractShaped(key='C-1', award_date=datetime.date(2024, 1, 15), recipient=ContractShaped_Recipient(display_name='Acme'))
```

---

## Attribute Access

`ShapedModel` is a `dict` subclass with `__getattr__` so fields are accessible
both as dictionary keys and as attributes:

```python
# Both styles work
shaped["key"]           # "C-1"
shaped.key              # "C-1"

# Nested models are also ShapedModel instances
shaped.recipient["display_name"]   # "Acme"
shaped.recipient.display_name      # "Acme"
```

Accessing a field that was not included in your shape raises a descriptive
`AttributeError` with suggestions:

```python
shaped.award_amount
# AttributeError: Field 'award_amount' not found in ContractShaped.
#   Available fields: 'key', 'award_date', 'recipient'
#   This field may not be included in your shape specification.
#   To include this field, add it to your shape parameter.
```

---

## Type Safety

The Python SDK enforces shape correctness at parse time via `ShapeParser.validate()`.
Nested structures are recursively materialized as `ShapedModel` instances, guaranteeing
the same access patterns at every depth. No static class generation happens at build time;
shapes are resolved at runtime.

---

## Caching

`TypeGenerator` caches descriptors using a thread-safe LRU cache (default: 100 entries).

`ShapeParser` also caches parse results keyed on the raw shape string.

---

## Nested Models

If a field is nested in the schema (e.g. `"recipient"` → `RecipientProfile`),
the generator recursively builds the nested descriptor, naming it
`{ParentType}_{FieldName}` (e.g. `ContractShaped_Recipient`). Each nested object
is also a `ShapedModel`, so attribute access and `repr` work uniformly at every level.

---

## Predefined Shape Constants

`ShapeConfig` provides opinionated defaults for each resource's list and detail methods.
Each `TangoClient` method applies its corresponding default automatically; pass `shape=`
to override.

```python
from tango import TangoClient, ShapeConfig

client = TangoClient(api_key="your-api-key")

# These are equivalent — list_contracts defaults to CONTRACTS_MINIMAL
contracts = client.list_contracts(limit=10)
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL, limit=10)

# Other resources
entities = client.list_entities(shape=ShapeConfig.ENTITIES_MINIMAL)
idvs = client.list_idvs(shape=ShapeConfig.IDVS_MINIMAL)
```

See [API Reference – ShapeConfig](api-reference.md#shapeconfig-predefined-shapes) for the
full table of constants.

---

<!-- doc: sdks/python/errors.md -->

# Error Handling

The SDK raises typed exceptions for HTTP errors and for shape-related failures. All exceptions are importable from `tango.exceptions` (and re-exported from the top-level `tango` package for the API errors).

For a compact reference of each class, see [`API_REFERENCE.md` § Error Handling](api-reference.md#error-handling). This guide covers the hierarchy, recovery patterns, and the shape-error classes that don't have a dedicated section there.

## Exception Hierarchy

```
TangoAPIError
├── TangoAuthError           (401 Unauthorized)
├── TangoNotFoundError       (404 Not Found)
├── TangoValidationError     (400 Bad Request)
├── TangoRateLimitError      (429 Too Many Requests)
└── ShapeError
    ├── ShapeValidationError    (invalid field names)
    ├── ShapeParseError         (invalid shape syntax)
    ├── TypeGenerationError     (dynamic type generation failure)
    └── ModelInstantiationError (model creation failure)
```

## API Errors

### TangoAPIError (base)

All API errors inherit from this class.

| Attribute | Type | Description |
|---|---|---|
| `status_code` | `int \| None` | HTTP status code |
| `response_data` | `dict` | Parsed response body (credentials redacted) |
| `message` | `str` | Human-readable error message |

```python
from tango import TangoClient
from tango.exceptions import TangoAPIError

client = TangoClient()

try:
    resp = client.list_contracts(limit=10)
except TangoAPIError as e:
    print(f"API error {e.status_code}: {e.message}")
```

### TangoAuthError (401)

Raised when the API key is missing, invalid, or expired.

```python
from tango.exceptions import TangoAuthError

try:
    client = TangoClient(api_key="invalid-key")
    client.list_contracts(limit=1)
except TangoAuthError:
    print("Check your API key")
```

### TangoNotFoundError (404)

Raised when a resource doesn't exist.

```python
from tango.exceptions import TangoNotFoundError

try:
    entity = client.get_entity("INVALID_UEI")
except TangoNotFoundError:
    print("Entity not found")
```

### TangoValidationError (400)

Raised for invalid request parameters (bad date format, unknown filter, etc.).

### TangoRateLimitError (429)

Raised when you exceed rate limits. Includes retry information.

| Attribute | Type | Description |
|---|---|---|
| `wait_in_seconds` | `int \| None` | Seconds to wait before retrying |
| `detail` | `str \| None` | Human-readable rate limit message |
| `limit_type` | `str \| None` | `"burst"` or `"daily"` |

```python
import time
from tango.exceptions import TangoRateLimitError

try:
    resp = client.list_contracts(limit=10)
except TangoRateLimitError as e:
    if e.wait_in_seconds:
        print(f"Rate limited ({e.limit_type}). Retrying in {e.wait_in_seconds}s...")
        time.sleep(e.wait_in_seconds)
        resp = client.list_contracts(limit=10)
```

> **Note:** The SDK does not include built-in retry or backoff. You are responsible for handling rate limit errors. See the [Rate Limits guide](https://docs.makegov.com/guides/patterns/rate-limits/) for strategies.

## Shape Errors

These are raised when there's a problem with the response shaping configuration, not the API itself. See [`SHAPES.md`](shapes.md) for shape syntax.

### ShapeValidationError

Raised when a shape string references field names that don't exist on the model.

```python
from tango.exceptions import ShapeValidationError

try:
    resp = client.list_contracts(shape="key,piid,nonexistent_field", limit=1)
except ShapeValidationError as e:
    print(f"Invalid shape: {e}")
    print(f"Shape string: {e.shape}")
```

### ShapeParseError

Raised when the shape string has invalid syntax (unbalanced parentheses, etc.).

| Attribute | Type | Description |
|---|---|---|
| `shape` | `str` | The invalid shape string |
| `position` | `int \| None` | Character position where parsing failed |

### TypeGenerationError

Raised when the SDK fails to generate a dynamic TypedDict for a shaped response.

### ModelInstantiationError

Raised when the SDK fails to create a model instance from API data.

| Attribute | Type | Description |
|---|---|---|
| `field_name` | `str \| None` | Field that caused the failure |
| `expected_type` | `type \| None` | Expected Python type |
| `actual_value` | `Any` | Value that couldn't be coerced |

## Catching Everything

To handle any SDK-raised error in one place, catch `TangoAPIError` and `ShapeError` (or just `Exception` at the outermost boundary):

```python
from tango.exceptions import TangoAPIError, ShapeError

try:
    resp = client.list_contracts(shape="key,piid", limit=10)
except TangoAPIError as e:
    # HTTP-layer problems (auth, rate limit, validation, etc.)
    print(f"API error {e.status_code}: {e.message}")
except ShapeError as e:
    # Shape-string or model-construction problems
    print(f"Shape error: {e}")
```

---

<!-- doc: sdks/python/index.md -->

# Tango Python SDK

[![PyPI](https://img.shields.io/pypi/v/tango-python.svg)](https://pypi.org/project/tango-python/)
[![Python Versions](https://img.shields.io/pypi/pyversions/tango-python.svg)](https://pypi.org/project/tango-python/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

A modern Python SDK for the [Tango API](https://tango.makegov.com) by MakeGov, featuring dynamic response shaping and comprehensive type hints.

## Features

- **Dynamic Response Shaping** - Request only the fields you need, reducing payload sizes by 60-80%
- **Full Type Safety** - Runtime-generated TypedDict types with accurate type hints for IDE autocomplete
- **Comprehensive API Coverage** - All major Tango API endpoints (contracts, IDVs, OTAs, entities, forecasts, opportunities, notices, grants, protests, webhooks, and more)
- **Flexible Data Access** - Dictionary-based response objects with validation
- **Modern Python** - Built for Python 3.12+ using modern async-ready patterns
- **Production-Ready** - Comprehensive test suite with VCR.py-based integration tests

## Installation

**Requirements:** Python 3.12 or higher

```bash
pip install tango-python
```

Or with uv:

```bash
uv pip install tango-python
```

## Quick Start

```python
from tango import TangoClient, ShapeConfig

# Initialize the client
client = TangoClient(api_key="your-api-key")

# List agencies
agencies = client.list_agencies()
print(f"Found {agencies.count} agencies")

# Get specific agency
agency = client.get_agency("GSA")
print(f"Agency: {agency.name}")

# Search contracts
contracts = client.list_contracts(
    limit=10
)
```

## Authentication

Most endpoints require an API key. You can obtain one from the [Tango API portal](https://tango.makegov.com).

```python
# With API key
client = TangoClient(api_key="your-api-key")

# From environment variable (TANGO_API_KEY)
client = TangoClient()
```

## Core Concepts

### Dynamic Response Shaping

Response shaping is the most powerful feature of the Tango SDK. It lets you request only the fields you need, dramatically reducing payload sizes and improving performance.

```python
from tango import TangoClient, ShapeConfig

client = TangoClient(api_key="your-api-key")

# Custom shape - only fields you need
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei),total_contract_value",
    limit=10
)

# Access fields using dictionary syntax OR as an attribute
for contract in contracts.results:
    print(f"PIID: {contract['piid']}")
    print(f"Recipient: {contract['recipient']['display_name']}")

for contract in contracts.results:
    print(f"PIID: {contract.piid}")
    print(f"Recipient: {contract.recipient.display_name}")

```

## API Methods

### Agencies

```python
# List all agencies
agencies = client.list_agencies(page=1, limit=25)

# Get specific agency by code
agency = client.get_agency("GSA")
```

### Contracts

```python
# List/search contracts with filtering
contracts = client.list_contracts(
    page=1,
    limit=25,
    # Filter parameters
    keyword="software",
    awarding_agency="4700",  # GSA agency code
    award_date_gte="2023-01-01",
    fiscal_year=2024,
    naics_code="541511"
)

# Filter by specific agency
contracts = client.list_contracts(
    awarding_agency="4700",  # GSA
    limit=50
)
```

**Available Filter Parameters:**

**Text Search:**
- `keyword` - Search contract descriptions (mapped to 'search' API param)

**Date Filters:**
- `award_date_gte`, `award_date_lte` - Award date range
- `pop_start_date_gte`, `pop_start_date_lte` - Period of performance start date range
- `pop_end_date_gte`, `pop_end_date_lte` - Period of performance end date range
- `expiring_gte`, `expiring_lte` - Contract expiration date range

**Party Filters:**
- `awarding_agency`, `funding_agency` - Agency codes
- `recipient_name`, `recipient_uei` - Vendor/recipient filters

**Classification:**
- `naics_code`, `psc_code` - Industry/product codes
- `set_aside_type` - Set-aside type

**Type Filters:**
- `fiscal_year`, `fiscal_year_gte`, `fiscal_year_lte` - Fiscal year filters
- `award_type` - Award type code

**Identifiers:**
- `piid` - Procurement Instrument Identifier
- `solicitation_identifier` - Solicitation ID

**Sorting:**
- `sort`, `order` - Sort results (e.g., `sort="award_date"`, `order="desc"`)

**Response Options:**
- `shape`, `flat`, `flat_lists` - Response shaping options

### IDVs, OTAs, OTIDVs

```python
# List IDVs (keyset pagination)
idvs = client.list_idvs(limit=25, awarding_agency="4700")

# Get single IDV with shaping
idv = client.get_idv("IDV_KEY", shape=ShapeConfig.IDVS_COMPREHENSIVE)

# OTAs and OTIDVs follow the same pattern
otas = client.list_otas(limit=25)
otidvs = client.list_otidvs(limit=25)
```

### Vehicles

```python
vehicles = client.list_vehicles(
    search="GSA schedule",
    ordering="-vehicle_obligations",
    shape=ShapeConfig.VEHICLES_MINIMAL,
)
vehicle = client.get_vehicle("UUID", shape=ShapeConfig.VEHICLES_COMPREHENSIVE)
awardees = client.list_vehicle_awardees("UUID")
orders = client.list_vehicle_orders("UUID", ordering="-obligated")
```

### Entities (Vendors/Recipients)

```python
# List entities with filters
entities = client.list_entities(search="Booz Allen", state="VA", limit=25)

# Get specific entity by UEI or CAGE code
entity = client.get_entity("ZQGGHJH74DW7")
```

### Forecasts

```python
forecasts = client.list_forecasts(agency="GSA", fiscal_year=2025, limit=25)
```

### Opportunities

```python
opportunities = client.list_opportunities(agency="DOD", active=True, limit=25)
```

### Notices

```python
notices = client.list_notices(agency="DOD", notice_type="Presolicitation", limit=25)
```

### Grants

```python
grants = client.list_grants(agency="HHS", status="F", limit=25)  # F = Forecasted
```

### Protests

```python
protests = client.list_protests(source_system="gao", outcome="Sustained", limit=25)
protest = client.get_protest("CASE_UUID")
```

### GSA eLibrary Contracts

```python
contracts = client.list_gsa_elibrary_contracts(schedule="MAS", limit=25)
contract = client.get_gsa_elibrary_contract("UUID")
```

### Reference Data

```python
# Offices, organizations, NAICS, PSC, subawards, business types
offices = client.list_offices(search="acquisitions")
organizations = client.list_organizations(level=1)
naics = client.list_naics(search="software")
get_naics = client.get_naics("541511")
psc = client.list_psc()
subawards = client.list_subawards(prime_uei="UEI123")
business_types = client.list_business_types()
mas_sins = client.list_mas_sins()
assistance = client.list_assistance_listings()
departments = client.list_departments()
```

### Resolve / Validate

```python
# Resolve a name to entity/org candidates
result = client.resolve(name="Lockheed Martin", target_type="entity")
for c in result.candidates:
    print(c.identifier, c.display_name)

# Validate an identifier
result = client.validate(identifier_type="uei", value="ABCDEF123456")
```

### IT Dashboard

```python
investments = client.list_itdashboard_investments(search="cloud", limit=25)
investment = client.get_itdashboard_investment("023-000001234")
```

### Entity Sub-resources

```python
contracts = client.list_entity_contracts("ABCDEF123456", limit=25)
idvs = client.list_entity_idvs("ABCDEF123456")
otas = client.list_entity_otas("ABCDEF123456")
metrics = client.get_entity_metrics("ABCDEF123456", months=12, period_grouping="month")
```

## Pagination

All list methods return a `PaginatedResponse` object with metadata:

```python
response = client.list_contracts(limit=25)

print(f"Total results: {response.count}")
print(f"Next page URL: {response.next}")
print(f"Previous page URL: {response.previous}")

# Iterate through results
for contract in response.results:
    print(contract['description'])

# Get next page (contracts use keyset/cursor pagination)
if response.next:
    next_response = client.list_contracts(cursor=response.cursor, limit=25)
```

## Error Handling

The SDK provides specific exception types for different error scenarios:

```python
from tango import (
    TangoClient,
    TangoAPIError,
    TangoAuthError,
    TangoNotFoundError,
    TangoRateLimitError,
    TangoValidationError
)

client = TangoClient(api_key="your-api-key")

try:
    contracts = client.list_contracts(limit=10)
except TangoAuthError:
    print("Invalid API key or authentication required")
except TangoNotFoundError:
    print("Resource not found")
except TangoValidationError as e:
    print(f"Invalid parameters: {e.message}")
    print(f"Details: {e.response_data}")
except TangoRateLimitError:
    print("Rate limit exceeded")
except TangoAPIError as e:
    print(f"API error: {e.message}")
```

## Advanced Features

### Custom Shapes

Create custom shapes to request exactly the fields you need:

```python
# Simple fields
contracts = client.list_contracts(
    shape="key,piid,description,total_contract_value"
)

# Nested relationships
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei),place_of_performance(*))"
)

# Wildcards for all fields
contracts = client.list_contracts(
    shape="key,piid,recipient(*)"
)
```

### Flattened Responses

The `flat=True` parameter is passed to the API, which returns dot-notation keys in the raw response. The SDK still wraps the result in a `ShapedModel` — access nested fields via attribute or dict syntax, not dot-notation string keys:

```python
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei)",
    flat=True
)
for contract in contracts.results:
    # Attribute access
    print(contract.recipient.display_name)
    # Dict access (nested, not flat string keys)
    print(contract['recipient']['display_name'])
```

### Webhook Tooling

The SDK ships first-class tooling for **building and testing webhook integrations against the Tango API** — including signing helpers, a local receiver, and a command-line tool covering the full lifecycle:

```bash
pip install 'tango-python[webhooks]'
```

This adds a `tango` console script with subcommands for the full webhook lifecycle:

```bash
# Discover what's available
tango webhooks list-event-types
tango webhooks fetch-sample --event-type entities.updated

# Local development
tango webhooks listen --port 8011 --secret $SECRET    # receiver
tango webhooks simulate --secret $SECRET --event-type entities.updated  # sign + print
tango webhooks simulate --secret $SECRET --event-type entities.updated \
    --to http://127.0.0.1:8011/tango/webhooks         # also POST

# Manage delivery endpoints
tango webhooks endpoints create|list|get|delete

# Force a real test delivery from Tango
tango webhooks trigger
```

The signing helpers (`verify_signature`, `generate_signature`) are pure stdlib and importable from the default install — your receiver code doesn't need the extra:

```python
from tango.webhooks import verify_signature

if not verify_signature(raw_body, secret, request.headers.get("X-Tango-Signature")):
    return 401, "invalid signature"
```

For the full guide — workflow, CLI reference, and programmatic patterns for pytest fixtures — see [`docs/WEBHOOKS.md`](webhooks.md).

### Type Hints with IDE Support

Import TypedDict types for IDE autocomplete:

```python
from tango import TangoClient, ShapeConfig
from tango.shapes import ContractMinimalShaped

client = TangoClient(api_key="your-api-key")
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL)

# Type hint enables IDE autocomplete
contract: ContractMinimalShaped = contracts.results[0]
print(contract["piid"])  # IDE knows this field exists
print(contract["recipient"]["display_name"])  # Nested fields too
```

## Development

This project uses [uv](https://docs.astral.sh/uv/) for dependency management and tooling.

### Setup

```bash
# Clone the repository
git clone https://github.com/makegov/tango-python.git
cd tango-python

# Install dependencies with uv
uv sync --all-extras

# Or install dev dependencies only
uv sync --group dev
```

### Testing

The SDK includes a comprehensive test suite with:
- **Unit tests** - Fast tests for core functionality
- **Integration tests** - Real API validation using VCR.py cassettes

```bash
# Run all tests
uv run pytest

# Run only unit tests
uv run pytest tests/ -m "not integration"

# Run only integration tests
uv run pytest tests/integration/

# Run integration tests with live API (requires TANGO_API_KEY)
export TANGO_API_KEY=your-api-key
export TANGO_USE_LIVE_API=true
uv run pytest tests/integration/

# Refresh cassettes with fresh API responses
export TANGO_API_KEY=your-api-key
export TANGO_REFRESH_CASSETTES=true
uv run pytest tests/integration/
```

See [tests/integration/README.md](https://github.com/makegov/tango-python/blob/main/tests/integration/README.md) for detailed testing documentation.

### Code Quality

```bash
# Format code
uv run ruff format tango/

# Lint code
uv run ruff check tango/

# Type checking
uv run mypy tango/

# Run all checks
uv run ruff format tango/ && uv run ruff check tango/ && uv run mypy tango/
```

### Project Structure

```
tango-python/
├── tango/                    # Main SDK package
│   ├── __init__.py          # Public API exports
│   ├── client.py            # TangoClient implementation
│   ├── models.py            # Data models and shape configs
│   ├── exceptions.py        # Exception classes
│   └── shapes/              # Dynamic model system
│       ├── __init__.py      # Shapes package exports
│       ├── parser.py        # Shape string parser
│       ├── generator.py     # TypedDict generator
│       ├── factory.py       # Instance factory
│       ├── schema.py        # Schema registry
│       ├── explicit_schemas.py  # Predefined schemas (Contract, Entity, Grant, etc.)
│       ├── models.py        # Shape specification models
│       └── types.py         # TypedDict exports
├── tests/                    # Test suite
│   ├── __init__.py
│   ├── conftest.py          # Pytest configuration
│   ├── test_client.py       # Unit tests for client
│   ├── test_models.py       # Model tests
│   ├── test_shapes.py       # Shape system tests
│   ├── cassettes/           # VCR.py HTTP cassettes
│   └── integration/         # Integration tests
│       ├── __init__.py
│       ├── README.md        # Integration test docs
│       ├── conftest.py      # Integration test fixtures
│       ├── validation.py    # Validation utilities
│       ├── test_agencies_integration.py
│       ├── test_contracts_integration.py
│       ├── test_entities_integration.py
│       ├── test_forecasts_integration.py
│       ├── test_grants_integration.py
│       ├── test_naics_integration.py
│       ├── test_notices_integration.py
│       ├── test_offices_integration.py
│       ├── test_opportunities_integration.py
│       ├── test_organizations_integration.py
│       ├── test_otas_otidvs_integration.py
│       ├── test_protests_integration.py
│       ├── test_reference_data_integration.py
│       ├── test_subawards_integration.py
│       ├── test_vehicles_idvs_integration.py
│       └── test_edge_cases_integration.py
├── docs/                     # Documentation
│   ├── API_REFERENCE.md     # Complete API reference
│   ├── DEVELOPERS.md        # Developer guide
│   ├── SHAPES.md            # Shape system guide
│   └── quick_start.ipynb    # Interactive quick start
├── scripts/                  # Utility scripts
│   ├── README.md
│   ├── check_filter_shape_conformance.py  # Filter + shape conformance (CI)
│   ├── fetch_api_schema.py
│   ├── generate_schemas_from_api.py
│   └── pr_review.py          # PR validation (lint, types, tests, conformance)
├── pyproject.toml           # Project configuration
├── uv.lock                  # Dependency lock file
├── LICENSE                  # MIT License
├── CHANGELOG.md            # Version history
└── README.md               # This file
```

## Documentation

- [Shape System Guide](shapes.md) - Comprehensive guide to response shaping
- [API Reference](api-reference.md) - Detailed API documentation
- [Developer Guide](https://github.com/makegov/tango-python/blob/main/docs/DEVELOPERS.md) - Technical documentation for developers
- [Webhooks Guide](webhooks.md) - Workflow, CLI reference, and programmatic patterns for webhook integrations
- [Quick Start Notebook](https://github.com/makegov/tango-python/blob/main/docs/quick_start.ipynb) - Interactive Jupyter notebook with examples

## Requirements

- Python 3.12 or higher
- httpx >= 0.27.0

## License

MIT License - see [LICENSE](https://github.com/makegov/tango-python/blob/main/LICENSE) for details.

## Support

For questions, issues, or feature requests:

- **Email**: [tango@makegov.com](mailto:tango@makegov.com)
- **Issues**: [GitHub Issues](https://github.com/makegov/tango-python/issues)
- **Documentation**: [https://docs.makegov.com/tango-python](https://docs.makegov.com/tango-python)

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Run lint and format: `uv run ruff format tango/ && uv run ruff check tango/`
4. Run type checking: `uv run mypy tango/`
5. Run tests: `uv run pytest`
6. (Optional) Run [filter and shape conformance](https://github.com/makegov/tango-python/blob/main/scripts/README.md#filter-and-shape-conformance) if you have the tango API manifest; CI will run it on push/PR
7. Commit your changes (`git commit -m 'Add amazing feature'`)
8. Push to the branch (`git push origin feature/amazing-feature`)
9. Open a Pull Request

For a single command that runs formatting, linting, type checking, and tests (and conformance when the manifest is present), use: `uv run python scripts/pr_review.py --mode full`

---

<!-- doc: sdks/python/pagination.md -->

# Pagination

The SDK uses two pagination strategies depending on the endpoint.

For per-method pagination parameters, see [`API_REFERENCE.md`](api-reference.md). This guide is the conceptual overview and iteration patterns.

## Page-Based Pagination

Most endpoints use traditional page-based pagination with `page` and `limit` parameters.

```python
from tango import TangoClient

client = TangoClient()

# First page
resp = client.list_entities(search="Booz Allen", limit=25)
print(f"Total: {resp.count}")
print(f"This page: {len(resp.results)}")
print(f"Next: {resp.next}")

# Next page
resp2 = client.list_entities(search="Booz Allen", limit=25, page=2)
```

### Iterating All Pages

```python
page = 1
all_results = []

while True:
    resp = client.list_entities(search="Booz Allen", limit=100, page=page)
    all_results.extend(resp.results)
    if not resp.next:
        break
    page += 1

print(f"Fetched {len(all_results)} of {resp.count} entities")
```

**Endpoints using page-based pagination:** entities, forecasts, opportunities, notices, grants, protests, subawards, vehicles, vehicle awardees, organizations, GSA eLibrary contracts, IT Dashboard investments, agencies, offices, business types, NAICS, webhook subscriptions, webhook endpoints.

## Cursor-Based Pagination

High-volume award endpoints use cursor-based (keyset) pagination for better performance on large datasets. Instead of a page number, you pass a `cursor` token from the previous response.

```python
from urllib.parse import parse_qs, urlparse

from tango import TangoClient

client = TangoClient()

# First page
resp = client.list_contracts(limit=25, sort="award_date", order="desc")
print(f"Total: {resp.count}")

# Get cursor from the next URL
if resp.next:
    qs = parse_qs(urlparse(resp.next).query)
    cursor = qs.get("cursor", [None])[0]

    # Fetch next page
    resp2 = client.list_contracts(
        limit=25,
        cursor=cursor,
        sort="award_date",
        order="desc",
    )
```

### Iterating All Pages

```python
from urllib.parse import parse_qs, urlparse

all_results = []
cursor = None

while True:
    resp = client.list_contracts(
        keyword="cloud",
        limit=100,
        cursor=cursor,
        sort="award_date",
        order="desc",
    )
    all_results.extend(resp.results)

    if not resp.next:
        break
    qs = parse_qs(urlparse(resp.next).query)
    cursor = qs.get("cursor", [None])[0]

print(f"Fetched {len(all_results)} of {resp.count} contracts")
```

**Endpoints using cursor-based pagination:** contracts, IDVs, IDV awards, IDV child IDVs, IDV transactions, OTAs, OTIDVs.

## PaginatedResponse

All list methods return a `PaginatedResponse` object:

| Field | Type | Description |
|---|---|---|
| `count` | `int` | Total number of results available |
| `next` | `str \| None` | Full URL for the next page, or `None` |
| `previous` | `str \| None` | Full URL for the previous page, or `None` |
| `results` | `list[T]` | List of results for this page |
| `cursor` | `str \| None` | Cursor token (cursor-based endpoints only) |
| `page_metadata` | `dict \| None` | Optional additional page metadata |

---

<!-- doc: sdks/python/shapes.md -->

# Response Shaping Guide

Response shaping lets you control which fields the API returns, making your requests faster and more efficient. Instead of receiving hundreds of fields you don't need, you specify exactly what you want.

**See also:** [API Reference](api-reference.md) for method parameters and [ShapeConfig (predefined shapes)](api-reference.md#shapeconfig-predefined-shapes); [Developer Guide](https://github.com/makegov/tango-python/blob/main/docs/DEVELOPERS.md) for dynamic models and maintainer conformance.

## Why Use Response Shaping?

**Performance Benefits:**
- **60-80% smaller responses** - Faster downloads, lower bandwidth costs
- **Faster API responses** - Less data to process and serialize
- **Clearer code** - Explicitly state what data you're using

**Example:** A full contract response is ~2.4 MB. With shaping, you can reduce it to 320 KB (87% smaller) while getting all the data you actually need.

## Quick Start

```python
from tango import TangoClient

client = TangoClient(api_key="your-api-key")

# Without shaping - returns ALL fields (slower, larger)
contracts = client.list_contracts(limit=10)

# With shaping - returns only what you specify (faster, smaller)
contracts = client.list_contracts(
    limit=10,
    shape="key,piid,recipient(display_name),total_contract_value"
)

# Or use a predefined shape constant
from tango import ShapeConfig
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL, limit=10)

# Access the data
for contract in contracts.results:
    print(f"{contract['piid']}: {contract['recipient']['display_name']}")
```

## Predefined shapes (ShapeConfig)

Instead of writing shape strings by hand, you can use the SDK’s predefined constants. Each list/get method has a default minimal shape when you omit `shape`; you can also pass a constant explicitly.

```python
from tango import TangoClient, ShapeConfig

client = TangoClient()

# These are equivalent (list_contracts defaults to CONTRACTS_MINIMAL)
contracts = client.list_contracts(limit=10)
contracts = client.list_contracts(shape=ShapeConfig.CONTRACTS_MINIMAL, limit=10)

# Other resources
entities = client.list_entities(shape=ShapeConfig.ENTITIES_MINIMAL)
idvs = client.list_idvs(shape=ShapeConfig.IDVS_MINIMAL)
grants = client.list_grants(shape=ShapeConfig.GRANTS_MINIMAL)
```

**Available constants:** Contracts (`CONTRACTS_MINIMAL`), Entities (`ENTITIES_MINIMAL`, `ENTITIES_COMPREHENSIVE`), Forecasts, Opportunities, Notices, Grants, IDVs, Vehicles (`VEHICLES_MINIMAL`, `VEHICLES_COMPREHENSIVE`, `VEHICLE_AWARDEES_MINIMAL`, `VEHICLE_ORDERS_MINIMAL`), Organizations, OTAs, OTIDVs, Subawards. See [API Reference – ShapeConfig](api-reference.md#shapeconfig-predefined-shapes) for the full table and which method uses which constant.

> **Vehicles `metrics(*)` expansion:** The vehicles surface bundles 12 computed metrics under a single `metrics(*)` expansion (e.g. `award_concentration_hhi`, `competed_rate`, `top_recipient_share`). It is included in `VEHICLES_COMPREHENSIVE` by default. The `agency_details`, `competition_details`, and `opportunity` shape entries are deprecated and emit `DeprecationWarning` if requested explicitly.

## Basic Shaping

### Simple Fields

List the fields you want, separated by commas:

```python
# Just the basics
contracts = client.list_contracts(
    shape="key,piid,description,award_date",
    limit=10
)

# Access the fields
for contract in contracts.results:
    print(f"{contract['piid']}: {contract['description']}")
    print(f"Date: {contract['award_date']}")
```

### Nested Fields

Use parentheses to select fields from nested objects:

```python
# Get recipient information
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei,cage_code)",
    limit=10
)

for contract in contracts.results:
    recipient = contract['recipient']
    print(f"Recipient: {recipient['display_name']}")
    print(f"UEI: {recipient['uei']}")
```

### Multiple Levels

You can nest as deeply as needed. Contract location information is on `place_of_performance` (not nested inside `recipient`):

```python
# Get place of performance details
contracts = client.list_contracts(
    shape="key,recipient(display_name),place_of_performance(city_name,state_code,zip_code)",
    limit=10
)

for contract in contracts.results:
    location = contract['place_of_performance']
    print(f"{location['city_name']}, {location['state_code']} {location['zip_code']}")
```

## Common Use Cases

### 1. Quick Lists and Dropdowns

When you just need basic info for a list or dropdown:

```python
# Minimal data for a dropdown
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name)",
    limit=100
)

# Build a dropdown
for contract in contracts.results:
    print(f"{contract['piid']} - {contract['recipient']['display_name']}")
```

### 2. Data Analysis

When analyzing contracts, focus on the metrics:

```python
# Get financial and timing data
contracts = client.list_contracts(
    shape="key,piid,award_date,fiscal_year,total_contract_value,obligated",
    awarding_agency="GSA",
    limit=1000
)

# Analyze
total_value = sum(c.get('total_contract_value', 0) or 0 for c in contracts.results)
print(f"Total contract value: ${total_value:,.2f}")
```

### 3. Geographic Analysis

When you need location data:

```python
# Get place of performance details
# Note: use city_name (not city); congressional_district is not a shape field
contracts = client.list_contracts(
    shape="key,piid,place_of_performance(city_name,state_code)",
    limit=100
)

# Group by state
from collections import Counter
states = Counter(c['place_of_performance']['state_code'] for c in contracts.results if c.get('place_of_performance') and c['place_of_performance'].get('state_code'))
print(f"Top states: {states.most_common(5)}")
```

### 4. Vendor Research

When researching vendors and recipients:

```python
# Get detailed vendor information
# Note: entity physical_address uses 'city' and 'state_or_province_code'
# business_types is a list of dicts with 'code' and 'description'
entities = client.list_entities(
    shape="uei,legal_business_name,dba_name,business_types,physical_address(city,state_or_province_code)",
    limit=50
)

for entity in entities.results:
    print(f"{entity['legal_business_name']}")
    print(f"Business Types: {', '.join(bt['code'] for bt in entity.get('business_types', []))}")
    if entity.get('physical_address'):
        addr = entity['physical_address']
        print(f"Location: {addr.get('city')}, {addr.get('state_or_province_code')}")
```

### 5. Agency Research

When analyzing agency activity:

```python
# Get agency and classification details
# Note: use awarding_office (not awarding_agency) for agency name/code sub-fields
contracts = client.list_contracts(
    shape="key,awarding_office(agency_name,agency_code),naics(code,description),psc(code,description),total_contract_value",
    fiscal_year=2024,
    limit=500
)

# Analyze by agency
from collections import defaultdict
by_agency = defaultdict(float)
for contract in contracts.results:
    if contract.get('awarding_office'):
        agency = contract['awarding_office']['agency_name']
        value = float(contract.get('total_contract_value', 0) or 0)
        by_agency[agency] += value

# Top agencies by value
top_agencies = sorted(by_agency.items(), key=lambda x: x[1], reverse=True)[:10]
for agency, value in top_agencies:
    print(f"{agency}: ${value:,.2f}")
```

## Advanced Techniques

### Using Wildcards

Get all fields from a nested object with `*`:

```python
# Get all recipient fields
contracts = client.list_contracts(
    shape="key,piid,recipient(*)",
    limit=10
)

# All recipient fields are now available
for contract in contracts.results:
    recipient = contract['recipient']
    # Has all fields: display_name, uei, cage_code, legal_business_name, etc.
```

### Flattening Responses

Convert nested structures to flat keys with dot notation:

```python
# Flatten nested objects
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name,uei)",
    flat=True,
    limit=10
)

# Fields are now flattened
for contract in contracts.results:
    print(contract['piid'])
    print(contract['recipient.display_name'])
    print(contract['recipient.uei'])
```

## Best Practices

### 1. Start Minimal, Add as Needed

Start with the minimum fields you need, then add more:

```python
# Start here
shape = "key,piid,recipient(display_name)"

# Add more as you need them
shape = "key,piid,recipient(display_name,uei),total_contract_value"

# Keep adding
shape = "key,piid,recipient(display_name,uei),total_contract_value,award_date,fiscal_year"
```

### 2. Use Shapes for Large Queries

The bigger the query, the more important shaping becomes:

```python
# Small query - shaping optional
contracts = client.list_contracts(limit=5)

# Medium query - shaping recommended
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name),total_contract_value",
    limit=100
)

# Large query - shaping highly recommended
contracts = client.list_contracts(
    shape="key,piid,recipient(display_name),total_contract_value",
    limit=1000
)
```

### 3. Reuse Common Shapes

Define shapes as constants for reuse:

```python
# Define your common shapes
SHAPES = {
    'list': "key,piid,recipient(display_name),total_contract_value",
    'detail': "key,piid,description,recipient(*),awarding_office(*),total_contract_value,award_date",
    'analysis': "key,fiscal_year,total_contract_value,obligated,award_date",
    'geographic': "key,piid,place_of_performance(city_name,state_code)"
}

# Use them
contracts = client.list_contracts(shape=SHAPES['list'], limit=100)
contract_detail = client.list_contracts(shape=SHAPES['detail'], limit=1)
```

### 4. Document Your Shapes

When using custom shapes in production, document why you chose those fields:

```python
# Dashboard summary shape
# - key: Contract identifier
# - piid: Display to users
# - recipient.display_name: Main label
# - total_contract_value: Summary metric
DASHBOARD_SHAPE = "key,piid,recipient(display_name),total_contract_value"

contracts = client.list_contracts(shape=DASHBOARD_SHAPE, limit=50)
```

## Field Reference

### Common Contract Fields

**Identifiers:**
- `key` - Unique contract identifier
- `piid` - Procurement Instrument Identifier
- `award_id` - Award identifier

**Basic Info:**
- `description` - Contract description
- `award_date` - Date awarded
- `fiscal_year` - Fiscal year

**Financial:**
- `total_contract_value` - Total contract value
- `obligated` - Total obligated amount (note: field is `obligated`, not `total_obligated`)
- `award_amount` - Initial award amount

**Parties:**
- `recipient(...)` - The vendor/recipient
- `awarding_agency(...)` - The agency awarding the contract
- `funding_agency(...)` - The agency funding the contract

**Classification:**
- `naics(code,description)` - Industry classification
- `psc(code,description)` - Product/Service code

**Location:**
- `place_of_performance(...)` - Where work is performed
- `recipient_location(...)` - Vendor location

### Common Entity Fields

**Basic:**
- `uei` - Unique Entity Identifier
- `cage_code` - Commercial and Government Entity code
- `legal_business_name` - Official business name
- `display_name` - Display name
- `dba_name` - Doing Business As name

**Classification:**
- `business_types` - Array of business type codes
- `primary_naics` - Primary NAICS code
- `naics_codes` - All NAICS codes

**Contact:**
- `email_address` - Email
- `entity_url` - Website
- `physical_address(...)` - Physical address
- `mailing_address(...)` - Mailing address

**Financial:**
- `federal_obligations(*)` - Expansion for total/active federal obligations

## Performance Comparison

Here's what you can expect when using shapes:

| Use Case | Fields Returned | Payload Size | vs. Full Response |
|----------|----------------|--------------|-------------------|
| Full response | ~200 fields | 2.4 MB | Baseline |
| Dropdown | 3-4 fields | 180 KB | 92% smaller |
| List view | 6-8 fields | 320 KB | 87% smaller |
| Detail view | 20-30 fields | 780 KB | 68% smaller |
| Analysis | 8-10 fields | 250 KB | 90% smaller |

## Troubleshooting

### Fields Not Appearing

If fields aren't showing up in the response:

1. **Check your shape syntax** - Make sure parentheses match and commas are correct
2. **Field doesn't exist** - The field might not exist for that record
3. **Typo** - Double-check field names (they're case-sensitive)

```python
# ❌ Wrong
shape = "key,piid recipient(display_name)"  # Missing comma

# ✅ Correct
shape = "key,piid,recipient(display_name)"
```

### Unexpected Structure

If the structure isn't what you expected:

```python
# Shape specifies nested structure
contracts = client.list_contracts(
    shape="key,recipient(display_name,uei)",
    limit=1
)

# Access nested fields
contract = contracts.results[0]
print(contract['recipient']['display_name'])  # Nested access
print(contract['recipient']['uei'])

# Use .get() for safety
display_name = contract.get('recipient', {}).get('display_name', 'Unknown')
```

## Examples by Resource Type

### Contracts

```python
# Minimal for lists
"key,piid,recipient(display_name),total_contract_value"

# For analysis (use 'obligated', not 'total_obligated')
"key,fiscal_year,award_date,total_contract_value,obligated,naics(code)"

# For geographic analysis (use city_name; congressional_district not available)
"key,piid,place_of_performance(city_name,state_code)"

# Full detail (use awarding_office for agency breakdown)
"key,piid,description,recipient(*),awarding_office(*),total_contract_value,award_date,naics(*),psc(*)"
```

### Entities

```python
# Minimal for lookups
"uei,legal_business_name,cage_code,business_types"

# For vendor research (entity physical_address uses state_or_province_code, not state_code)
"uei,legal_business_name,dba_name,business_types,physical_address(city,state_or_province_code),primary_naics"

# Full profile
"uei,legal_business_name,dba_name,cage_code,business_types,physical_address(*),email_address,entity_url"
```

### Forecasts

```python
# Minimal
"id,title,anticipated_award_date,fiscal_year"

# With classification
"id,title,anticipated_award_date,fiscal_year,naics_code,status"
```

### Opportunities

```python
# Minimal
"opportunity_id,title,solicitation_number,response_deadline"

# With details
"opportunity_id,title,solicitation_number,description,response_deadline,active,naics_code,psc_code"
```

## Next Steps

- **Try the examples** - Copy and paste these examples to get started
- **Experiment** - Start with minimal shapes and add fields as needed
- **Profile your queries** - Use network tools to see the size difference
- **Define patterns** - Create reusable shapes for your common queries

For more help, see:
- [API Reference](api-reference.md) - Method parameters, [ShapeConfig table](api-reference.md#shapeconfig-predefined-shapes), and field context
- [Developer Guide](https://github.com/makegov/tango-python/blob/main/docs/DEVELOPERS.md) - Dynamic models, predefined shapes in depth, and SDK conformance (for maintainers)
- [Quick Start Guide](https://github.com/makegov/tango-python/blob/main/docs/quick_start.ipynb) - Interactive examples

---

<!-- doc: sdks/python/webhooks.md -->

# Webhooks Guide

This guide covers everything `tango-python` provides for **building, testing, and operating webhook integrations against the Tango API**: signing helpers, a local receiver, a command-line tool, and management commands for the underlying endpoints and alerts.

If you only need the SDK method signatures, see [`API_REFERENCE.md` § Webhooks](api-reference.md#webhooks). For the API-level contract (signing scheme, event taxonomy, retry behavior), see the [Tango Webhooks Partner Guide](https://docs.makegov.com/webhooks-user-guide/).

---

## Contents

- [Install](#install)
- [Concepts in 60 seconds](#concepts-in-60-seconds)
- [Quickstart: zero to receiving](#quickstart-zero-to-receiving)
- [CLI reference](#cli-reference)
  - [`tango webhooks listen`](#tango-webhooks-listen)
  - [`tango webhooks simulate`](#tango-webhooks-simulate)
  - [`tango webhooks trigger`](#tango-webhooks-trigger)
  - [`tango webhooks fetch-sample`](#tango-webhooks-fetch-sample)
  - [`tango webhooks list-event-types`](#tango-webhooks-list-event-types)
  - [`tango webhooks endpoints`](#tango-webhooks-endpoints)
- [Programmatic use](#programmatic-use)
  - [Signature verification in your handler](#signature-verification-in-your-handler)
  - [`WebhookReceiver` in pytest fixtures](#webhookreceiver-in-pytest-fixtures)
  - [`simulate.sign` and `simulate.deliver`](#simulatesign-and-simulatedeliver)
- [Common workflows](#common-workflows)
- [Troubleshooting](#troubleshooting)

---

## Install

The signing helpers ship with the default install:

```bash
pip install tango-python
```

The CLI (`tango webhooks ...`) and the local receiver class are gated behind an optional extra:

```bash
pip install 'tango-python[webhooks]'
```

This adds [`click`](https://palletsprojects.com/projects/click) as a runtime dependency. The base SDK install stays unchanged.

After installing the extra, the `tango` console script is on your `PATH`:

```bash
tango webhooks --help
```

---

## Concepts in 60 seconds

Tango webhooks have two pieces of state:

| Concept | What it is | Tango term |
|---|---|---|
| **Endpoint** | The URL Tango POSTs to, plus a generated signing secret | `WebhookEndpoint` |
| **Alert** | A saved-search filter saying *which matches* to deliver | `WebhookAlert` (filter subscription) |
| **Delivery** | A single signed POST Tango makes when a matching event fires | (the request itself) |

A typical setup:

1. **Create an endpoint** (`POST /api/webhooks/endpoints/`) with the public URL of your handler. Tango returns a `secret` — save it; it's used to sign every delivery.
2. **Create one or more alerts** (`POST /api/webhooks/alerts/`) describing the saved-search matches you want delivered (e.g. opportunities matching `naics=541511`). Each alert maps to one of five `alerts.*.match` event types.
3. **Tango POSTs** to your endpoint when matching events fire. The body is JSON; the header `X-Tango-Signature: sha256=<hex>` is the HMAC-SHA256 of the raw body bytes keyed by your endpoint's secret.
4. **Your handler verifies the signature**, parses the body, and acts on it.

---

## Quickstart: zero to receiving

Assumes you have a `TANGO_API_KEY` and want to receive entity-update webhooks for a specific UEI.

### 1. See what you can subscribe to

```bash
export TANGO_API_KEY=...
tango webhooks list-event-types
# alerts.opportunity.match   New/updated opportunity matched a saved alert
# alerts.contract.match      New/updated contract matched a saved alert
# alerts.entity.match        Entity matched a saved alert
# alerts.grant.match         Grant matched a saved alert
# alerts.forecast.match      Forecast matched a saved alert
```

### 2. See what a payload looks like

```bash
tango webhooks fetch-sample --event-type alerts.entity.match
```

Prints the canonical JSON shape Tango will deliver. No POST, no signature — just the body.

### 3. Run a local receiver

In one shell, start a listener with a chosen secret:

```bash
export TANGO_WEBHOOK_SECRET=dev_secret
tango webhooks listen --port 8011
```

In another shell, drive it with the canonical sample, signed locally:

```bash
tango webhooks simulate \
  --secret $TANGO_WEBHOOK_SECRET \
  --event-type alerts.entity.match \
  --to http://127.0.0.1:8011/tango/webhooks
```

The listener should print a `verified` delivery with the alerts.entity.match body. You now have a feedback loop: edit your handler, re-run `simulate`, see the result.

### 4. Wire up the real Tango → your handler path

When you're ready for end-to-end testing against Tango itself, expose your local listener via a tunnel (`ngrok http 8011`, `cloudflared tunnel`, etc.) and register that public URL with Tango, then create an alert via the SDK:

```bash
# Use the public URL the tunnel gave you.
tango webhooks endpoints create --name dev --url https://<your-tunnel>.ngrok.io/tango/webhooks
# Save the `secret` from the response — that's what your handler uses to verify.
```

```python
# Create an alert (filter subscription) via the SDK
from tango import TangoClient

client = TangoClient()
client.create_webhook_alert(
    name="watch UEI ABC123",
    query_type="entity",
    filters={"uei": "ABC123"},
)
```

To force a real test delivery from Tango (without waiting for an actual event):

```bash
tango webhooks trigger
```

You should see a `verified` delivery in your local listener with the signature value generated by Tango — not by `simulate`.

---

## CLI reference

All commands live under `tango webhooks`. Options that talk to Tango's API (`--api-key`, `--base-url`) read `TANGO_API_KEY` and `TANGO_BASE_URL` if not passed explicitly.

### `tango webhooks listen`

Run a local HTTP receiver. Verifies signatures, optionally forwards each delivery downstream, prints a one-line summary plus the JSON body for each delivery.

```bash
tango webhooks listen \
  --port 8011 \
  --host 127.0.0.1 \
  --path /tango/webhooks \
  --secret $TANGO_WEBHOOK_SECRET \
  --forward-to http://127.0.0.1:4242/wh
```

Options:

- `--port` (default `8011`)
- `--host` (default `127.0.0.1` — loopback only, by design)
- `--path` (default `/tango/webhooks`)
- `--secret` / `TANGO_WEBHOOK_SECRET` — if empty, signatures are not verified (the listener accepts everything; useful for inspecting payloads when you don't have the right secret yet)
- `--forward-to URL` — mirror each delivery to a downstream URL, preserving body bytes and the `X-Tango-Signature` header
- `--require-signature / --allow-unsigned` — override the default policy (default: require when `--secret` is set)

Press Ctrl+C to stop. Rejected (signature-mismatch) deliveries are still printed with the label `UNVERIFIED` so you can debug what arrived.

### `tango webhooks simulate`

Sign a payload locally with the same scheme Tango uses, then either print the signed request or POST it to a receiver.

**Without `--to`** — just print the headers + body a real Tango delivery would have:

```bash
tango webhooks simulate --secret dev_secret --event-type alerts.entity.match
```

Output includes `delivered: false`, the headers (`Content-Type`, `X-Tango-Signature`), and the JSON payload.

**With `--to`** — also POST the signed body to a receiver:

```bash
tango webhooks simulate \
  --secret dev_secret \
  --event-type alerts.entity.match \
  --to http://127.0.0.1:8011/tango/webhooks
```

Output includes `delivered: true`, the receiver's status code, and the receiver's response body.

Three sources for the payload (mutually exclusive):

| Flag | Source | When to use |
|---|---|---|
| `--event-type X` | Fetches the canonical sample for `X` from Tango | You want a realistic body without setting up an alert |
| `--payload-file PATH` | Reads a JSON file | You're testing a specific shape (regression, edge case) |
| *(neither)* | A built-in placeholder envelope | Smoke-testing the wiring |

### `tango webhooks trigger`

Ask Tango to send a real test delivery to your configured endpoint. Wraps `POST /api/webhooks/endpoints/test-delivery/`. Requires `--api-key`.

```bash
tango webhooks trigger
tango webhooks trigger --endpoint-id <uuid>
```

Output is JSON: `success`, `status_code` (the HTTP code Tango got from your endpoint), `response_time_ms`, `endpoint_url`, `message`, `error`. Exit code is non-zero if delivery failed.

### `tango webhooks fetch-sample`

Print the canonical sample payload for one event type, or the full mapping if `--event-type` is omitted. Wraps `GET /api/webhooks/endpoints/sample-payload/`. Read-only.

```bash
tango webhooks fetch-sample --event-type alerts.entity.match
tango webhooks fetch-sample  # all event types
```

### `tango webhooks list-event-types`

List every event type Tango supports with a one-line description.

```bash
tango webhooks list-event-types
```

### `tango webhooks endpoints`

Manage **where Tango delivers**.

```bash
tango webhooks endpoints list [--page N] [--limit N]
tango webhooks endpoints get  ENDPOINT_ID
tango webhooks endpoints create --name NAME --url URL [--inactive]
tango webhooks endpoints delete ENDPOINT_ID [--yes]
```

`create` returns the generated `secret` once — save it. `--name` is required and must be unique per user (uniqueness is enforced on `(user, name)`, so you can have multiple endpoints with distinct names). `delete` prompts for confirmation; `--yes` skips. `--inactive` registers the endpoint disabled (no deliveries until you re-enable it).

### Managing alerts

Alerts (filter subscriptions) are the canonical way to control what Tango delivers. There is no CLI subgroup for them — use the SDK directly:

```python
from tango import TangoClient

client = TangoClient()

client.list_webhook_alerts()
client.get_webhook_alert("ALERT_UUID")
client.create_webhook_alert(
    name="watch UEI ABC123",
    query_type="entity",
    filters={"uei": "ABC123"},
)
client.update_webhook_alert("ALERT_UUID", name="Renamed")
client.delete_webhook_alert("ALERT_UUID")
```

For multi-endpoint accounts, pass `endpoint=<uuid>` to `create_webhook_alert` to pin which endpoint the alert delivers to.

---

## Programmatic use

The CLI is built on top of small importable pieces. You can use them directly in your own code — most usefully, in tests.

### Signature verification in your handler

`verify_signature` is pure stdlib (no SDK dependencies, no `click`). Call it on the raw request body, not on a re-serialized parsed body — the HMAC is computed over exact bytes.

```python
from tango.webhooks import verify_signature

# In your Flask / FastAPI / Django / Starlette / whatever handler:
def handle_webhook(request):
    body = request.body  # raw bytes
    signature = request.headers.get("X-Tango-Signature")
    if not verify_signature(body, secret=ENDPOINT_SECRET, signature_header=signature):
        return 401, {"error": "invalid_signature"}
    payload = json.loads(body)
    # ... act on the events ...
    return 200, {"ok": True}
```

`verify_signature` returns `False` for missing/empty/malformed headers — it never raises. Comparison is constant-time (`hmac.compare_digest`).

### `WebhookReceiver` in pytest fixtures

The CLI's `listen` command is a thin wrapper around `tango.webhooks.WebhookReceiver`, which is a context-manager-friendly local HTTP server. Use it directly in tests to verify your code emits webhook calls correctly, or to drive your handler with realistic deliveries.

```python
from tango import WebhookReceiver  # WebhookReceiver is exported from the top-level tango package
from tango.webhooks import generate_signature, verify_signature
import httpx

def test_my_handler_processes_entity_update():
    with WebhookReceiver(secret="test_secret").run() as rx:
        # Trigger whatever in your code-under-test should send a webhook
        # (e.g. a publisher, or in this case a manual POST).
        body = b'{"events":[{"event_type":"alerts.entity.match","alert_id":"ABC"}]}'
        sig = generate_signature(body, "test_secret")
        # generate_signature returns the wire form ("sha256=<hex>") — assign
        # directly to the header without wrapping.
        httpx.post(rx.url, content=body, headers={"X-Tango-Signature": sig})

        assert len(rx.deliveries) == 1
        assert rx.deliveries[0].verified
        assert rx.deliveries[0].body_json["events"][0]["uei"] == "ABC"
```

`WebhookReceiver` options:

- `secret: str = ""` — shared secret. Empty means "don't verify."
- `path: str = "/tango/webhooks"` — URL path to accept.
- `host: str = "127.0.0.1"` / `port: int = 0` — bind address. `0` lets the OS pick a free port.
- `forward_to: str | None = None` — mirror each delivery to a downstream URL.
- `max_history: int = 256` — cap on the in-memory `deliveries` deque.
- `on_delivery: Callable[[Delivery], None] | None = None` — fires for every recorded delivery, including signature-failed ones.
- `require_signature: bool | None = None` — override default (require iff `secret` is set).

Each `Delivery` has: `received_at`, `path`, `signature_header`, `body_bytes`, `body_json`, `verified`, `remote_addr`, `forward_status`, `forward_error`.

### `simulate.sign` and `simulate.deliver`

`simulate.sign` is the offline counterpart — it produces the exact wire form a Tango delivery would have, so you can drive your handler from a unit test:

```python
from tango.webhooks import sign

signed = sign({"events": [{"event_type": "alerts.entity.match"}]}, secret="s")
assert signed.headers["X-Tango-Signature"].startswith("sha256=")

# Use `signed.body` as the raw bytes and `signed.headers` directly:
response = my_app.test_client().post(
    "/webhooks", data=signed.body, headers=signed.headers
)
```

`simulate.deliver` does the same but POSTs the result to a URL — `WebhookReceiver` works as a target:

```python
from tango.webhooks import simulate
from tango import WebhookReceiver

with WebhookReceiver(secret="s").run() as rx:
    result = simulate.deliver(target_url=rx.url, payload={...}, secret="s")
    assert result.status_code == 200
```

---

## Common workflows

### "I'm starting fresh — set me up to receive entity updates"

```bash
export TANGO_API_KEY=...
# 1. Confirm event types
tango webhooks list-event-types
# 2. Stand up a tunnel so Tango can reach you
ngrok http 8011 &
# 3. Register your endpoint
tango webhooks endpoints create --name dev --url https://<id>.ngrok.io/tango/webhooks
# (save the `secret` from the response into TANGO_WEBHOOK_SECRET)
# 4. Create an alert via the SDK
python -c '
from tango import TangoClient
TangoClient().create_webhook_alert(
    name="entities", query_type="entity", filters={"uei": "<UEI>"}
)'
# 5. Run the listener pointed at your downstream handler
tango webhooks listen --port 8011 --secret $TANGO_WEBHOOK_SECRET \
  --forward-to http://localhost:4242/wh
# 6. Force a test delivery
tango webhooks trigger
```

### "I want to develop my handler offline"

You don't need a Tango account or any tunnel:

```bash
# Run the handler however you normally would on, e.g., :4242
tango webhooks listen --port 8011 --secret dev --forward-to http://127.0.0.1:4242/wh

# In another shell, drive it. Use Tango-shaped bodies if you have an API key:
tango webhooks simulate --secret dev --event-type alerts.entity.match \
  --to http://127.0.0.1:8011/tango/webhooks

# Or use a custom shape from a file (no API key required):
tango webhooks simulate --secret dev --payload-file ./fixtures/edge.json \
  --to http://127.0.0.1:8011/tango/webhooks
```

### "I want to test my handler in CI, no network"

In pytest, use `WebhookReceiver` and `simulate.deliver` together — both are pure-Python and don't talk to Tango:

```python
from tango.webhooks import simulate
from tango import WebhookReceiver

def test_handler_round_trip():
    with WebhookReceiver(secret="s").run() as rx:
        result = simulate.deliver(
            target_url=rx.url,
            payload={"events": [{"event_type": "alerts.entity.match", "alert_id": "X"}]},
            secret="s",
        )
        assert result.status_code == 200
        assert rx.deliveries[0].verified
```

### "I need to inspect what bytes Tango actually sends"

```bash
tango webhooks simulate --secret $TANGO_WEBHOOK_SECRET --event-type alerts.entity.match
# Prints { "delivered": false, "headers": {...}, "sent_payload": {...} }
```

This is the shape your handler will receive — including the exact `X-Tango-Signature` value it should verify.

---

## Troubleshooting

**Signature always fails.** Verify on raw bytes, not on a re-serialized parsed body. The HMAC is over exact bytes; reformatting whitespace or reordering keys breaks it. Most web frameworks expose the raw body separately from a parsed JSON shortcut — use the raw one.

**`tango: command not found`.** Install the extra: `pip install 'tango-python[webhooks]'`. The console script is registered only when `click` is available.

**Listener prints `WARNING: no --secret provided`.** You started `listen` without `--secret` and without `TANGO_WEBHOOK_SECRET` set. Every delivery will be accepted with `verified=False`. Useful for inspecting payloads when you don't have the secret yet, but unsafe in any shared environment.

**`fetch-sample` returns 401.** Set `TANGO_API_KEY` (or pass `--api-key`). `fetch-sample` reads from Tango's API.

**`endpoints create` returns 400 or "endpoint already exists".** Endpoint names are unique per user — if you've already created one with that `--name`, either pick a different name or use `endpoints list` to find the existing one and reuse it.

**`simulate --event-type X` fails with HTTP 4xx.** Tango doesn't recognize the event type. Run `list-event-types` to see the current list.

**`trigger` returns `success: false`.** Tango reached your endpoint but got a non-2xx response. Check `endpoint_url` and `response_body` in the output, then look at your handler's logs.

---

<!-- doc: webhooks/index.md -->

# Tango – Webhooks Partner Guide

Welcome! This guide walks you through enabling and consuming outbound webhooks from Tango so your application can react to fresh federal-spending data without polling our API.

Jump to:

- [Examples](#examples)

---

## 1. What you get

Tango pushes a JSON payload to your server whenever new data matches a saved filter — a **filter alert**. Today this includes:

- Awards / contracts
- Opportunities
- Entities
- Grants
- Forecasts

- Near–real-time: you'll be notified minutes after Tango's ingestion cycle finishes.
- Lightweight: one payload lists the IDs that changed and a small summary per match — pull full details from the existing REST API as needed.
- **Filter-based**: define a saved query (the same filters you'd pass to the API) and receive an event whenever new or updated rows match.
- **Reliability features**: automatic circuit breaker pattern for endpoint health management and intelligent retry strategies.

> Note (Awards): For correctness, awards webhooks are published **after award bundling/materialization** so a webhook will not arrive before the corresponding award is queryable via the API.

### 1.1 When events are published ("data is ready")

We intentionally publish events only once the underlying data is queryable via the API.

- **Contracts** (`alerts.contract.match`): published once new contracts are queryable via the API.
- **Opportunities** (`alerts.opportunity.match`): published after each opportunities refresh.
- **Entities** (`alerts.entity.match`): published after each entities refresh (including DSBS updates).
- **Grants** (`alerts.grant.match`): published after each grants refresh.
- **Forecasts** (`alerts.forecast.match`): published after each forecasts refresh.

### 1.2 Filter subscriptions (alerts)

Filter subscriptions let you create persistent monitoring rules based on **query filters**. For example, "alert me when new opportunities matching NAICS 541512 appear at HHS" or "notify me of new contracts awarded to small businesses in Virginia."

You define a **query type** (the resource to monitor) and a set of **filters** (the same query parameters you would use on the corresponding API endpoint). Tango periodically evaluates your filters against new and updated data; when matches are found, Tango delivers a webhook to your endpoint with `alerts.<query_type>.match` events.

#### Available query types

| Query type | Resource monitored | Filters from |
| --- | --- | --- |
| `opportunity` | Contract opportunities | `/api/opportunities/` filters |
| `contract` | Contracts | `/api/contracts/` filters |
| `idv` | IDVs | `/api/idvs/` filters |
| `ota` | OTAs | `/api/otas/` filters |
| `otidv` | OTIDVs | `/api/otidvs/` filters |
| `entity` | Entities | `/api/entities/` filters |
| `grant` | Grants | `/api/grants/` filters |
| `forecast` | Forecasts | `/api/forecasts/` filters |

#### Alert event types

When a filter subscription matches, the delivered event uses the `alerts.*` namespace. There are five:

- `alerts.opportunity.match`
- `alerts.contract.match` — also emitted for `idv`, `ota`, and `otidv` query types
- `alerts.entity.match`
- `alerts.grant.match`
- `alerts.forecast.match`

If you subscribe to `idv`, `ota`, or `otidv`, matches arrive as `alerts.contract.match` events; inspect `query_type` on the event to distinguish.

#### Frequency options

| Frequency | Description | Tier requirement |
| --- | --- | --- |
| `realtime` | Evaluated after each ingestion cycle | All tiers |
| `daily` | Evaluated once per day | All tiers |
| `weekly` | Evaluated once per week | All tiers |
| `custom` | Custom cron expression (5-field) | Pro+ (Micro and above) |

#### Tier limits

Filter subscriptions are available to **all tiers**, including Free:

| Tier | Max filter subscriptions |
| --- | --- |
| Free | 1 |
| Micro | 3 |
| Small | 5 |
| Medium | 10 |
| Large | 25 |
| Enterprise | custom |

#### Convenience API: `/api/webhooks/alerts/`

The alerts API provides a simpler shape for creating and managing filter subscriptions:

**Create an alert:**

```txt
POST /webhooks/alerts/
Content-Type: application/json
```

```json
{
  "name": "HHS IT services opportunities",
  "query_type": "opportunity",
  "filters": {
    "agency": "7500",
    "naics": "541512"
  },
  "frequency": "daily",
  "endpoint": "<your-endpoint-id>"
}
```

Returns **201 Created** with the alert details. If a subscription with the same `query_type` + `filters` already exists, returns **200 OK** with the existing alert (dedup).

!!! info "When `endpoint` is required"
    The `endpoint` field is **required** if your account has more than one webhook endpoint configured (the server returns 400 otherwise so deliveries can't go to the wrong receiver). Accounts with a single endpoint may omit it — Tango routes to that endpoint automatically. List your endpoints with `GET /api/webhooks/endpoints/` or `client.list_webhook_endpoints()` in the SDKs.

> **Tip — multi-value filters:** Use `|` (pipe) to match **any** of several values. The same syntax works on every filter that accepts multiple values (`naics`, `psc`, agencies, set-asides, etc.) — including the resource-ID filters described in [Following specific resources](#following-specific-resources) below.
>
> ```json
> "filters": {"naics": "541511|541512"}
> ```
>
> One known exception: `/api/contracts/`'s `uei` filter is single-value only. To watch multiple vendors, create one alert per vendor — see the [vendor watchlist recipe](recipes/vendor-watchlist.md).

#### Following specific resources

Every resource you can `GET` by its detail endpoint can also be **followed** by that same identifier in an alert. Pass the detail-endpoint ID as a filter and you'll be notified when that resource (or any of a set of them) is created or modified.

| `query_type`                              | Filter key       | Example value                                            |
| ----------------------------------------- | ---------------- | -------------------------------------------------------- |
| `opportunity`                             | `opportunity_id` | `"<uuid-1>\|<uuid-2>\|<uuid-3>"`                         |
| `contract` / `idv` / `ota` / `otidv`      | `key`            | `"<key-1>\|<key-2>\|<key-3>"`                            |
| `grant`                                   | `grant_id`       | `"123\|456\|789"`                                        |
| `forecast`                                | `id`             | `"12\|34\|56"`                                           |

The filter name matches the detail-endpoint lookup field for each resource — `GET /api/contracts/{key}/` → `"key": "..."`, `GET /api/opportunities/{opportunity_id}/` → `"opportunity_id": "..."`, and so on. Single-value or pipe-OR (`id1|id2|id3`) both work; up to **500 IDs per filter**.

```json
{
  "name": "Follow my watchlist",
  "query_type": "contract",
  "filters": {"key": "<key-1>|<key-2>|<key-3>"},
  "frequency": "realtime"
}
```

**Prefer one alert with many IDs over many alerts with one ID each.** A grouped alert is one cron evaluation, one indexed query, and one delivery per tick — and it counts as a single subscription against your tier limit (see [Tier limits](#tier-limits)). One-alert-per-thing burns through your subscription budget quickly and produces more inbound traffic to your endpoint.

> **Note — `entity`:** the `uei` filter on `entity` alerts is still single-value (see the multi-value tip above). To watch several vendors, create one entity alert per UEI, or use the [vendor watchlist recipe](recipes/vendor-watchlist.md).

**List alerts:**

```txt
GET /webhooks/alerts/
```

Response:

```json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "alert_id": "e4c4...",
      "name": "HHS IT services opportunities",
      "query_type": "opportunity",
      "filters": {"agency": "7500", "naics": "541512"},
      "frequency": "daily",
      "cron_expression": null,
      "status": "active",
      "created_at": "2026-03-14T12:00:00Z",
      "last_checked_at": null,
      "match_count": 0
    }
  ]
}
```

**Update an alert:**

```txt
PATCH /webhooks/alerts/{alert_id}/
```

You can update `name`, `frequency`, `cron_expression`, and `is_active`. Filters and query type cannot be changed after creation (delete and recreate instead).

**Delete an alert:**

```txt
DELETE /webhooks/alerts/{alert_id}/
```

#### Dedup behavior

If you create a filter subscription with the same `query_type` and filter parameters as an existing one (for the same endpoint), the API returns the existing subscription instead of creating a duplicate. The dedup is based on a SHA-256 hash of the canonical filter representation.

#### Current status

The filter subscription API is fully functional for creating, listing, updating, and deleting alerts. A background task evaluates filter subscriptions on their configured schedule (realtime, daily, weekly, or custom cron) and delivers matches automatically.

---

## 2. Access requirements

Filter subscriptions are available to **all tiers**, including Free, with per-tier caps on how many you can create simultaneously (see the table in §1.2 — Tier limits). No tier upgrade is required to start using webhooks.

---

## 3. On-boarding checklist

1. **Provide a callback URL** — a publicly reachable `https://` endpoint that accepts HTTP POSTs. Recommended path: `/tango/webhooks`.
2. **Receive your shared secret** — Tango generates a 32-byte hex secret (64 hex chars) and shares it out-of-band. You'll use this to verify signatures.
3. **Test your endpoint** — use the test delivery endpoint (§5.5) to verify connectivity.
4. **Create one or more filter alerts** — define a `query_type` + `filters` per saved query you want to monitor. See §1.2 (Convenience API) and the [recipes](recipes/index.md).

> **Note**  We create the initial Webhook Endpoint record for you. All subsequent management of subscriptions/alerts is done via the Subscription API below.

---

## 4. Security & authenticity

Every POST from Tango includes an HMAC-SHA-256 signature:

```txt
X-Tango-Signature: sha256=<hex digest>
```

The digest is computed over the raw request body using your secret. Verify it like so (Python snippet):

```python
import hmac, hashlib, os, flask, json

SECRET = os.environ["TANGO_WEBHOOK_SECRET"]
app = flask.Flask(__name__)

@app.post("/tango/webhooks")
def recv():
    body = flask.request.get_data()
    sig  = flask.request.headers.get("X-Tango-Signature", "")[7:]  # strip "sha256="

    if not hmac.compare_digest(
        hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest(), sig):
        return "Invalid signature", 401

    payload = json.loads(body)
    # ... handle events ...
    return "ok", 200
```

If you respond with any **2xx** status, Tango marks the delivery as successful.
Non-2xx responses and time-outs are retried (at-least-once delivery). Current retry limits are error-aware:

- 4xx: up to 2 attempts total
- 5xx: up to 5 attempts total
- network errors/timeouts: up to 7 attempts total

---

## 5. Subscription API

Base URL: `https://tango.makegov.com/api/webhooks/`
Auth: **API Key** – send `X-API-KEY: <your-key>` (or an OAuth2 bearer token via `Authorization: Bearer <token>`)

### 5.0 Discover supported event types

Tango exposes a discovery endpoint so clients can validate configurations without hard-coding event types:

```txt
GET /webhooks/event-types/
```

Response:

```json
{
  "event_types": [
    {
      "event_type": "alerts.contract.match",
      "description": "A new or modified contract matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.opportunity.match",
      "description": "A new or modified opportunity matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.entity.match",
      "description": "A new or modified entity matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.grant.match",
      "description": "A new or modified grant matched a saved filter alert.",
      "schema_version": 1
    },
    {
      "event_type": "alerts.forecast.match",
      "description": "A new or modified forecast matched a saved filter alert.",
      "schema_version": 1
    }
  ]
}
```

> Note: Only some event types may be actively emitted at any given time. If you create an alert for a query type that has no matching new data, you simply won't receive events until matches appear.

### 5.1 List current alerts / filter subscriptions

```txt
GET /webhooks/alerts/
```

Response

```json
{
  "count": 1,
  "next": null,
  "previous": null,
  "results": [
    {
      "alert_id": "e4c4…",
      "name": "Track DOD IT services > $1M",
      "query_type": "contract",
      "filters": {"funding_agency": "DOD", "naics": "541512", "obligated_gte": "1000000"},
      "frequency": "realtime",
      "cron_expression": null,
      "status": "active",
      "created_at": "2024-06-01T12:00:00Z",
      "last_checked_at": "2024-06-01T18:00:00Z",
      "match_count": 14
    }
  ]
}
```

### 5.3 Update an existing alert

```txt
PATCH /webhooks/alerts/{alert_id}/
```

Updatable fields: `name`, `frequency`, `cron_expression`, `is_active`. `filters` and `query_type` are immutable — delete and recreate to change them.

### 5.4 Delete an alert

```txt
DELETE /webhooks/alerts/{alert_id}/
```

If you delete every alert, you will stop receiving webhooks until you create at least one new one.

> Note: You must maintain at least one active alert to receive webhooks.

### 5.5 Test webhook delivery

```txt
POST /webhooks/endpoints/test-delivery/
```

Sends a test webhook to your endpoint to verify connectivity. Returns detailed information about the delivery attempt.

Response (success):

```json
{
  "success": true,
  "status_code": 200,
  "response_time_ms": 145,
  "message": "Test delivery successful! Response time: 145ms. Your webhook endpoint is configured correctly."
}
```

Response (failure):

```json
{
  "success": false,
  "status_code": 500,
  "message": "Test delivery failed with status 500. Please check your endpoint implementation and ensure it returns 2xx status codes.",
  "response_body": "<first 1000 chars of response>"
}
```

### 5.6 Get sample payload

```txt
GET /webhooks/endpoints/sample-payload/?event_type=alerts.contract.match
```

Returns a sample webhook payload for testing your handler implementation.

Notes:

- `GET /webhooks/endpoints/sample-payload/` (no params) returns samples for all supported event types.
- `GET /webhooks/endpoints/sample-payload/?event_type=<event_type>` returns a single sample.

Response:

```json
{
  "event_type": "alerts.contract.match",
  "sample_delivery": {
    "timestamp": "2026-05-13T00:36:46.157804",
    "events": [
      {
        "event_type": "alerts.contract.match",
        "created_at": "2026-05-13T00:36:46.157779",
        "subscription_id": "11111111-2222-3333-4444-555566667777",
        "new_ids": ["MATCH_001", "MATCH_002"],
        "modified_ids": []
      }
    ]
  },
  "signature_header": "X-Tango-Signature: sha256=<hmac_sha256_signature>",
  "note": "The signature is generated using HMAC-SHA256 with your endpoint's secret key."
}
```

!!! warning "Preview vs production payload shape"
    The `/webhooks/endpoints/sample-payload/` endpoint emits a **simplified preview** — no `delivery_id`, no `matches.new[]` summary objects, just `new_ids` / `modified_ids`. **Real deliveries from the dispatcher carry the richer shape documented in §6**, including `delivery_id`, the `matches.new` / `matches.modified` arrays with per-record summary fields, and the echoed `filters` / `query_type`. Build your receiver against §6, not against this sample. (Server-side reconciliation of the two shapes is tracked separately.)

---

## 6. Payload format

A single delivery looks like:

```json
{
  "timestamp": "2026-05-11T18:20:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.contract.match",
      "created_at": "2026-05-11T18:20:12.482Z",
      "alert_id": "e4c4...-...-...",
      "query_type": "contract",
      "filters": {"funding_agency": "DOD", "naics": "541512", "obligated_gte": "1000000"},
      "matches": {
        "new_count": 2,
        "modified_count": 0,
        "new": [
          {"id": "CONT_AWD_...", "piid": "W15QKN24C1234", "obligated": 1450000, "recipient_uei": "ABC123..."},
          {"id": "CONT_AWD_...", "piid": "FA8773-24-C-...", "obligated": 2300000, "recipient_uei": "DEF456..."}
        ],
        "modified": [],
        "new_ids": ["CONT_AWD_...", "CONT_AWD_..."],
        "modified_ids": []
      },
      "checked_at": "2026-05-11T18:20:12.000Z"
    }
  ]
}
```

The `new_ids` / `modified_ids` arrays are deprecated but still sent; prefer `matches.new` / `matches.modified` which carry summary objects per match.

### 6.1 Per-event fields

| Field | Type | Notes |
| --- | --- | --- |
| `event_type` | string | One of `alerts.{contract,opportunity,entity,grant,forecast}.match` |
| `alert_id` | UUID | Stable id of the alert that produced this match |
| `query_type` | string | `contract` / `opportunity` / `entity` / `grant` / `forecast` |
| `filters` | object | Echo of the alert's saved filters |
| `matches.new` | array | Newly-matched records (summary objects, not full API rows) |
| `matches.modified` | array | Existing records whose key fields changed |
| `matches.new_count` / `matches.modified_count` | int | Counts of the above |
| `checked_at` | ISO8601 | When the evaluator scanned for matches |

Match summary objects always include the resource's primary id (`id` for contracts, `opportunity_id` for opportunities, `uei` for entities, `grant_id` for grants, `forecast_id` for forecasts) plus a small set of high-signal fields. Pull the full record from the corresponding `/api/<resource>/` endpoint when you need more.

Key facts:

- **Batched** — events are grouped per endpoint per dispatch run.
- **At-least-once** — retries can cause duplicates; your handler must be idempotent. If you need a de-dupe key, prefer the top-level `delivery_id` (UUID, stable across retries of the same dispatch).
- **Server-side filtered** — only events matching your alerts are sent.

## Examples

These are copy/paste alert payloads for common use cases.

For full how-tos with receiver code and pitfalls, see the [recipes](recipes/index.md).

### Track DOD IT-services contracts over $1M

```json
{
  "name": "DOD IT services > $1M",
  "query_type": "contract",
  "filters": {
    "funding_agency": "DOD",
    "naics": "541512",
    "obligated_gte": "1000000"
  },
  "frequency": "realtime"
}
```

### Watch HHS opportunities in NAICS 541512

```json
{
  "name": "HHS opportunities — IT consulting (541512)",
  "query_type": "opportunity",
  "filters": {
    "agency": "7500",
    "naics": "541512"
  },
  "frequency": "realtime"
}
```

### Track entity registrations changing in your pipeline

```json
{
  "name": "Pipeline entities — registration changes",
  "query_type": "entity",
  "filters": {
    "uei": "ABC123XYZ456"
  },
  "frequency": "daily"
}
```

(Entities currently take a single `uei` per alert — for multi-entity tracking, create one alert per UEI.)

### Watch cybersecurity grants by CFDA

```json
{
  "name": "DHS cybersecurity grants",
  "query_type": "grant",
  "filters": {
    "agency": "DHS",
    "cfda_number": "97.067"
  },
  "frequency": "daily"
}
```

### Monitor upcoming Air Force IT acquisitions in the forecast

```json
{
  "name": "Air Force forecasts — IT (541512 + nearby)",
  "query_type": "forecast",
  "filters": {
    "agency": "AF",
    "naics_starts_with": "5415"
  },
  "frequency": "weekly"
}
```

---

## 8. Best practices on your side

1. **Make your handler idempotent** — Tango guarantees at-least-once delivery, so your endpoint may receive the same event more than once. Deduplicate on `delivery_id` (see [Delivery guarantee](#delivery-guarantee)).
2. **Return 200 quickly** — enqueue the work in your own job queue and respond; do not block processing.
3. **Test your endpoint first** — use the test delivery endpoint to verify connectivity before going live.
4. **Harden the endpoint** — HTTPS only, accept `POST` only, max-payload 256 KB.
5. **Store the last successful timestamp** — helps spot missed deliveries.
6. **Use exponential back-off when pulling details** — Tango's public API has rate limits; stagger follow-up fetches if you receive a large batch.
7. **Tighten your filters** — broad filters generate large match batches; narrow `filters` to what you actually act on.
8. **Monitor endpoint health** — Tango uses circuit breaker patterns to protect both systems from cascading failures.

---

## 9. Reliability features

### Delivery guarantee

Tango webhooks provide **at-least-once delivery**. This means:

- Every event that matches one of your alerts **will** be delivered to your endpoint (assuming it is reachable).
- In rare cases (e.g., a transient failure between sending the HTTP request and recording its success), the **same event may be delivered more than once**.

Each webhook request includes a stable `delivery_id` (UUID) that uniquely identifies the dispatch. Use this as an idempotency key on your side to safely deduplicate:

```python
# Example: skip already-processed deliveries
delivery_id = payload["delivery_id"]
if already_processed(delivery_id):
    return HttpResponse(status=200)

process(payload)
mark_processed(delivery_id)
```

**Why not exactly-once?** Guaranteeing exactly-once delivery over HTTP is not possible without cooperation from the consumer. Tango prevents duplicate *internal* delivery records via database constraints, but the HTTP POST itself can be retried if acknowledgment is lost. Designing your webhook handler to be idempotent ensures correct behavior regardless of delivery count.

### Circuit Breaker Pattern

Tango implements automatic circuit breaker protection for webhook endpoints:

- **Automatic failure detection** — After repeated failures (currently: 5 consecutive failures), the circuit opens
- **Cool-down** — While open, deliveries are skipped for a short period (currently: 5 minutes)
- **Recovery testing** — After cool-down, Tango allows a single probe delivery (half-open)
- **Automatic recovery** — Upon success, normal operation resumes; upon failure, the circuit re-opens (currently: 10 minute cool-down)
- **Resource protection** — Prevents wasting resources on consistently failing endpoints

This ensures both Tango and your systems remain stable even during outages.

### Retry Strategy

Failed webhooks are retried with:

- **Error-aware retry limits** — Retry caps differ by error type:
  - Client errors (4xx): up to 2 attempts total
  - Server errors (5xx): up to 5 attempts total
  - Network errors/timeouts: up to 7 attempts total

---

## 10. Troubleshooting

| Symptom | Most likely cause | Next steps |
| --- | --- | --- |
| Receive 401 from Tango on the alerts API | Missing/invalid API key | Ensure `X-API-KEY: <your-key>` header (or `Authorization: Bearer <token>` for OAuth2) |
| Webhook payloads stop arriving | Circuit breaker activated due to failures | Fix endpoint issues; circuit will auto-recover after cool-down |
| Test delivery fails | Endpoint connectivity issues | Check HTTPS cert, firewall rules, response time |
| Signature mismatch | Using wrong secret or modified body | Re-sync secret; ensure you hash the **raw bytes** exactly |
| Not receiving expected events | Filter doesn't match upstream data | Run the same filter against `/api/<resource>/` first — if it returns nothing there, no events will fire |
| Receiving too many events | Filter too broad | Tighten `filters`; lower `frequency` (e.g. `daily` instead of `realtime`); split into multiple narrower alerts |

Need help?  Email [support@makegov.com](mailto:support@makegov.com) with your endpoint URL & the approximate timestamp of the last delivery you saw.

---

Happy shipping! 🚀

---

<!-- doc: webhooks/recipes/awards-by-naics.md -->

# Awards by NAICS — stream new contracts in your industry codes

Get a webhook whenever new contracts land in one or more NAICS codes you care about. Optionally narrow by agency, set-aside, or dollar floor.

## The 1-line answer

Create a single filter alert on `query_type=contract` with the NAICS codes you want — use `|` to OR multiple codes into one alert.

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "IT consulting NAICS — 541511 / 541512 / 541519",
    "query_type": "contract",
    "filters": { "naics": "541511|541512|541519" },
    "frequency": "realtime"
  }'
```

That's it. One alert covers any number of NAICS codes.

## Step 1 — Verify the filter against the contracts API

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/contracts/?naics=541511|541512|541519&ordering=-award_date&limit=5"
```

If this returns recent contracts in your codes, you're good.

## Step 2 — Create the alert

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "IT consulting NAICS",
        "query_type": "contract",
        "filters": {
          "naics": "541511|541512|541519",
          "obligated_gte": "100000"
        },
        "frequency": "realtime"
      }'
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    alert = client.create_webhook_alert(
        name="IT consulting NAICS",
        query_type="contract",
        filters={
            "naics": "541511|541512|541519",
            "obligated_gte": "100000",
        },
        frequency="realtime",
    )
    print(alert.alert_id, alert.status)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const alert = await client.createWebhookAlert({
      name: "IT consulting NAICS",
      query_type: "contract",
      filters: {
        naics: "541511|541512|541519",
        obligated_gte: "100000",
      },
      frequency: "realtime",
    });
    console.log(alert.alert_id, alert.status);
    ```

## Step 3 — Refine with optional filters

Stack any of these on top of `naics` to narrow the alert. All filters are AND-combined.

| Filter | Notes |
| --- | --- |
| `funding_agency` / `awarding_agency` | Vector-backed agency match — `"DOD"`, `"Navy"`, `"NAVSEA"` all work. Pick the role you actually care about. |
| `psc` | Six-digit PSC code; same `\|` multi-value syntax as `naics`. |
| `set_aside` | `SBA`, `8A`, `WOSB`, `SDVOSB`, etc. Multi-value via `\|`. |
| `obligated_gte` / `obligated_lte` | Dollar thresholds. |
| `recipient_state` / `place_of_performance` | Geographic filters. |
| `ordering` | Doesn't apply to alerts (the evaluator processes new matches as they arrive). |

Example with all the dials turned:

```json
{
  "name": "DOD IT services > $1M, small business",
  "query_type": "contract",
  "filters": {
    "funding_agency": "DOD",
    "naics": "541511|541512|541519",
    "obligated_gte": "1000000",
    "set_aside": "SBA|8A|WOSB|SDVOSB"
  },
  "frequency": "realtime"
}
```

## Step 4 — Receive `alerts.contract.match` events

```json
{
  "timestamp": "2026-05-12T18:20:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.contract.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "contract",
      "filters": {
        "naics": "541511|541512|541519",
        "obligated_gte": "100000"
      },
      "matches": {
        "new_count": 3,
        "modified_count": 0,
        "new": [
          {"id": "CONT_AWD_...", "piid": "W15QKN24C1234", "obligated": 1450000, "recipient_uei": "ABC123..."},
          {"id": "CONT_AWD_...", "piid": "FA8773-24-C-...", "obligated": 230000, "recipient_uei": "DEF456..."},
          {"id": "CONT_AWD_...", "piid": "N00033-24-D-...", "obligated": 5400000, "recipient_uei": "GHI789..."}
        ],
        "modified": []
      },
      "checked_at": "2026-05-12T18:20:12.000Z"
    }
  ]
}
```

For the full receiver implementation (signature verification, idempotency, fast 2xx), see [Stream contract awards in real time](../../guides/how-to/stream-awards.md#step-2-build-the-receiver).

## Limitations

- **One alert per saved query.** If you want different downstream routing per NAICS, create separate alerts and dispatch on `alert_id` in your receiver.
- **Tier caps apply.** Free 1 / Micro 3 / Small 5 / Medium 10 / Large 25 simultaneous alerts.
- **Realtime is "after each ingestion cycle"** — FPDS ingest runs roughly twice daily. Realtime delivers within minutes of each refresh, not within minutes of upstream FPDS publication.
- **`naics_starts_with` is not a contracts filter.** Contracts only support exact `naics`. If you need a NAICS prefix, list the codes explicitly with `|`.

## Related

- [Webhooks user guide](../../index.md) — protocol reference
- [Stream contract awards in real time](../../guides/how-to/stream-awards.md#step-2-build-the-receiver) — full how-to including receiver code
- [Contracts API reference](../../api-reference/contracts.md) — full filter list

---

<!-- doc: webhooks/recipes/forecast-pipeline.md -->

# Forecast pipeline — monitor upcoming agency opportunities

Get a webhook when new procurement forecasts post that match your industry / agency / fiscal-year window. Forecasts are agency-published lists of opportunities they *plan* to release — useful for capture planning months ahead of the actual solicitation.

Tango ingests forecasts from HHS, DHS, GSA, COMMERCE, DOE, TREASURY, DOI, DOL, DOT, VA, and NRC. See the [Forecasts data dictionary](../../data-dictionary/forecasts.md#forecasts-we-track) for the current list.

## The 1-line answer

Create a filter alert on `query_type=forecast`. Filters are the same query parameters you'd pass to `/api/forecasts/`.

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "VA IT services forecast — FY26",
    "query_type": "forecast",
    "filters": {
      "agency": "VA",
      "naics_starts_with": "5415",
      "fiscal_year": "2026"
    },
    "frequency": "weekly"
  }'
```

That's it. Tango re-evaluates weekly and POSTs `alerts.forecast.match` events when new forecasts match.

## Step 1 — Verify the filter against the forecasts API

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/forecasts/?agency=VA&naics_starts_with=5415&fiscal_year=2026&ordering=anticipated_award_date&limit=5"
```

If this returns the forecasts you expect, the alert will fire when new ones land.

## Step 2 — Create the alert

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "VA + DHS — IT consulting forecasts FY26",
        "query_type": "forecast",
        "filters": {
          "agency": "VA",
          "naics_starts_with": "5415",
          "fiscal_year": "2026",
          "award_date_after": "2026-04-01",
          "award_date_before": "2026-12-31"
        },
        "frequency": "weekly"
      }'
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    alert = client.create_webhook_alert(
        name="VA — IT consulting forecasts FY26",
        query_type="forecast",
        filters={
            "agency": "VA",
            "naics_starts_with": "5415",       # NAICS prefix — matches 541511, 541512, 541519, etc.
            "fiscal_year": "2026",
            "award_date_after": "2026-04-01",
            "award_date_before": "2026-12-31",
        },
        frequency="weekly",
    )
    print(alert.alert_id, alert.status)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const alert = await client.createWebhookAlert({
      name: "VA — IT consulting forecasts FY26",
      query_type: "forecast",
      filters: {
        agency: "VA",
        naics_starts_with: "5415",          // NAICS prefix
        fiscal_year: "2026",
        award_date_after: "2026-04-01",
        award_date_before: "2026-12-31",
      },
      frequency: "weekly",
    });
    console.log(alert.alert_id, alert.status);
    ```

## Step 3 — Available forecast filters

These are the keys accepted by `/api/forecasts/` and therefore by `query_type=forecast` alerts.

| Filter | Notes |
| --- | --- |
| `agency` | Agency acronym (e.g. `VA`, `DHS`, `HHS`, `GSA`). |
| `source_system` | Source system identifier — useful when an agency publishes forecasts via multiple feeds. |
| `naics_code` | Exact NAICS match. |
| `naics_starts_with` | NAICS prefix match (e.g. `5415` for any 5415xx). |
| `fiscal_year` | Exact fiscal year. |
| `fiscal_year_gte` / `fiscal_year_lte` | Fiscal year range. |
| `award_date_after` / `award_date_before` | Anticipated award date range (`YYYY-MM-DD`). |
| `modified_after` / `modified_before` | Date the forecast was last modified in Tango. |
| `status` | Forecast status (case-insensitive partial match). |
| `title` | Substring match on title. |
| `search` | Full-text search over title/description (vector-backed). |

!!! note "Filter names matter"
    NAICS filtering uses **`naics_code`** (exact) or **`naics_starts_with`** (prefix) — there is no plain `naics` filter on `/api/forecasts/`. There is also **no `set_aside` filter** on forecasts — that's a SAM/FPDS-side concept not present in agency forecast publications.

For full filter docs, see the [Forecasts API reference](../../api-reference/forecasts.md#filtering).

## Step 4 — Receive `alerts.forecast.match` events

```json
{
  "timestamp": "2026-05-12T08:00:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.forecast.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "forecast",
      "filters": {
        "agency": "VA",
        "naics_starts_with": "5415",
        "fiscal_year": "2026"
      },
      "matches": {
        "new_count": 2,
        "modified_count": 0,
        "new": [
          {"id": "...", "external_id": "VA-2026-PRE-08712", "title": "VistA Modernization Phase IV", "agency": "VA", "naics_code": "541512", "anticipated_award_date": "2026-09-30"},
          {"id": "...", "external_id": "VA-2026-PRE-08813", "title": "Health Data Lake Refresh", "agency": "VA", "naics_code": "541511", "anticipated_award_date": "2026-08-15"}
        ],
        "modified": []
      },
      "checked_at": "2026-05-12T08:00:12.000Z"
    }
  ]
}
```

Pull full forecast details from `/api/forecasts/{id}/` (or via `client.get_forecast(id)` / `client.getForecast(id)`) when you need more than the summary fields.

## Limitations

- **No `set_aside` filter on forecasts.** Forecasts don't expose a set-aside dimension; that's an FPDS/SAM concept that only shows up downstream when the actual opportunity / award posts.
- **Forecast cadences are slow.** Most agencies refresh forecasts monthly or quarterly. `weekly` is typically the right cadence; `realtime` works but won't actually fire more often than the underlying refresh.
- **Coverage varies by agency.** Only the 11 source agencies listed above are ingested today. Filtering on `agency=DOJ` returns nothing because Tango doesn't ingest DOJ forecasts.
- **Tier caps apply.** Free 1 / Micro 3 / Small 5 / Medium 10 / Large 25 simultaneous alerts.

## Related

- [Webhooks user guide](../../index.md) — protocol reference
- [Forecasts API reference](../../api-reference/forecasts.md) — full filter list
- [Forecasts data dictionary](../../data-dictionary/forecasts.md) — field-by-field reference, including which agencies are tracked
- [Recipes index](index.md)

---

<!-- doc: webhooks/recipes/grants-by-agency.md -->

# Grants by agency — track new grant opportunities

Get a webhook when new grant opportunities post at agencies you care about, optionally narrowed by CFDA number, applicant type, or response window.

## The 1-line answer

Create a filter alert on `query_type=grant`. Filters are the same query parameters you'd pass to `/api/grants/`.

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "DHS cybersecurity grants",
    "query_type": "grant",
    "filters": {
      "agency": "DHS",
      "cfda_number": "97.067"
    },
    "frequency": "daily"
  }'
```

That's it. Tango re-evaluates daily and POSTs `alerts.grant.match` events when new grants match.

## Step 1 — Verify the filter against the grants API

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/grants/?agency=DHS&cfda_number=97.067&ordering=-posted_date&limit=5"
```

If this returns the grants you expect, the alert will fire when new ones land.

## Step 2 — Create the alert

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "DHS cybersecurity grants — open in next 60d",
        "query_type": "grant",
        "filters": {
          "agency": "DHS",
          "cfda_number": "97.067",
          "applicant_types": "00",
          "response_date_after": "2026-05-12",
          "response_date_before": "2026-07-12"
        },
        "frequency": "daily"
      }'
    ```

=== "Python (tango-python)"

    ```python
    import os
    from datetime import date, timedelta
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    today = date.today()
    sixty_days = today + timedelta(days=60)

    alert = client.create_webhook_alert(
        name="DHS cybersecurity grants — open in next 60d",
        query_type="grant",
        filters={
            "agency": "DHS",
            "cfda_number": "97.067",
            "applicant_types": "00",                  # State governments
            "response_date_after": today.isoformat(),
            "response_date_before": sixty_days.isoformat(),
        },
        frequency="daily",
    )
    print(alert.alert_id, alert.status)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const today = new Date().toISOString().slice(0, 10);
    const sixtyDays = new Date(Date.now() + 60 * 24 * 3600 * 1000)
      .toISOString()
      .slice(0, 10);

    const alert = await client.createWebhookAlert({
      name: "DHS cybersecurity grants — open in next 60d",
      query_type: "grant",
      filters: {
        agency: "DHS",
        cfda_number: "97.067",
        applicant_types: "00",                       // State governments
        response_date_after: today,
        response_date_before: sixtyDays,
      },
      frequency: "daily",
    });
    console.log(alert.alert_id, alert.status);
    ```

## Step 3 — Available grant filters

These are the keys accepted by `/api/grants/` and therefore by `query_type=grant` alerts.

| Filter | Notes |
| --- | --- |
| `search` | Full-text search across title and description (vector-backed). |
| `agency` | Substring match on agency abbreviation. `"DHS"` matches `DHS-FEMA`, `DHS-CISA`, etc. |
| `opportunity_number` | Exact opportunity number. |
| `cfda_number` | CFDA number, **substring** match (e.g. `"97.067"`). |
| `status` | Opportunity status (case-insensitive choice). |
| `applicant_types` | Eligibility / applicant type code (case-insensitive choice). |
| `funding_categories` | Funding category codes. |
| `funding_instruments` | Funding instrument codes. |
| `posted_date_after` / `posted_date_before` | Posted date range (`YYYY-MM-DD`). |
| `response_date_after` / `response_date_before` | Application deadline range (`YYYY-MM-DD`). |

!!! note "Filter names matter"
    The filter is `cfda_number` (not `cfda`) and `applicant_types` (not `eligibility`). Using the wrong key returns 400.

For full filter docs, see the [Grants API reference](../../api-reference/grants.md#filtering).

## Step 4 — Receive `alerts.grant.match` events

```json
{
  "timestamp": "2026-05-12T08:00:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.grant.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "grant",
      "filters": {
        "agency": "DHS",
        "cfda_number": "97.067"
      },
      "matches": {
        "new_count": 2,
        "modified_count": 1,
        "new": [
          {"grant_id": "...", "opportunity_number": "DHS-26-CISA-067-001", "title": "Cybersecurity Grant Program FY26", "agency_code": "DHS-CISA"},
          {"grant_id": "...", "opportunity_number": "DHS-26-CISA-067-002", "title": "State and Local Cybersecurity Pilot", "agency_code": "DHS-CISA"}
        ],
        "modified": [
          {"grant_id": "...", "opportunity_number": "DHS-25-CISA-067-014", "title": "...", "agency_code": "DHS-CISA"}
        ]
      },
      "checked_at": "2026-05-12T08:00:12.000Z"
    }
  ]
}
```

Pull full grant details from `/api/grants/{grant_id}/` (or via the SDK's `client.get_grant(grant_id)` / `client.getGrant(grantId)`) when you need more than the summary fields.

## Limitations

- **No multi-value `agency` shortcut.** `agency` is a substring match; if you need to watch DHS *and* HHS, create two alerts (or use `search=` with a broader term).
- **Polling-friendly cadences.** Grants don't post every minute — `daily` or `weekly` is usually plenty. `realtime` works but most agencies refresh on a daily cycle anyway.
- **Tier caps apply.** Free 1 / Micro 3 / Small 5 / Medium 10 / Large 25 simultaneous alerts.

## Related

- [Webhooks user guide](../../index.md) — protocol reference
- [Grants API reference](../../api-reference/grants.md) — full filter list
- [Recipes index](index.md)

---

<!-- doc: webhooks/recipes/index.md -->

# Recipes — webhook alerts

Filter-based webhook alerts let Tango re-evaluate a saved query on a schedule and POST you the matches as `alerts.<query_type>.match` events. These recipes are end-to-end how-tos for the most common monitoring use cases.

## What's in this section

| Recipe | Goal |
| --- | --- |
| [Vendor watchlist](vendor-watchlist.md) | Get notified when specific UEIs win new contracts. |
| [Awards by NAICS](awards-by-naics.md) | Stream new contracts in one or more NAICS codes (with optional agency / set-aside / dollar-floor refinements). |
| [Track entity changes](track-entity-changes.md) | Monitor SAM.gov registration updates — status flips, address moves, NAICS reassignment, socioeconomic re-cert. |
| [Grants by agency](grants-by-agency.md) | Track new grant opportunities at one or more agencies, optionally filtered by CFDA + applicant type. |
| [Forecast pipeline](forecast-pipeline.md) | Monitor upcoming opportunities in agency procurement forecasts (HHS, DHS, GSA, COMMERCE, DOE, TREASURY, DOI, DOL, DOT, VA, NRC). |

## Prerequisites

All recipes assume:

- An active Tango account on any tier (filter alerts are available on Free)
- An API key (`X-API-KEY`) — see [Authentication](../../getting-started/authentication.md)
- A configured webhook endpoint — Tango provisions the endpoint record for you; ask support if you don't have one yet
- Your shared signing secret stored as `TANGO_WEBHOOK_SECRET` server-side

## SDK quick reference

Each recipe shows three side-by-side variants. The snippets below are **templates** — substitute your own `name`, `query_type`, and `filters`. See [`webhooks-user-guide.md`](../index.md#convenience-api-apiwebhooksalerts) for the full canonical schema, including when the optional `endpoint` field is required.

=== "curl"

    ```bash
    # Template — replace the placeholders with your alert spec.
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "<your alert name>",
        "query_type": "<opportunity|contract|idv|ota|otidv|entity|grant|forecast>",
        "filters": { "<filter_name>": "<filter_value>" },
        "frequency": "realtime"
      }'
    ```

=== "Python (tango-python)"

    ```python
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])
    alert = client.create_webhook_alert(
        name="...",
        query_type="...",
        filters={"...": "..."},
        frequency="realtime",
    )
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY });
    const alert = await client.createWebhookAlert({
      name: "...",
      query_type: "...",
      filters: { "...": "..." },
      frequency: "realtime",
    });
    ```

## OR vs AND filter syntax

- Within a single filter, **OR** is `|` — `naics: "541511|541512"` matches either.
- Across multiple filters, the API takes the **AND** — `agency: "DHS"` + `naics: "541512"` matches only DHS opportunities in 541512.
- One known exception: the `uei` filter on `/api/contracts/` is single-value only. Multi-vendor watchlists need one alert per UEI — see the [vendor-watchlist recipe](vendor-watchlist.md).

## Test the filter against the API first

Always verify your filter returns the data you expect against the underlying API before turning it into an alert. If `/api/<resource>/?<filters>` returns nothing, the alert will never fire.

```bash
# Same filters you'd pass to the alert
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/contracts/?funding_agency=DOD&naics=541512"
```

## Related

- [Webhooks user guide](../../index.md) — protocol details, payload format, retry/circuit-breaker, security
- [Stream contract awards in real time](../../guides/how-to/stream-awards.md) — the canonical contract-monitoring how-to with full receiver code
- [Search opportunities by NAICS](../../guides/how-to/search-opportunities-naics.md) — opportunity filtering walkthrough

---

<!-- doc: webhooks/recipes/track-entity-changes.md -->

# Track entity changes — monitor SAM.gov registration updates

Get a webhook when a vendor in your pipeline updates their SAM.gov registration — status flips (Active ↔ Inactive), address moves, NAICS reassignment, socioeconomic re-certification, CAGE updates. These changes can make or break proposal eligibility, set-aside qualification, and partner-vetting workflows, and they're invisible to award-side monitoring until a contract actually posts.

This is the entity-record companion to the [vendor watchlist](vendor-watchlist.md) recipe. Vendor-watchlist tracks **new contracts** for a known UEI (`query_type=contract`); this recipe tracks **changes to the entity record itself** (`query_type=entity`).

## The 1-line answer

Create a filter alert on `query_type=entity`. Filters are the same query parameters you'd pass to `/api/entities/`.

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "Pipeline vendor — ACME Corp registration",
    "query_type": "entity",
    "filters": { "uei": "ABC123XYZ4567" },
    "frequency": "daily"
  }'
```

That's it. Tango re-evaluates daily and POSTs `alerts.entity.match` events when the entity record changes (or first appears, if the UEI is brand-new in SAM).

!!! warning "One alert per vendor — `uei` is single-value"
    The `/api/entities/` `uei` filter is **exact-match, single-value only** (`lookup_expr="iexact"`). To watch N vendors by UEI, create N alerts — one per UEI. Same constraint as the [vendor-watchlist recipe](vendor-watchlist.md).

    For *attribute-based* watches (e.g. "all small businesses in 541512 in Maryland"), see the second example below — that pattern uses `naics` / `socioeconomic` / `state` instead of `uei` and only needs one alert.

## Step 1 — Verify the filter against the entities API

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/entities/?uei=ABC123XYZ4567"
```

If this returns the entity record you expect, the alert will fire on changes. If it returns nothing, double-check the UEI — Tango only ingests entities present in SAM.gov.

## Step 2 — Create the alert (track a portfolio by UEI)

=== "curl"

    ```bash
    for uei in ABC123XYZ4567 DEF456UVW7890 GHI789RST0123; do
      curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
        -H "Content-Type: application/json" \
        "https://tango.makegov.com/api/webhooks/alerts/" \
        -d "{
          \"name\": \"Pipeline entity — $uei\",
          \"query_type\": \"entity\",
          \"filters\": { \"uei\": \"$uei\" },
          \"frequency\": \"daily\"
        }"
    done
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    pipeline = [
        ("ACME Corp",       "ABC123XYZ4567"),
        ("Beta Industries", "DEF456UVW7890"),
        ("Gamma LLC",       "GHI789RST0123"),
    ]

    for label, uei in pipeline:
        alert = client.create_webhook_alert(
            name=f"Pipeline entity — {label}",
            query_type="entity",
            filters={"uei": uei},
            frequency="daily",
        )
        print(alert.alert_id, alert.status, label)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const pipeline = [
      { label: "ACME Corp",       uei: "ABC123XYZ4567" },
      { label: "Beta Industries", uei: "DEF456UVW7890" },
      { label: "Gamma LLC",       uei: "GHI789RST0123" },
    ];

    for (const { label, uei } of pipeline) {
      const alert = await client.createWebhookAlert({
        name: `Pipeline entity — ${label}`,
        query_type: "entity",
        filters: { uei },
        frequency: "daily",
      });
      console.log(alert.alert_id, alert.status, label);
    }
    ```

`daily` is the right frequency for entity tracking — SAM.gov refreshes nightly, so re-checking more often just burns alert budget without surfacing more changes. Use `realtime` only if you specifically need same-cycle delivery once SAM data lands in Tango.

## Step 3 — Filter by attribute, not identity

Sometimes you want to discover *new* entities matching a profile, not watch a known list. For example: "alert me when a new small business registers in NAICS 541512 in Maryland." That's one alert with no `uei`:

=== "curl"

    ```bash
    curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
      -H "Content-Type: application/json" \
      "https://tango.makegov.com/api/webhooks/alerts/" \
      -d '{
        "name": "New MD small businesses in 541512",
        "query_type": "entity",
        "filters": {
          "naics": "541512",
          "socioeconomic": "A2",
          "state": "MD"
        },
        "frequency": "daily"
      }'
    ```

=== "Python (tango-python)"

    ```python
    alert = client.create_webhook_alert(
        name="New MD small businesses in 541512",
        query_type="entity",
        filters={
            "naics": "541512",
            "socioeconomic": "A2",   # SBA Small Business
            "state": "MD",
        },
        frequency="daily",
    )
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    const alert = await client.createWebhookAlert({
      name: "New MD small businesses in 541512",
      query_type: "entity",
      filters: {
        naics: "541512",
        socioeconomic: "A2",   // SBA Small Business
        state: "MD",
      },
      frequency: "daily",
    });
    ```

Available `EntityFilter` keys (see the [Entities API reference](../../api-reference/entities.md) for the full list):

| Filter | Notes |
| --- | --- |
| `uei` | Exact UEI (`iexact`, single value). |
| `cage_code` (alias `cage`) | CAGE code (`iexact`). Pick one or the other — sending both is rejected. |
| `name` | Substring match on `legal_business_name` (min length 2). |
| `naics` | NAICS code; matches against the entity's registered NAICS list. |
| `psc` | PSC code; matches against the entity's registered PSC list. |
| `socioeconomic` | Business-type code (e.g. `A2` small business, `27` woman-owned, `XX` 8(a)). |
| `state` | Physical-address state code (e.g. `MD`, `VA`). |
| `zip_code` | Physical-address ZIP (exact). |
| `purpose_of_registration_code` | SAM purpose-of-registration code (e.g. `Z2` All Awards). |
| `total_awards_obligated_gte` / `_lte` | Lifetime contract+subaward total (USD); IDV/OTA excluded. |
| `search` | Full-text search across name + UEI + CAGE (min length 2). |

## Step 4 — Receive `alerts.entity.match` events

Tango POSTs a signed JSON batch to your endpoint. Entity events split matches into **`new`** (first-time-registered UEIs that match your filter) and **`modified`** (existing records whose SAM data changed since the last check):

```json
{
  "timestamp": "2026-05-12T06:05:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.entity.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "entity",
      "filters": { "uei": "ABC123XYZ4567" },
      "matches": {
        "new_count": 0,
        "modified_count": 1,
        "new": [],
        "modified": [
          {
            "id": "ABC123XYZ4567",
            "legal_business_name": "ACME Corporation",
            "registration_status": "Active"
          }
        ]
      },
      "checked_at": "2026-05-12T06:05:12.000Z"
    }
  ]
}
```

The summary fields delivered for `query_type=entity` are: `id` (the UEI), `legal_business_name`, and `registration_status`. To see *what changed* (which field flipped), pull the full record from `/api/entities/{uei}/` (or `client.get_entity(uei)` / `client.getEntity(uei)`) and diff against your last-known copy on the receiver side — Tango doesn't ship a field-level diff in the alert payload.

For the full receiver implementation (signature verification, idempotency, fast 2xx, error handling), see [Stream contract awards in real time](../../guides/how-to/stream-awards.md#step-2-build-the-receiver) — the receiver shape is identical, only the event type and summary fields change.

## Step 5 — Manage the alert

```bash
# List your alerts
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/"

# Pause a vendor without deleting
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/" \
  -d '{"is_active": false}'

# Remove a vendor permanently
curl -X DELETE -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/"
```

`name`, `frequency`, `cron_expression`, and `is_active` are updatable. `query_type` and `filters` are immutable — to change the watched UEI, delete and recreate.

## Troubleshooting

| Symptom | Cause | What to do |
| --- | --- | --- |
| Alert re-fires every day with the same `modified` UEI and nothing visibly changed | SAM bumps `modified` timestamps on full-record refresh even when no field flipped. The evaluator picks up `modified >= last_checked_at` regardless of what changed. | Diff the full record against your last-known copy on the receiver side; if no fields you care about changed, drop the event. |
| First-ever evaluation fires for a UEI you've watched before in another tool | The `matches.new` bucket on the first run reflects records *created* in Tango since the lookback cutoff (default 24h on first eval) — not "first time you've ever seen this entity." A UEI freshly ingested into Tango lands in `new` even if it's been in SAM for years. | Treat `matches.new` on a brand-new alert as "Tango first saw this," not "SAM first saw this." For the latter, check `entity.created_date` from the full record. |
| Status flip happens twice within one check window (Active → Inactive → Active) | The evaluator sees the net state at evaluation time, not the intermediate flip. Two flips in 24h with `frequency=daily` look like no change at all. | If you need every transition, use `frequency=realtime` (5-minute eval cadence) and accept the higher alert-budget burn. |
| `cage` and `cage_code` rejected together | The filter accepts `cage` as an alias for `cage_code`; sending both at once is rejected with 400. | Pick one. They filter the same column. |
| `state=MD` returns nothing for a vendor you know is in Maryland | `state` filters on the **physical address** state, not the mailing address. Some entities register the two separately. | Verify with `curl ".../api/entities/?uei=<uei>"` and check `physical_address.state_or_province_code`. |
| Alert never fires even though SAM clearly updated | Check `last_checked_at` on the alert (`GET /api/webhooks/alerts/<alert_id>/`). If it's recent but no events were delivered, the SAM ingestion pipeline may not have refreshed yet — Tango ingests SAM nightly. | Wait one full ingestion cycle, then re-check. If still nothing, ping support with the `alert_id`. |

## Limitations

- **`uei` is single-value.** One alert per vendor when watching by UEI. See the warning at the top.
- **No field-level diff in the payload.** You get the post-change summary; compute the diff on your side from the full record.
- **Tier caps apply.** Free 1 / Micro 3 / Small 5 / Medium 10 / Large 25 simultaneous alerts across all alert types combined.
- **Daily is usually the right cadence.** SAM refreshes nightly. `realtime` works but won't surface changes faster than the underlying data refresh.

## Related

- [Vendor watchlist](vendor-watchlist.md) — track new **contracts** for known UEIs (the contract-side complement to this recipe)
- [Webhooks user guide](../../index.md) — protocol details, payload format, retry/circuit-breaker, security
- [Entities API reference](../../api-reference/entities.md) — full filter list for `query_type=entity`
- [Recipes index](index.md)

---

<!-- doc: webhooks/recipes/vendor-watchlist.md -->

# Vendor watchlist — track new contracts from specific UEIs

Get a webhook every time a vendor on your watchlist wins a new contract.

## The 1-line answer

Create one filter alert per vendor UEI on `query_type=contract`. Tango re-evaluates each filter against `/api/contracts/` and POSTs `alerts.contract.match` events when matches appear.

```bash
curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/" \
  -d '{
    "name": "Vendor watchlist — ACME Corp",
    "query_type": "contract",
    "filters": { "uei": "ABC123XYZ4567" },
    "frequency": "realtime"
  }'
```

That's it. Repeat for each vendor in your watchlist.

!!! warning "One alert per vendor — `uei` is single-value"
    The `/api/contracts/` API's `uei` filter is **exact-match, single-value only**. There is no `|` multi-value support on this filter today. To watch N vendors, create N alerts — one per UEI.

    *Future:* If multi-value `uei` lands on `/api/contracts/`, this recipe collapses to a single alert per watchlist with `uei: "UEI1|UEI2|UEI3"`. Tracked as a backlog enhancement; no ETA.

## Step 1 — Verify each UEI returns data

For each vendor on your list, prove the filter works against the contracts API first.

```bash
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/contracts/?uei=ABC123XYZ4567&ordering=-award_date&limit=5"
```

If this returns the recent contracts you expect, the alert will fire on new ones. If it returns nothing, double-check the UEI is correct and the vendor has FPDS history.

## Step 2 — Create one alert per vendor

=== "curl"

    ```bash
    for uei in ABC123XYZ4567 DEF456UVW7890 GHI789RST0123; do
      curl -X POST -H "X-API-KEY: $TANGO_API_KEY" \
        -H "Content-Type: application/json" \
        "https://tango.makegov.com/api/webhooks/alerts/" \
        -d "{
          \"name\": \"Vendor watchlist — $uei\",
          \"query_type\": \"contract\",
          \"filters\": { \"uei\": \"$uei\" },
          \"frequency\": \"realtime\"
        }"
    done
    ```

=== "Python (tango-python)"

    ```python
    import os
    from tango import TangoClient

    client = TangoClient(api_key=os.environ["TANGO_API_KEY"])

    vendors = [
        ("ACME Corp",      "ABC123XYZ4567"),
        ("Beta Industries", "DEF456UVW7890"),
        ("Gamma LLC",      "GHI789RST0123"),
    ]

    for label, uei in vendors:
        alert = client.create_webhook_alert(
            name=f"Vendor watchlist — {label}",
            query_type="contract",
            filters={"uei": uei},
            frequency="realtime",
        )
        print(alert.alert_id, alert.status, label)
    ```

=== "Node (@makegov/tango-node)"

    ```typescript
    import { TangoClient } from "@makegov/tango-node";

    const client = new TangoClient({ apiKey: process.env.TANGO_API_KEY! });

    const vendors = [
      { label: "ACME Corp",       uei: "ABC123XYZ4567" },
      { label: "Beta Industries", uei: "DEF456UVW7890" },
      { label: "Gamma LLC",       uei: "GHI789RST0123" },
    ];

    for (const { label, uei } of vendors) {
      const alert = await client.createWebhookAlert({
        name: `Vendor watchlist — ${label}`,
        query_type: "contract",
        filters: { uei },
        frequency: "realtime",
      });
      console.log(alert.alert_id, alert.status, label);
    }
    ```

Each alert returns **201 Created** on first run, **200 OK** on subsequent runs (dedup on `query_type` + `filters`).

## Step 3 — Receive `alerts.contract.match` events

Tango POSTs a signed JSON batch to your endpoint. The match summary uses the **delivered** shape (richer than the API's sample-payload echo):

```json
{
  "timestamp": "2026-05-12T18:20:14Z",
  "delivery_id": "8c5e3f6a-...-9b21",
  "events": [
    {
      "event_type": "alerts.contract.match",
      "alert_id": "e4c4...-...-...",
      "query_type": "contract",
      "filters": { "uei": "ABC123XYZ4567" },
      "matches": {
        "new_count": 1,
        "modified_count": 0,
        "new": [
          {
            "id": "CONT_AWD_W15QKN24C1234_9700_-NONE-_-NONE-",
            "piid": "W15QKN24C1234",
            "obligated": 2450000,
            "recipient_uei": "ABC123XYZ4567"
          }
        ],
        "modified": []
      },
      "checked_at": "2026-05-12T18:20:12.000Z"
    }
  ]
}
```

Use `matches.new` and `matches.modified` for the per-record summary. Pull full contract details from `/api/contracts/{id}/` when you need more than the summary fields.

For the full receiver implementation (signature verification, idempotency, fast 2xx, error handling), see [Stream contract awards in real time](../../guides/how-to/stream-awards.md#step-2-build-the-receiver).

## Step 4 — Manage the watchlist

```bash
# List your alerts
curl -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/"

# Pause a vendor without deleting
curl -X PATCH -H "X-API-KEY: $TANGO_API_KEY" \
  -H "Content-Type: application/json" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/" \
  -d '{"is_active": false}'

# Remove a vendor permanently
curl -X DELETE -H "X-API-KEY: $TANGO_API_KEY" \
  "https://tango.makegov.com/api/webhooks/alerts/<alert_id>/"
```

`name`, `frequency`, `cron_expression`, and `is_active` are updatable. `query_type` and `filters` (which include `uei`) are immutable — to change the watched UEI, delete and recreate.

## Limitations

- **`uei` is single-value on `/api/contracts/`.** One alert per vendor. See the warning at the top of this page.
- **Tier caps apply.** Free tier gets 1 alert total; Micro 3; Small 5; Medium 10; Large 25. A 5-vendor watchlist requires Small or higher.
- **Only contracts.** This recipe covers `query_type=contract`. To watch a vendor's *registration changes* (DSBS updates, address changes, etc.) instead, use `query_type=entity` with `uei` — same N-alerts pattern applies.
- **Following specific contracts (not vendors)?** If your watchlist is a fixed set of *contracts* — not a vendor's whole contract history — use the `key` filter instead: `{"query_type": "contract", "filters": {"key": "<key-1>|<key-2>|<key-3>"}}`. One alert, up to 500 contracts per filter. See [Following specific resources](../index.md#following-specific-resources) in the user guide.
- **No multi-vendor consolidation.** All alerts deliver to the same endpoint, but each vendor's matches arrive in their own event (one per alert per dispatch). Group on the receiver side via `alert_id` if you need to fan out by vendor in your downstream system.

## Related

- [Webhooks user guide](../../index.md) — full protocol reference
- [Stream contract awards in real time](../../guides/how-to/stream-awards.md#step-2-build-the-receiver) — receiver implementation, signature verification, common pitfalls
- [Contracts API reference](../../api-reference/contracts.md) — full filter list for `query_type=contract`