The SAM.gov Opportunities API v2 accepts a naics query parameter. The documentation implies it will filter results to matching NAICS codes. It doesn’t.
What actually happens
GET https://api.sam.gov/prod/opportunities/v2/search?api_key=...&naics=541511&limit=100
Returns opportunities with NAICS codes 541330, 561210, 711510, and anything else currently open — not just 541511. The filter is parsed, no error is returned, but the result set is unfiltered.
This appears to be a persistent bug in the v2 API. It isn’t mentioned in the documentation.
Fix: filter client-side
Fetch without the naics parameter and apply your own filter on the naicsCode field in the response:
import urllib.request, json
SAM_KEY = "your_api_key"
TARGET_NAICS = {'541511', '541512', '541513', '541519', '518210'}
url = f"https://api.sam.gov/prod/opportunities/v2/search?api_key={SAM_KEY}&limit=100&postedFrom=01/01/2026"
req = urllib.request.Request(url, headers={"Accept": "application/json"})
data = json.loads(urllib.request.urlopen(req, timeout=30).read())
all_opps = data.get("opportunitiesData", [])
filtered = [o for o in all_opps if str(o.get("naicsCode", "")) in TARGET_NAICS]
print(f"Total returned: {len(all_opps)}, matching NAICS: {len(filtered)}")
The naicsCode field in the response payload is accurate — the problem is only with the server-side query filter.
Agency and deadline fields are also unreliable
While we’re here: fullParentPathName (the agency name) is frequently empty even when the opportunity clearly belongs to a specific agency. responseDeadLine is missing on pre-solicitations.
If you need accurate agency/deadline data, enrich after the fact by querying for the specific notice ID:
def enrich_opp(opp_id, api_key):
url = f"https://api.sam.gov/prod/opportunities/v2/search?api_key={api_key}¬iceid={opp_id}&limit=1"
req = urllib.request.Request(url, headers={"Accept": "application/json"})
data = json.loads(urllib.request.urlopen(req, timeout=10).read())
opps = data.get("opportunitiesData", [])
if not opps:
return {}
o = opps[0]
return {
"agency": o.get("fullParentPathName") or o.get("departmentName", ""),
"deadline": o.get("responseDeadLine") or o.get("archiveDate", ""),
"naics": str(o.get("naicsCode", "")),
}
Stay under 10 requests/second. A time.sleep(0.15) between calls keeps you safely within the rate limit.