# Spain Public Procurement Scraper — Tender Notices (`studio-amba/contrataciondelestado-es-scraper`) Actor

Extract Spanish public procurement tender notices from the official Plataforma de Contratación del Sector Público open-data feed. Filter by keyword, region, CPV code and status. Returns buyer, CPV codes, estimated value, deadline and a direct link to the notice. No login, no API key.

- **URL**: https://apify.com/studio-amba/contrataciondelestado-es-scraper.md
- **Developed by:** [Studio Amba](https://apify.com/studio-amba) (community)
- **Categories:** Business, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 result scrapeds

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?

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

## Spain Public Procurement Scraper — Contratación del Estado Tenders

Extract Spanish public procurement tender notices from the Plataforma de Contratación del Sector Público (contrataciondelestado.es), Spain's national public procurement platform run by the Ministerio de Hacienda. Filter by keyword, region, CPV code and notice status. Returns the buyer, estimated value, CPV codes, submission deadline, procedure type, and a direct link to the notice. No login, no API key.

### How to scrape Spain public procurement data

This actor reads the platform's own official open-data feed instead of scraping HTML pages. contrataciondelestado.es publishes every notice it processes — tender announcements, prior notices, award decisions, resolutions — as a single public ATOM feed in the CODICE/UBL schema (the EU's standard procurement XML format). The actor downloads that feed, walks its pagination (`rel="next"`) back through recent activity, and parses each entry into a flat, structured record. Because this is the platform's own syndicated data, every field this actor returns is exactly what the contracting authority submitted — no HTML layout to break, no anti-bot to defeat.

Leave the input empty and the actor returns notices currently open for submission — the highest-value dataset for finding new business. Add a keyword, a region, or a CPV prefix to narrow the search.

#### Who benefits from this data?

- **Government contractors and system integrators** — Monitor new Spanish public tenders in your sector (construction, IT, services, supplies) across every level of government: ministries, regional governments, city councils, public agencies.
- **B2B sales and business development teams** — Build a live list of Spanish public buyers procuring in your category, filtered by Autonomous Community or province.
- **Bid consultancies and public-sector advisors** — Feed new tender alerts into a client-facing monitoring tool without maintaining a scraper against a government portal.
- **Market and competitive researchers** — Track procurement volume, contract values and procedure types across Spain's public sector over time.
- **Journalists and public-spending researchers** — Investigate what Spanish public bodies are buying, from whom, for how much, and in which region.

#### Search by keyword

