SAM.gov API for contract opportunities: how it works and where it trips you up
There are two official ways to get SAM.gov contract opportunities into code: the Get Opportunities Public API, which needs a key, and the public daily data extract, a CSV with no key needed. Which one fits depends on how much you pull and how fresh it has to be.
Option 1: the Get Opportunities Public API
Endpoint: GET https://api.sam.gov/opportunities/v2/search
What GSA's documentation says you need:
api_key, a public API key. Request it on your SAM.gov Account Details page. You need a SAM.gov account, and GSA limits requests per day based on your account's role.postedFromandpostedTo, both mandatory, inMM/dd/yyyyformat, and no more than one year apart. Leave either one out and you get "PostedFrom and PostedTo are mandatory".limit, up to 1000. The default is 1, so a request withoutlimitreturns a single record.- Optional filters:
ptype(notice type, e.g.osolicitation,kcombined synopsis/solicitation,rsources sought),ncode(NAICS),typeOfSetAside,organizationName,state,solnum,rdlfrom/rdlto(response deadline), and more.
Gotchas
- Only the latest active version. GSA says the API "only provides the latest active version of the opportunity". For full version history (amendments), they point you to Data Services.
- The description is a link, not text.
descriptionis a URL that needs your API key appended to fetch the body. - Paging is ambiguous. The docs describe
offsetas a "page index", and third-party write-ups disagree about how it behaves. The safest approach is to keep each request under 1000 results, for example one day at a time, and checktotalRecordsagainst what you got. - Active notices refresh daily and archived ones weekly, according to GSA.
import os, requests
from datetime import date, timedelta
URL = "https://api.sam.gov/opportunities/v2/search"
KEY = os.environ["SAM_API_KEY"]
rows = []
for back in range(7): # one day per request keeps results under the limit
day = (date.today() - timedelta(days=back)).strftime("%m/%d/%Y")
r = requests.get(URL, timeout=60, params={
"api_key": KEY, "postedFrom": day, "postedTo": day,
"ncode": "541511", "limit": 1000, "offset": 0})
r.raise_for_status()
data = r.json()
got = data.get("opportunitiesData") or []
if data.get("totalRecords", 0) > len(got):
print(f"{day}: {data['totalRecords']} matches but only {len(got)} returned; narrow the filter")
rows += got
for o in rows[:10]:
print(o["postedDate"], o.get("type"), o.get("solicitationNumber"), o["title"][:70])
Option 2: the public daily extract (no key)
SAM.gov Data Services publishes the full Contract Opportunities list as a public CSV, regenerated once a day:
https://sam.gov/api/prod/fileextractservices/v1/api/download/Contract%20Opportunities/datagov/ContractOpportunitiesFullCSV.csv?privacy=Public
It's one large file, encoded as Windows-1252 rather than UTF-8. Columns include NoticeId, Sol#, Type, PostedDate, ResponseDeadLine, NaicsCode and SetASideCode. You don't need a key and there's no daily request quota, but the data can be up to a day behind the website. Stream it rather than loading it into memory.
We built this
Disclosure: Your Evenings Ltd built and sells this. It isn't an official government product and isn't affiliated with GSA or SAM.gov. SAM.gov & Grants.gov Scraper on the Apify Store reads the public daily extract for you, so you don't need a SAM.gov API key:
- It filters by keyword (whole phrases), NAICS prefix, agency, set-aside, notice type, posted window and due date, checked in code on every row.
- It returns Grants.gov opportunities in the same schema if you want both.
- Every row has
source_updated_at, so you can see how fresh it is. SAM.gov rows can be up to about a day old. monitorModeon a daily schedule returns only notices you haven't seen before.trackSolicitationNumberswatches the solicitations you're bidding on and reports new notices, amendments and deadline moves the next day.
More on tracking changes: SAM.gov amendment alerts.
FAQ
Does the SAM.gov API need a key? Yes. The Get Opportunities API needs a public API key from your SAM.gov Account Details page. The daily Contract Opportunities CSV extract doesn't.
What date format does SAM.gov's API use?
MM/dd/yyyy for postedFrom, postedTo, rdlfrom and rdlto. The posted-date range can be at most one year.
Why does my SAM.gov API call return only one result?
limit defaults to 1. Set it, up to 1000.
Product names and logos belong to their owners. Your Evenings Ltd is not affiliated with or endorsed by them. This product uses the Grants.gov API but is not endorsed or certified by the U.S. Department of Health and Human Services.