Skip to content

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.

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.

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=true vs notice_type. active=true filters by Tango's archive flag — i.e., the opportunity isn't archived/expired. It does not filter by phase. If you want only currently-biddable solicitations, combine active=true with notice_type=Solicitation (or Combined Synopsis/Solicitation) and response_deadline_after=<today>.
  • NAICS code format. Six-digit string, no hyphens, no leading zeros stripped. "541512" is correct; 541512 (int) works in JSON but URL-encode it as a string. Five-digit and four-digit prefixes are not matched — use the NAICS browse path to find the exact six-digit codes you want, then |-OR them together.
  • Default ordering. Without ordering=, list responses come back in an undefined order. If you care about "soonest deadline first," always pass ordering=response_deadline. This matters double when paginating.
  • naics_code isn't in the default shape. If opp.naics_code raises AttributeError, you forgot the shape= override from Step 2.