Enter a keyword to match against the notice title, contracting authority and description (the description includes the tender's official summary line: expediente ID, buyer, amount and status). Leave `searchQuery` empty to skip the keyword filter entirely.

#### Filter by status

`tenderStatus` filters by the notice's official lifecycle stage, using the same status codes the platform itself uses:

- `PUB` (default) — En plazo: open for submission, currently accepting bids
- `EV` — Pendiente de adjudicación: bids received, under evaluation
- `ADJ` — Adjudicada: awarded
- `RES` — Resuelta: resolved / formalized
- `PRE` — Anuncio previo: prior information notice
- `ANUL` — Anulada: cancelled
- `ALL` — no status filter, return every notice type

#### Filter by region or CPV code

`region` matches against the buyer's Autonomous Community or province (Spanish names, e.g. `Cataluña`, `Andalucía`, `Sevilla`). `cpvPrefix` matches the first digits of any CPV (Common Procurement Vocabulary) code on the notice — use `45` for construction works, `72` for IT services, `71` for architecture and engineering, and so on.

### Input parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|--------------|
| `searchQuery` | String | — (no filter) | Keyword matched against title, buyer and description |
| `tenderStatus` | Enum | `PUB` | `PUB`, `ALL`, `EV`, `ADJ`, `RES`, `PRE`, `ANUL` — see above |
| `region` | String | — (no filter) | Autonomous Community or province, partial match |
| `cpvPrefix` | String | — (no filter) | CPV code prefix, e.g. `45` |
| `maxResults` | Integer | `50` | Maximum number of notices to return (1–5,000) |
| `maxPages` | Integer | `5` | How many feed pages to walk back through (1–30) — see Limitations |
| `proxyConfiguration` | Object | Apify proxy | Apify proxy settings |

### What data does this scraper extract?

| Field | Type | Description |
|-------|------|--------------|
| `itemTitle` | String | Notice title |
| `referenceNumber` | String | Buyer's internal file number ("expediente") |
| `organization` | String | Contracting authority name |
| `status` | String | Official status label (En plazo, Adjudicada, Resuelta, etc.) |
| `publicationDate` | String | Earliest official publication date found on the notice |
| `deadline` | String | Bid submission deadline (ISO date, with time where published) |
| `estimatedValue` | String | Estimated contract value (overall, or tax-exclusive if that is all the notice publishes) |
| `currency` | String | Currency of `estimatedValue`, usually `EUR` |
| `category` | String | Contract type: Suministros (goods), Servicios (services), Obras (works), or a concession type |
| `cpvCodes` | Array | CPV codes on the notice, each with an approximate division-level description |
| `region` | String | Province and Autonomous Community, combined |
| `autonomousCommunity` | String | Autonomous Community, isolated (empty for central-government buyers) |
| `province` | String | Province or place of performance |
| `procedureType` | String | Procurement procedure: Abierto, Restringido, Negociado, Contrato menor, etc. |
| `lots` | Integer | Number of lots ("lotes") the contract is split into, 0 if not split |
| `noticeTypes` | Array | Which official notice types have been published for this file (e.g. contract notice, award notice) |
| `itemDescription` | String | The notice's official summary line |
| `contactInfo` | Object | Buyer phone, email and website where published |
| `url` | String | Direct link to the notice on contrataciondelestado.es |
| `scrapedAt` | String | ISO timestamp of when the data was scraped |

### Example output

```json
{
    "itemTitle": "Suministro de diverso material de oficina, papel y material de imprenta para el Ayuntamiento de Palencia",
    "referenceNumber": "2025/13499",
    "organization": "Junta de Gobierno Local del Ayuntamiento de Palencia",
    "status": "En plazo (abierto a licitación)",
    "publicationDate": "2026-09-01",
    "deadline": "2026-09-30T23:59:00",
    "estimatedValue": "269701.45",
    "currency": "EUR",
    "category": "Suministros",
    "cpvCodes": [
        { "code": "30000000", "description": "Office and computing machinery and supplies" }
    ],
    "region": "Palencia, Castilla y León",
    "autonomousCommunity": "Castilla y León",
    "province": "Palencia",
    "procedureType": "Abierto",
    "lots": 3,
    "noticeTypes": ["DOC_PIN_RTL", "DOC_CD", "DOC_CN"],
    "itemDescription": "Id licitación: 2025/13499; Órgano de Contratación: Junta de Gobierno Local del Ayuntamiento de Palencia; Importe: 117261.5 EUR; Estado: PUB",
    "contactInfo": {
        "phone": "979718186",
        "email": "licitaciones@aytopalencia.es",
        "website": "http://www.aytopalencia.es"
    },
    "url": "https://contrataciondelestado.es/wps/poc?uri=deeplink:detalle_licitacion&idEvl=iby7ltxH1cIeIBJRHQiPkQ%3D%3D",
    "scrapedAt": "2026-09-03T14:16:44.736Z"
}
```

### Tips for best results

- **Daily monitoring**: Schedule a run with the default `tenderStatus: PUB` and no keyword to catch every newly opened tender, then filter locally by CPV or region.
- **Broad discovery first**: Leave `searchQuery` and `region` empty on the first run to see the spread of currently open notices across Spain, then narrow once you know which terms and regions matter to you.
- **Awards, not just open tenders**: Set `tenderStatus` to `ADJ` or `RES` to see who won recent contracts and for how much, instead of what's currently open.
- **CPV over keyword for precision**: Spanish notice titles are free text and vary a lot between buyers; a CPV prefix (e.g. `45` for construction) is a more reliable filter than a keyword when you care about a specific sector.

### How much does it cost to run?

The feed itself is free public data. Your only cost is Apify compute time and proxy usage. Each feed page is a single HTTP request covering 150–500 notices, so cost scales with how many pages the run needs to walk (governed by `maxPages` and how quickly your filters find matches), not with `maxResults` directly.

| Search | Estimated time | Estimated cost | Cost per result |
|--------|-----------------|-----------------|-------------------|
| 20 open notices, no filter | ~15 seconds | ~$0.01 | ~$0.0005 per result |
| 50 notices, keyword filter | ~30 seconds | ~$0.02 | ~$0.0004 per result |
| 500 notices, `maxPages: 20` | ~2–4 minutes | ~$0.10 | ~$0.0002 per result |

Actual cost depends on your Apify plan and proxy usage; a run's final usage cost only settles after the run reports SUCCEEDED — reading the dataset mid-run will undercount it.

### Use as an API

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")

run = client.actor("studio-amba/contrataciondelestado-es-scraper").call(run_input={
    "searchQuery": "obras",
    "tenderStatus": "PUB",
    "region": "Cataluña",
    "maxResults": 50,
})

for tender in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(f"{tender['itemTitle']} | {tender['organization']} | {tender['estimatedValue']} {tender['currency']}")
```

### Limitations

- **Feed pagination goes back in time, not forward**: the feed is a chronological activity log, most recent first. `maxPages` bounds how far back the actor walks looking for matches — narrow filters (a rare keyword, a small region) may need a higher `maxPages` to find enough results.
- **CPV descriptions are division-level, not exact**: `cpvCodes[].description` is derived from the CPV code's 2-digit division (e.g. all `30xxxxxx` codes map to "Office and computing machinery and supplies"), not the full 8-digit official label. The `code` itself is always exact — look it up against the official CPV vocabulary if you need the precise wording.
- **`autonomousCommunity` is empty for central-government buyers**: ministries and other national-level bodies don't sit under a regional entry in the platform's own buyer hierarchy, so there is no region to extract — this is a property of the source data, not a scraper gap. `province` (place of performance) is populated for almost every notice.
- **National platform only**: contrataciondelestado.es is Spain's central platform, used by the central government, most autonomous communities, and most local councils. A small number of regions and large cities run separate regional e-procurement portals not covered by this feed.
- **Deadline has no timezone marker**: the feed publishes submission deadlines as local Spain time without an explicit UTC offset; this actor passes the date and time through as published.

### Other European procurement scrapers

Check the [Studio AMBA actor catalog](https://apify.com/studio-amba) for more European public procurement data, including e-Vergabe (Germany), ezamowienia.gov.pl (Poland), hankintailmoitukset.fi (Finland), udbud.dk (Denmark), and other national procurement platforms.

### Maintained by Studio Amba

We run 700+ scrapers for European websites, and the priority is keeping
every one of them working. Automated runs test this actor against the live
source and verify the output is complete. When the source changes, the
scraper usually gets repaired the same day, without you having to report
anything. Questions and issues go straight to the people who built it, and
most get an answer the same day.

### Need this data on a schedule, or a custom version?

We run this scraper as a managed service for businesses: scheduled runs,
deduplication, delta detection, and delivery to your inbox, Google Sheets,
or API, maintenance included. We can also build a custom version with your
exact fields and filters, or combine multiple sources into one feed.

See [studioamba.dev/services](https://studioamba.dev/services/) or email
<hello@studioamba.dev> for a free data sample.

### Your feedback

Found a bug or want a feature? Open an issue on the [Issues tab](https://console.apify.com/actors/studio-amba~contrataciondelestado-es-scraper/issues).

# Actor input Schema

## `searchQuery` (type: `string`):

Search notices by keyword, matched against the title, contracting authority and description. Leave empty to return notices without a keyword filter.

## `tenderStatus` (type: `string`):

Filter by the notice's official status. 'Open for submission' returns tenders currently accepting bids — the highest-value dataset for finding new business. Leave on 'All statuses' to also include notices under evaluation, awarded, resolved or prior notices.

## `region` (type: `string`):

Filter by the contracting authority's Autonomous Community or province (Spanish name, e.g. 'Cataluña', 'Andalucía', 'Palencia'). Partial, case-insensitive match. Leave empty for all of Spain.

## `cpvPrefix` (type: `string`):

Only return notices with at least one CPV (Common Procurement Vocabulary) code starting with this prefix, e.g. '45' for construction works or '72' for IT services. Leave empty for all categories.

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

Maximum number of tender notices to return.

## `maxPages` (type: `integer`):

The feed is paginated in chronological batches (the first page covers the latest activity, ~150-500 notices per subsequent page going back in time). This caps how many pages the actor will walk back through looking for matches, to bound run time and data cost. Increase this if your filters are narrow and you are not getting enough results.

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

Apify proxy configuration.

## Actor input object example

```json
{
  "searchQuery": "obras",
  "tenderStatus": "PUB",
  "maxResults": 20,
  "maxPages": 5,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

No description

# 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 = {
    "searchQuery": "obras",
    "maxResults": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("studio-amba/contrataciondelestado-es-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 = {
    "searchQuery": "obras",
    "maxResults": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("studio-amba/contrataciondelestado-es-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 '{
  "searchQuery": "obras",
  "maxResults": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call studio-amba/contrataciondelestado-es-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,studio-amba/contrataciondelestado-es-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/lPG3Y2XzRz9sLI1fo/builds/USsSf5mlPFVXoY3ib/openapi.json
