Federal procurement data · API implementation
The SAM.gov API: build a pipeline, not a one-off call.
Build a reliable SAM.gov Opportunities API pipeline with API-key setup, filters, pagination, validation, amendment capture and bulk-history options.
A reliable SAM.gov API pipeline sends explicit posted-date and page parameters, retains every raw response, collects descriptions and public resources separately, and reconciles current API state with SAM.gov Data Services. The public Contract Opportunities endpoint is useful, but it is not a complete amendment stream: GSA says it returns the latest active opportunity version and directs users to Data Services for all versions.
The production search endpoint is:
GET https://api.sam.gov/opportunities/v2/search
Every production request should explicitly send an API key, postedFrom, postedTo, limit, and the zero-based page index named offset. The posted-date window must use MM/dd/yyyy and may span no more than one year. The documented page maximum is 1,000 records; the default is only one record. (GSA Get Opportunities Public API)
Those facts are enough to make a successful request. They are not enough to make a trustworthy feed.
- 01
Bounded query
Send the key, posted-date slice, explicit limit, page index, and only stable filters.
- 02
Raw evidence
Store the redacted request contract, response body, retrieval time, status, and page hash.
- 03
Page audit
Prove pages are distinct, reconcile collected rows to totalRecords, and record every slice.
- 04
Source model
Keep noticeId, raw solicitation number, type, base type, source dates, and nulls intact.
- 05
Resources
Fetch public descriptions and attachments separately; record access state and file evidence.
- 06
Reconciliation
Re-read overlapping windows and compare API state with bulk and archived Data Services files.
The public API is the collection edge. The evidence store, version strategy, resource archive, and reconciliation process make it a data product.
SAM.gov API: the short answer
Use this implementation sequence:
- Confirm that the public Contract Opportunities API v2 is the correct SAM.gov service for the question.
- Keep the API key in a server-side secret store and redact it anywhere a request URL can appear.
- Query short, explicit posted-date slices with
limit=1000and page indexes0, 1, 2…. - Save the raw response and a redacted run manifest before normalizing it.
- Reconcile collected pages to
totalRecords, detect repeated pages, and deduplicate by the source notice identifier. - Treat
description,resourceLinks, and other URLs as separate retrieval jobs with their own access and quality states. - Preserve
noticeId, the raw solicitation number, notice type, base type, organization path, dates, and source nulls. - Re-read overlapping windows because the API has no documented modified-since cursor or stable ordering.
- Use SAM.gov Data Services and retained snapshots when amendment history or reproducible backfills matter.
- Publish freshness, page reconciliation, missing-resource, duplicate, and historical-coverage measures with the data.
This guide assumes that SAM.gov Contract Opportunities is already the right US federal source. If the decision is still SAM.gov versus TED, Find a Tender, or Contracts Finder, start with the government tender source comparison. If the goal is contract actions or federal spending rather than opportunity notices, use the awarded-contract research guide.
Choose the correct SAM.gov API
“The SAM.gov API” is not one endpoint. SAM.gov exposes distinct services for distinct domains and permissions.
Use the public Opportunities API for published notice discovery
The public v2 search endpoint returns published Contract Opportunities records that match a bounded posted-date query. The source population includes presolicitations, solicitations, combined synopsis/solicitations, sources sought, special notices, justifications, award notices, surplus-property notices, and the documented Intent to Bundle Requirements (DoD-Funded) type. It is a federal procurement-notice source, not a state-and-local portal and not a complete contract-action ledger. (SAM.gov Contract Opportunities; GSA public Opportunities API)
Use it for jobs such as:
- collecting covered federal opportunity notices into an internal search product;
- filtering notices by agency, notice type, NAICS, classification, set-aside, location, or deadline;
- maintaining a current opportunity table with source links and contact fields;
- retrieving available descriptions and public attachments; and
- feeding a downstream qualification or monitoring workflow.
Do not call every returned record an open bid. A Sources Sought notice is market research; a Justification can describe a non-competitive path; an Award Notice is an outcome publication; and a record marked active can still have a passed response deadline. Source type, deadline, cancellation state, archive state, and the full notice evidence must remain separate.
Do not substitute the Opportunity Management API
The Opportunity Management API supports government workflows for creating, publishing, revising, and managing notices. Its authentication, roles, system-account permissions, IP validation, and endpoints are different. It is valuable documentation for understanding history, parent relationships, resources, and lifecycle actions, but a public reader should not design a collection service around management permissions it does not have.
Keep opportunity notices separate from award data
The public opportunity response can contain an award object for applicable notice types. That does not make it a complete federal contract-award source. Contract actions, IDVs, obligations, modifications, and spending analysis belong to the separate SAM.gov Contract Awards API and USAspending data model. Likewise, entity registration and exclusions use separate SAM.gov services.
Write the source contract at the top of the pipeline:
Published US federal Contract Opportunities records retrieved through the SAM.gov public Opportunities API v2, within stated posted-date slices and retrieval cutoffs, supplemented by identified Data Services files and successfully retrieved public resources.
That sentence prevents the feed from silently becoming “all government contracts.”
Get and protect a SAM.gov API key
A registered user can request a public API key from the Account Details area of SAM.gov. Production and alpha keys come from their respective environments. The public Opportunities documentation says daily request limits vary by account and role, but it does not publish one universal numeric quota for this endpoint. Do not copy a limit from another SAM.gov API and present it as an Opportunities guarantee. (GSA authentication guidance)
The API requires api_key in the query string. That creates an operational risk: URLs are commonly captured by application logs, reverse proxies, error trackers, command history, observability tools, and copied diagnostic messages.
Use these controls:
- store the key in a server-side secret manager or protected environment variable;
- call SAM.gov from a backend worker, not public browser code;
- redact the
api_keyparameter before logging the request; - disable full-query capture at proxy and tracing layers where possible;
- never persist the key in a raw-response manifest or attachment URL;
- separate production and alpha configuration;
- never share an individual-account API key; and
- update API keys and system-account passwords at least every 90 days, as required by the current SAM.gov Terms of Use.
An internal request object can contain the key in memory. The durable manifest should contain only api_key_present: true, the environment, and a non-secret key label if key-level operations must be traced.
Budget requests before the first backfill
Request limits matter because one opportunity can trigger more than one HTTP call:
- one or more paginated search calls;
- a description request;
- zero or more resource downloads;
- retries after eligible transient failures; and
- later overlap or reconciliation calls.
Estimate the page and resource budget by slice before attempting a multi-year replay. If the budget is tight, use the bulk files for the historical bootstrap and reserve API calls for current enrichment and resource capture.
Make the first Opportunities v2 request
The canonical production endpoint in GSA’s version section is:
GET https://api.sam.gov/opportunities/v2/search
Send all five core parameters explicitly, even though the prose documentation and linked OpenAPI specification do not describe optionality in exactly the same way:
api_key=REDACTED
postedFrom=08/04/2026
postedTo=08/05/2026
limit=1000
offset=0
The date format is MM/dd/yyyy, not ISO yyyy-MM-dd. GSA documents a maximum one-year span between postedFrom and postedTo. The same maximum applies when a response-deadline range is provided. GSA’s page also contains older examples with /prod/ in the path; use the endpoint shown in the current v2 version-control block and keep an integration test in both alpha and production. (GSA request parameters and examples)
Canonical public-search endpoint
https://api.sam.gov/opportunities/v2/search- Response population
- Latest active opportunity version matching the query
- History route
- SAM.gov Data Services and retained source snapshots
Collector invariants
- One manifest per slice: dates, filters, page count, row count, hashes, and cutoff.
- One raw object per response: normalize only after the source evidence is durable.
- One explicit policy for nulls: JSON null, literal “null”, absent, and failed resource are different states.
api_keyEvery request
Required by the public endpoint
Never retain it in URLs, logs, manifests, or error messages
postedFromEvery request
Start of the mandatory posted-date window
Use MM/dd/yyyy; do not send ISO dates
postedToEvery request
End of the mandatory posted-date window
Keep the pair within the documented one-year maximum
limitEvery request
Explicit records requested per page
Default is only 1; documented maximum is 1,000
offsetEvery request
Zero-based page index
Advance 0, 1, 2…; it is not documented as a row offset
Preserve the contract disagreement as a test
The prose table calls the posted dates required and describes defaults for limit and offset. The linked OpenAPI file marks limit and offset required while describing the date parameters differently. A production client should not guess which omission is safe. Sending all five removes the ambiguity.
The first contract test should assert:
- HTTP 200 and a JSON object for a known valid slice;
- numeric
totalRecords,limit, andoffsetvalues after parsing; - an
opportunitiesDataarray, including when it is empty; - returned limit and page index matching the request;
- no API key in the recorded URL or error body; and
- a stored retrieval timestamp and content hash.
Do not start normalization until this source-level contract passes.
Filter for the notice stages you need
The documented filters include notice type, solicitation number, notice ID, title, organization code or name, place-of-performance state or ZIP, set-aside code or description, NAICS, classification code, and response-deadline range. Older deptname and subtier request fields are deprecated. Prefer the current organization filters and retain the returned full hierarchy path fields. (GSA request parameters)
Keep the raw procurement type code
The current public documentation lists nine principal ptype codes. Their meanings belong in the source model, not in a single open-or-closed flag.
rSources Sought
Market research, not a bid-ready solicitation
pPresolicitation
Forward signal; inspect later related notices
oSolicitation
Candidate competition; verify deadline and documents
kCombined Synopsis/Solicitation
Candidate competition; verify requirements and response window
sSpecial Notice
Context-dependent; often not a competitive opportunity
uJustification
Non-competitive or limited-source evidence, not an open bid
aAward Notice
Outcome-stage notice, not an open opportunity
gSale of Surplus Property
Separate acquisition context
iIntent to Bundle Requirements (DoD-Funded)
Preserve the code and raw label; do not silently modernize historical values
The official pages have changed some labels over time. Persist ptype, the returned type, the returned baseType, and the raw source label. Map them to a versioned internal stage only after storage.
Avoid the tempting status shortcut
The public page lists values such as active, inactive, archived, cancelled, and deleted, but still marks the request parameter Coming Soon. Feature-test its production behavior before relying on it. Even if it works, it cannot answer “can a supplier still respond?” by itself. Derive actionability from notice type, response deadline, archive and cancellation evidence, the latest accepted snapshot, and accessible source documents.
Partition by source facts, not convenient keywords
Keyword searches are useful for discovery but weak as completeness boundaries. Titles and descriptions vary; attachments can carry decisive requirements; and a keyword can disappear in a revision. For a repeatable feed, make organization, type, classification, set-aside, geography, and date the primary partitions. Apply textual matching after the evidence has been collected.
Paginate and partition every bounded query
The response supplies totalRecords, limit, offset, and opportunitiesData. GSA describes offset as the page index, starting at zero. With limit=1000, request page indexes 0, 1, 2, and so on—not row offsets 0, 1000, 2000. The documentation provides no worked second-page example, stable ordering guarantee, cursor, or snapshot-isolation promise. (GSA pagination fields)
For each slice:
- request page zero;
- record its
totalRecordsand returned page metadata; - hash the ordered source IDs and the complete raw body;
- request the next page index;
- fail the slice if a non-empty page repeats the previous page’s ID hash;
- stop on an empty page or when
(page_index + 1) × limit >= totalRecords; - compare the final unique count with the first and last reported totals; and
- retain the discrepancy rather than hiding it with deduplication.
Use smaller date slices when the result set can move
Offset pagination over a changing result set can create gaps or repeats even when the client follows the documented page-index rule. A record inserted or revised while the walk is running can shift later pages. Smaller daily or weekly slices reduce the moving population and make reruns cheaper.
Large backfills should partition by non-overlapping calendar slices of at most one year, then deliberately overlap boundary dates for validation. Because the documentation does not state whether date boundaries are inclusive or which timezone defines them, treat the overlap as evidence gathering, not wasted work.
Deduplicate after proving what happened
Deduplication is not a substitute for page reconciliation. If the API returns a row twice, retain both page-level observations, report the duplicate, and emit one current source record downstream. If the source total is 2,004, the collected page rows are 2,003, and the unique count is 2,002, the run did not “succeed with 2,002 records.” It produced a documented coverage discrepancy that needs a retry or reconciliation.
Build a complete SAM.gov API collector
The example below shows the essential control flow. It intentionally collects one short date slice, sends all core parameters, advances the documented page index, checks for a repeated page, records changing totals, and keeps the API key out of the returned manifest.
import hashlib
import json
import os
import time
import requests
ENDPOINT = "https://api.sam.gov/opportunities/v2/search"
API_KEY = os.environ["SAM_GOV_API_KEY"]
LIMIT = 1000
ALLOWED_FILTERS = {
"ptype", "solnum", "noticeid", "title", "state", "zip",
"organizationCode", "organizationName", "typeOfSetAside",
"typeOfSetAsideDescription", "ncode", "ccode", "rdlfrom", "rdlto",
}
def collect_slice(posted_from, posted_to, extra_filters=None):
filters = dict(extra_filters or {})
unsupported = set(filters) - ALLOWED_FILTERS
if unsupported:
raise ValueError(f"Unsupported filter names: {sorted(unsupported)}")
page_index = 0
raw_pages = []
seen_page_fingerprints = set()
observed_totals = []
failure_reason = None
while True:
params = {
**filters,
"api_key": API_KEY,
"postedFrom": posted_from,
"postedTo": posted_to,
"limit": LIMIT,
"offset": page_index,
}
try:
response = requests.get(ENDPOINT, params=params, timeout=60)
except requests.RequestException:
raise RuntimeError(
f"SAM.gov request failed for page {page_index}"
) from None
if response.status_code == 404:
rows = []
payload = {"opportunitiesData": []}
total = None
failure_reason = "http_404_requires_context_validation"
elif not response.ok:
raise RuntimeError(
f"SAM.gov returned HTTP {response.status_code} "
f"for page {page_index}"
)
else:
payload = response.json()
rows = payload.get("opportunitiesData") or []
returned_limit = int(payload.get("limit"))
returned_offset = int(payload.get("offset"))
if returned_limit != LIMIT or returned_offset != page_index:
raise RuntimeError(
f"Page contract mismatch at index {page_index}"
)
total = int(payload.get("totalRecords") or 0)
observed_totals.append(total)
page_ids = [str(row.get("noticeId")) for row in rows]
fingerprint = hashlib.sha256(
json.dumps(page_ids, separators=(",", ":")).encode()
).hexdigest()
if rows and fingerprint in seen_page_fingerprints:
raise RuntimeError(f"Repeated non-empty page at index {page_index}")
seen_page_fingerprints.add(fingerprint)
raw_pages.append({
"page_index": page_index,
"retrieved_at_epoch": time.time(),
"http_status": response.status_code,
"content_type": response.headers.get("content-type"),
"response_sha256": hashlib.sha256(response.content).hexdigest(),
"raw_body": response.content,
"payload": payload,
})
if failure_reason:
break
if not rows or (page_index + 1) * LIMIT >= total:
break
page_index += 1
all_rows = [
row
for page in raw_pages
for row in (page["payload"].get("opportunitiesData") or [])
]
unique_notice_ids = {
row.get("noticeId") for row in all_rows if row.get("noticeId")
}
missing_notice_ids = sum(1 for row in all_rows if not row.get("noticeId"))
stable_total = bool(observed_totals) and len(set(observed_totals)) == 1
expected_rows = observed_totals[-1] if observed_totals else 0
reconciliation_ok = (
failure_reason is None
and stable_total
and len(all_rows) == expected_rows
and len(unique_notice_ids) == expected_rows
and missing_notice_ids == 0
)
manifest = {
"endpoint": ENDPOINT,
"posted_from": posted_from,
"posted_to": posted_to,
"filters": filters,
"limit": LIMIT,
"pages": len(raw_pages),
"rows": len(all_rows),
"unique_notice_ids": len(unique_notice_ids),
"missing_notice_ids": missing_notice_ids,
"observed_totals": observed_totals,
"failure_reason": failure_reason,
"reconciliation_status": (
"accepted" if reconciliation_ok else "quarantined"
),
"api_key_present": True,
}
return raw_pages, manifest
This is a collector skeleton, not a complete production service. It retains the exact response bytes in memory and quarantines runs whose counts do not reconcile. It also quarantines every 404 until the deployment has contract-tested the precise SAM.gov no-data response, because an endpoint, proxy, or route failure must never become a false zero. Production code should write response bytes to durable object storage before normalization. Add bounded retries, schema validation, key rotation, stricter content-type handling, metrics, scheduling, and a resource queue. Do not print prepared request URLs, raw request objects, raw response objects, or requests exceptions: they can contain the API key in the URL.
Make the raw page immutable
The saved page is evidence of what the API returned at a particular cutoff. Store it under a content-addressed or run-addressed key and never rewrite it after normalization. A useful object path includes source, environment, slice dates, page index, retrieval timestamp, and response hash—but never the API key.
The normalized current table can be replaced. The raw evidence cannot.
Capture descriptions, links and attachments
The API response does not necessarily contain the complete opportunity text or tender pack inline.
descriptionis a URL to the opportunity description, not the description body. GSA says the public API key must be appended when it is fetched.resourceLinksis a nullable array of direct resource URLs.additionalInfoLinkcan point to another source.uiLinkis not a durable public API identifier; GSA warns it can require an appropriate role and return 404 otherwise.- controlled or private resources remain access-controlled even when the notice itself is public. (GSA response fields; Opportunity Management resource rules)
Build a separate resource queue. For each URL, record:
- parent
noticeIdand accepted source-snapshot hash; - source URL with secrets removed;
- resource type: description, uploaded attachment, or external link;
- first-seen and retrieval timestamps;
- HTTP status and final URL;
- content type and byte length;
- SHA-256 for successfully retrieved bytes;
- access state: public, missing, controlled, forbidden, or transient failure; and
- parser or antivirus outcome if downstream processing is enabled.
Do not append the SAM.gov API key to arbitrary external URLs. Add it only where the official SAM.gov resource contract requires it. Enforce host allowlists and redirect limits, because a source field can point outside SAM.gov.
A missing attachment is not an empty attachment
Keep these states distinct:
- no
resourceLinksfield; - JSON
null; - an empty array;
- a URL that returns not found;
- a URL that requires access;
- a transient server failure; and
- a successfully downloaded zero-byte file.
Collapsing them into attachments = 0 destroys the evidence needed to diagnose coverage and access.
Fetch resources while they are available
GSA’s Data Services CSV files do not recreate every resource-link relationship. If durable tender documents matter, retrieve public resources during current ingestion, subject to terms, access controls, and request budgets. A later historical CSV backfill cannot be assumed to restore the exact attachment state that existed on the publication day.
Preserve source identifiers and raw fields
The API returns several fields that look interchangeable but are not.
noticeId is the source-record key
Store the opaque noticeId exactly as returned. Use it as the primary source-record identifier for the public response. Do not parse business meaning from its shape.
solicitationNumber is a business reference
The solicitation number can be useful for search and cautious lifecycle grouping, but official examples include surrounding whitespace, and real notice chains can reuse or vary the displayed value. Keep:
solicitation_number_raw;- a trimmed display value;
- an optional normalized search value;
- the issuing organization context; and
- the rule version used by any inferred grouping.
Never overwrite the raw value or use the normalized string alone as a global primary key.
Keep source type and base type
type describes the current opportunity type, while baseType describes the original type in the public response model. Both can matter when a notice evolves. Store the source values before mapping them to internal stages such as research, planned, bid-ready, award, cancelled, or archived.
Prefer hierarchy codes to deprecated labels
The response documents fullParentPathCode and fullParentPathName, while the older department, subtier, and office fields are deprecated. Retain both when present for source fidelity, but normalize organizations against the SAM.gov Federal Hierarchy Public API rather than treating a display name as a stable agency key.
Expect schema irregularities
The official documentation and examples show conditions a strict pipeline must tolerate without silently coercing:
- JSON null and literal string values such as
"null"; activerepresented as"Yes"or"No", not a Boolean;- award amounts documented as numbers but demonstrated as quoted decimals;
- naming variations such as
subtierandsubTier; - inconsistent spellings around the response-deadline field;
- posted dates shown as date-only in examples despite a date-time description; and
- optional contacts, place of performance, classifications, awards, and resources.
Parse flexibly into a typed staging layer, retain the raw token, and publish coercion failures. Do not make malformed source data disappear by replacing it with a plausible default.
Capture amendments and historical versions
GSA states that the public Opportunities API returns only the latest active version. It is therefore a current search interface, not a complete amendment ledger. The Opportunity Management API documents parent opportunity IDs, revision indexes, action dates, revision reasons, related opportunities, cancellations, archives, and history actions, but public search responses do not expose that complete model. GSA directs historical-version users to Data Services. (GSA public API overview)
Never overwrite the only copy of yesterday
For every accepted notice snapshot, store:
noticeId;- retrieval timestamp and source slice;
- raw-response hash;
- normalized-payload hash;
- source fields used for change detection;
- resource inventory hash; and
- parser and mapping versions.
When a later payload differs, retain both snapshots. Derive a change event that points to the before and after evidence. A current table can reference the latest accepted snapshot, but it should not contain the only history.
Distinguish observed change from source-declared revision
If the public API returns a changed payload for the same noticeId, you have observed a state difference. Do not invent an official revision number or revision reason that the public response did not provide. If Data Services or an authorized history source supplies an explicit version relationship, retain that separately and upgrade the event’s evidence level.
Keep related notices as relationships
A sources-sought notice, presolicitation, solicitation, cancellation, award notice, and later follow-on can share a business context without being the same record. Model evidence-backed parent or related IDs where available. Keep heuristic links based on solicitation number, title, office, and dates in a separate relationship table with method and confidence. Never merge source records to make the lifecycle look cleaner.
Run incremental updates without false confidence
The public endpoint documents posted-date filters, not a modified-since cursor. It does not document stable ordering, boundary inclusivity, or the timezone used by the request dates. A job that saves “last seen timestamp” and asks only for later posted dates has no documented completeness guarantee for changes to older notices.
Use three collection loops instead.
Recent-window collection
Short overlapping posted-date slices, every page, descriptions, and available public attachments.
Required evidence
Raw page hashes, totalRecords reconciliation, resource statuses, and duplicate-rate check.
State and event derivation
Compare the accepted source snapshot with the prior snapshot; preserve both before deriving changes.
Required evidence
New, changed, archived, cancelled, deadline-changed, and resource-changed counts with evidence links.
Bulk-history reconciliation
Refresh archived files, replay boundary windows, and compare API coverage with Data Services extracts.
Required evidence
Gap report by day, notice type and organization; unresolved differences remain visible.
A recent-window job optimizes freshness. A bulk replay optimizes historical coverage. Neither substitutes for the other.
Loop 1: collect recent posted-date slices
Run short overlapping slices frequently enough for the business freshness target. Persist every raw page and resource result. Re-running the overlap protects against late availability, moving pagination, boundary ambiguity, and retry gaps.
Loop 2: derive changes from snapshots
Compare source snapshots only after each slice passes validation. Derive new notice, changed field, changed deadline, newly archived, newly unavailable, and resource-change observations. The separate government contract tracker guide explains how to route those events without turning formatting changes into emergency alerts.
Loop 3: reconcile with bulk history
SAM.gov Data Services exposes current and archived Contract Opportunities file areas, including fiscal-year archived files. Use them for the historical bootstrap and scheduled reconciliation. GSA says active notices in the public API are updated daily and archived notices weekly, so a current API pass and an archived-file pass do not have the same freshness promise. (Contract Opportunities Data Services; archived files)
File presence is not proof of continuous coverage. Record the exact filename, source URL, retrieval timestamp, byte size, checksum, header version, fiscal-year label, row count, and any anomalous dates. Test the actual minimum and maximum source dates in every file.
Use expanding re-read horizons
A practical policy can combine:
- a short overlap on every frequent run;
- a wider rolling re-read daily;
- month or quarter replays on a scheduled cadence; and
- fiscal-year bulk reconciliation after archived refreshes.
Set the horizons from the cost of a missed change, request budget, source behavior observed in production, and the available bulk history. Publish the oldest date that is still being actively rechecked.
Handle errors, quotas and retries
The public documentation lists 200 success, 400 bad request, 404 no data, and 500 server error; its OpenAPI definition also lists 401 and 403. It does not document a complete rate-limit response contract for this endpoint. Build for the documented statuses and defensively handle other HTTP responses without pretending they are guaranteed. (GSA HTTP responses)
Use one explicit response policy across the collector and its resource queue.
200Accept after validation
Check content type, schema, page metadata, hashes, and reconciliation.
400Fail the slice
Fix the date, filter, page, or limit contract; do not retry unchanged.
401 / 403Escalate access
Check the key and permissions; stop blind retries.
404Interpret context
Empty search, missing description, and missing resource are different states.
429Honor if observed
Use Retry-After when present and record the undocumented endpoint behavior.
5xxRetry within a ceiling
Apply exponential backoff with jitter, then fail visibly.
Retry the request, not the interpretation
Retries must use the same redacted request contract. Do not silently widen dates, remove filters, or skip the failing page to make the job green. A successful fallback query is a different extraction and needs its own manifest.
Treat quota as a design input
Use explicit limit=1000, cache successfully retrieved descriptions and resources by content evidence, and avoid repeatedly downloading unchanged files. Measure calls by endpoint and outcome. A useful quota dashboard shows:
- search calls and successful pages;
- description and resource calls;
- retries by status class;
- calls per accepted unique notice;
- remaining or inferred budget when exposed; and
- collection slices deferred because the safety budget was reached.
When the source does not publish a numeric limit for the account, report observed behavior and configured ceilings—not an invented official quota.
Validate and reconcile every extraction run
A green HTTP status is transport success. A trustworthy run also passes source, pagination, schema, identity, and resource checks.
Run-level manifest
Record at least:
- endpoint and environment;
- posted-date and response-deadline windows;
- filters and page size;
- retrieval start and finish timestamps;
- page indexes attempted, accepted, retried, and failed;
- first, last, minimum, and maximum observed
totalRecords; - raw row count and unique
noticeIdcount; - repeated-page and cross-page duplicate counts;
- raw object hashes and storage references;
- description and resource outcomes;
- parser, schema, and mapping versions; and
- Data Services files used in reconciliation.
Quality gates
Reject or quarantine the slice when:
- a non-empty page fingerprint repeats;
- the returned page metadata disagrees with the request;
totalRecordschanges during one paginated slice, making the offset walk non-reconcilable;- a required top-level collection field disappears;
- page collection stops before its stated total without an explained source change;
- the API key appears in a stored URL, log, or manifest;
- a content-type or schema change prevents raw evidence from being parsed; or
- source objects cannot be stored durably.
Warn, but do not fabricate values, when:
- a solicitation number is blank or repeated;
- classification, deadline, organization, contact, or place of performance is missing;
- literal
"null"or unexpected field types occur; - a public description or resource cannot be retrieved; or
- a current record has no historical match in the available bulk files.
Publish denominators with the feed
Useful coverage measures include:
page reconciliation = accepted page rows ÷ expected rows at the stated cutoff
description coverage = successfully retrieved descriptions ÷ notices with a description URL
resource retrieval coverage = successfully retrieved public resources ÷ public resource URLs attempted
organization-code coverage = notices with a usable hierarchy code ÷ accepted notices
historical reconciliation = current source records matched to retained or bulk evidence ÷ current source records tested
The denominator and cutoff belong beside every percentage. “99% complete” without them is not an operational claim.
Choose API, Data Services or a normalized download
The correct access route depends on the job.
One-off discovery
SAM.gov web search
Fast human verification; not a reproducible bulk feed
Current filtered JSON
Public Opportunities API v2
Source-native control; you own pages, resources, history, quotas, and QA
Historical bootstrap
SAM.gov Data Services
Better replay path; profile file schemas and resource coverage
Authorized publishing
Opportunity Management API
Government roles, permissions, and system-account controls
Cross-source analysis
Normalized dataset or managed feed
Less connector ownership; review the provider's evidence contract
Do not force one interface to do every job. A strong architecture often uses Data Services for bootstrap, the public API for current collection and public resources, retained snapshots for observed change history, and a normalized delivery layer for consumers.
The government tenders dataset provides a ready-made route for US, EU, and UK opportunity analysis. It is an alternative to building the connector, not a replacement for the official record. Every normalized row should retain the source URL, source ID, retrieval cutoff, and coverage statement.
SAM.gov API production checklist
Before calling the collector production-ready, verify:
- [ ] the endpoint is the current public Opportunities v2 search endpoint;
- [ ] all five core parameters are explicit on every request;
- [ ] dates use
MM/dd/yyyyand slices remain within the documented maximum; - [ ]
offsetadvances as page indexes0, 1, 2…; - [ ] page hashes, row counts, unique IDs, and
totalRecordsare reconciled; - [ ] raw pages and redacted manifests are immutable and replayable;
- [ ] no API key appears in client code, logs, traces, stored URLs, or error reports;
- [ ] source nulls, literal
"null", inconsistent types, and naming variations are profiled; - [ ]
noticeIdand raw solicitation numbers are stored separately; - [ ] organization hierarchy codes and source labels are retained;
- [ ] descriptions and resources have a separate queue and access-state model;
- [ ] public, missing, forbidden, controlled, and transient resource states stay distinct;
- [ ] current snapshots are retained before deriving changes;
- [ ] no public-API observation is described as an official revision without source evidence;
- [ ] overlap, wider re-reads, and bulk-history reconciliation are scheduled;
- [ ] 400, 401/403, 404, defensive 429, and 5xx policies are tested;
- [ ] quota and retry budgets stop runaway collection;
- [ ] validation failures block publication rather than silently shrinking the dataset; and
- [ ] the delivered data states its source scope, retrieval cutoff, and coverage denominators.
If any unchecked item can cause a missed notice, false deadline, exposed credential, irreproducible total, or lost document, it is a release gate.
Frequently asked questions
What is the SAM.gov Opportunities API endpoint?
The current public production search endpoint documented by GSA is https://api.sam.gov/opportunities/v2/search. The alpha endpoint is https://api-alpha.sam.gov/opportunities/v2/search. Send an API key, posted-date range, explicit limit, and page index with every request.
Do I need a SAM.gov API key to download contract opportunities?
Yes for the public Opportunities API. Request the public key from the Account Details area of the appropriate SAM.gov environment. Keep it server-side and redact it from URLs and logs because this endpoint accepts the key as a query parameter.
How does SAM.gov API pagination work?
GSA documents offset as a zero-based page index and limit as records per page. Use page indexes 0, 1, 2…, set an explicit limit up to the documented 1,000 maximum, and reconcile collected pages against totalRecords. Test page distinctness because GSA does not document stable ordering or snapshot isolation.
Can I use a modified-since timestamp with the SAM.gov Opportunities API?
The public documentation exposes posted-date filters, not a documented modified-since cursor. Use overlapping posted-date re-reads, retain snapshots, schedule wider replays, and reconcile with Data Services rather than claiming a last-seen timestamp is complete.
Does the public SAM.gov API include every amendment?
No. GSA says the public endpoint returns only the latest active opportunity version and directs users to Data Services for all versions. Retain your own accepted snapshots and use official bulk or history evidence when complete version lineage matters.
Is a SAM.gov active notice always open for bids?
No. The response’s active field distinguishes active from archived in the documented model; it does not prove that the notice is a bid-ready competition or that its response deadline remains open. Evaluate notice type, deadline, cancellation and archive evidence, and the source documents.
How do I download SAM.gov opportunity descriptions and attachments?
Treat them as separate resource requests. The description field is a URL that requires the API key when fetched, while resourceLinks contains available resource URLs and can be null. Record status, access state, retrieval time, content type, size, and checksum. Do not append the key to arbitrary external URLs.
What should I use as the unique SAM.gov opportunity ID?
Store the returned noticeId as the opaque public-API source-record key. Keep the raw solicitation number separately for display, search, and cautious grouping. Do not assume solicitation numbers are clean, globally unique, or one-to-one with lifecycle records.
Should I use the API or SAM.gov bulk data?
Use the API for bounded current queries and resource retrieval. Use Data Services for historical bootstrap, all-version investigation, and scheduled archive reconciliation. Many reliable systems use both, then retain raw source evidence behind a normalized delivery layer.
Does the Opportunities API replace SAM.gov Contract Awards or USAspending?
No. Award notices inside Contract Opportunities are procurement notices. They are not a complete contract-action, obligation, modification, or payment ledger. Use SAM.gov Contract Awards and USAspending for post-award financial and transaction analysis.