# Public Tenders Feed (unofficial TED + SAM.gov) (`plusonets/public-tenders-feed`) Actor

Unofficial search of newly published public-procurement notices from the EU TED Search API, plus US SAM.gov opportunities when you supply your own API key. One normalized row per notice.

- **URL**: https://apify.com/plusonets/public-tenders-feed.md
- **Developed by:** [Olalekan Johnson](https://apify.com/plusonets) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 notice results

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

## Public Tenders Feed (unofficial TED + SAM.gov)

Search newly published public-procurement notices and save one normalized row per notice.

This Actor is unofficial. It is not affiliated with, endorsed by, or maintained by the Publications Office of the European Union, TED, the European Commission, the U.S. General Services Administration, or SAM.gov.

TED is queried through the official Search API (`POST https://api.ted.europa.eu/v3/notices/search`), which needs no API key. SAM.gov is optional. When you select it, the Actor calls the official Get Opportunities API with the public API key you paste into `samGovApiKey`. The key is a secret input. It is sent only to `api.sam.gov` and is not written to the dataset or the log.

### Default run

The prefilled input searches TED only, for the keyword `software`, from yesterday through today in the Europe/Brussels calendar, and stops at 10 notices. On 23 October 2026 that run returned 10 TED notices published that day, in a little over a second.

A keyword match is TED full text. A common word can appear in notices whose main subject is something else. Add a CPV code such as `72` when you want the IT-services branch.

### Input

| Field | Required | Default | What it does |
| --- | --- | --- | --- |
| `keywords` | No | `["software"]` | TED searches stemmed full text (`FT~`). SAM.gov searches the opportunity title only, which is the filter the Opportunities API provides. An empty list searches by the other filters alone. |
| `cpvCodes` | No | empty | EU CPV codes for TED. `72000000`, `72000000-1`, or a prefix such as `72`. A prefix matches that branch. SAM.gov ignores these. |
| `naicsCodes` | No | empty | Exactly 6 digits. Sent to SAM.gov as `ncode`. Shorter codes are rejected because SAM.gov does not treat them as prefixes. TED ignores these. |
| `countries` | No | empty | ISO alpha-2 or alpha-3 buyer country (`FR`, `DEU`, `SWE`). TED filters `buyer-country`. SAM.gov runs when the list is empty or includes `US` / `USA`, and is skipped otherwise. |
| `dateFrom`, `dateTo` | No | yesterday and today, Europe/Brussels | Inclusive publication days, `YYYY-MM-DD`. The window can cover at most 365 days. |
| `noticeTypes` | No | every type in the window | TED codes such as `cn-standard` and `can-social`. SAM.gov letters such as `o` (solicitation), `k` (combined), `p` (pre-solicitation), `r` (sources sought), `s` (special notice), `a` (award). A code is sent only to the source that uses it. |
| `maxResults` | No | `10` | Stop after this many saved notices, from 1 to 1000. |
| `sources` | No | `["ted"]` | `ted`, `sam`, or both. `sam` requires `samGovApiKey`. |
| `onlyNew` | No | `false` | Skip ids saved by an earlier run. See below. |
| `samGovApiKey` | For SAM.gov | empty | Your own SAM.gov public API key. |

```json
{
  "keywords": ["software"],
  "sources": ["ted"],
  "maxResults": 10,
  "onlyNew": false
}
```

### Output

Each dataset item has the same fields for both sources: `source`, `noticeId`, `title`, `buyer`, `country`, `classificationScheme`, `classificationCodes`, `value`, `currency`, `publicationDate`, `deadline`, `noticeType`, `url`, `attribution`.

`value` and `currency` are null when the source did not publish an amount. On TED, the amount is the procedure estimate when TED sends one, otherwise the first lot estimate. On SAM.gov, the amount is the award amount, in USD, when the record has one.

This row is one of the 10 notices from the default run on 23 October 2026:

```json
{
  "source": "ted",
  "noticeId": "736753-2026",
  "title": "Suomi – Tietotekniset palvelut: neuvonta, ohjelmistojen kehittäminen, Internet ja tuki – Palvelimien turvasäilö",
  "buyer": "Meidän IT ja talous Oy",
  "country": "FIN",
  "classificationScheme": "CPV",
  "classificationCodes": ["72000000"],
  "value": 450000,
  "currency": "EUR",
  "publicationDate": "2026-10-23",
  "deadline": "2026-11-23",
  "noticeType": "cn-standard",
  "url": "https://ted.europa.eu/en/notice/-/detail/736753-2026",
  "attribution": "Procurement notice from the Supplement to the Official Journal of the European Union (TED). © European Union, 1998-2026. Reuse is permitted for commercial or non-commercial purposes. https://ted.europa.eu/en/legal-notice"
}
```

The title is the notice's official language when TED provides it. The URL is the English TED notice page when TED provides that link. A SAM.gov row uses `source` `sam`, `classificationScheme` `NAICS`, and a `https://sam.gov/opp/{noticeId}/view` URL with no API key in it.

### Pricing

Charge one `notice-result` event for each notice saved. Suggested price: **$0.003** per notice. At that price, a default run of 10 notices costs $0.03, and a full run of 1,000 notices costs $3.00.

| Event | When it is charged | Suggested price |
| --- | --- | --- |
| `notice-result` | One notice written to the dataset | $0.003 |
| `apify-actor-start` | Not used | Do not add this event |
| `apify-default-dataset-item` | Not used | Do not add this event |

Notices that were already seen in `onlyNew` mode are not charged. When the run reaches the user's maximum charge, saving stops.

On Apify, set the pricing model to pay per event before publishing:

1. Add the custom event `notice-result` at $0.003 and mark it as the primary event.
2. Remove `apify-default-dataset-item` and `apify-actor-start`, so a saved notice is billed once.
3. Set the minimum run charge to $0.003, which is one notice.
4. Leave Actor permissions on **Limited**. This Actor only reads its input and writes its own dataset and, when `onlyNew` is on, the named store `public-tenders-feed-state`.

Local runs log that charging was ignored, because pay-per-event pricing exists only on the platform. The notices are still saved.

### Only new notices

Turn on `onlyNew` for a scheduled watch. The Actor loads notice ids from the named key-value store `public-tenders-feed-state`, record `seen-notice-ids`, skips those ids, and appends the ids it saves. The record keeps the most recent 20,000 ids.

The first run with `onlyNew` on saves the current matches and remembers them. A later run can finish with an empty dataset when nothing new was published. That is a successful run. Leave `onlyNew` off in the default input so the Apify daily test, which replays that input, keeps receiving notices.

### Limits

- `maxResults` is at most 1,000. The date window is at most 365 days, matching the longest `postedFrom` / `postedTo` range SAM.gov accepts.
- TED fair use, from the Publications Office, allows 700 HTTP requests in the last minute and 600 notice views or downloads in 6 minutes. This Actor calls the Search API only. It does not download notice XML, PDF, or HTML. It waits 0.35 seconds between TED calls, asks for at most 50 notices per page, and stops after 20 pages.
- TED returns at most the fields needed for the normalized row, with `onlyLatestVersions` on, newest publication date first.
- SAM.gov quotas belong to your key. A basic non-federal key is about 10 requests per day and resets at midnight UTC. A key with a SAM.gov role is about 1,000 per day. This Actor sends at most 8 SAM.gov requests per run, at least 1 second apart, and at most 100 records per request. One run can combine at most 8 keyword × NAICS searches before pagination.
- SAM.gov returns the latest active version of an opportunity. The public API has no full-text parameter, so keywords filter the title.
- The signed notice on TED, and the notice page on SAM.gov, remain the authentic text. Amounts are omitted when the source does not publish them. A TED value can be an estimate for the whole procedure or, when that figure is absent, the first lot.

### Attribution and licence

Procurement notices in the Supplement to the Official Journal of the European Union can be reused for commercial or non-commercial purposes, unless a notice says otherwise. © European Union, 1998-2026. The editorial content of the TED website is licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/): reuse requires credit and a note of changes. SIMAP metadata is dedicated under CC0. SIMAP logos are not used.

