# SAM.gov Federal Contract Opportunities & Attachments (`oswaldocarabano/sam-gov-data-scraper`) Actor

Scrape public U.S. federal contract opportunities from SAM.gov: solicitations, awards, sources sought, agency and contact details, the full solicitation text, and the solicitation documents themselves. No API key, no login, no session cookies.

- **URL**: https://apify.com/oswaldocarabano/sam-gov-data-scraper.md
- **Developed by:** [Oswaldo Carabano](https://apify.com/oswaldocarabano) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.50 / 1,000 contract notices

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## SAM.gov Federal Contract Opportunities & Attachments

Every U.S. federal contract opportunity, as clean JSON — **including the solicitation
documents.**

**No API key. No login. No session cookies.** You paste nothing and configure nothing
before your first run. Hit *Start* with the defaults and you get contract notices.

***

### Why that first line matters

Several SAM.gov scrapers ask you to bring your own `api.sam.gov` API key. That means
registering with SAM.gov, waiting for approval, and then owning the problem when the
key's quota runs out or it expires — the actor stops working because of something *you*
did correctly.

This one talks to the same public endpoint the SAM.gov search page itself uses. There is
nothing to register and nothing to renew.

***

### What you get

One row per notice, 62 fields, `snake_case`, `null` where a value is genuinely absent —
**never a missing key**, so `row.award_amount` is safe on every row.

| | |
|---|---|
| **Identity** | `notice_id`, `solicitation_number`, `title`, `notice_type`, `url` |
| **Lifecycle** | `is_active`, `is_canceled`, `published_at`, `modified_at`, `archive_at`, `modification_count` |
| **Deadline** | `response_due_local`, `response_due_utc`, `response_due_precision`, `response_timezone` |
| **Classification** | `naics_code`, `naics_title`, `psc_code`, `psc_title`, `set_aside_code`, `set_aside` |
| **Agency** | `agency_name`, `agency_id`, `agency_code`, plus the full `agency_hierarchy` |
| **Contact** | `contact_name`, `contact_email`, `contact_phone`, plus `contacts_all` |
| **Place of performance** | `pop_country`, `pop_state`, `pop_city`, `pop_zip` |
| **Award** | `award_number`, `award_date`, `award_amount`, `awardee_name`, `awardee_uei`, `awardee_cage_code` |
| **The solicitation text** | `description_html` **and** `description_text` |
| **Documents** | `attachments[]` with `name`, `size_bytes`, `mime_type` and a stable `url` |

#### Fill rates, measured — not estimated

On a sample of **13,434 notices** across five days spread over ten months:

```
100.0 %  notice_id, title, notice_type, published_at, modified_at, archive_at
 99.6 %  contact_name          99.2 %  contact_email
 99.5 %  solicitation_number   99.4 %  agency_name
 97.8 %  psc_code              96.1 %  naics_code
 84.3 %  description_html      75.2 %  response_due_*
 46.1 %  set_aside_code        32.7 %  contact_phone
```

Award fields sit at ~23% **of all notices** — but that is because most notices are not
awards. Within award notices (`notice_type_code = "a"`, about a quarter of the index),
`award_amount` and `awardee_name` are effectively always present.

We do not list a field here unless we have measured it.

***

### The documents, and the honest number

Turn on **"Also fetch solicitation documents"** and every notice gains an
`attachments[]` array. There are two kinds and the row tells you which:

| `kind` | What it is | Fields |
|---|---|---|
| `file` | The real document — the PDF, the SOW, the price matrix, the amendment | `name`, `size_bytes`, `mime_type`, `url` |
| `link` | A URL to the agency's own portal, when that is all the agency posted | `name` (its label), `url` |

**Measured on a stratified sample of 829 notices across 70 agency × notice-type cells,
covering 96.7% of the active index:**

**36.7% ± 4.6** of active notices carry at least one downloadable `file`, and the rate
varies a lot by notice type:

| Notice type | Share of index | Carries a file |
|---|---|---|
| `r` Sources Sought | 7% | **60%** |
| `s` Special Notice | 6% | 56% |
| `u` Justification | 1% | 52% |
| `o` Solicitation | 18% | **49%** |
| `k` Combined Synopsis/Solicitation | 34% | 39% |
| `p` Presolicitation | 8% | 32% |
| `a` Award Notice | 24% | **14%** |

And it varies even more by **agency**, which is the part worth knowing before you plan a
run:

| Agency | Solicitations carrying a file |
|---|---|
| Veterans Affairs | ~100% |
| General Services Administration | ~90% |
| **Department of Defense** (63% of the whole index) | **~28%** |

DoD posts a large share of its documents on its own portals — you get a `link` to
`neco.navy.mil` instead of the file. That is why the DoD number drags the overall rate
down, and why filtering by agency changes what you get more than filtering by type does.

Average file size: **1.5 MB**.

`file_count` and `link_count` are separate fields on purpose. A link to
`neco.navy.mil` is not the solicitation, and we are not going to count it as one.

#### Downloading a document

The `url` we give you is **stable and unsigned** — it keeps working long after the run
finishes, and you can store it. It is a `303` redirect to a short-lived signed S3 link,
so your client has to **follow redirects** and finish the download promptly:

```bash
curl -L -o sow.pdf "<attachments[0].url>"      # -L is required
```

Verified: the bytes you get back match the `size_bytes` we report, exactly.

#### What we do not return

Attachments that SAM.gov marks as restricted, export-controlled, deleted, or
non-public are **not returned at all — not even their URL**, because publishing the URL
to a controlled document is publishing the document. This is checked against six
separate flags with an allowlist, not a blocklist.

***

### The deadline field, and why there are three of them

This is the one place where being precise costs an extra column, and it is worth it.

`responseDate` from SAM.gov is not always a real instant. Measured on 1,566 notices with
a deadline:

| `response_due_precision` | Share | What it means |
|---|---|---|
| `instant` | **61.0%** | Real offset. `response_due_utc` is a true UTC instant you can convert |
| `date_only` | 38.8% | The notice says "due on the 25th" with no time. `response_due_utc` is `null` |
| `local_naive` | 0.2% | A wall-clock time with no zone. `response_due_utc` is `null` |

SAM.gov stamps midnight UTC on the `date_only` ones. We do not pass that through as an
instant, because it would look like a precise deadline and it is not one. Use
`response_due_local` for display and `response_due_utc` only when
`response_due_precision` is `instant`.

***

### Also available

**Exclusion records** (federal debarment list, 168,206 records) go to a separate
`exclusions` dataset. Set "Maximum exclusion records" above 0.

⚠️ We deliberately do **not** offer a "is this vendor debarred?" lookup. 63% of the list
is individuals and only ~20% of those carry a UEI, so matching by name alone
manufactures false positives — and a false positive here tells you a real person is
debarred when they are not.

***

### Coverage

```
5,591,183   notices in the index, back to 2000
   49,702   active right now  (the default: turn off "Only active notices" for the archive)
   ~2,500   published per calendar day
```

Sweeping is done by publication date, one calendar day per request, newest first. Rows
are deduplicated by `notice_id` before they are delivered, so **you are never charged
for a duplicate**.

***

### Pricing

| Event | Price |
|---|---|
| Actor start | $0.00001 |
| Notice delivered | **$0.0025** |
| Attachment listed | $0.0005 |
| Exclusion record | $0.0005 |

**You are not charged for error rows, and you are not charged for duplicates.** Charging
happens after a row is delivered, never in a batch at the end — so a run that stops
early has charged you for exactly what you received.

***

### Notes

- Runs are deliberately polite: 4 concurrent requests with a 1-second delay. SAM.gov is
  public infrastructure paid for by taxpayers, and we are not going to hammer it because
  we can.
- `robots.txt` is fetched on every single run and enforced in the request layer. The
  actor never requests a disallowed path.
- Notices are immutable once published: a SAM.gov amendment creates a *new* notice that
  links back to the previous one. That is why cached rows stay correct, and why
  `from_cache`, `fetched_at` and `data_age_hours` are on every row.
- U.S. Government works are not subject to copyright (17 U.S.C. § 105), and FAR Part 5
  requires agencies to publish these notices so that businesses can find them.

**Data removal:** <https://samgov.actorstack.dev> · `privacy@actorstack.dev`

# Actor input Schema

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

How many contract notices to return. You are charged per notice delivered, never for duplicates and never for error rows. Hard cap: 50,000 per run.

## `activeOnly` (type: `boolean`):

Active notices are the ones still open or recently awarded: about 49,700 of them. Turn this off to reach the full archive of 5.6 million notices going back to 2000.

## `publishedWithinDays` (type: `integer`):

How far back to sweep, counting from today. Ignored if you set explicit dates below. About 2,500 notices are published per calendar day.

## `publishedFrom` (type: `string`):

Start of the publication date range, inclusive. Leave empty to use the day count above.

## `publishedTo` (type: `string`):

End of the publication date range, exclusive. Leave empty to use today.

## `keywords` (type: `string`):

Free-text search across the notice title and description. Wrap a phrase in double quotes to match it exactly.

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

Which kinds of notice to return. Solicitations and combined synopsis/solicitations are the ones that carry the solicitation documents; award notices almost never do.

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

Limit to these NAICS industry codes, for example 541511 for custom computer programming. Present on 96% of notices.

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

Limit to these Product Service Codes, for example R425. Present on 98% of notices.

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

Limit to notices reserved for a small business category, for example SBA, 8A, WOSB, SDVOSBC or HZC. A set-aside code is present on 46% of notices.

## `agencyIds` (type: `array`):

Limit to these agencies by SAM.gov organization ID. Useful ones: 100000000 Department of Defense, 100006688 General Services Administration, 100006568 Veterans Affairs, 100011942 Homeland Security.

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

Two-letter U.S. state codes for where the work is performed. Note that only about a third of notices state a place of performance.

## `officeStates` (type: `array`):

Two-letter U.S. state codes for the contracting office that issued the notice. Better filled in than place of performance.

## `scrapeAttachments` (type: `boolean`):

Adds the attachment list to every notice: file name, size, MIME type and a stable download URL. This costs one extra request per notice, so a run takes noticeably longer. Measured on a stratified sample: 37% of active notices carry at least one downloadable file — 49% of solicitations, 39% of combined synopsis/solicitations, 14% of award notices. It also varies by agency: near 100% for Veterans Affairs, about 28% for the Department of Defense, which posts many documents on its own portals. Those come back as a link instead, clearly marked.

## `maxExclusions` (type: `integer`):

Also fetch the federal exclusion list (debarment records) into a separate dataset. Set to 0 to skip. Note that 63% of the list is individuals and only a fifth of those carry a UEI, so records cannot be reliably matched to a vendor by name alone.

## `maxCacheAgeDays` (type: `integer`):

Accept cached notices up to this many days old. SAM.gov notices are immutable once published, so a cached row stays correct; set to 0 to force a fresh fetch anyway. Every row tells you which it was through from\_cache and data\_age\_hours.

## `maxConcurrency` (type: `integer`):

How many requests to run in parallel. The default of 4 is deliberately polite: SAM.gov is public infrastructure paid for by taxpayers.

## Actor input object example

```json
{
  "maxResults": 1000,
  "activeOnly": true,
  "publishedWithinDays": 7,
  "publishedFrom": "",
  "publishedTo": "",
  "keywords": "aircraft parts",
  "noticeTypes": [
    "k",
    "o"
  ],
  "naicsCodes": [
    "541511"
  ],
  "pscCodes": [],
  "setAsideCodes": [],
  "agencyIds": [],
  "placeOfPerformanceStates": [],
  "officeStates": [],
  "scrapeAttachments": false,
  "maxExclusions": 0,
  "maxCacheAgeDays": 90,
  "maxConcurrency": 4
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per notice: title, type, agency, contact, NAICS and PSC codes, set-aside, response deadline, award details and the full solicitation text.

## `attachments` (type: `string`):

One row per attachment, with a stable download URL. Populated only when the attachments option is on.

## `exclusions` (type: `string`):

Federal debarment records. Populated only when the exclusions option is on.

## `errors` (type: `string`):

One row per request that could not be completed. These rows are never charged.

# 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 = {
    "maxResults": 1000,
    "publishedWithinDays": 7,
    "keywords": "aircraft parts",
    "noticeTypes": [
        "k",
        "o"
    ],
    "naicsCodes": [
        "541511"
    ],
    "maxExclusions": 0,
    "maxCacheAgeDays": 90,
    "maxConcurrency": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("oswaldocarabano/sam-gov-data-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 = {
    "maxResults": 1000,
    "publishedWithinDays": 7,
    "keywords": "aircraft parts",
    "noticeTypes": [
        "k",
        "o",
    ],
    "naicsCodes": ["541511"],
    "maxExclusions": 0,
    "maxCacheAgeDays": 90,
    "maxConcurrency": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("oswaldocarabano/sam-gov-data-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 '{
  "maxResults": 1000,
  "publishedWithinDays": 7,
  "keywords": "aircraft parts",
  "noticeTypes": [
    "k",
    "o"
  ],
  "naicsCodes": [
    "541511"
  ],
  "maxExclusions": 0,
  "maxCacheAgeDays": 90,
  "maxConcurrency": 4
}' |
apify call oswaldocarabano/sam-gov-data-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,oswaldocarabano/sam-gov-data-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/mhQerayvx2FQcbBTC/builds/Sfi2tWG5TNMqvN2XO/openapi.json
