DEV Community

CBLU2005
CBLU2005

Posted on

Track SAM.gov contract opportunities without an API key (or a $300/month GovCon subscription)

If you sell anything to the US federal government, you live and die by SAM.gov's Contract Opportunities — every proposed contract action over $25,000 gets posted there. The problem is consuming it:

  • The website is slow, aggressively bot-protected, and unusable for bulk work.
  • The official API requires a SAM.gov account, an API key that expires every 90 days, and has rate limits that hurt (Google "sam.gov api key" and enjoy the pain threads).
  • The GovCon SaaS tools that repackage this data start around $100/month and run into the thousands.

Here's the thing almost nobody seems to know: the sam.gov website's own search backend is a public, keyless JSON API. No account. No API key. No 90-day key rotation. It's the same endpoint your browser hits when you search Contract Opportunities — it's just never advertised as an API.

(You may have read older posts pointing at SAM.gov's daily CSV extract on S3 — ContractOpportunitiesFullCSV.csv. That was the best free path for years, but since late July 2026 SAM.gov has been regenerating it as a header-only stub: 47 column names, zero rows. If your pipeline was built on it, that's why it's suddenly empty. The search API below is the working replacement — and unlike the 200+ MB daily download, it filters server-side.)

The endpoint

https://sam.gov/api/prod/sgs/v1/search/
Enter fullscreen mode Exit fullscreen mode

Standard query parameters, JSON out. The important ones:

Param Meaning
index=opp&mode=search&responseType=json boilerplate — always send these
page / size paging; size max 100
sort=-modifiedDate newest first
is_active=true only still-open notices
naics comma-separated NAICS codes (prefixes work)
pop_state place-of-performance states, e.g. TX,FL
set_aside set-aside codes (SBA, 8A, WOSB, SDVOSBC...)
publish_date.from / .to needs an explicit UTC offset, e.g. 2026-07-01-04:00
q + qMode=ALL keyword search

Check it yourself — active custom-programming solicitations, newest first:

curl "https://sam.gov/api/prod/sgs/v1/search/?index=opp&mode=search&responseType=json&sort=-modifiedDate&size=5&page=0&is_active=true&naics=541511" \
  -H "Accept: application/hal+json"
Enter fullscreen mode Exit fullscreen mode

Two things the search results don't carry: the contracting officer's contacts and the full description. Those live on a second keyless endpoint, one call per notice:

https://sam.gov/api/prod/opps/v2/opportunities/<noticeId>
Enter fullscreen mode Exit fullscreen mode

DIY: a NAICS watcher in ~35 lines of Python

import requests

SEARCH = "https://sam.gov/api/prod/sgs/v1/search/"
DETAIL = "https://sam.gov/api/prod/opps/v2/opportunities/{}"
HEADERS = {"Accept": "application/hal+json"}

params = {
    "index": "opp", "mode": "search", "responseType": "json",
    "sort": "-modifiedDate", "size": 100, "page": 0,
    "is_active": "true",
    "naics": "541511,541512",            # custom programming, systems design
    "publish_date.from": "2026-07-01-04:00",
}

while True:
    data = requests.get(SEARCH, params=params, headers=HEADERS, timeout=60).json()
    results = data.get("_embedded", {}).get("results", [])
    if not results:
        break
    for r in results:
        # contacts + description live on the detail endpoint
        d = requests.get(DETAIL.format(r["_id"]), headers=HEADERS, timeout=60).json()
        data2 = d.get("data2", {})
        poc = next(iter(data2.get("pointOfContact") or []), {})
        due = (data2.get("solicitation", {}).get("deadlines", {}).get("response") or "?")[:10]
        print(f'{(r.get("publishDate") or "")[:10]}  {r["title"][:70]}  '
              f'due={due}  poc={poc.get("email", "")}  '
              f'https://sam.gov/opp/{r["_id"]}/view')
    if (params["page"] + 1) * params["size"] >= data["page"]["totalElements"]:
        break
    params["page"] += 1
Enter fullscreen mode Exit fullscreen mode

That's a working daily bid-pipeline feed with zero accounts and zero dollars. Cron it, pipe it to Slack, done.

Things you'll add within a week of using it seriously: the per-notice detail calls are 1:1 with results, so you'll want concurrency with a cap and retry/backoff (sam.gov will throttle a tight loop — it's the same bot protection the website has); the API stops paging at 10,000 records per query, so broad searches need date-windowing; the date params silently return wrong windows without that UTC offset; set-aside and notice-type filtering take internal codes; and you'll want dedupe across days. None of it is hard; all of it is maintenance.

The maintained shortcut

I package exactly this — the same search API the sam.gov site uses, paged, enriched with the per-notice contact details, all filters server-side, with the (currently broken) CSV extract kept as an automatic fallback source — as an Apify actor:

SAM.gov Contract Opportunities Scraper

Everything still biddable in construction trades, small-business set-aside, in Texas or Florida:

{
    "naicsCodes": ["236", "237", "238"],
    "setAsides": ["SBA"],
    "popStates": ["TX", "FL"],
    "postedAfter": "2026-06-01",
    "responseDueAfter": "2026-07-11",
    "maxResults": 1000
}
Enter fullscreen mode Exit fullscreen mode

Out comes clean JSON/CSV/Excel with structured contacts, award data on award notices, and direct SAM.gov links. Schedule it daily in Apify, add a webhook, and you have the core of the $300/month GovCon alert products for about the price of a coffee per month (pricing is per record returned).

Which route should you take?

  • One NAICS code, personal use → the Python script above. Seriously, it's ~35 lines.
  • Multiple filters, scheduling, teammates who want CSVs, or feeding an AI agent → the actor (it's also callable as an MCP tool via Apify, which makes "any new cybersecurity RFPs today?" a one-line agent query).
  • Enterprise BD with pipeline analytics → that's when the SaaS subscriptions earn their keep.

The data itself is public, official, and free either way — which is exactly how government contracting data should be.

Top comments (0)