# Licitaciones públicas de España - open data (no oficial) (`rianone/spain-public-tenders`) Actor

Licitaciones públicas de España desde el feed oficial de la PLACSP: registros tipados con objeto, estado, tipo de contrato, valor estimado, CPV, órgano de contratación, fechas y adjudicatario. CSV/Excel listos. No oficial: actor no afiliado a la PLACSP.

- **URL**: https://apify.com/rianone/spain-public-tenders.md
- **Developed by:** [Riccardo Pongetti](https://apify.com/rianone) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 licitacións

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 — PLACSP open data (Apify Actor)

Public tenders published on Spain's Public Sector Procurement Platform (PLACSP), from the
official open-data syndication feed **sindicacion\_643** — *"Licitaciones publicadas en los
perfiles del contratante ubicados en la Plataforma de Contratación del Sector Público,
excluyendo los contratos menores"*.

- **100% legal / no scraping**: official Atom feed published by the Dirección General del
  Patrimonio del Estado (Ministerio de Hacienda), reusable under Ley 19/2013 and
  Ley 37/2007. No key, no login, no cookies, no captcha.
- **Clean, typed records**: object, status, contract type, estimated value, CPV codes,
  contracting authority (name + NIF + DIR3), place of performance (region/NUTS), dates,
  and — when awarded — the awardee (name + NIF) and award amount.
- **Ready-to-open CSV and Excel exports** (RFC-4180 CSV, styled XLSX with real numeric
  cells, autofilter and a provenance sheet).
- **Unofficial**: not affiliated with, endorsed by or sponsored by the PLACSP or the
  Ministry of Finance.

### How it works

The Actor fetches the live feed
`https://contrataciondelsectorpublico.gob.es/sindicacion/sindicacion_643/licitacionesPerfilesContratanteCompleto3.atom`,
parses each Atom `<entry>` (CODICE 2.07 `ContractFolderStatus`), normalises it into a flat
typed record, applies the requested filters, and pushes one dataset item per returned
tender. The payload is cached in the key-value store for `cacheMinutes` (default 60) so
repeated runs do not hammer the endpoint.

### Inputs

| Field | Type | Default | Description |
|---|---|---|---|
| `statuses` | array | `[]` (all) | Filter by tender status: `PUB`, `EV`, `ADJ`, `RES`, `PRE`. |
| `keywords` | array | `[]` | Partial match on object / ID / authority (accents optional). |
| `cpvCodes` | array | `[]` | Filter by CPV code (prefix match, e.g. `4523` = construction). |
| `contractTypes` | array | `[]` | Filter by contract type: `1`=Suministros, `2`=Servicios, `3`=Obras, `8`/`22`/`50`=concesiones/otros. |
| `contractingAuthorities` | array | `[]` | Partial match on authority name or NIF. |
| `regions` | array | `[]` | Partial match on region / NUTS (e.g. `Madrid`, `ES521`). |
| `minValueEur` / `maxValueEur` | number | — | Filter on estimated value. |
| `awardedOnly` | boolean | `false` | Only tenders that already have an awardee. |
| `maxResults` | integer | `0` (no limit) | Caps the run (and its cost). The feed holds the most recent updates, up to ~500 entries. |
| `exportFiles` | array | `["csv","xlsx"]` | Also write ready-to-open files (free, no events). |
| `exportLanguage` | string | `es` | Export language: `es` / `en`. |
| `chargeReport` | boolean | `true` | Add the aggregate `run-report` event. |
| `cacheMinutes` | integer | `60` | Cache the source payload this long (0 = always fetch fresh). |

### Output

**Dataset items** (one per tender):

```
tenderId, object, status, statusCode, contractType, contractTypeCode, link,
estimatedValueEur, budgetEur, budgetNetEur, cpvCodes[],
contractingAuthority, contractingAuthorityNif, contractingAuthorityDir3,
region, regionNuts, countryCode, publishedAt, submissionDeadline,
awardDate, awardeeName, awardeeNif, awardAmountEur, awardAmountNetEur,
source, sourceUrl, sourceLicence, sourceUpdatedAt
```

**Key-value store**: `SUMMARY` (run metadata + statistics) and, when enabled, the export
files `licitaciones-es.csv` / `licitaciones-es.xlsx` (or `-en.`).

### Pay-per-event pricing

| Event | Price | When |
|---|---|---|
| `tender-charged` | **$0.02** | Once per returned tender. |
| `run-report` | **$0.01** | Once per run (optional aggregate statistics). |

Examples (cost to the customer):

- Latest 20 tenders → `20 × $0.02 + $0.01` = **$0.41**
- Awarded construction tenders (say 40 match) → `40 × $0.02 + $0.01` = **$0.81**
- Full feed (~284 entries) → `284 × $0.02 + $0.01` = **$5.69**

`maxResults` is the cost guardrail: a "give me everything" run on a busy day costs a few
dollars, never surprises the customer.

### Coste real (créditos + tokens) vs ingreso esperado

Estimates to validate in Apify Console before/after first payout:

- **Compute (créditos)**: one GET of ~8 MB + parse of ~284 XML entries runs in a couple of
  seconds of a small container. Estimated **~$0.01–0.05/run** in compute credits.
- **Tokens (LLM)**: zero at runtime (the Actor is deterministic Python; no LLM calls).
  The only token spend was one-time, building the Actor.
- **Ingreso bruto por run completo** (284 licitaciones): ~$5.69.
- **Apify platform fee** (pay-per-event revenue share): budget ~20–30% (confirm in Console).
  Net per full run ≈ **$4.0–4.5**, against ~$0.03 compute ⇒ margin ≈ 99% of revenue.

Bottom line: the marginal cost of serving a run is negligible; the value is the data. The
real question is demand (B2B tender alerts), not unit economics. Do not run un-capped
automations against the full feed for yourself — that is what `maxResults` and the cache
are for.

### Local testing (no Apify token)

```bash
uv venv .venv --python 3.13
uv pip install --python .venv/bin/python 'apify>=2.0.0' openpyxl pytest
.venv/bin/python -m pytest tests -q                          # logic
.venv/bin/python -m src.main --local examples/input-latest.json   # real end-to-end
```

### Publication checklist (account owner — NOT done here)

1. Create/verify the Apify account and complete **KYC**.
2. `apify push` (or connect the repo in Apify Console and build).
3. In Console → Monetization, enable **pay-per-event** with the two events above.
4. Verify the charge path end-to-end with a real paid run (one item) before marketing.
5. Confirm the exact platform fee and compute-credit cost; update this README.

This repo has been built and tested locally; it is **not** published.

# Actor input Schema

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

Only these tender statuses are returned. Leave empty for all.

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

Partial match on the tender object, ID or contracting authority (accents optional).

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

Filter by CPV code (prefix match, e.g. '4523' = construcción).

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

Filter by contract type code: 1=Suministros, 2=Servicios, 3=Obras.

## `contractingAuthorities` (type: `array`):

Partial match on the contracting authority name or NIF.

## `regions` (type: `array`):

Partial match on the region / NUTS code (e.g. Madrid, ES521).

## `minValueEur` (type: `number`):

Only tenders with an estimated value at or above this.

## `maxValueEur` (type: `number`):

Only tenders with an estimated value at or below this.

## `awardedOnly` (type: `boolean`):

Keep only tenders that already have an awardee.

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

Caps the run (and the pay-per-event cost). Leave empty or 0 for everything in the feed (the feed holds the most recent updates, up to ~500).

## `exportFiles` (type: `array`):

Writes downloadable files to the run's key-value store: a standard CSV and an Excel workbook with money columns as real numbers, autofilter and a provenance sheet. Free - they do not add events.

## `exportLanguage` (type: `string`):

Language of the exported files.

## `chargeReport` (type: `boolean`):

Adds the run-report event (aggregate statistics across the returned tenders).

## `cacheMinutes` (type: `integer`):

Reuses a previously downloaded feed if it is younger than this. Keeps the load on the PLACSP endpoint low. Use 0 to always fetch fresh.

## Actor input object example

```json
{
  "awardedOnly": false,
  "maxResults": 200,
  "exportFiles": [
    "csv",
    "xlsx"
  ],
  "exportLanguage": "es",
  "chargeReport": true,
  "cacheMinutes": 60
}
```

# Actor output Schema

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

Registros de licitaciones en el dataset por defecto.

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("rianone/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 = {}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rianone/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/HmErAYKfnhed9XEmt/builds/5VIuDhGJUTalEnoLc/openapi.json
