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.