Source: [TED legal notice](https://ted.europa.eu/en/legal-notice).

Changes this Actor makes to TED search fields: it keeps a single title and buyer string in the notice's official language when that text exists, otherwise English; it trims timestamps to a calendar day without shifting the timezone; it deduplicates CPV codes; and it keeps the procedure estimate when TED sends one, otherwise the first lot estimate. It does not reproduce TED web pages.

SAM.gov opportunity records are U.S. federal government data retrieved through the [Get Opportunities Public API](https://open.gsa.gov/api/get-opportunities-public-api/). Use of that API is subject to SAM.gov's own terms and to the quota on your key. Each SAM.gov row carries a short attribution string. The Actor does not follow description URLs that would need the API key appended.

### Run locally

Python 3.12.

```bash
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt -r requirements-dev.txt
.venv/bin/pytest
APIFY_LOCAL_STORAGE_DIR=./storage .venv/bin/python -m src
```

With no `INPUT.json` in the local key-value store, the run uses the default TED search. Results are written under `storage/datasets/default/`.

```bash
RUN_LIVE=1 .venv/bin/pytest tests/test_live.py
```

The live test calls TED with the default input and expects a non-empty dataset. Unit tests mock HTTP and do not call the network.

Build and deploy with the Apify CLI from this directory (`apify push`). The image is `apify/actor-python:3.12`. Memory default is 512 MB.

# Actor input Schema

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

Words or phrases to find. TED searches the full notice text (stemmed). SAM.gov searches the opportunity title only, which is all the public Opportunities API supports. Leave the list empty to search by date, CPV, NAICS, country, or notice type alone. The default keyword is software.

## `cpvCodes` (type: `array`):

EU Common Procurement Vocabulary codes for TED. Use 8 digits (72000000), a code with a check digit (72000000-1), or a shorter prefix such as 72. A prefix matches the whole branch. Ignored by SAM.gov.

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

6-digit NAICS codes for SAM.gov. The API parameter is ncode. Shorter codes are rejected here because SAM.gov does not treat them as prefixes. Ignored by TED.

## `countries` (type: `array`):

ISO 3166-1 alpha-2 or alpha-3 buyer countries, for example FR, DEU, or SWE. TED filters on buyer country. SAM.gov is US-only: it is queried when this list is empty or includes US or USA, and skipped otherwise.

## `dateFrom` (type: `string`):

First publication day, YYYY-MM-DD, inclusive. Leave both date fields empty to search yesterday and today in the Europe/Brussels calendar, which is the TED publication clock.

## `dateTo` (type: `string`):

Last publication day, YYYY-MM-DD, inclusive. The range can cover at most 365 days.

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

Optional. Leave empty for every type in the date window. TED eForms codes include cn-standard, cn-social, cn-desg, pin-cfc-standard, pin-cfc-social, pin-only, pin-rtl, pin-buyer, pin-tran, can-standard, can-social, can-desg, can-modif, can-tran, pmc, qu-sy, subco, veat, compl, brin-ecs, and brin-eeig. SAM.gov ptype codes: o solicitation, k combined synopsis/solicitation, p pre-solicitation, r sources sought, s special notice, a award, u justification, g surplus sale, i intent to bundle. Each code is sent only to the source that uses it.

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

Stop after this many saved notices, across both sources. Already seen notices are not counted when Only new notices is on.

## `sources` (type: `array`):

ted calls the EU TED Search API and needs no key. sam calls the US SAM.gov Opportunities API and needs samGovApiKey.

## `onlyNew` (type: `boolean`):

When on, skip notice ids already saved by a previous run and remember the ids you save now. State is kept in the named key-value store public-tenders-feed-state. Leave this off for the default run: a scheduled watch that finds nothing new is a successful empty dataset, and the Apify daily test needs the default input to return notices.

## `samGovApiKey` (type: `string`):

Your own public API key from the SAM.gov Account Details page. It is a secret input, sent only to api.sam.gov, and never written to the dataset or the log. Leave it empty when you search TED only.

## Actor input object example

```json
{
  "keywords": [
    "software"
  ],
  "cpvCodes": [],
  "naicsCodes": [],
  "countries": [],
  "noticeTypes": [],
  "maxResults": 10,
  "sources": [
    "ted"
  ],
  "onlyNew": false
}
```

# Actor output Schema

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

One normalized row per public-procurement notice.

# 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": [
        "software"
    ],
    "cpvCodes": [],
    "naicsCodes": [],
    "countries": [],
    "noticeTypes": [],
    "maxResults": 10,
    "sources": [
        "ted"
    ],
    "onlyNew": false
};

// Run the Actor and wait for it to finish
const run = await client.actor("plusonets/public-tenders-feed").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": ["software"],
    "cpvCodes": [],
    "naicsCodes": [],
    "countries": [],
    "noticeTypes": [],
    "maxResults": 10,
    "sources": ["ted"],
    "onlyNew": False,
}

# Run the Actor and wait for it to finish
run = client.actor("plusonets/public-tenders-feed").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": [
    "software"
  ],
  "cpvCodes": [],
  "naicsCodes": [],
  "countries": [],
  "noticeTypes": [],
  "maxResults": 10,
  "sources": [
    "ted"
  ],
  "onlyNew": false
}' |
apify call plusonets/public-tenders-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plusonets/public-tenders-feed"
        }
    }
}
```

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/UG4LdfkgcL2CTJkmj/builds/p4zotzpqJnfgKvnK6/openapi.json
