Official, async-first Rust SDK for the Tango API — federal contracts, IDVs, entities, opportunities, grants, vehicles, and more, with dynamic response shaping so you fetch only the fields you need.
In development — v0.1.0. Not yet at sibling-SDK parity. The public API may shift before v1.0.0. Pin to a specific tag if you depend on this in production.
Sibling SDKs (tango-node, tango-python) are at v1.0.0; tango-go is at v0.1.0 with the same surface. This Rust port ships the full transport, error model, retry / rate-limit handling, webhook signing (in the separate tango-webhooks crate), and the ~75-method API surface. Same 0.x → 1.0 graduation logic as Go: the transport and types are stable; the surface stabilizes once it matches sibling parity.
- Async-first — built on
tokio+reqwest. One runtime, cleanfutures::Stream-based pagination. - Dynamic response shaping — request exactly the fields you need via 21 built-in
SHAPE_*presets or a custom comma-separated field list. - Typed errors — single
Errorenum with rich payload variants (Auth,NotFound,Validation,RateLimit,Timeout,Api,Transport,Decode,Build). Programmatic dispatch viaerr.status()anderr.is_retryable(). - Smart retries — automatic backoff on 5xx / 408 / 429 / transport errors, honoring the server's
Retry-Afterheader. - Compile-time-checked client builder — via
bon. Missingapi_key? Won't compile. - Async pagination —
PageStream<T>implementsfutures::Stream. Yields one item at a time, fetches successive pages automatically. - Forward-compatible models — every typed model carries
#[serde(flatten)] extra: HashMap<String, Value>so a server-side schema addition surfaces inrecord.extra["new_field"]rather than being silently dropped. - Webhook signing — in the separate
makegov-tango-webhookscrate: zero transport deps, HMAC-SHA256 verification, constant-time viasubtle.
[dependencies]
makegov-tango = "0.1"# Optional: webhook signing for receiversmakegov-tango-webhooks = "0.1"Crates publish under the makegov- prefix on crates.io; Rust imports stay short — use tango::Client; and use tango_webhooks::verify; — thanks to a [lib] name shim in each crate (same pattern as the aws-sdk-* family).
Requires Rust 1.80 or later.
use tango::Client;use futures::TryStreamExt;#[tokio::main]asyncfnmain() -> tango::Result<()>{let client = Client::builder().api_key("your-api-key").build()?;// Single pagelet page = client.list_contracts(
tango::ListContractsOptions::builder().awarding_agency("9700").shape(tango::SHAPE_CONTRACTS_MINIMAL).limit(25u32).build(),).await?;for record in page.results{println!("{:?}", record.get("piid"));}// Walk every page as a streamletmut stream = client.iterate_contracts(
tango::ListContractsOptions::builder().awarding_agency("9700").fiscal_year("2025").build(),);whileletSome(record) = stream.try_next().await? {println!("{:?}", record.get("piid"));}Ok(())}get_agency returns the typed AgencyRecord; forward-compatible fields land in agency.extra.
# use tango::Client;
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;let agency = client.get_agency("9700",None).await?;println!("{}", agency.name.unwrap_or_default());
# Ok(())}# use tango::{Client,GetEntityOptions,SHAPE_ENTITIES_COMPREHENSIVE};
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;let entity = client.get_entity("ABC123DEF456",Some(GetEntityOptions::builder().shape(SHAPE_ENTITIES_COMPREHENSIVE).build()),).await?;
# Ok(())}Pass an API key via Client::builder().api_key(...), or set TANGO_API_KEY in the environment and call Client::from_env(). You can also override the base URL via TANGO_BASE_URL for staging environments, or via the builder:
# use tango::Client;
# asyncfnrun() -> tango::Result<()>{let client = Client::builder().api_key("your-key").base_url("https://staging.tango.makegov.com".to_string()).build()?;
# Ok(())}Every list and get endpoint accepts a shape parameter selecting which fields the API returns. The SDK ships 21 presets matching the Node, Python, and Go SDKs:
# use tango::{Client,ListContractsOptions,SHAPE_CONTRACTS_MINIMAL};
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;// Preset:let page = client.list_contracts(ListContractsOptions::builder().shape(SHAPE_CONTRACTS_MINIMAL).build(),).await?;// Custom:let page = client.list_contracts(ListContractsOptions::builder().shape("key,piid,recipient(uei,display_name)").build(),).await?;
# Ok(())}See docs/SHAPES.md for the full grammar and the available presets.
List endpoints support both page-based and cursor-based pagination. Page<T> exposes the next URL plus the extracted cursor for keyset endpoints:
# use tango::{Client,ListIDVsOptions};
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;let page = client.list_idvs(ListIDVsOptions::builder().limit(50u32).build()).await?;// Fetch the next page:let next_page = client.list_idvs(ListIDVsOptions::builder().limit(50u32).cursor(page.cursor.unwrap_or_default()).build()).await?;
# Ok(())}Or just iterate via PageStream<T>:
# use tango::{Client,ListIDVsOptions};
# use futures::TryStreamExt;
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;letmut stream = client.iterate_idvs(ListIDVsOptions::builder().build());whileletSome(idv) = stream.try_next().await? {// process one IDV at a timelet _ = idv;}
# Ok(())}Or drain the whole thing:
# use tango::{Client,ListIDVsOptions};
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;let all = client.iterate_idvs(ListIDVsOptions::builder().build()).collect_all().await?;
# Ok(())}After each request, client.rate_limit_info() returns a snapshot of the rate-limit headers:
# use tango::Client;
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;ifletSome(info) = client.rate_limit_info(){println!("remaining={:?} limit={:?} reset_in={:?}",
info.remaining, info.limit, info.reset_in
);}
# Ok(())}The client also automatically retries 429s, honoring Retry-After. See docs/CLIENT.md for the full retry semantics.
Webhook signing lives in the separate makegov-tango-webhooks crate so a receiver service doesn't have to pull in the full SDK:
use tango_webhooks::{verify,SIGNATURE_HEADER};fncheck(raw_body:&[u8],header_value:&str,secret:&str) -> bool{verify(raw_body, header_value, secret)}The CRUD methods for managing webhook endpoints and alerts live on tango::Client:
list_webhook_endpoints, create_webhook_endpoint, update_webhook_endpoint, delete_webhook_endpoint, test_webhook_endpoint, list_webhook_alerts, create_webhook_alert, etc.
See docs/WEBHOOKS.md for the full surface, signature format, and middleware patterns.
Every fallible call returns Result<T, tango::Error>. The Error enum carries rich payloads — match the variant to dispatch on the failure mode:
use tango::{Client,Error};
# asyncfnrun() -> tango::Result<()>{
# let client = Client::builder().api_key("x").build()?;match client.get_agency("9700",None).await{Ok(agency) => println!("{}", agency.name.unwrap_or_default()),Err(Error::NotFound{ .. }) => println!("not found"),Err(Error::RateLimit{ retry_after, limit_type, .. }) => {println!("rate limited; retry in {retry_after}s (bucket: {limit_type:?})");}Err(Error::Validation{ message, .. }) => eprintln!("bad input: {message}"),Err(Error::Api{ status, message, .. }) => eprintln!("status={status}: {message}"),Err(e) => returnErr(e),}
# Ok(())}err.is_retryable() reports the SDK's retry decision (it already retries internally; this is for callers building their own retry policies on top). err.status() returns the HTTP status when one is associated with the error.
The SDK exposes ~75 methods on Client covering every endpoint in the sibling SDKs. The most-used 15 are listed here; the full method-by-method reference lives in docs/API_REFERENCE.md.
| Resource | List | Get | Iterate |
|---|---|---|---|
| Agencies | list_agencies | get_agency(typed: AgencyRecord) | — |
| Contracts | list_contracts | — | iterate_contracts |
| Entities | list_entities | get_entity | iterate_entities |
| IDVs | list_idvs | get_idv | iterate_idvs |
| Vehicles | list_vehicles | get_vehicle | iterate_vehicles |
| OTAs | list_otas | get_ota | iterate_otas |
| OTIDVs | list_otidvs | get_otidv | iterate_otidvs |
| Opportunities | list_opportunities | — | iterate_opportunities |
| Notices | list_notices | — | iterate_notices |
| Forecasts | list_forecasts | — | iterate_forecasts |
| Grants | list_grants | — | iterate_grants |
| Protests | list_protests | get_protest(typed: ProtestRecord) | iterate_protests |
| IT Dashboard | list_itdashboard | get_itdashboard | iterate_itdashboard |
| NAICS / PSC | list_naics / list_psc | get_naics / get_psc | — |
| Webhooks (CRUD) | list_webhook_endpoints / list_webhook_alerts | get_ / create_ / update_ / delete_ / test_ | — |
Sub-resources and lookups: list_entity_contracts / _idvs / _otas / _otidvs / _subawards / _lcats / get_entity_metrics, list_idv_awards / _child_idvs / _transactions / _lcats, list_agency_awarding_contracts / _funding_contracts, list_vehicle_awardees / _orders, list_otidv_awards, list_gsa_elibrary_contracts, list_business_types, list_offices, list_departments, list_mas_sins, list_assistance_listings, list_lcats (dispatcher). Meta: resolve, validate, get_version, list_api_keys, search_opportunity_attachments. Metrics dispatcher: list_metrics.
See docs/API_REFERENCE.md for full signatures, filter fields, and quirks.
This is a Cargo workspace with two published crates:
.
├── crates/
│ ├── tango/ # main SDK
│ └── tango-webhooks/ # HMAC-SHA256 signing/verification (no transport deps)
├── docs/ # in-repo guides
├── justfile # task runner (test / fmt / lint / cover / release-check)
├── Cargo.toml # workspace manifest
└── README.md
In-repo guides:
docs/ARCHITECTURE.md— crate layout, request lifecycle, design rationaledocs/CLIENT.md— builder options, env vars, retry semantics, error model, rate-limit observabilitydocs/API_REFERENCE.md— method-by-method reference for every public methoddocs/SHAPES.md— shape grammar, the 21 presets,flat/flat_lists, trade-offsdocs/WEBHOOKS.md— receiving deliveries, CRUD methods, troubleshooting
External:
- API docs (Rust): https://docs.rs/makegov-tango · https://docs.rs/makegov-tango-webhooks
- API reference: https://docs.makegov.com
- Sibling SDKs:
@makegov/tango-node·tango-python·tango-go
just test# cargo test --workspace
just fmt # cargo fmt --all
just lint # cargo clippy --workspace --all-features -- -D warnings
just cover # cargo llvm-cov --workspace --lcov --output-path lcov.info
just ci # fmt-check + lint + test (run before opening a PR)
just release-check # cargo publish --dry-run for both crates- Rust 1.80 or later.
MIT — see LICENSE.
Open an issue at https://github.com/makegov/tango-rust/issues or email support@makegov.com.
PRs welcome. See CONTRIBUTING.md. Run just ci before opening — fmt, clippy, and the full test suite must pass.