# SAM.gov Opportunities Scraper (`axiomworks/sam-gov-opportunities-scraper`) Actor

Collect public SAM.gov opportunities and return structured records with titles, notice types, solicitation numbers, agencies, dates, NAICS codes, and contact details. Search by multiple keywords, set a modification-date lookback, and use scheduled runs to emit only new or changed notices.

- **URL**: https://apify.com/axiomworks/sam-gov-opportunities-scraper.md
- **Developed by:** [Axiom Works](https://apify.com/axiomworks) (community)
- **Categories:** Business
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.79 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

**Returns:** one row per SAM.gov opportunity with title, noticeType, solicitationNumber, agency, modifiedDate, naicsCode and contactEmail.
**Input:** `{"keywords":["cybersecurity"],"postedWithinDays":30,"maxItems":20}`
**Best for:** contract opportunity monitoring, procurement research, and bid pipeline review.

```json
{"id": "351fd0d0842f411fadbe22471214399a", "title": "INDUSTRY DAY:  Fort Carson Equipment Concentration Site - Construction Management at Risk using Other Transaction Authority (CM@Risk w/OTA)", "noticeType": "Special Notice", "solicitationNumber": "W912QR-IndustryDay-FortCarsonECS-CMARwithOTA", "agency": "DEPT OF DEFENSE", "naicsCode": "236220", "modifiedDate": "2026-10-13T21:29:50+00:00"}
```

### What does SAM.gov Opportunities Scraper do?

SAM.gov Opportunities Scraper collects public federal contracting notices using keyword searches. It returns structured records for reviewing opportunities, comparing agencies, and tracking changed notices without opening the search page every day. This SAM.gov scraper reads the public website's search and opportunity detail endpoints. No SAM.gov login or official SAM.gov API key is required.

Search results are requested in modification date order. The Actor retrieves details for each matching notice, applies any NAICS filter, and saves qualifying records to an Apify dataset. Multiple search terms share one overall result limit. Notices that appear under more than one term are emitted once per run, using their stable opportunity IDs.

This tool is independently developed and is not affiliated with or endorsed by SAM.gov, the General Services Administration, or any contracting agency. The dataset represents what the public service returned at collection time. It is a research input, not a determination of eligibility or an assurance that a procurement remains open.

### What data can you get?

Each dataset row describes one opportunity. Fields that the source does not supply are null. Titles and dates come from search records, while classification and contact information come from opportunity details. The following table lists all output fields so consumers can build a predictable import mapping.

| Field | Meaning |
| --- | --- |
| id | Stable SAM.gov opportunity identifier |
| sourceUrl | Public detail API endpoint used for the record |
| title | Opportunity title as published |
| noticeType | Notice type label, such as Solicitation |
| solicitationNumber | Published solicitation reference |
| agency | Top-level department name in the hierarchy |
| subAgency | Second-level organization name |
| office | Contracting office name, or deepest available organization |
| publishDate | Publication date from the search response |
| modifiedDate | Modification date from the search response |
| responseDate | Response deadline when published |
| naicsCode | Primary NAICS code, or first available NAICS code |
| classificationCode | Product or service classification code |
| setAside | Published set-aside code when available |
| placeOfPerformance | Address components joined into a readable string |
| contactName | Primary point of contact name, or first listed contact |
| contactEmail | Email address from that same contact |
| contactPhone | Published phone for that same contact, or null |
| archiveDate | Date SAM.gov archives the notice (YYYY-MM-DD), or null |
| description | Published description body, possibly containing HTML |
| placeOfPerformanceCity | Published work-location city name, or null |
| placeOfPerformanceState | Published work-location state code, or null |
| placeOfPerformanceCountry | Published work-location country code, or null |

Keep identifiers and codes as text to preserve leading zeros. Dates retain source formatting and offsets. Sanitize description markup before displaying it in an application.

### How to use

To focus a capture pipeline, set agencyNames to one or more organization-name fragments, such as \["DEFENSE"]. Matching is case-insensitive and checks every organization in the published hierarchy, including offices. Alternatives within the list are combined with OR; this filter and every other selected filter must all match. Agency filtering happens before detail retrieval, avoiding detail requests for unrelated organizations.

Use placeOfPerformanceStates with published state codes, such as \["CO"], to find work in a specific location. This filter checks the work location after detail retrieval, rather than the contracting office address. A missing work-location state cannot match a selected state. The separate placeOfPerformanceCity, placeOfPerformanceState and placeOfPerformanceCountry columns let you group or route opportunities without parsing the existing readable placeOfPerformance string. Default empty lists keep the previous search behavior.

Open the Actor, enter one or more keywords, choose your lookback window, and run it. Start with a modest result limit and inspect the dataset before scheduling repeated collection. Once your search matches the work you need, save the input as an Apify task and configure an appropriate schedule in Apify Console.

The lookback window uses modification time, rather than the original publication date. A notice first published months ago can qualify if its deadline or description changed recently. Active search records can still contain deadlines in the past, so compare responseDate separately when building a bid pipeline. The tool does not decide whether a deadline extension or amendment makes a notice actionable.

For monitoring, enable onlyNewSinceLastRun. The Actor stores emitted IDs and their modification timestamps in the named key-value store sam-gov-opportunities-monitor. A later run suppresses an unchanged ID and emits it again when its modification timestamp changes. Only successfully emitted records update that state; records beyond maxItems remain eligible for a later run.

Monitoring tasks share history in the named store. Disable onlyNewSinceLastRun for a complete view. Avoid concurrent monitoring runs because state updates can overwrite each other.

### Input

| Argument | Default | How to choose it |
| --- | --- | --- |
| keywords | Required | Array of search terms; one search is made for each term |
| noticeTypes | Empty array | Restrict results to selected notice categories |
| naicsCodes | Empty array | Match primary NAICS codes from detail records |
| pscCodes | Empty array | Match classificationCode against exact PSC codes |
| setAsideCodes | Empty array | Match setAside against exact eligibility codes |
| agencyNames | Empty array | Match organization-name fragments, case-insensitive |
| placeOfPerformanceStates | Empty array | Match exact published work-location state codes |
| postedWithinDays | 7 | Modification lookback, from 1 to 3650 days |
| onlyNewSinceLastRun | false | Persist emitted IDs and suppress unchanged notices |
| maxItems | 50 | Overall maximum, from 1 to 10000 results |
| proxyConfiguration | Apify proxy enabled | Datacenter proxy with retries on fresh sessions; switches to residential IPs by itself if SAM.gov keeps blocking datacenter requests |

```json
{
  "keywords": ["cybersecurity"],
  "noticeTypes": ["Solicitation", "Sources Sought"],
  "naicsCodes": [],
  "pscCodes": [],
  "setAsideCodes": [],
  "agencyNames": [],
  "placeOfPerformanceStates": [],
  "postedWithinDays": 30,
  "onlyNewSinceLastRun": false,
  "maxItems": 20,
  "proxyConfiguration": {"useApifyProxy": true}
}
```

Accepted notice types are Presolicitation, Solicitation, Combined Synopsis/Solicitation, Sources Sought, Special Notice, Award Notice, Intent to Bundle Requirements, Justification, and Sale of Surplus Property. Names are matched without regard to capitalization. Leaving noticeTypes empty requests all supported categories from the search service.

NAICS, PSC, and set-aside filtering happens after detail retrieval, because the detail response supplies the primary code. Details are fetched in batches of up to five, capped by the remaining result limit. This avoids fetching surplus details when only one or two more rows are needed. A narrow filter can require more requests before the dataset fills. maxItems limits emitted rows, rather than search candidates or requests. When sufficient qualifying notices exist, the Actor continues across pages until that number is reached. When fewer exist, the dataset contains the available matches.

PSC codes are compared with classificationCode, while set-aside codes are compared with setAside. Values within each filter are alternatives; different filters must all match. Codes are compared without regard to capitalization. These filters are off by default, and missing source values cannot satisfy a selected filter. For example, pscCodes \["D302", "R499"] includes either code, while setAsideCodes \["SBA"] further restricts the same search to that published set-aside code. NONE is an explicit source value, not a substitute for missing information.

contactPhone comes from the same primary or first point of contact used for contactName and contactEmail. It preserves the published string, including any formatting or extension. Some notices publish the phone as 'Telephone: <number>' in the name slot; the number is also copied to contactPhone (and an 'Electronic Mail:' value to contactEmail), while contactName keeps the published string. Phones inside descriptions are not guessed. Missing phone values are null. No extra request or enrichment service is used to collect this field.

If a detail request remains unavailable after retries, that notice is skipped rather than saved as an incomplete search-only row. The log identifies the notice and the run continues. This also prevents a failed detail request from bypassing a selected classification filter. An unexpected response shape is caught for the current search; other search terms can still return results. A blocked or empty search ends successfully with an actionable status, without saving an error row. Empty-result messages suggest broadening keywords, increasing postedWithinDays, removing filters, or disabling onlyNewSinceLastRun to include unchanged notices. Persistent request failures suggest retrying later or changing proxyConfiguration.

The keyword field accepts ordinary search terms, including Unicode text. It does not accept an opportunity URL as a special direct lookup instruction. Use the public search vocabulary that appears in contracting documents. If a term returns nothing, try a broader phrase or extend the lookback window. Missing keywords or invalid numerical settings cause a clear input failure.

### Output

The opening example above is a real record from a local run for cybersecurity, with maxItems set to one. The following second example was collected locally with pscCodes \["1660"] and setAsideCodes \["none"]. It shows the new phone field and the exact source codes used by the filters:

```json
{
  "id": "f88dcff0842841ae80f852b5151ac325",
  "sourceUrl": "https://sam.gov/api/prod/opps/v2/opportunities/f88dcff0842841ae80f852b5151ac325",
  "title": "Remanufacture of B-1B Heat Exchangers",
  "noticeType": "Presolicitation",
  "solicitationNumber": "FD20302502453",
  "agency": "DEPT OF DEFENSE",
  "naicsCode": "336413",
  "classificationCode": "1660",
  "setAside": "NONE",
  "contactName": "Matthew Churchwell",
  "contactEmail": "matthew.churchwell.2@us.af.mil",
  "contactPhone": "405-739-4809"
}
```

Export the dataset as JSON for typed processing, CSV for spreadsheets, or Excel for analyst review. Treat null as missing source information, rather than proof that a requirement does not apply. In particular, set-aside and place-of-performance information can be sparse. A contracting document may contain details that are not populated in the structured response.

Use id as the primary key in a long-lived database and modifiedDate as an update marker. solicitationNumber is useful for human review but should not replace the opportunity ID because separate notices can share a procurement reference. The Actor does not download attachments or follow links mentioned inside descriptions.

### How much does it cost?

This Actor is charged per result. Each saved opportunity triggers one result event. Consult the Pricing tab for the current charging terms and use maxItems to control the maximum number of emitted records. Empty searches do not create result events. Duplicate notices encountered across terms do not create an additional dataset row or result event.

There is no enrichment event and no AI enrichment processor in this implementation. The Actor copies public source fields and formats the performance address. A narrow NAICS filter, longer lookback, or several overlapping terms can increase the work needed to obtain the same number of rows, but result charging follows the records actually emitted.

### Use with AI agents (MCP)

Connect an MCP client to Apify's MCP server and restrict the tools to this Store path:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=axiomworks/sam-gov-opportunities-scraper"
    }
  }
}
```

Configure authentication through your client's supported secure mechanism. Ask the agent to provide keywords explicitly, since the editor's prefilled values are examples rather than API defaults. An example prompt is: “Run axiomworks/sam-gov-opportunities-scraper for cybersecurity opportunities modified in the last 30 days, with maxItems 20, and compare title, agency, responseDate and naicsCode.”

For repeated monitoring, ask: “Use keywords janitorial services, select Solicitation and Sources Sought, and return at most 10 notices. Keep the sourceUrl and solicitationNumber in the review table.” Have the agent distinguish a missing responseDate from an actual deadline and retain links or IDs for manual source checks.

### Integrations and API

The same input object works through Apify tasks and the Actor API. These examples use environment variables for authentication. Keep tokens out of saved inputs, shared notebooks, and exported datasets. The API client starts a run and then reads the dataset; it does not require a SAM.gov credential.

Python example:

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("axiomworks/sam-gov-opportunities-scraper").call(
    run_input={"keywords": ["cybersecurity"], "postedWithinDays": 30, "maxItems": 20}
)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
```

