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.
import { TangoClient, ShapeConfig } from "@makegov/tango-node";
// Models (optional)
import type { Contract } from "@makegov/tango-node/models";All methods are async and return Promises.
List federal departments and subagencies.
const resp = await client.listAgencies({ page: 1, limit: 25 });| Name | Type | Description |
|---|---|---|
page |
number |
Page number (default 1). |
limit |
number |
Max results per page (default 25, max 100). |
PaginatedResponse<AgencyLike>
Fetch a single agency by its code.
const agency = await client.getAgency("2000");Returns a shaped Agency object. Responses are materialized via the dynamic model pipeline (dates parsed, nested objects built).
Search and list contract records.
const resp = await client.listContracts({
keyword: "cloud",
naics_code: "541511",
shape: ShapeConfig.CONTRACTS_MINIMAL,
flat: true,
});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 |
key is also a typed filter — pass a contract key (or several separated by |) to fetch specific records through the list endpoint.
The same key filter exists on listIdvs, listOtas, and listOtidvs.
Sorting:
sort: "award_date",
order: "desc" // -> ordering="-award_date"Pagination + shaping options:
shape: string,
flat: boolean,
flatLists: boolean,
page: number,
limit: number,
cursor: string, // mutually exclusive with `page` — if provided, `page` is ignoredContracts 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.
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 provide a solicitation-centric grouping of related IDVs.
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
const vehicle = await client.getVehicle("00000000-0000-0000-0000-000000000001", {
shape: ShapeConfig.VEHICLES_COMPREHENSIVE,
});Notes:
- On vehicle detail,
searchfilters expandedawardees(...)when included in yourshape(it does not filter the vehicle itself). - When using
flat: true, you can override the joiner withjoiner(default".").
const awardees = await client.listVehicleAwardees("00000000-0000-0000-0000-000000000001", {
shape: ShapeConfig.VEHICLE_AWARDEES_MINIMAL,
});IDVs (indefinite delivery vehicles) are the parent “vehicle award” records that can have child awards/orders under them.
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 thanpage.
const idv = await client.getIdv("SOME_IDV_KEY", {
shape: ShapeConfig.IDVS_COMPREHENSIVE,
});Lists child awards (contracts) under an IDV.
const awards = await client.listIdvAwards("SOME_IDV_KEY", { limit: 25 });const children = await client.listIdvChildIdvs({ key: "SOME_IDV_KEY", limit: 25 });const tx = await client.listIdvTransactions("SOME_IDV_KEY", { limit: 100 });const resp = await client.listEntities({
search: "Acme",
shape: ShapeConfig.ENTITIES_MINIMAL,
});Filters:
searchcage(CAGE code, typed alongside the existingcage_code)- any field names supported by the API
Fetch a single entity by UEI or CAGE.
Returns a shaped entity object with nested addresses/fields based on the shape.
Forecast search, with optional shaping.
id is a typed filter for fetching specific forecast records through the list endpoint.
Search SAM.gov opportunities with shaping.
opportunity_id is a typed filter for fetching specific opportunities through the list endpoint.
The canonical agency/department/office hierarchy. level filters by hierarchy depth: 1 = department, 2 = agency, 3 = sub-agency, and so on.
const orgs = await client.listOrganizations({
level: 1, // 1 = department, 2 = agency, 3 = sub-agency, …
include_inactive: false,
search: "Defense",
limit: 25,
});const org = await client.getOrganization("ORG_KEY");const offices = await client.listOffices({ search: "acquisitions" });const office = await client.getOffice("4732XX");Deprecated. Use
listOrganizations({ level: 1 })instead. The standalone departments endpoint is retained for backward compatibility and will be removed in a future API version.
const depts = await client.listDepartments({ page: 1, limit: 25 });const dept = await client.getDepartment("097");Other Transaction Agreements — non-FAR-based awards.
Uses keyset pagination (cursor + limit).
const otas = await client.listOtas({ limit: 25, awarding_agency: "4700" });const ota = await client.getOta("OTA_KEY");Other Transaction IDVs — umbrella OT agreements with child awards.
Uses keyset pagination (cursor + limit).
const otidvs = await client.listOtidvs({ limit: 25 });const otidv = await client.getOtidv("OTIDV_KEY");const awards = await client.listOtidvAwards("OTIDV_KEY", { limit: 25 });const subs = await client.listSubawards({ prime_uei: "ABC123DEF456", limit: 25 });const contracts = await client.listGsaElibraryContracts({ schedule: "MAS", limit: 25 });Fetch a single GSA eLibrary contract by UUID, with the standard shape / flat / flatLists / joiner options.
Defaults to ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL when no shape is passed.
const contract = await client.getGsaElibraryContract("00000000-0000-0000-0000-000000000001", {
shape: ShapeConfig.GSA_ELIBRARY_CONTRACTS_MINIMAL,
});const protests = await client.listProtests({ source_system: "gao", limit: 25 });naics_code is a typed filter sent to the API verbatim (it is not remapped to naics, unlike the contracts alias).
const protest = await client.getProtest("CASE_UUID");const investments = await client.listItDashboard({ search: "cloud", limit: 25 });const investment = await client.getItDashboard("023-000001234");listItDashboard also accepts previous_uii as a typed filter, for tracing an investment across UII renumbering.
OMB budget appendix accounts with lifecycle amounts (requested → enacted → apportioned → obligated → outlayed), derived ratios, and trends.
const accounts = await client.listBudgetAccounts({
fiscal_year: 2025,
agency_code: "097",
unobligated_balance__gte: 1_000_000_000,
ordering: "-unobligated_balance",
});ListBudgetAccountsOptions types the full filter surface of /api/budget/accounts/ — every numeric lifecycle, ratio, and trend field exposes an exact / __gte / __lte triplet, and categorical filters carry __in / __icontains variants.
The range filters use the API's dunder wire names (double underscore, e.g. fiscal_year__gte) — these are passed through verbatim.
A representative sample:
| Filter family | Example params |
|---|---|
| Identity / categorical | federal_account_symbol, fiscal_year, agency_code__in, bureau_name__icontains, bea_category, subfunction_code, account_title__icontains |
| Lifecycle amounts | requested_ba__gte, enacted_ba__lte, apportioned__gte, obligated_total__gte, outlayed_total__lte, unobligated_balance__gte |
| Contract / assistance breakdowns | contract_obligated__gte, assistance_outlayed__lte, contract_share_of_obligated_capped__gte |
| Ratios | obligated_to_apportioned_pct__gte, apportioned_to_enacted_pct_capped__lte, outlayed_to_obligated_pct__gte, unobligated_pct__gte |
| Trends | enacted_ba_yoy_pct__gte, obligated_yoy_pct__lte, enacted_ba_5yr_cagr__gte, ba_growth_next_year_pct__gte, actual_vs_requested_contract__gte |
See ListBudgetAccountsOptions in src/client.ts for the complete list — every filter is a typed, autocompleted option.
Any of the numeric fields is a valid ordering target (ordering: "-unobligated_balance" ranks by largest headroom first), and search covers account title, agency name, and bureau name.
Legacy aliases. Three pre-1.2 option names are kept and remapped to the params the API actually understands: fiscal_year_gte → fiscal_year__gte, fiscal_year_lte → fiscal_year__lte, and account_title → account_title__icontains.
An explicitly passed dunder param wins over its alias.
const account = await client.getBudgetAccount("ACCOUNT_ID");Quarterly lifecycle history and top recipients for one account.
const quarters = await client.getBudgetAccountQuarters("ACCOUNT_ID");
const recipients = await client.getBudgetAccountRecipients("ACCOUNT_ID");DLA DIBBS solicitations and awards: RFQs, RFPs, and award history.
const rfqs = await client.listDibbsRfqs({
nsn: "5310-01-234-5678",
open: true,
shape: ShapeConfig.DIBBS_RFQS_MINIMAL,
});Typed filters: nsn, part_number, solicitation, purchase_request, organization, status_code, set_aside, open, quantity_min / quantity_max, issue_date_after / issue_date_before, return_by_date_after / return_by_date_before, search, ordering.
const rfps = await client.listDibbsRfps({ open: true, limit: 25 });Typed filters: nsn, part_number, solicitation, organization, buyer_code, open, issued_date_after / issued_date_before, closes_date_after / closes_date_before, search, ordering.
const awards = await client.listDibbsAwards({ awardee_cage: "1ABC2", limit: 25 });Typed filters: award_number, delivery_order_number, solicitation, purchase_request, nsn, part_number, awardee_cage, entity, organization, total_contract_price_min / total_contract_price_max, award_date_after / award_date_before, posted_date_after / posted_date_before, search, ordering.
Two API behaviors worth knowing:
is_openis derived at query time fromreturn_by_date(RFQs) /closes_date(RFPs) — filter with theopenoption rather than shaping onis_open.- DIBBS
total_contract_priceis the order total repeated on every line item — never sum it across rows; deduplicate on award + delivery-order number first.
State, local and education procurement — solicitations that never appear on SAM.gov because they were never federal. Coverage is partial and grows one jurisdiction at a time.
This data does not join to the federal data: no UEI, no PIID, no agency-hierarchy key and no NAICS/PSC crosswalk. organization(*) here is three strings, not the federal 7-key office payload.
const open = await client.listSledOpportunities({
state: "TX",
response_deadline_before: "2026-10-01",
ordering: "response_deadline",
});Typed filters: state, jurisdiction, status, active, agency, solicitation_number, solicitation_type, has_documents, revision_kind, naics, nigp, unspsc, category, category_code, posted_after / posted_before, response_deadline_after / response_deadline_before, first_seen_after / first_seen_before, change_seen_after, modified_after / modified_before, platform, native_id, external_id, search, ordering.
Two defaults to know before your first call:
- Passing neither
statusnoractivereturns open solicitations only. Only about a fifth of the corpus is open, and a portal drops a closed solicitation rather than restating it, so the API defaults the list tostatus=open. Pass an explicitstatusto page the whole corpus;status: "open|unknown"also reaches the standing rosters and dateless RFIs thatunknowncovers.getSledOpportunity()returns a solicitation whatever its status. statusis Tango's answer, not the portal's. It is derived from the portal's word, the deadline and the clock, and refreshed every fifteen minutes. The portal's own word is served assource_status, is frozen at last capture, and is not filterable — most of what it calls open already has a passed deadline.
?search= is ranked over title, agency, identifiers, category labels and description, widened by the solicitations whose attachment text matched. A row that matched on its description carries a snippet with the matching passage; a title-or-agency match honestly carries none. Attachment matching contributes ids only — a caller learns that a document matched, never what it said.
category_codes scheme tagging is mid-migration, so naics matches only the small tagged share. Use category_code to match a code under any scheme, including the untagged pre-migration strings.
const row = await client.getSledOpportunity(id, {
shape: "opportunity_id,title,status,meta(*),attachments(*),revisions(*)",
});meta.attachment_count can be lower than row.attachments.length. Some portals auto-generate a cover sheet alongside the real documents; it is listed and flagged is_generated_summary but excluded from the count and from has_documents. The count answers "does this record hold its solicitation package"; the array answers "what files exist". size_bytes and char_count only mean something as a pair.
Reading a document body — attachments(extracted_text):
const row = await client.getSledOpportunity(id, {
shape: "opportunity_id,attachments(name,size_bytes,extracted_text)",
});Requires a Small plan or above and Tango API 4.25.1+. Three rules:
- You have to name it.
attachments(*)does not carry the body and neitherShapeConfigdefault names it — the API only resolves it for a caller who asked, so a default would make every detail fetch pay for a document nobody wanted to read. - The key is absent, not null, whenever the text is not being served to you: below Small (withheld and named in
meta.upgrade_hints), on a contested document, or where it could not be resolved. - A contested document never returns text, at any plan — its stored bytes disagree with what the record advertised.
Searching document text and reading it are separate. search matches inside attachment text on every plan and returns no fragment of it; the body is a per-record read on Small and above.
raw(*) needs a Small plan or above and is explicitly unstable: its shape varies by portal platform.
const revisions = await client.listSledOpportunityRevisions(id, { kind: "deadline_change" });Typed filters: kind, source_declared, observed_after / observed_before.
observed_at is the scrape that saw the change, not the date the agency made it. No state portal emits amendment notices, so kind is Tango's inference from the diff on about 95% of revisions, resolution is that state's crawl cadence, and history starts when Tango began reading the jurisdiction rather than when the solicitation was posted.
Unlike the revisions(*) expand, this route serves enrichment rows — Tango's own detail fetch filling in coverage rather than an agency amendment. Pass kind: "enrichment" for only those. changes (the per-field before and after) needs a Small plan, which is why it is left out of SLED_REVISIONS_MINIMAL; changed_fields is in the default and available at every plan.
const coverage = await client.getSledCoverage();
for (const row of coverage.states as Array<Record<string, unknown>>) {
console.log(row.state, row.total_count, row.by_status, row.last_change_observed_at);
}Call this before treating a per-state count as market size. A thin result for a state is at least as likely to be a portal Tango does not read as a quiet market, and that is the ambiguity this endpoint exists to resolve. Every state row carries all five status buckets whether or not they have rows, so a total and two buckets never invite subtraction. Takes no parameters and is neither shaped nor paginated.
const forecasts = await client.listSledForecasts({ state: "MD", advertisement_after: "2026-10-01" });Typed filters: state, agency, procurement_category, procurement_method, contract_number, incumbent_name, advertisement_after / advertisement_before, first_seen_after / first_seen_before, modified_after / modified_before, search, ordering.
- Forecasts carry no liveness at all — no deadline to have passed, so no
status, noactive, and no open-only default. Currency is the caller's call fromestimated_advertisement_date. estimated_advertisement_dateis the start of the published quarter, not a posting date.estimated_advertisement_rawkeeps the portal's own words ("Q3 (Jan.-March 2027)"), and a large share of rows publish no quarter at all.estimated_value(min,max,raw)is parsed from a free-text award band at serve time. A band naming one number is a floor, somaxis null — never read a missingmaxas an unbounded ceiling.incumbent_nameis published text, not a resolved Tango entity.
SAM.gov exclusion records (debarments, suspensions, and other ineligibility actions).
const exclusions = await client.listExclusions({
active: true,
classification_type: "Firm",
shape: ShapeConfig.EXCLUSIONS_MINIMAL,
});Typed filters: uei, entity_uei, cage_code, npi, classification_type, exclusion_type, exclusion_program, excluding_agency_code, excluding_agency_name, active, delisted, activate_date_after / activate_date_before, termination_date_after / termination_date_before, update_date_after / update_date_before, search, ordering.
is_currently_excluded is derived at query time — filter with active: true for records currently in effect rather than shaping on it.
SBIR/STTR topics and DoD DSIP solicitation cycles.
const topics = await client.listSbirTopics({
agency: "DOD",
year: 2026,
shape: ShapeConfig.SBIR_TOPICS_MINIMAL,
});Typed filters: topic_number, solicitation_number, agency, activity, year, doc_source, open_date_after / open_date_before, close_date_after / close_date_before, release_date_after / release_date_before, search, ordering.
const cycles = await client.listSbirSolicitations({ program: "SBIR", year: 2026 });Typed filters: solicitation_number, solicitation_status, program, activity, cycle_name, out_of_cycle, year, start_date_after / start_date_before, end_date_after / end_date_before, search, ordering.
Requires either { uei } (entity LCATs) or { idvKey } (IDV LCATs) — throws TangoValidationError if neither is provided.
const lcats = await client.listLcats({ uei: "ABCDEF123456" });
// or:
const lcats = await client.listLcats({ idvKey: "GS-00F-XXXX" });Labor Categories (/api/idvs/{key}/lcats/) attached to an IDV.
const lcats = await client.listIdvLcats("GS-00F-XXXX", { limit: 25 });List metrics for a NAICS code, PSC code, or entity. ownerType, ownerId, months, and periodGrouping are all required.
const metrics = await client.listMetrics({
ownerType: "naics",
ownerId: "541511",
months: 12,
periodGrouping: "month",
});const m = await client.getNaicsMetrics("541511", 12, "month");const m = await client.getPscMetrics("D302", 12, "month");const m = await client.getEntityMetrics("ABCDEF123456", 12, "month");const naics = await client.listNaics({ search: "software" });
const code = await client.getNaics("541511");const psc = await client.listPsc({ has_awards: true });
const code = await client.getPsc("D302");has_awards: true restricts the list to PSC codes that actually appear on awards.
const sins = await client.listMasSins();
const sin = await client.getMasSin("54151S");const listings = await client.listAssistanceListings();
const listing = await client.getAssistanceListing("10.310");const types = await client.listBusinessTypes();
const bt = await client.getBusinessType("A6");Resolve a free-text name to ranked entity or organization candidates.
const result = await client.resolve({ name: "Lockheed Martin", target_type: "entity" });
// result.candidates[0].display_name, result.countRequired fields: name, target_type ("entity" | "organization").
Validate the format of a PIID, solicitation number, or UEI.
const result = await client.validate({ type: "uei", value: "ABCDEF123456" });Required fields: type ("piid" | "solicitation" | "uei"), value.
const contracts = await client.listEntityContracts("ABCDEF123456", { limit: 25 });const idvs = await client.listEntityIdvs("ABCDEF123456");const subawards = await client.listEntitySubawards("ABCDEF123456");const contracts = await client.listAgencyAwardingContracts("4700", { limit: 25 });const contracts = await client.listAgencyFundingContracts("4700", { limit: 25 });Semantic search over opportunity attachments. q is required.
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. |
All list methods can be iterated page-by-page via the generic iterate() helper or the named convenience wrappers.
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, iterateDibbsRfqs, iterateDibbsRfps, iterateDibbsAwards, iterateExclusions, iterateSbirTopics, iterateSbirSolicitations, iterateSledOpportunities, iterateSledForecasts.
const v = await client.getVersion();const keys = await client.listApiKeys();Webhook APIs let Large / Enterprise users manage subscription filters for outbound Tango webhooks.
Discover supported event_type values.
const info = await client.listWebhookEventTypes();In production, MakeGov provisions the initial endpoint for you. These methods are most useful for dev/self-service.
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.
// 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);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).
const result = await client.testWebhookEndpoint("ENDPOINT_UUID");
console.log(result.success, result.status_code);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.
const result = await client.testWebhookDelivery({ endpointId: "ENDPOINT_UUID" });Fetch Tango-shaped sample deliveries.
const sample = await client.getWebhookSamplePayload({ eventType: "alerts.contract.match" });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.
// 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:
nameandquery_typeare required on create.query_typeis singular (e.g."contract", not"contracts").- Only
name,frequency,cronExpression, andisActiveare writable viaupdateWebhookAlert—query_typeandfiltersare read-only after creation.
The API does not currently expose a public /api/webhooks/deliveries/ or redelivery endpoint. Use:
testWebhookEndpoint(endpointId)for connectivity checksgetWebhookSamplePayload()for building handlers + alert payloads
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).
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:
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 for Fastify and framework-agnostic examples.
All thrown by async methods:
TangoAPIErrorTangoAuthErrorTangoNotFoundErrorTangoRateLimitErrorTangoTimeoutErrorTangoValidationErrorShapeErrorShapeParseErrorShapeValidationErrorTypeGenerationErrorModelInstantiationError
When the API rejects a request with a structured 400 payload (shape errors especially), TangoValidationError exposes it without any hand-parsing of responseData:
err.issues— the API's issue entries, e.g.[{ path: "tradeoff_process", reason: "unknown_field" }]; an empty array when the response carried no structured issues.err.availableFields— the endpoint's valid field set when the API includes one, elsenull.
try {
await client.listContracts({ shape: "key,tradeoff_process" });
} catch (err) {
if (err instanceof TangoValidationError) {
for (const issue of err.issues) console.error(issue.path, issue.reason);
console.error("valid fields:", err.availableFields);
}
}All list endpoints return:
interface PaginatedResponse<T> {
count: number;
next: string | null;
previous: string | null;
pageMetadata: Record<string, unknown> | null;
meta: Record<string, unknown> | null;
agencyWarnings: string[];
unresolvedAgencyTokens: Record<string, string[]>;
resolvedAgencies: Record<string, Array<Record<string, unknown>>>;
cursor: string | null;
results: T[];
}You can follow next / previous manually, pass cursor back on keyset-paginated endpoints, or use the iterate* helpers.
meta carries any response-level metadata the API attached to the page — currently agency-filter resolution diagnostics.
Three parsed views are always present (empty rather than throwing when meta is absent or malformed):
agencyWarnings— human-readable notes about agency tokens that were dropped or matched loosely; a non-empty list means part of your filter did not apply, so a small or emptyresultsis not evidence that no such records exist.unresolvedAgencyTokens— tokens that matched no organization, keyed by filter name; check this to fail loudly in a pipeline instead of trusting a silently-narrowed result set.resolvedAgencies— the organizations each token actually resolved to, keyed by filter name; agency resolution is fuzzy, so checking the resolvednameis the only way to catch a token matching an agency you did not intend.
const resp = await client.listContracts({ awarding_agency: "Navvy" });
if (resp.agencyWarnings.length > 0) {
console.warn(resp.agencyWarnings);
console.warn("unresolved:", resp.unresolvedAgencyTokens);
console.warn("resolved to:", resp.resolvedAgencies);
}