Skip to content

Changelog

This is the most up-to-date change log for Tango and the Tango API.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

Production Badge

[Unreleased]

Added

Changed

Breaking

Fixed

Internal / Infra

[4.12.7] - 2026-06-26

Changed

  • Each matched webhook event is now delivered as its own HTTP request with one top-level delivery_id. Previously events for the same endpoint could be combined into a single request, so a receiver expecting more than one event in the events array now gets one per request.

Fixed

  • Webhook delivery payloads now include the top-level delivery_id described in the payload format and Slack how-to — a UUID that stays the same across retries, so you can use it as an idempotency key to dedupe deliveries.
  • The "Send test" delivery and sample payload endpoint now return events in the same shape as real alert deliveries (event_type, query_type, filters, matches), so you can validate your receiver against a representative payload. Test deliveries stay marked with test: true.

[4.12.4] - 2026-06-23

Fixed

  • Made LinkedIn (and other social) sign-in more resilient. When the identity provider was temporarily rate-limiting us, some login attempts failed with an error; sign-in now stays working through those brief provider hiccups.

[4.12.1] - 2026-06-18

Fixed

  • Fixed alerts that trigger a Claude Code routine: these deliveries were failing to start the routine, and now send the correct request format so the routine runs reliably with the matched results as context. These deliveries are also sent once and not retried, so a transient hiccup can no longer start the same routine twice.

[4.12.0] - 2026-06-18

Added

  • Manage your webhook endpoints right from your account page — create, edit, and enable or disable them without touching the API. Endpoints that fail to deliver for a sustained period are now disabled automatically, and we email you a heads-up with a link to re-enable.

Changed

  • Webhooks are now available on all plans.

[4.11.0] - 2026-06-18

Added

  • Webhook endpoints can now send custom request headers (such as an Authorization header), so alerts can deliver to authenticated destinations — including triggering a Claude Code routine when an alert matches. Header values are stored encrypted and are never returned in API responses.

Changed

  • Contract data ingestion now runs as one ordered pipeline with a freshness alert, so a stalled refresh is caught within a day instead of silently serving stale data.

[4.10.0] - 2026-06-17

Changed

  • Webhook alerts are now evaluated against our data lake — faster, and no longer paused or throttled for matching too broadly.

Breaking

  • The alert filter preflight endpoint (POST /api/webhooks/subscriptions/check-filter/) has been removed, along with the automatic pausing of broad alerts.
  • The weekly alert frequency has been removed; use a custom cron expression for a weekly cadence.

[4.9.0] - 2026-06-16

Added

  • Webhook alerts that match too broadly are now paused automatically and you'll get an email explaining how to narrow the filter (use a sub-agency instead of a whole department, add NAICS/PSC/keyword constraints, or split it into several alerts) and a link to re-enable it. A "firehose" filter that matches tens of thousands of records can't be evaluated efficiently, so it's safer to flag it than to let it flood your endpoint. Creating or updating an alert with a too-broad filter is now rejected up front, and a new POST /api/webhooks/subscriptions/check-filter/ endpoint lets you preview how many records a filter would match before you save it.

Fixed

  • Webhook alert evaluation is faster and no longer times out on broad filters; entity, grant, and forecast alerts evaluate against an index instead of a full table scan.

[4.8.0] - 2026-06-10

Added

  • /api/forecasts/ now includes USDA procurement forecasts (source_system=USDA, ~2,400 records).

Changed

  • Labor-category rates (/api/idvs/{key}/lcats/, /api/entities/{uei}/lcats/) now refresh daily. One-time effect: modified timestamps reset table-wide at cutover.
  • opportunity_history on /api/grants/ detail responses is now populated; it was previously always null.
  • Forecast modified timestamps now update only when a forecast actually changes, so modified_after filters and webhook alerts reflect true changes.
  • /api/protests/: ~9,900 recovered historical GAO protests are restored, and existing cases carry fresher outcome and decision data.

Internal / Infra

  • GSA eLibrary contract metadata refreshes daily through the new ingest pipeline. The sins array may list codes in a different order (it is semantically a set), and last_updated now tracks GSA's newest published value, including downward corrections.
  • On /api/contracts/, object-valued code/description fields such as tradeoff_process, fair_opportunity_limited_sources, and subcontracting_plan can now be named bare in a shape (e.g. ?shape=piid,tradeoff_process) and behave exactly like tradeoff_process(*), matching how competition already works. Previously a bare reference returned a 400 with reason unknown_field even though the field was listed in available_fields. Shape errors for fields that genuinely require sub-selection now report the reason requires_subselection instead of unknown_field.

[4.7.9] - 2026-06-09

Changed

  • More trustworthy company-match confidence. Name resolution no longer reports high confidence for near-miss matches (similar-looking but different companies); confidence tiers now require the returned company's name to actually account for what you searched.
  • Substantially more accurate company name resolution. Searching by short names ("Booz Allen"), common abbreviations (GDIT, HII, SAIC), or ticker symbols (LMT, MSFT) now reliably finds the right company, and vendors with several registrations under one parent company return a single, correct match instead of duplicates.

[4.7.6] - 2026-06-08

