# SAM.gov Contract Opportunities from the Public Data Extract (`wx-grid-forecasts/sam-contract-opportunities`) Actor

US federal contract opportunity notices from SAM.gov's public data extract, filtered by NAICS, keyword, notice type, set-aside, state, posted date and deadline. Each row links to the notice on SAM.gov; contact, award and free-text description fields are not included.

- **URL**: https://apify.com/wx-grid-forecasts/sam-contract-opportunities.md
- **Developed by:** [WX Grid Forecasts](https://apify.com/wx-grid-forecasts) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 opportunity rows

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SAM.gov Contract Opportunities from the Public Data Extract

This Actor returns US federal contract opportunity notices (solicitations, combined synopsis/solicitations, presolicitations, sources
sought, special notices, justifications and other notice types) from SAM.gov's public "Contract Opportunities" data extract, filtered the
way you ask, one dataset row per notice. It is for firms deciding which federal solicitations to answer, and for people tracking federal
procurement.

### What a row contains, and what it does not

Each row holds the notice's identity, dates, classification, set-aside, contracting office and place of performance, plus `link`: the
notice page on SAM.gov. **In every row, contact and award fields are not included; free-text descriptions are not included.** To read
the notice text, its attachments or its point of contact, follow the row's `link` to the notice on SAM.gov.

- The extract's ten contact columns (primary and secondary contact title, name, email, phone, fax) are never read.
- The award columns (award number, award date, amount, awardee) and the additional-info link are never read.
- The notice description is read only to match your keywords; it is not returned.
- The title is returned. Links, email addresses and phone numbers inside a title are replaced with `[link removed]` or
  `[contact detail removed]` (FAR/DFARS clause numbers are kept).

### Where the data comes from

The source is the CSV data extract that SAM.gov publishes through its Data Services for the "Contract Opportunities" domain
(`ContractOpportunitiesFullCSV.csv`, privacy level Public), served by sam.gov through a signed Amazon S3 link
(falextracts.s3.amazonaws.com). SAM.gov describes the data as "a government-wide list of notices of proposed contract actions expected to
exceed $25,000". The extract read on 2026-10-16 was about 200 MB with 47 columns and holds tens of thousands of notices. SAM.gov
republishes the extract (its Last-Modified time was read once, on 2026-10-16; how often it changes is not yet measured); a notice posted
today appears only in a later file.

SAM.gov's Terms of Use (https://sam.gov/about/terms-of-use, read 2026-10-16) say under "Reuse and Copyright": "Most material on our site is
free of copyright and may be copied and distributed without permission. Works produced by federal government employees in the course of
their employment are generally not protected by copyright and are in the public domain in the U.S." They also say "You may also download
some SAM.gov data." This Actor downloads the one published extract file, once per run. It does not scrape SAM.gov pages and does not use
SAM.gov's search API or keyed APIs: no account, no login, no API key.

### Fields

All fields are strings unless noted. Text fields the extract leaves empty are empty strings; a date the extract leaves empty is left
out of the row (for example `response_deadline` on many award and special notices).

- Identity: `opportunity_id` (the SAM.gov notice id, lower-case; stable across runs), `notice_id` (as the extract writes it), `title`,
  `solicitation_number`, `notice_type`, `base_type`, `link` (the notice page on sam.gov)
- Dates, ISO 8601 as the extract gives them: `posted_date` (for example `2026-10-15T17:03:19`; the extract states no time zone),
  `response_deadline` (a date, or a date-time with offset), `response_deadline_date` (its calendar date, for filtering), `archive_date`
- Classification: `naics_code`, `classification_code` (product/service code), `set_aside_code`, `set_aside`, `active` (boolean),
  `archive_type`
- Contracting office: `department`, `sub_tier`, `office`, `cgac`, `fpds_code`, `aac_code`, `organization_type`, `org_city`, `org_state`,
  `org_zip`, `org_country`
- Place of performance: `pop_street_address`, `pop_city`, `pop_state`, `pop_zip`, `pop_country`
- Run bookkeeping: `change` (`new`, `changed` or `unchanged` relative to the cursor, see below), `content_hash`, `source`,
  `extract_last_modified` (the file's Last-Modified header), `product_version`, `retrieved_at`

### Input

- `naics`: NAICS code prefixes, for example `["5415"]` for all 5415xx codes, or full codes.
- `keywords` and `keyword_match`: case-insensitive words or phrases searched in the title and the notice description; match any or all.
- `notice_types`: exact SAM.gov type names, for example `["Solicitation", "Sources Sought"]`.
- `set_aside_codes`: SAM.gov codes such as `SBA`, `SDVOSBC`, `WOSB`, `8A`, `HZC`; `NONE` keeps notices without a set-aside.
- `states`: two-letter place-of-performance states.
- `posted_days` or `posted_since`: a posted-date window (the later of the two applies when both are set). Posted dates carry no time zone
  and are compared with today's UTC date. The extract usually holds notices up to the previous day, so `posted_days: 0` normally returns
  nothing.
- `deadline_within_days`: only notices whose response deadline falls between today and N days ahead.
- `active_only` (default true), `new_only` (default false), `cursor_key` (default `default`), `max_rows` (default 1000, at most 20000;
  0 delivers nothing).
- `probe_order` (default false): a diagnostic that reads the whole file, returns no rows and charges nothing; the run log shows the record
  count, how posted dates are ordered and how long the read took.

The pre-filled example returns active notices with a 5415 NAICS prefix posted in the last 7 days, at most 500 rows.

#### New since last run

Every run stores a cursor (key `cursor-<cursor_key>`) in a named key-value store in your Apify account, `sam-opps-cursor`, with the
id and content hash of each notice it matched. The named store outlasts the run, so your next run reads the same cursor. With `new_only` on, a run delivers only notices that are new to that cursor or whose content changed (the description counts,
so an amended notice shows as `changed`); unchanged ones are skipped and not charged. A `new_only` run whose cursor key last read the
same extract to the end (same ETag, else the same Last-Modified time) stops after reading the file's headers: a frequent schedule
therefore costs little between SAM.gov's updates of the file (how often it updates is not yet measured). Each row's `change` field says which case it was.
Use one cursor key per scheduled filter set. The cursor is saved every 200 delivered rows and again when the run ends, including a run
stopped by an error or the charge limit, so the next run does not deliver those rows again. It keeps ids for 120 days, at most 100,000.

### Pricing

**$0.002 per row delivered.** That is the only priced event. The pre-filled example, if it matches 500 notices, costs $1.00. A scheduled
daily run with `new_only` that finds 40 new notices costs $0.08. A run that matches nothing, a probe run, or a run that stops because the
extract's columns changed delivers no rows and charges nothing for rows.

### Example row (abbreviated; every field listed above is present)

```json
{"opportunity_id": "fbbff874cd7f42af99108b6e1d9ad6e0", "notice_id": "fbbff874cd7f42af99108b6e1d9ad6e0",
 "title": "DA01--Replacement Model for VA.gov", "solicitation_number": "36C10B26Q0131", "notice_type": "Sources Sought",
 "base_type": "Sources Sought", "posted_date": "2026-10-15T07:00:47", "response_deadline": "2026-05-05T12:00:00-04:00",
 "response_deadline_date": "2026-05-05", "active": true, "naics_code": "541519", "classification_code": "DA01",
 "set_aside_code": "", "set_aside": "", "department": "VETERANS AFFAIRS, DEPARTMENT OF", "sub_tier": "VETERANS AFFAIRS, DEPARTMENT OF",
 "office": "TECHNOLOGY ACQUISITION CENTER NJ (36C10B)", "pop_state": "", "org_state": "NJ",
 "link": "https://sam.gov/workspace/contract/opp/fbbff874cd7f42af99108b6e1d9ad6e0/view", "change": "new",
 "source": "SAM.gov Contract Opportunities public data extract (sam.gov Data Services; U.S. Government work, free of copyright)",
 "extract_last_modified": "Fri, 16 Oct 2026 03:30:28 GMT"}
```

### How a run works, and its limits

- Each run streams the extract (about 200 MB) and keeps the rows that match. It never holds the file in memory.
- Every run reads the whole extract, whatever the posted window, so the rows a run returns do not depend on how the file is ordered.
  Measured on 2026-10-16: about 200 MB and 71,196 records, a 9-second download and about 1 second of parsing on a desktop machine; run
  times on Apify are recorded only after the Actor's own test runs there. The Actor's default run options are 512 MB of memory and a
  600 seconds timeout. If a run is cut off at its timeout, the cursor keeps what was
  saved last: at most the last 199 rows delivered before the cut can be delivered again by the next `new_only` run.
- The run stops after `max_rows` rows, or when the run's charge limit is reached; either way the rows delivered are the first matches in
  file order (newest-first when checked).
- Before reading any record, the Actor checks that the extract's header still has the 47 columns it was built against. If SAM.gov changes
  the file, the run fails with a message naming the columns and nothing is charged for rows. A record the CSV parser cannot read is
  skipped and counted in the run log.
- The extract mixes UTF-8 and Windows-1252 text; both are decoded.
- The Actor writes rows to its default dataset and keeps the `new_only` cursor in one named key-value store, `sam-opps-cursor`, in your
  Apify account; that store stays until you delete it. It needs no proxy and calls no other Actor.
- Do not run two runs with the same `cursor_key` at the same time: the cursor is shared, the last run to save wins, and some rows could
  then be delivered (and charged) twice. Use a different `cursor_key` for each filter set you schedule.

This Actor is not affiliated with the U.S. General Services Administration or SAM.gov. It passes the extract's notice fields through as
published; check the notice page on SAM.gov before acting on a deadline.

Report problems in the Issues tab.

# Actor input Schema

## `naics` (type: `array`):

Keep notices whose NAICS code starts with any of these. '5415' matches 541511, 541512, 541519 and so on; a full 6-digit code matches exactly that code.

## `keywords` (type: `array`):

Case-insensitive words or phrases searched in the title and the notice description, for example 'cybersecurity' or 'help desk'. The description is used only for matching; it is not returned (open the notice's link to read it).

## `keyword_match` (type: `string`):

Whether a notice must contain any of the keywords or all of them.

## `notice_types` (type: `array`):

Exact notice types as SAM.gov names them: Solicitation, Combined Synopsis/Solicitation, Presolicitation, Sources Sought, Special Notice, Award Notice, Justification, Sale of Surplus Property, Consolidate/(Substantially) Bundle. Empty keeps all types.

## `set_aside_codes` (type: `array`):

SAM.gov set-aside codes, for example SBA (total small business), SDVOSBC, SDVOSBS, WOSB, 8A, HZC, VSA. 'NONE' keeps notices with no set-aside (the extract writes NONE or leaves the field empty).

## `states` (type: `array`):

Two-letter state or territory codes of the place of performance, for example VA, MD, DC. Notices with no place of performance stated are excluded when this is set.

## `posted_days` (type: `integer`):

Keep notices posted within this many days before today (UTC; the extract states no time zone for posted dates). The extract usually holds notices up to the previous day, so 0 (today only) normally returns nothing. Leave empty to not filter by posted date.

## `posted_since` (type: `string`):

Keep notices posted on or after this date (YYYY-MM-DD). When both this and 'Posted in the last N days' are set, the later date applies.

## `deadline_within_days` (type: `integer`):

Keep only notices whose response deadline falls between today and this many days ahead. Notices without a deadline are excluded when this is set.

## `active_only` (type: `boolean`):

Keep only notices the extract marks Active = Yes.

## `new_only` (type: `boolean`):

Deliver only notices that were not delivered by an earlier run with the same cursor key, or whose content changed since. Every run updates the cursor, so this can be switched on after a first full run.

## `cursor_key` (type: `string`):

Name of the 'seen' cursor kept in the named key-value store sam-opps-cursor in your account. Use a different key for each filter set you schedule, and do not run two runs with the same key at once.

## `max_rows` (type: `integer`):

Stop after this many rows have been delivered (and charged). 0 to 20000; 0 delivers no rows.

## `probe_order` (type: `boolean`):

Reads the whole extract and delivers no rows, so nothing is charged for rows. The run log shows the number of records, how the posted dates are ordered and how long the read took. Filters, the cursor and the row cap are ignored.

## Actor input object example

```json
{
  "naics": [
    "5415"
  ],
  "keywords": [],
  "keyword_match": "any",
  "notice_types": [],
  "set_aside_codes": [],
  "states": [],
  "posted_days": 7,
  "active_only": true,
  "new_only": false,
  "cursor_key": "default",
  "max_rows": 500,
  "probe_order": false
}
```

# Actor output Schema

## `results` (type: `string`):

The rows this run delivered, one charged event each.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "naics": [
        "5415"
    ],
    "keywords": [],
    "notice_types": [],
    "set_aside_codes": [],
    "states": [],
    "posted_days": 7,
    "max_rows": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("wx-grid-forecasts/sam-contract-opportunities").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "naics": ["5415"],
    "keywords": [],
    "notice_types": [],
    "set_aside_codes": [],
    "states": [],
    "posted_days": 7,
    "max_rows": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("wx-grid-forecasts/sam-contract-opportunities").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "naics": [
    "5415"
  ],
  "keywords": [],
  "notice_types": [],
  "set_aside_codes": [],
  "states": [],
  "posted_days": 7,
  "max_rows": 500
}' |
apify call wx-grid-forecasts/sam-contract-opportunities --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,wx-grid-forecasts/sam-contract-opportunities"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/62TULLbkqYn4QwqoF/builds/Lbi6bNgoWTTYSLVYc/openapi.json
