Skip to article

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.

Published August 5, 202622 min readBy DanielReviewed by Alexandra

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.

A reliable SAM.gov opportunity pipeline
  1. 01

    Bounded query

    Send the key, posted-date slice, explicit limit, page index, and only stable filters.

  2. 02

    Raw evidence

    Store the redacted request contract, response body, retrieval time, status, and page hash.

  3. 03

    Page audit

    Prove pages are distinct, reconcile collected rows to totalRecords, and record every slice.

  4. 04

    Source model

    Keep noticeId, raw solicitation number, type, base type, source dates, and nulls intact.

  5. 05

    Resources

    Fetch public descriptions and attachments separately; record access state and file evidence.

  6. 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:

  1. Confirm that the public Contract Opportunities API v2 is the correct SAM.gov service for the question.
  2. Keep the API key in a server-side secret store and redact it anywhere a request URL can appear.
  3. Query short, explicit posted-date slices with limit=1000 and page indexes 0, 1, 2….
  4. Save the raw response and a redacted run manifest before normalizing it.
  5. Reconcile collected pages to totalRecords, detect repeated pages, and deduplicate by the source notice identifier.
  6. Treat description, resourceLinks, and other URLs as separate retrieval jobs with their own access and quality states.
  7. Preserve noticeId, the raw solicitation number, notice type, base type, organization path, dates, and source nulls.
  8. Re-read overlapping windows because the API has no documented modified-since cursor or stable ordering.
  9. Use SAM.gov Data Services and retained snapshots when amendment history or reproducible backfills matter.
  10. 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_key parameter 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)

The minimum request contract

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.
01
api_key

Every request

Required by the public endpoint

Never retain it in URLs, logs, manifests, or error messages

02
postedFrom

Every request

Start of the mandatory posted-date window

Use MM/dd/yyyy; do not send ISO dates

03
postedTo

Every request

End of the mandatory posted-date window

Keep the pair within the documented one-year maximum

04
limit

Every request

Explicit records requested per page

Default is only 1; documented maximum is 1,000

05
offset

Every 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, and offset values after parsing;
  • an opportunitiesData array, 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.

Notice code is not opportunity state
01r

Sources Sought

Market research, not a bid-ready solicitation

02p

Presolicitation

Forward signal; inspect later related notices

03o

Solicitation

Candidate competition; verify deadline and documents

04k

Combined Synopsis/Solicitation

Candidate competition; verify requirements and response window

05s

Special Notice

Context-dependent; often not a competitive opportunity

06u

Justification

Non-competitive or limited-source evidence, not an open bid

07a

Award Notice

Outcome-stage notice, not an open opportunity

08g

Sale of Surplus Property

Separate acquisition context

09i

Intent 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:

  1. request page zero;
  2. record its totalRecords and returned page metadata;
  3. hash the ordered source IDs and the complete raw body;
  4. request the next page index;
  5. fail the slice if a non-empty page repeats the previous page’s ID hash;
  6. stop on an empty page or when (page_index + 1) × limit >= totalRecords;
  7. compare the final unique count with the first and last reported totals; and
  8. 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.

The API response does not necessarily contain the complete opportunity text or tender pack inline.

  • description is a URL to the opportunity description, not the description body. GSA says the public API key must be appended when it is fetched.
  • resourceLinks is a nullable array of direct resource URLs.
  • additionalInfoLink can point to another source.
  • uiLink is 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 noticeId and 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 resourceLinks field;
  • 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";
  • active represented as "Yes" or "No", not a Boolean;
  • award amounts documented as numbers but demonstrated as quoted decimals;
  • naming variations such as subtier and subTier;
  • 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.

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.

Three loops, three different guarantees
Frequent01

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.

Daily02

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.

Weekly / periodic03

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.

Response handling policy
200

Accept after validation

Check content type, schema, page metadata, hashes, and reconciliation.

400

Fail the slice

Fix the date, filter, page, or limit contract; do not retry unchanged.

401 / 403

Escalate access

Check the key and permissions; stop blind retries.

404

Interpret context

Empty search, missing description, and missing resource are different states.

429

Honor if observed

Use Retry-After when present and record the undocumented endpoint behavior.

5xx

Retry 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 noticeId count;
  • 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;
  • totalRecords changes 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.

Choose access by job
01

One-off discovery

SAM.gov web search

Fast human verification; not a reproducible bulk feed

02

Current filtered JSON

Public Opportunities API v2

Source-native control; you own pages, resources, history, quotas, and QA

03

Historical bootstrap

SAM.gov Data Services

Better replay path; profile file schemas and resource coverage

04

Authorized publishing

Opportunity Management API

Government roles, permissions, and system-account controls

05

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/yyyy and slices remain within the documented maximum;
  • [ ] offset advances as page indexes 0, 1, 2…;
  • [ ] page hashes, row counts, unique IDs, and totalRecords are 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;
  • [ ] noticeId and 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.