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}&noticeid={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.