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 -H "X-API-KEY: $TANGO_API_KEY" \
"https://tango.makegov.com/api/opportunities/?naics=541512&active=true&ordering=response_deadline"
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)
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.
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:
# 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"
# 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)
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 });
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 below). Full reference: NAICS API.
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 -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"
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)
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. For the syntax (nested expansions, wildcards, flattening), see the Response Shaping pattern guide.
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 -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"
resp = client.list_opportunities(
naics="541512",
active=True,
agency="DOD",
response_deadline_after="2026-05-15",
ordering="response_deadline",
limit=50,
)
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.
Multi-NAICS search¶
Most vendors care about a portfolio of NAICS codes, not just one. Use | to pass multiple:
curl -H "X-API-KEY: $TANGO_API_KEY" \
"https://tango.makegov.com/api/opportunities/?naics=541511|541512|541519&active=true&ordering=response_deadline"
resp = client.list_opportunities(
naics="541511|541512|541519",
active=True,
ordering="response_deadline",
limit=50,
)
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.
# 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"
# 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
// 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 -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)"
# 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"])
// 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)");
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.
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.
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; this is the abbreviated version for "watch a NAICS portfolio."
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 -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>"
}'
# 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)
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 above. Tier limits and cron-vs-realtime semantics in the Webhooks user guide.
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.
Common pitfalls¶
active=truevsnotice_type.active=truefilters 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, combineactive=truewithnotice_type=Solicitation(orCombined Synopsis/Solicitation) andresponse_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 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 passordering=response_deadline. This matters double when paginating. naics_codeisn't in the default shape. Ifopp.naics_coderaisesAttributeError, you forgot theshape=override from Step 2.