# Spain Public Tenders - licitaciones y concursos publicos (`opendata_es/spain-public-tenders`) Actor

Every public tender from Spain's Plataforma de Contratacion del Sector Publico (PCSP), normalized in English: filter by CPV, budget, region and deadline, with award winners. Licitaciones, concursos y contratos publicos de Espana como datos (perfil del contratante, BOE).

- **URL**: https://apify.com/opendata_es/spain-public-tenders.md
- **Developed by:** [c.d.s Studio](https://apify.com/opendata_es) (community)
- **Categories:** Lead generation, Open source
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## 🏛️ Spain Public Tenders — licitaciones públicas de España (PCSP)

> Every public tender update from Spain's **Plataforma de Contratación del Sector Público**,
> normalized in English, filterable by CPV, budget, region and deadline — **with award winners
> and cumulative history** the official feed throws away.
> Todas las licitaciones públicas de España (publicaciones, modificaciones y adjudicaciones),
> normalizadas en inglés, filtrables por CPV, importe, comunidad y plazo — **con adjudicatarios
> e histórico acumulado** que la fuente oficial no conserva.

***

### 🇪🇸 Español

#### Qué hace

La Plataforma de Contratación del Sector Público (PCSP) sindica cada actualización de
licitación por ATOM (`licitacionesPerfilesContratanteCompleto3`): publicaciones,
modificaciones y **adjudicaciones de toda España**. El feed es gratis pero **bruto**:

| Problema de la fuente oficial | Qué hace este actor |
|---|---|
| XML CODICE crudo con namespaces ubicuos (`cac-place-ext`, `cbc`, `cac`) | JSON limpio en inglés, `camelCase`, estable |
| El feed es **una ventana deslizante**: lo publicado se pierde | Histórico diario acumulado + diff de novedades contra la ejecución anterior |
| Sin filtros de ningún tipo: te baja **todo España** | Filtras por comunidad autónoma (**Canarias de serie**), CPV (prefijo), importe mín/máx, tipo, estado y plazo |
| Códigos cripticos (`EV`, `79342200`, typeCode `2`) | Traducción oficial a inglés de estados, tipos y procedimientos (listas .gc de la propia plataforma) |
| El plazo de presentación está enterrado (`TenderSubmissionDeadlinePeriod`) | `submissionDeadline` ISO + `daysToDeadline` (cuenta atrás) + alerta de cierre |
| Las adjudicaciones hay que cazarlas en el `summary` | `winnerName`, `winnerTaxId`, importes neto/bruto por lote (`awards`) |
| La comunidad autónoma no existe como campo: hay que deducirla del NUTS | `autonomousCommunity` + `autonomousCommunityCode` (NUTS-2/3, con fallback a la jerarquía del órgano) |
| Paginación manual (`rel="next"`) de páginas de ~500 entradas y varios MB | Camina `maxPages` páginas solo, con reintentos |
| Sin resumen ni contadores | `OUTPUT` con desglose por tipo/estado/comunidad, presupuestos totales y noticias |

#### Para quién

- **Pymes y proveedores** — «todas las licitaciones de Canarias de mi sector (CPV),
  por encima de 50.000 €, que cierren en menos de 15 días».
- **Consultoras de licitaciones y business development** — pipeline diario de
  oportunidades por región/importe/CPV, sin construir el ETL del feed CODICE.
- **Analistas de datos públicos y periodismo** — la **serie temporal** de estados
  (publicada → evaluación → adjudicada) y de adjudicatarios que la fuente no guarda.
- **ERPs y CRMs** — enriquecer el pipeline comercial con licitaciones en JSON listo.

#### Ejemplos de entrada

Licitaciones de Canarias (por defecto) del sector construcción por encima de 200.000 €,
avisándome de las que cierran en menos de 15 días:

```json
{
  "communities": ["Canarias"],
  "cpvCodes": ["45"],
  "minBudget": 200000,
  "deadlineWithinDays": 15,
  "alertWebhookUrl": "https://hooks.tu-dominio.com/plazo"
}
```

Todo España, servicios de IT (CPV 48), solo abiertas:

```json

{ "communities": [], "cpvCodes": ["48"], "contractTypes": ["services"], "onlyOpen": true }
```

Las grandes adjudicadas de toda España, con adjudicatario e importes:

```json
{ "communities": [], "statuses": ["ADJ", "RES"], "minBudget": 1000000 }
```

Los alias de comunidad funcionan: `islas canarias`, `euskadi`, `comunitat valenciana`,
`illes balears`, `la rioja`… o el nombre oficial. Vacío = toda España.

#### Salida (una fila por licitación, la más reciente primero)

```json
{
  "id": "19950042",
  "folderNumber": "04/2026",
  "title": "Servicio integral de comunicación digital para SODECAN",
  "status": "EV",
  "statusLabel": "award_pending",
  "type": "services",
  "budgetNet": 80000,
  "budgetGross": 85600,
  "currency": "EUR",
  "cpvCodes": ["79342200", "79341100"],
  "mainCpv": "79342200",
  "region": "Gran Canaria",
  "nutsCode": "ES705",
  "autonomousCommunity": "Canarias",
  "autonomousCommunityCode": "ES70",
  "procedure": "simplified_open",
  "submissionDeadline": "2026-07-13T22:59:00+02:00",
  "daysToDeadline": -95,
  "authorityName": "Consejería Delegada de la Sociedad para el Desarrollo Económico de Canarias, SODECAN S.A.",
  "authorityTaxId": "A28485548",
  "buyerProfileUrl": "https://contrataciondelestado.es/wps/poc?uri=deeplink:perfilContratante&idBp=...",
  "detailUrl": "https://contrataciondelestado.es/wps/poc?uri=deeplink:detalle_licitacion&idEvl=...",
  "hasAward": true,
  "winnerName": "ORANGE ESPAGNE, S.A.U.",
  "winnerTaxId": "A82009812",
  "awardedAmountNet": 65404,
  "awards": [{ "winnerName": "ORANGE ESPAGNE, S.A.U.", "lotId": "1", "amountNet": 65404 }],
  "statusChange": "PUB>EV",
  "updatedAt": "2026-10-15T20:07:31.720000+02:00",
  "fetchedAt": "2026-10-16T01:11:28+00:00"
}
```

Además:

- **`OUTPUT`** (key-value store): resumen del run — entradas vistas, licitaciones que
  matchean, desglose por tipo/estado/comunidad, presupuesto total y medio, adjudicaciones,
  plazos al cierre y el diff de novedades (nuevas, cambios de estado, nuevas adjudicaciones).
- **Dataset `licitaciones-historico`**: una fila por licitación/día (`date` ISO). Con un
  **Schedule** diario construyes la serie temporal de estados y adjudicaciones que la
  plataforma no conserva.

#### Programarlo

Apify Console → *Schedules* → cada 6-12 h (el feed se actualiza de continuo; 2 páginas
≈ las últimas 24-48 h de toda España). Con una ejecución diaria ya no se te escapa nada;
con dos al día pillas los mismos-day awards.

#### Honestidad sobre los datos

- El feed trae **actualizaciones**: una misma licitación aparece varias veces (publicación,
  modificación, adjudicación). El actor deduplica por licitación quedándose la versión más
  reciente de la ventana descargada; más páginas = ventana más amplia.
- `daysToDeadline` se calcula contra la fecha del run; los plazos ya vencidos dan días
  negativos (no se ocultan).
- La comunidad autónoma se deduce del NUTS (nivel 2/3); si el registro solo trae NUTS-1 o
  «España», cae a la jerarquía del órgano contratante. En la muestra real: **95,4 %**
  identificado; el resto queda sin `autonomousCommunity` y no se inventa.
- Presupuesto: se usa `EstimatedOverallContractAmount` (neto) y, si no existe,
  `TaxExclusiveAmount` o el importe adjudicado neto. Los importes son los declarados por
  los órganos; puede haber lotes y acuerdos posteriores.
- Los códigos de estado/tipo/procedimiento se traducen con las **listas oficiales .gc**
  de la plataforma (SyndicationContractFolderStatusCode-2.04, ContractCode-2.08,
  SyndicationTenderingProcessCode-2.07).
- Datos públicos sin información personal de terceros (los adjudicatarios son personas
  jurídicas publicadas por la propia plataforma).

***

### 🇬🇧 English

#### What it does

Spain's public procurement platform (PCSP) syndicates every tender update as raw
CODICE Atom XML — new notices, amendments and **award decisions for the whole country**.
This actor turns that firehose into production-ready JSON:

- Filters: **autonomous community** (Canarias by default), **CPV prefix**, **min/max
  budget**, contract type, folder status, open-only, **deadline alert window**.
- Clean typed output in English: budgets as floats, NUTS → autonomous community,
  official code lists translated (status, contract type, procedure).
- **Submission deadline** extracted with a day countdown and `closingSoon` alerts
  (optional webhook POST).
- **Award winners**: name, tax ID, net/gross amounts, per-lot detail.
- **Daily history dataset** + diff against the previous run: new tenders, status
  changes (`PUB>EV`), new awards — the memory the sliding-window feed lacks.
- Run summary in `OUTPUT`: counts by type/status/community, budget totals, deadlines.

#### Input

See the input schema. Everything is optional; defaults are `communities: ["Canarias"]`
and `maxPages: 2` (≈ the last 1-2 days of all Spain; each page is up to ~500 updates).
Community aliases accepted: `islas canarias`, `euskadi`, `comunitat valenciana`…

#### Who buys this

SMEs and suppliers chasing regional tenders in their CPV sector, bid-consultancy
pipelines, data journalists tracking who wins public money, and ERP/CRM vendors
enriching their commercial pipeline.

***

### 💰 Pricing — pay per event

Este actor se monetiza por **evento**, con las **mismas claves de evento** que
*spain-fuel-prices* (catálogo uniforme: `actor-start` / `station-result` /
`history-snapshot` / `price-alert`). El reparto de Apify es **80 % para el
desarrollador**.

| Evento | Cuándo se cobra | Precio |
|---|---|---|
| `actor-start` | 1 por ejecución (descarga y parseo de las páginas del feed) | **$0,02** |
| `station-result` | 1 por licitación entregada, normalizada y filtrada | **$0,0004** |
| `history-snapshot` | 1 por ejecución que añade el día al histórico y difea novedades | **$0,01** |
| `price-alert` | 1 por licitación que cierra dentro de la ventana pedida | **$0,001** |

Los valores exactos están en [`.actor/pay_per_event.json`](.actor/pay_per_event.json)
(mismas claves que cobra `src/main.py`).

#### Por qué sale a 25–120 €/mes

Cifras medidas sobre la muestra real (629 actualizaciones en 2 páginas; 54 licitaciones
canarias con 283 M € de presupuesto agregado; 220 adjudicaciones). Tipo de cambio
aproximado 0,92 €/$. **Estimaciones, no ingresos garantizados**: dependen de que haya
clientes.

| Perfil de cliente | Consumo mensual | Bruto/mes | Neto dev (80 %) |
|---|---|---|---|
| **A — pymes canarias**: 1 comunidad, 2 ejecuciones/día | 60 runs × (0,02 + ~60×0,0004 + 0,01) | $7,20 | **$5,8 ≈ 5,3 €** |
| **B — consultora regional multi-CPV**: 2 comunidades + alertas, 4 ejecuciones/día | 120 runs × ≈0,11 | $13,20 | **$10,6 ≈ 9,7 €** |
| **C — consultora nacional**: toda España, 4 ejecuciones/día | 120 runs × (0,02 + ~1250×0,0004 + 0,01) | $85,20 | **$68,2 ≈ 63 €** |
| **D — C + 3×A + 2×B** | suma | $117,60 | **$94 ≈ 87 €** |

El coste de infraestructura lo cubre el evento: una ejecución nacional (2 páginas,
\~629 entradas) tarda ~15 s de descarga+parseo en local.

***

### 🛠️ Desarrollo y publicación

```bash
## 1. Tests del núcleo contra el feed real de la PCSP (18 tests)
python -m pytest tests/test_licitaciones.py -v

## 2. Ejecución local completa (crea ./storage con dataset y OUTPUT)
python -m venv .venv && .venv/Scripts/pip install -r requirements.txt
##    deja tu entrada en storage/key_value_stores/default/INPUT.json
.venv/Scripts/python -m src

## 3. Publicación en Apify Store  ← ÚNICO paso que requiere la cuenta
npx -y apify-cli login            # abre el navegador, pega el API token
npx -y apify-cli push             # construye la imagen y sube el actor
##    luego, en Apify Console:
##    Monetization -> Pay per event -> pegar los 4 eventos de .actor/pay_per_event.json
##    Publication  -> Publish to Store (categoría: Business)
```

#### Estructura

```
.actor/
  actor.json            # metadatos del actor
  input_schema.json     # formulario de entrada (12 campos)
  dataset_schema.json   # vista de tabla «Newest first» en la Console
  pay_per_event.json    # pricing exacto a pegar en Monetization
src/
  licitaciones.py       # núcleo puro: descarga, normaliza, filtra, difea
  main.py               # wrapper de plataforma: storages, cobro, webhook, OUTPUT
  __main__.py
tests/
  test_licitaciones.py  # 18 tests, 14 de ellos contra el feed real cacheado
Dockerfile              # apify/actor-python:3.12
sample_feed/            # caché local de páginas reales del feed (gitignored)
```

Fuente: `https://contrataciondelsectorpublico.gob.es/sindicacion/sindicacion_643/licitacionesPerfilesContratanteCompleto3.atom`
(sindicación de contenido de la Plataforma de Contratación del Sector Público —
Ministerio de Hacienda; reutilizable bajo la Ley 37/2007).

# Actor input Schema

## `communities` (type: `array`):

Filter by autonomous community (NUTS-2), accent-insensitive, aliases accepted: 'islas canarias', 'euskadi', 'comunitat valenciana'... Empty = all Spain. Defaults to Canarias.

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

Common Procurement Vocabulary codes, prefix match: '45' = all construction, '79342200' = exact service. Empty = everything.

## `minBudget` (type: `number`):

Only tenders with a net budget (or awarded amount) at or above this value, in EUR.

## `maxBudget` (type: `number`):

Only tenders with a net budget (or awarded amount) at or below this value, in EUR.

## `contractTypes` (type: `array`):

Official ContractCode list translated: works, services, goods, or the special ones (services_concession, public_works_concession...). Empty = every type.

## `statuses` (type: `array`):

Folder status codes from the platform: PUB (open, in submission period), EV (award pending), ADJ (awarded), RES (resolved), PRE (prior notice), ANUL (cancelled). Empty = every status.

## `onlyOpen` (type: `boolean`):

Keep only tenders still in submission period: status PUB or a submission deadline that has not passed.

## `deadlineWithinDays` (type: `integer`):

Flag tenders whose submission deadline falls within this many days (closingSoon) and fire the deadline alert. 0 = off.

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

The syndicated feed pages backwards through history (~500 tender updates per page, several MB each). 2 pages ≈ the last 1-2 days of all Spain.

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

Hard cap on dataset rows. 0 = no cap.

## `storeHistory` (type: `boolean`):

Appends one row per tender/day to the named dataset and diffs new tenders, status changes and new awards against the previous run.

## `alertWebhookUrl` (type: `string`):

Optional. Receives a POST with the tenders closing within the alert window.

## Actor input object example

```json
{
  "communities": [
    "Canarias"
  ],
  "onlyOpen": false,
  "deadlineWithinDays": 0,
  "maxPages": 2,
  "maxItems": 0,
  "storeHistory": true
}
```

# Actor output Schema

## `tenders` (type: `string`):

Every tender returned by this run as dataset items: status, title, contracting authority, net and gross budget in EUR, CPV codes, contract type, autonomous community (NUTS), procedure, submission deadline with countdown days, award winners with amounts and deep link to the platform.

## `summary` (type: `string`):

One JSON record with the counts of the run: pages parsed, entries seen, tenders matched, breakdown by type/status/community, total and average budget, awards included, deadlines closing soon and the news diff against the previous run.

# 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 = {
    "communities": [
        "Canarias"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("opendata_es/spain-public-tenders").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 = { "communities": ["Canarias"] }

# Run the Actor and wait for it to finish
run = client.actor("opendata_es/spain-public-tenders").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 '{
  "communities": [
    "Canarias"
  ]
}' |
apify call opendata_es/spain-public-tenders --silent --output-dataset

```

## MCP server setup

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

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/VLvH3BdYcSr71ypwW/builds/z2lYJjR2MO4KCUCXr/openapi.json