JavaScript example:

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('axiomworks/sam-gov-opportunities-scraper').call({
    keywords: ['cybersecurity'], postedWithinDays: 30, maxItems: 20,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

cURL example:

```bash
curl --request POST   --header "Authorization: Bearer ${APIFY_TOKEN}"   --header 'Content-Type: application/json'   --data '{"keywords":["cybersecurity"],"postedWithinDays":30,"maxItems":20}'   'https://api.apify.com/v2/acts/axiomworks~sam-gov-opportunities-scraper/run-sync-get-dataset-items'
```

For longer runs, use asynchronous execution and retrieve the dataset when the run succeeds. Connect completed datasets to spreadsheets, databases, or procurement dashboards using your existing Apify integration. Store the original IDs alongside any internal lead or bid identifiers so you can reconcile later amendments reliably.

### FAQ

#### Why did the run return no items?

The search may not match active notices in the selected time window, or the filters may exclude every candidate. Monitoring mode can also suppress all unchanged records. A temporary site block or unavailable detail service can prevent collection. The status message and log distinguish these situations where possible; broaden the search or retry later as appropriate.

#### Does the Actor use the official SAM.gov API?

It uses the public website's internal JSON endpoints on sam.gov. It does not use the separate official service that requires an API key. Those internal endpoints can change without notice, so integrations should check run status and validate incoming records before replacing a production dataset.

#### Are descriptions summarized?

Descriptions are copied from the public detail response. They may contain HTML and can be long. No model rewrites, translates, ranks, or summarizes the text. Handle markup in the consuming application and keep the original body when provenance matters.

#### Does the tool submit bids or contact officials?

It collects notice records only. It does not submit proposals, send messages, access restricted documents, or evaluate vendor qualifications. Contact fields are published points of contact attached to the procurement notice.

### Is it legal to scrape SAM.gov?

This Actor is not affiliated with or endorsed by SAM.gov or the U.S. General Services Administration.

This Actor collects publicly accessible government contracting information without logging in. You are responsible for how you use and retain the resulting data, including any contact information. Review applicable rules and the source's terms before adopting a recurring workflow. Collection does not establish permission for unrelated outreach or access to restricted procurement materials.

The implementation uses modest request spacing and retries temporary blocks with backoff. It does not solve CAPTCHAs or access account-only content. Always verify procurement decisions against the current notice and applicable solicitation documents; the exported dataset can become outdated after a run finishes.

### Feedback

Report a problem with the input you used, the run identifier, and an example opportunity ID. Describe the expected field and the value you observed so the extraction can be checked against the public source. Do not include tokens, proxy credentials, or private documents in a report. If the results met your needs, a rating on the Actor's Store page helps others assess the tool.

# Actor input Schema

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

Search public active SAM.gov notices with one search per term, e.g. \["cybersecurity", "janitorial services"]. Each term accepts 1-200 characters; URLs and IDs are not direct lookups. Results are deduplicated across terms and share maxItems. Required; no API default.

## `noticeTypes` (type: `array`):

Include notice labels, e.g. \["Solicitation", "Sources Sought"], using the listed choices (case-insensitive). Default \[] includes all types. Combined with keyword, modification-window and code filters.

## `naicsCodes` (type: `array`):

Include exact primary NAICS code strings, e.g. \["541512"]. Default \[] includes all codes. Applied after detail retrieval; missing codes cannot match. Values within this list are alternatives; all selected filter fields must match.

## `postedWithinDays` (type: `integer`):

Include notices modified in the last 1-3650 days, e.g. 30; default 7. Uses modifiedDate, not publishDate, and can include older notices that changed recently. Increase this window if a keyword returns no results.

## `onlyNewSinceLastRun` (type: `boolean`):

Set true to emit only new IDs or changed modifiedDate values, e.g. true for scheduled monitoring; default false returns matching notices each run. History is shared in named store sam-gov-opportunities-monitor across this account’s runs. An unchanged search can return zero rows.

## `maxItems` (type: `integer`):

Limit total emitted notices across all keywords to 1-10000, e.g. 20; default 50. Duplicates and filtered notices do not count. Returns exactly this many when enough matches exist, otherwise fewer. This limits results, not requests.

## `proxyConfiguration` (type: `object`):

Choose SAM.gov routing, e.g. {"useApifyProxy":true}; default enables Apify datacenter proxy with fresh-session retries. Persistent datacenter blocks trigger RESIDENTIAL fallback if available. Set apifyProxyGroups to \["RESIDENTIAL"] to use it initially. Persistent blocks yield zero rows successfully; retry later or change proxyConfiguration.

## `pscCodes` (type: `array`):

Include exact classificationCode values, e.g. \["D302", "R499"], case-insensitive. Default \[] includes all PSC codes. Applied after detail retrieval together with other filters; missing codes cannot match.

## `setAsideCodes` (type: `array`):

Include exact setAside values, e.g. \["SBA", "8A", "NONE"], case-insensitive. Default \[] includes all set-asides. Applied after detail retrieval together with other filters. NONE matches an explicit source code; missing values cannot match.

## `agencyNames` (type: `array`):

Include notices whose published agency, subagency or office name contains any supplied text, case-insensitive, e.g. \["DEFENSE"]. Default \[] includes all organizations. Applied before detail requests and combined with all other filters.

## `placeOfPerformanceStates` (type: `array`):

Include exact published place-of-performance state codes, case-insensitive, e.g. \["CO", "VA"]. Default \[] includes all states. Applied after detail retrieval; notices without a state cannot match. This is the work location, not the contracting office address.

## Actor input object example

```json
{
  "keywords": [
    "cybersecurity"
  ],
  "noticeTypes": [],
  "naicsCodes": [],
  "postedWithinDays": 30,
  "onlyNewSinceLastRun": false,
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "pscCodes": [],
  "setAsideCodes": [],
  "agencyNames": [],
  "placeOfPerformanceStates": []
}
```

# Actor output Schema

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

Dataset with one item per result, in the order found.

# 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 = {
    "keywords": [
        "cybersecurity"
    ],
    "postedWithinDays": 30,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("axiomworks/sam-gov-opportunities-scraper").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 = {
    "keywords": ["cybersecurity"],
    "postedWithinDays": 30,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("axiomworks/sam-gov-opportunities-scraper").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 '{
  "keywords": [
    "cybersecurity"
  ],
  "postedWithinDays": 30,
  "maxItems": 20
}' |
apify call axiomworks/sam-gov-opportunities-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,axiomworks/sam-gov-opportunities-scraper"
        }
    }
}
```

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/XPU1UrXKbeRogs7py/builds/uQVgiWprb94wZs2Kh/openapi.json
