# SAM.gov Opportunity & Amendment Change Tracker (`hollowoaklabs/samgov-amendment-change-tracker`) Actor

Track federal contract opportunities from SAM.gov public data and see exactly what changed when a notice is amended: deadline moves, set-aside, NAICS, place of performance, contacts and description text.

- **URL**: https://apify.com/hollowoaklabs/samgov-amendment-change-tracker.md
- **Developed by:** [Christopher Hulsey](https://apify.com/hollowoaklabs) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.00 / 1,000 opportunity delivereds

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 Opportunity & Amendment Change Tracker

Federal contract opportunities from SAM.gov, filtered your way, with a plain-English answer to the question SAM.gov's own notifications leave open: **what actually changed in this amendment?**

When an agency amends a notice, SAM.gov tells you *that* it changed, not *what* changed. You have to reopen the notice and compare it by eye. This Actor does the comparison for you. Every amended opportunity comes with a field-by-field diff against its previous version.

```
Response deadline moved later by 5 day(s): Oct 23, 2026 12:00 PM (UTC-5) → Oct 28, 2026 12:00 PM (UTC-5);
Description: 2 sentence(s) added, 2 removed (first addition: "...An amendment to this Request for Quotes
effective 10/22/2026 extends the due date to 10/28/2026, changes the period of performance...")
```

*(Real output for a VA HVAC solicitation in Nashville, TN, from the Oct 22, 2026 data.)*

### What it does

- **Filters** opportunities by NAICS code (or prefix), set-aside type, place-of-performance state, keywords, exclude-keywords, notice type and posting window.
- **Groups versions.** SAM.gov posts each amendment as a new notice. The Actor links the versions of one opportunity (same agency, office and solicitation number) and compares the latest with the one before it.
- **Diffs these fields:** response deadline (with the shift in days), notice type (for example Presolicitation → Solicitation), set-aside, NAICS, product/service code, title, place of performance, archive date, primary and secondary points of contact, award details, and the description text. For the description you get the sentences that were added and removed, not just "text changed."
- **Flags noise.** Changes that are only case, punctuation or whitespace are marked `cosmetic`. `hasMaterialChange` tells you whether anything that matters changed.
- **Remembers between runs.** Schedule it daily and each run outputs only opportunities that are new or changed since the last run. It also catches notices edited in place under the same notice ID.
- **Skips runs when there's no new data.** SAM.gov refreshes its data file about once a day. If the file hasn't changed since your last run, the Actor exits in seconds with one free run-summary row and no charges.

### Data source

All data comes from the **SAM.gov "Contract Opportunities" public data extract**, the official bulk file the U.S. General Services Administration publishes for public use. No login, no scraping of web pages, no proxies.

Optional: if you add your own free **api.sam.gov API key**, the Actor also calls the official SAM.gov Opportunities API to compare attachment lists for amended notices (files added or removed).

> This Actor is an independent tool. It is **not affiliated with, endorsed by, or sponsored by** the U.S. General Services Administration or SAM.gov. Always confirm details on the official notice before you bid. Each result links to it.

### Quick start

1. Enter your NAICS codes (for example `561720` janitorial, `238220` HVAC/plumbing, or a prefix such as `23` for all construction).
2. Optionally pick set-asides (for example *Any small-business set-aside*), states and keywords.
3. Run it once to see the current picture. Every currently amended opportunity already shows its diff on the first run, because the SAM.gov file still contains earlier versions.
4. Schedule it daily (for example 7:00 AM ET) with **Posted within days** set to 2. Later runs return only what's new or changed.

Use **Only amended / changed opportunities** if you only want the amendment feed.

### Input

| Field | What it does |
|---|---|
| `naicsCodes` | NAICS codes or prefixes. Empty = all. |
| `setAsideTypes` | SAM.gov set-aside codes (SBA, 8A, HZC, SDVOSBC, WOSB, …) or `ANY_SMALL_BUSINESS`. |
| `states` | Two-letter place-of-performance states. Falls back to the contracting office's state when the notice has no place of performance (can be turned off). |
| `keywords` / `excludeKeywords` | Searched in title and description. |
| `postedWithinDays` | The latest version was posted within N days. Default 7. |
| `noticeTypes` | Defaults to biddable types. Add *Award Notice* to follow awards. |
| `onlyChanged` | Output only opportunities with an earlier version to compare against. |
| `openOnly` | Skip notices whose response deadline has passed. Default on. |
| `skipAlreadyDelivered` | Don't re-output (or re-charge) an unchanged opportunity this tracker already delivered. Default on. |
| `trackerName` | Keeps separate watchlists separate. |
| `maxResults` | Upper limit per run (default 1,000). |
| `includeDescription`, `descriptionMaxChars`, `includeContacts` | Control output size and content. |
| `samApiKey`, `maxAttachmentLookups` | Optional attachment comparison through the official API. |

### Output

One item per opportunity (its latest version). Key fields:

```json
{
  "noticeId": "588009fe95064eefaa0bed9bdceeeeeb",
  "title": "Solicitation - BWT Operation and Maintenance",
  "solicitationNumber": "W9127826RA005",
  "noticeType": "Solicitation",
  "agency": "DEPT OF DEFENSE",
  "postedDate": "2026-10-21T13:34:35",
  "responseDeadline": "2026-11-09T09:00:00-06:00",
  "naicsCode": "561210",
  "setAside": "Total Small Business Set-Aside",
  "placeOfPerformance": {"city": null, "state": null, "zip": "35401", "street": "Black Warrior-Tombigbee, Tuscaloosa, Alabama ..."},
  "samUrl": "https://sam.gov/opp/588009fe95064eefaa0bed9bdceeeeeb/view",
  "changeStatus": "amended",
  "versionCount": 6,
  "previousNoticeId": "4465b0dcae314d1990f554c6bd9204f3",
  "hasMaterialChange": true,
  "changeSummary": "Response deadline moved later by 24 day(s): Oct 16, 2026 2:00 PM (UTC-6) → Nov 9, 2026 9:00 AM (UTC-6); ...",
  "changes": [
    {"field": "responseDeadline", "from": "2026-10-16T14:00:00-06:00", "to": "2026-11-09T09:00:00-06:00", "shiftDays": 24, "shiftHours": 571.0}
  ],
  "descriptionDiff": {"addedSentences": ["... *****Amendment 0004*****"], "removedSentences": ["... *****Amendment 0003*****"], "similarity": 0.952},
  "attachmentsDiff": null
}
```

`changeStatus` values:

- `new`: no earlier version known.
- `amended`: a newer notice replaced an earlier version.
- `updated`: the same notice ID was edited since your last run.
- `unchanged`: only returned if you turn off `skipAlreadyDelivered`.

**Runs with nothing new.** Every result row has `"type": "opportunity"`. If a run has nothing new to deliver (SAM.gov hasn't published a new file, everything matching was already delivered, or nothing matches your filters), the dataset gets exactly one row with `"type": "run-summary"`, `"newOrChanged": 0`, a plain-language `message` and the file's `fileLastModified`. That row is **never charged**. Filter on `type == "opportunity"` in integrations if you only want results.

The **Overview** and **Changes only** dataset views show the most useful columns. Export to JSON, CSV or Excel, or connect to Google Sheets, Slack, email or Zapier with Apify integrations.

### Limits (please read)

- **Attachments.** The public bulk file has no attachment lists. Attachment changes are reported only if you supply your own api.sam.gov key. Without a key, `attachmentsDiff` is `null`. Some amendments change only an attachment (a revised SOW PDF or a Q\&A file). In those cases the summary says no published field changed and that the change may be in attachments.
- **Attachment contents** (PDF and Word text) are not diffed.
- **Long descriptions.** SAM.gov's file caps description text at about 32,000 characters. Results flag this with `descriptionPossiblyTruncatedBySource`.
- **Freshness.** SAM.gov regenerates the file about once a day (recently around 03:30 UTC). This tool is for daily tracking, not minute-by-minute alerts.
- **Version grouping** relies on agency, office and solicitation number. Notices without a solicitation number are grouped by title, which is less reliable.
- Contact details are the government points of contact SAM.gov publishes with each notice. Turn them off with `includeContacts: false`.

### Reliability

- Reads the official file only (no page scraping), so site redesigns and bot-blocking don't break it.
- Streams the file (about 215 MB) to disk with retries, then checks the size and header.
- Refuses to run on a file with far fewer rows than normal rather than reporting false "removals."
- Memory: about 100 MB for a typical filtered run. A worst case (every notice type, nationwide, no filters, with stored history) peaked at about 660 MB, so the default memory is 1 GB.

### Pricing

Pay per event:

- **$0.004 per opportunity returned** ($4 per 1,000).
- **$0.05 per run that processes a new SAM.gov file.** Each such run downloads the full official file. Runs skipped because SAM.gov published nothing new are not charged at all.
- Typical cost: a daily tracker that returns 10 opportunities costs about $0.09 a day.

### FAQ

**Why do I see changes on my very first run?** SAM.gov's file keeps earlier versions of amended notices, so the Actor can compare versions right away.

**Can I track several unrelated searches?** Yes. Give each one its own `trackerName`.

**Where is the history stored?** In a named key-value store in *your* Apify account (`samgov-change-tracker-<trackerName>`). Delete it to start fresh.

# Actor input Schema

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

Match opportunities whose NAICS code starts with any of these (e.g. 561720 for janitorial, or 2382 for all building-equipment contractors). Leave empty for all.

## `setAsideTypes` (type: `array`):

Keep only these set-asides. Leave empty for all, including unrestricted.

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

Two-letter state codes, e.g. AL, GA, TX. Leave empty for all.

## `useOfficeStateWhenNoPlaceOfPerformance` (type: `boolean`):

Some notices leave the place of performance empty. When on, the contracting office's state is used for the state filter instead.

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

Keep opportunities whose title or description contains at least one of these words or phrases (case-insensitive, matches word starts). Leave empty for all.

## `excludeKeywords` (type: `array`):

Drop opportunities whose title or description contains any of these.

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

Only opportunities whose latest version was posted in the last N days. Use 1-2 for a daily schedule.

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

Defaults to biddable notice types. Add Award Notice to follow awards.

## `onlyChanged` (type: `boolean`):

Output only opportunities that have an earlier version to compare against (amended, or updated since your last run). New, never-amended notices are skipped.

## `openOnly` (type: `boolean`):

Skip notices whose response deadline has passed or that SAM.gov marks inactive.

## `skipAlreadyDelivered` (type: `boolean`):

On scheduled runs, don't output (or charge for) an opportunity again unless it changed since this tracker last delivered it.

## `trackerName` (type: `string`):

Version history is remembered per tracker name in a named key-value store in your account (samgov-change-tracker-<name>). Use different names for independent watchlists.

## `maxResults` (type: `integer`):

Upper limit on items output (and charged) per run. 0 = no limit.

## `includeDescription` (type: `boolean`):

Include the notice's full description text.

## `descriptionMaxChars` (type: `integer`):

Trim description text in the output to this many characters. 0 = no trimming. Diffs always use the full text.

## `includeContacts` (type: `boolean`):

Include the contracting officer contacts SAM.gov publishes with each notice, and report contact changes.

## `skipIfFileUnchanged` (type: `boolean`):

SAM.gov refreshes the data file about once a day. If it has not changed since this tracker's last run, finish immediately with no results and no charge.

## `samApiKey` (type: `string`):

Optional. Your own free api.sam.gov key. When set, attachment lists are fetched from the official SAM.gov Opportunities API for amended notices so added/removed files can be reported. Public keys have low daily limits.

## `maxAttachmentLookups` (type: `integer`):

Two calls are used per amended opportunity (current and previous version). Keep within your SAM.gov key's daily limit.

## Actor input object example

```json
{
  "naicsCodes": [
    "561720",
    "561730"
  ],
  "states": [],
  "useOfficeStateWhenNoPlaceOfPerformance": true,
  "postedWithinDays": 7,
  "noticeTypes": [
    "Combined Synopsis/Solicitation",
    "Solicitation",
    "Presolicitation",
    "Sources Sought",
    "Special Notice",
    "Modification/Amendment/Cancel"
  ],
  "onlyChanged": false,
  "openOnly": true,
  "skipAlreadyDelivered": true,
  "trackerName": "default",
  "maxResults": 1000,
  "includeDescription": true,
  "descriptionMaxChars": 0,
  "includeContacts": true,
  "skipIfFileUnchanged": true,
  "maxAttachmentLookups": 10
}
```

# Actor output Schema

## `opportunities` (type: `string`):

New and amended contract opportunities matching your filters, each with a field-by-field 'what changed' report.

# 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 = {
    "naicsCodes": [
        "561720",
        "561730"
    ],
    "states": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("hollowoaklabs/samgov-amendment-change-tracker").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 = {
    "naicsCodes": [
        "561720",
        "561730",
    ],
    "states": [],
}

# Run the Actor and wait for it to finish
run = client.actor("hollowoaklabs/samgov-amendment-change-tracker").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 '{
  "naicsCodes": [
    "561720",
    "561730"
  ],
  "states": []
}' |
apify call hollowoaklabs/samgov-amendment-change-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,hollowoaklabs/samgov-amendment-change-tracker"
        }
    }
}
```

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/aDW7JlWo5mfkUMoi8/builds/lhVwvMzf3b8LsGLxT/openapi.json