Changed

  • Upgraded dj-stripe 2.9.1 → 2.10.3 (#2520). Under-the-hood plumbing change to the library that mirrors Stripe data into Tango's database: each Stripe object is now stored as a single JSON blob with the same Python attribute names exposed via property accessors. No user-visible change to checkout, billing, or self-serve plan management. Unlocks the queryable churn-feedback work introduced in #2519 — we can now ask the database for "subscriptions cancelled because of price" instead of reading each one from the Stripe dashboard. Deploy includes a one-time djstripe migration + manage.py sync_stripe_data re-sync from the Stripe API (sub-minute given current data volume).

Internal / Infra

  • IT Dashboard ingest moved to the lakehouse pipeline. Behind-the-scenes plumbing change — federal IT investments now load through the Parquet-based sync pipeline. No change to /api/itdashboard/ responses.

[4.7.4] - 2026-06-05

Added

  • Your name on sign-up. The sign-up form now asks for your first and last name, so Tango can greet you by name and personalize your welcome email.

Changed

  • Refreshed sign-in & sign-up pages. The login, sign-up, and email-verification screens got a visual and copy polish for a clearer, more consistent passwordless experience.

Fixed

  • Clearer "create your account" page. When you enter an email that doesn't have an account yet, the account-creation page no longer shows confusing "already have an account? sign in instead" wording.

[4.7.3] - 2026-06-04

Added

  • DoD contract-attribution signals on budget accounts. /api/budget/accounts/ now returns three new fields by default: attribution_status, attribution_confidence, and contract_obligated_estimated. The underlying USAspending File C data captures only a small share of the Department of Defense's contract obligations, so DoD federal accounts previously looked identical to accounts with no contract activity at all (contract_obligated came back null), dropping them out of any contract-share filter or ranking. Use attribution_status to tell "no contracts" apart from "no source data," and fall back to contract_obligated_estimated (an FPDS-based estimate) where the primary figure is missing. All three are null for account-years that haven't been backfilled yet.

[4.7.2] - 2026-06-03

Added

  • Bot protection on sign-up and email sign-in code requests. The sign-up form and the "Email me a sign-in code" form on the login page now include a quick Cloudflare Turnstile challenge to keep automated traffic out. You may see a brief verification check before submitting.
  • Microsoft sign-in + passwordless email sign-in. You can now sign in (and sign up) with a Microsoft work or school account, or with just your email and a one-time sign-in code — no password and no social provider required. The new "Email me a sign-in code" entry point appears on the login page alongside the social buttons.

Breaking

  • Email sign-up is now open. Previously Tango required a social provider (Google, GitHub, LinkedIn) to create an account. Sign-up with email + one-time code is now supported, so the /accounts/signup/ page is reachable. If your integration assumed every Tango user is backed by a social identity, expect to see passwordless email accounts as well.

[4.7.0] - 2026-06-02

Changed

  • Entity descriptions are now derived at render time. GET /api/entities/{uei}/?shape=…(*) keeps returning {code, description} for entity_structure, entity_type, profit_structure, organization_structure, purpose_of_registration, and country_of_incorporation — same payload shape, same values for known codes. Behind the scenes, the description is now derived from a built-in code-to-label map (or, for country, django-genc's country table) rather than read from a stored *_desc column. For codes outside the known set, description comes back as null. state_of_incorporation(*) has no map and now consistently returns {code, description: null} for every state.

Breaking

  • Entity flag fields now serialize as bool. Four fields on /api/entities/{uei}/ change wire type: registered, registration_status, uei_status, public_display_flag. JSON true / false / null replace the previous string tokens ("Active" / "Inactive" / "Y" / "NPDY" / "Not Active"). Webhook alerts (alerts.entity.match) ship registration_status in the match summary and are subject to the same change. Mapping: registration_status "Active"true, "Inactive"false; uei_status "Active"true, "Not Active"false; public_display_flag "Y"true, "NPDY"false (a non-obvious source token — interpret as "Not Publicly DisplaYed"); registered "Y"true, any other value → null (no "N" token was observed in production data for this field). Any unmapped source value on any of the four → null. None of the four publicly-surfaced fields ever emitted a literal "N" token, so no "N"false mapping exists on the public surface. Action for integrators: webhook filters and client code that compared these fields to string tokens (e.g. registration_status === "Active") need to switch to bool comparison (registration_status === true). The 7 *_desc top-level fields (purpose_of_registration_desc, entity_structure_desc, entity_type_desc, profit_structure_desc, organization_structure_desc, state_of_incorporation_desc, country_of_incorporation_desc) are removed from the default entity payload — use the ?shape=…(*) expansion for any of these to keep getting {code, description} derived at render time.
  • source_type removed from /api/entities/ output. The field was auto-set to "sam" for every row and duplicated information already exposed via source. Any client reading entity.source_type should switch to entity.source.
  • Stricter value sets on two entity enums. evs_source is now constrained to {"D&B", "E&Y"} (or null), and exclusion_status_flag to {"Y", "N", "D"} (or null). Out-of-set legacy values were coerced to null during the upgrade. Field shape is unchanged — values still serialize as strings. exclusion_status_flag = "D" means "delinquent" and is kept intentionally alongside "Y" / "N"; a matching data-dictionary update at docs.makegov.com is tracked as a follow-up.

Removed

  • Internal field_provenance JSONB column on entities (and the related CDC changelog tables) is gone. The column was never exposed via the API; field-level arbitration is handled upstream now. (#2376)
  • The internal load_entities / load_dsbs SAM ingest loaders are gone. Entity data now flows entirely through the parquet-driven sync_entities family — no API surface change. (#2296)

[4.6.12] - 2026-06-01

Fixed

  • Upgrading your plan now gives you the full daily request allowance right away. Previously, requests already made on your old tier counted against your new tier's daily budget immediately after an upgrade, so a paid plan could start the day with less headroom than expected. Upgrading to a higher tier now starts you on a clean daily budget. Additionally, requests rejected for exceeding your daily limit (HTTP 429) no longer count toward your daily usage.

[4.6.11] - 2026-05-29

Fixed

  • Webhook alerts now deliver reliably again. Filter-based webhook alerts — opportunity, contract, entity, grant, and forecast matches — had stopped being evaluated, so matching records were not delivered to your endpoints. Alert evaluation has been corrected and delivery has resumed.

[4.6.10] - 2026-05-28

Changed

  • Webhook deliveries now identify themselves. Outbound webhook requests from Tango — including test deliveries — now send a User-Agent header of the form MakeGov-Tango-Webhooks/<version> (+https://docs.makegov.com/) instead of a generic HTTP-client default. Receivers can use this to recognize, allowlist, and log MakeGov webhook traffic.

[4.6.8] - 2026-05-26

Added

  • Federal budget data is now in the API. A new family of /api/budget/ endpoints exposes the full federal-account budget lifecycle — request → enacted → apportioned → obligated → outlayed — for ~2,000 federal accounts per fiscal year across FY17–FY26. Each account includes pre-computed execution ratios, year-over-year and 5-year trends, the contract/assistance/unlinked breakdown, and links to the underlying OMB Appendix.
    • List & search: GET /api/budget/accounts/?search=acquisition+services+fund&fiscal_year=2024 matches account title, agency, or bureau. Rich filters support pipeline detection (?apportioned_to_enacted_pct__gte=0.9&obligated_to_apportioned_pct__lte=0.3), forward-look (?ba_growth_next_year_pct__gte=0.5&contract_share_of_obligated_capped__gte=0.5), and request-vs-actual (?actual_vs_requested_contract__lte=0.5).
    • Drill-down: each account exposes /recipients/ (top contract recipients with hydrated contract details inline) and /quarters/ (TAS-level quarterly obligation/outlay flow, FY21+) — both paginated with the standard count / next / previous / results envelope. ?shape=*,appendix(*) adds the President's Budget Appendix object-class breakdown and ?shape=*,narratives(*) adds the narrative + appropriations text. Both shape leaves return null when no data is available for the account-year, so consumers branch on truthiness once.
  • Budget as context, not just a destination. Budget data is embedded where the question is usually being asked:
    • Organizations get two opt-in budget shape leaves. GET /api/organizations/{key}/?shape=name,budget_appropriation(*) returns the "what Congress appropriated" rollup for departments. GET /api/organizations/{key}/?shape=name,budget_spending(*) returns the "what this office (and its subtree) is spending on contracts" rollup, walking down the hierarchy so middle-tier orgs aggregate their descendants automatically. Ask for one, the other, or both — each has a fixed shape. Both support ?shape=...,budget_appropriation[fiscal_year=2024](https://github.com/makegov/tango/blob/main/*) for historic views.
    • Entities get GET /api/entities/{uei}/budget-flows/ — the federal accounts that paid this vendor, paginated, with the surrounding account context (agency, enacted BA, contract share), the funding office hierarchy, and the contracts hydrated inline (key, NAICS, PSC, obligation, award date).

Fixed

  • Procurement forecast refreshes no longer stall when one agency publishes a non-JSON document. A binary publication from one agency was causing the entire combined-format forecast refresh to fail, leaving other agencies' forecasts (HHS, GSA, DOI, DOL, DOT, VA, NRC, DOE, DHS, COMMERCE, ENERGY, TREASURY) out of date. Unsupported document formats are now skipped per-agency with a warning; remaining agencies refresh normally.

[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

Breaking

  • Entity flag fields now boolean. registered, registration_status, uei_status, public_display_flag, bonding_flag, disaster_registry_flag, debt_subject_to_offset, credit_card_usage, edi_information_flag now serialize as JSON true/false/null instead of string tokens ("Active" / "Inactive" / "Y" / "N" / "NPDY" / "NA"). Webhook subscribers filtering on registration_status === "Active" need to switch to === true. Seven *_desc companion fields (purpose_of_registration_desc, entity_structure_desc, entity_type_desc, profit_structure_desc, organization_structure_desc, state_of_incorporation_desc, country_of_incorporation_desc) are dropped from the top-level entity payload — use the ?shape=…(*) expansions to get {code, description} derived at render time.

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.

[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 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_fieldsordering_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. 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. (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