Search US federal contract opportunities with one API
The SAM.gov API returns US federal contract opportunities as clean JSON, with no api_key and no registration.
3 active endpoints. Every call is 1 credit.
- POST/sam-gov/v1/search
- POST/sam-gov/v1/opportunity
- POST/sam-gov/v1/attachments
What SAM.gov endpoints does ReefAPI ship?
3 live read endpoints. Read-only data API: no writes, no account actions, no dashboard access on the target site.
SAM.gov API
3 of 3 endpoints, ready to run
Every federal notice matching a phrase, each with its response deadline in UTC and in local time with the timezone named, the buying department and office with its full chain, the notice type and a text preview.
// Press "Try it" and this pane shows exactly what the // live site returned this second — including an empty // result, if that is the truth. No key, no account.
How the SAM.gov API works
SAM.gov is a normal ReefAPI surface — the same four rules that hold for every other engine on the key.
No OAuth app, no request signing, no per-site account. One key covers all 185 engines.
Every route is a POST with a JSON body. Parameters are validated against the published schema before anything is charged.
Credits, not seats. Failed and blocked calls are never charged, and cache hits cost nothing.
One envelope everywhere. meta carries latency_ms, record_count and the endpoint that answered.
Find what is still open, then read the whole requirement
Search gives you a 250-character preview and a deadline. The requirement itself is in the full notice and, more often, in the documents attached to it.
{"q": "cybersecurity", "notice_types": "r", "active_only": true, "limit": 10}Sources-sought and still open: the stage where a buyer is shopping and a response can still shape the requirement.
{"id": "..."}The complete text, the set-aside, the classification codes and the award block when there is one.
{"id": "..."}The statement of work and the pricing sheet, with a direct public download URL each. A notice with none says so explicitly rather than returning an empty list.
Three credits turn a keyword into a qualified, dated, documented federal opportunity. Deadlines arrive both in UTC and in local time with the timezone named, so you can diary them without guessing.
curl -X POST https://api.reefapi.com/sam-gov/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"q":"web scraping","limit":5}'{
"ok": true,
"data": { … },
"meta": {
"api": "sam-gov",
"endpoint": "search",
"mode": "live",
"latency_ms": …,
"record_count": …
},
"error": null
}The parameters that actually change your result count
Four parameters move the numbers here, and one of them behaves differently from the other three because it is applied after fetching rather than upstream. Knowing which is which stops you from reading total_matches as a filtered count. All figures measured on 2026-08-27.
| Parameter | What it does | Measured effect |
|---|---|---|
| q with phrase true (the default) | Quotes your term so it matches as one phrase | 'web scraping' returned total_matches 11, and query_sent echoed the quoted form |
| phrase false | Every word anywhere in the notice, in any order | The same query returned total_matches 76 |
| active_only true | Applied upstream. Removes anything past its deadline or archived | 11 matches became 1 |
| published_within_days | Applied AFTER fetching, so total_matches stays unfiltered | 'data extraction' held total_matches 485 while filtered_out reported 91 rows removed |
| notice_types | Nine codes. An unknown one is rejected, not ignored | ['zz'] returned INVALID_PARAM listing p, k, r, o, g, s, i, a, u |
| offset plus limit | Hard ceiling of 10,000, the index's own maxAllowedRecords | offset 9999 with limit 2 returned INVALID_PARAM |
| sort newest | Applied locally after fetching, because upstream date sorting is broken | meta echoes upstream_sort '-modifiedDate' and upstream_date_filter_supported false |
| id | The 32-hex notice id, which is NOT the solicitation number | ffabe21b876649d2a1669edb4858cd48 for solicitation W91ZLK-26-A-0123D |
| description_preview | 250 characters on search rows only | The same notice's full body from `opportunity` ran 3,642 characters |
| NAICS and PSC | Only on `opportunity`, never on a search row | naics_codes ['518210'] (an array of 6-digit strings), product_service_code 'B506' (a single 4-character code) |
Two notices can carry near-identical solicitation numbers and be different records. A single search returned W91Zlk-26-A-01234D as a Sources Sought and W91ZLK-26-A-0123D as a Special Notice, same buying office, same project. The 32-hex id is the only safe key: use it for deduplication and for every follow-up call.
How current is it, how much of it can I still bid on, and how deep does paging go
Measured on 2026-08-28 across six searches on the same phrase with different filters, plus one notice opened in full.
One broad phrase returned 8,965 matching notices. Restricted to the last ONE day, the first 25 rows were all published the same day we measured, the newest about two hours before the call — and the raw upstream ordering, which does no local sorting at all, returned the same-day rows too. Two independent routes to the same answer, so this is a live procurement feed and not an artefact of how the page was sorted.
The same 8,965 matches dropped to 964 with the still-open filter on. That is the honest ratio: most of what is indexed has closed. Restricting to sources-sought notices gave 2,126, and sources-sought that are still open gave 62. Each of those is a different question, and the counts tell you which one you asked.
Sorted newest, the top hit for a security phrase was a notice for a wheeled excavator — the phrase was somewhere in the boilerplate, and newest means newest, not best. Sorted by relevance, the same query's top three were all genuinely on topic. Use relevance to explore and newest to monitor.
Every row carries the response deadline as UTC, again as a local time with an offset, and the timezone name beside it. Federal deadlines are stated in local time and a mis-converted one is a missed bid; nothing here makes you infer it.
On 25 rows: id, URL, title, solicitation number, notice type and code, text preview, publish and modify dates, all three deadline fields, the active and cancelled flags, the buyer name and the buyer block were 25 of 25. The buyer block names the department, the office, its code, its location and the full chain from cabinet department down to the buying office.
The response reports the index's own hard ceiling: offset plus limit may not exceed 10,000. On a 9,000-match query that is effectively the whole result set; on a broader one it is not, so narrow with notice type or the still-open filter rather than paging into it. A notice with no public documents returns a NOT_FOUND that says so in words, not an empty array you could mistake for a fetch failure.
What people build with SAM.gov
The jobs this data is most often used for.
endpoints
credit per call
GovCon business-development teams call search to catch new sources-sought notices and solicitations in their space as they publish.
Bid/no-bid workflows use opportunity to read the full solicitation body, the set-aside type and the response deadline before committing.
Capture teams use the contacts published on an opportunity to reach the contracting office directly.
Competitive analysts follow awardee across award notices to see who is winning work under a given product-service code.
What SAM.gov data costs
The cheapest call here is 1 credit, so $15/mo (Pro) buys 10,000 of them — $1.50 per 1,000 credits. Credits roll over and never expire, and failed or blocked calls are not charged.
Full pricing →- 1,000 free credits on signup, no card
- One key, all 185 APIs, one credit pool
- Failed and blocked calls are never charged
- Credits roll over and never expire
Call it in two lines
Sign up, get 1,000 credits and one key that works on every engine. Then this is the whole protocol.
curl -X POST https://api.reefapi.com/sam-gov/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"q":"web scraping","limit":5}'import requests
r = requests.post(
"https://api.reefapi.com/sam-gov/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"q": "web scraping",
"limit": 5
},
)
print(r.json()["data"])Have a question? We got answers.
The questions people actually ask before wiring up SAM.gov.
Get a free key →What is the difference between the notice id and the solicitation number?▾
The id is a 32-character hex string minted by the system and unique per notice, such as ffabe21b876649d2a1669edb4858cd48. It is what `opportunity` and `attachments` take, and it is what appears in the sam.gov/opp/<id>/view URL, so pasting that whole URL works too. The solicitation number is the buying office's own reference, and it is neither unique nor consistently formatted: one search returned W91Zlk-26-A-01234D and W91ZLK-26-A-0123D as two separate notices for the same project. Key on the id.
Why is my exact-phrase search returning so few results?▾
Because phrase matching is the default and it is doing its job. 'web scraping' returned 11 notices as a quoted phrase and 76 with phrase false, which loosens to every word anywhere in any order. The response echoes back exactly what was sent upstream in query_sent, so you can always confirm which mode you got. Start with the phrase for precision, and switch to phrase false when you are casting a wide net over a category rather than hunting one term.
Why does total_matches stay the same when I set published_within_days?▾
Because that filter runs here, after the rows arrive, rather than upstream. A search for 'data extraction' with published_within_days 90 kept total_matches at 485, the unfiltered upstream count, and reported filtered_out 91 for the rows removed locally. The practical consequence: the parameter narrows what you receive but does not reduce what was fetched, so treat total_matches as the size of the unfiltered match set and use returned plus filtered_out to understand your page. active_only is the opposite case; it runs upstream and cut 11 matches to 1.
Which notice-type codes should I ask for?▾
Nine exist and they answer different questions. 'r' Sources Sought means the buyer is still asking who can do this, which is the cheapest door for a new vendor. 'p' Presolicitation announces an upcoming requirement. 'o' Solicitation and 'k' Combined Synopsis/Solicitation are the live RFPs you can bid on. 's' Special Notice covers industry days and RFIs. 'a' Award Notice tells you who won and therefore who the incumbent is. 'u' Justification and Approval explains a sole-source award, 'i' covers bundling intent, 'g' surplus property sales. An unrecognized code is rejected with the full list in the error rather than being silently dropped.
How are deadlines and timezones represented?▾
Three fields that agree with each other. response_deadline is UTC with a Z suffix, response_deadline_local is the same instant carried with its offset, and response_timezone is an IANA name such as America/New_York. A deadline measured as 2026-08-18T15:00:00Z came back locally as 2026-08-18T11:00:00-04:00. Award notices return all three as null, on 3 of 3 measured, because an award has no response deadline. archive_date is a bare date with no time at all.
What tells me whether a notice is still live?▾
Three independent booleans, and they are not redundant. is_active is the one to filter on for biddability. is_archived says the record has aged out of the active index, and it pairs with archive_date. is_cancelled says the buyer withdrew the requirement, which is different from letting it expire. One measured notice returned is_active false with is_archived true and archive_date 2026-08-18, alongside revision '2' (a string, not a number) which counts amendments. The search row for the same notice reported revisions 2 as an integer, so watch the type when you compare them.
How do I reach the contracting officer?▾
The `opportunity` action returns a contacts[] array as published on the notice, typed as primary and secondary. A measured notice returned a primary contact with a name, a .mil e-mail address and a phone number as bare digits with no formatting or extension separator, plus a secondary contact whose phone was null. title was null on both. Two contacts on one notice is common and the primary is the one to write to; the secondary is usually the specialist who will actually answer.
How do I get the actual statement of work?▾
Call `attachments` with the same notice id. It returned two files for a measured notice, each with a name, kind, size_bytes, posted_at and a direct public download URL under the sam.gov file-resource path, with access 'public'. The real requirement usually lives in those PDFs rather than in the body. One shape to expect: the `mime` field carries a file extension such as '.pdf' rather than a MIME type, so branch on the extension or on the name, not on a media type string.
What is the SAM.gov API?▾
SAM.gov API is a ReefAPI endpoint group for us federal contract opportunities, response deadlines and award notices. It returns live JSON through POST requests under /sam-gov/v1.
Is the SAM.gov API free to try?▾
Yes. ReefAPI starts with 1,000 free credits, no card required. SAM.gov calls use the same shared credit balance as every other ReefAPI engine.
Do I need a SAM.gov login or account?▾
No login to SAM.gov is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.
How fresh is the SAM.gov data?▾
The page example is captured from a live search call, and production requests fetch live data through ReefAPI rather than a static sample.
How many credits does the SAM.gov API use?▾
SAM.gov actions currently cost 1 credit per successful call. Failed or blocked calls are free, and all APIs draw from one credit pool.
Can I call SAM.gov from an AI assistant or MCP client?▾
Yes. Connect ReefAPI once through MCP and your assistant can call sam-gov actions with the same key, credit pool and JSON envelope used by normal REST requests.
2 Government & Tenders APIs on the same key
One key, one credit pool, one response envelope. If you are pulling SAM.gov, you are one call away from the rest of the category — no second contract, no second integration.
Need something this API does not do?
Name the endpoint, the field, or a source we do not carry yet. We ship new APIs every week and you would be first to get the key. Real people read every message and reply the same day.
Try it on your own data before you pay anything
The call above is the real endpoint, not a recording. A free key gives you 1,000 credits, the other 184 APIs, and the same envelope everywhere.
Endpoints, parameters and credit costs on this page are read from the live catalog and cannot drift from what the API accepts. Field notes were captured on 2026-08-28.