# AENA Airport Stats Scraper (Traffic JSON) (`steadfetch/aena-estadisticas-extractor`) Actor

Returns monthly passenger, operations and cargo statistics for every airport in the AENA Spanish airport network as JSON from AENA official XLS reports. One validated record per airport and month; a repeat run of the same month emits nothing.

- **URL**: https://apify.com/steadfetch/aena-estadisticas-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Business, Travel
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 airport record delivereds

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

## AENA Airport Statistics Scraper (Spanish Airport Traffic Data)

Monthly passenger, operations and cargo statistics for every airport in AENA's
Spanish network, extracted from AENA's **official monthly XLS reports**.

- **Source:** [AENA monthly statistical reports](https://www.aena.es/es/estadisticas/informes-mensuales.html) (public, no login, no CAPTCHA on these routes).
- **One record per airport and month.**
- **No personal data:** only aggregated airport traffic figures.

### What it does

A run does two things:

1. Reads AENA's monthly reports listing (`?anio=YYYY`) and picks the requested
   month -- or the latest published month when no period is given.
2. Downloads that month's XLSX and joins its three side-by-side tables
   (passengers, operations and cargo) by airport name, emitting one validated
   record per airport.

Airports are matched across the three tables after normalising accents, case
and repeated spaces. When an airport is missing from one of the tables, the
corresponding fields are `null` -- values are never inferred or invented.
`Total ...` rows are excluded in v1 (they can be derived by summing the
airports).

### Example output

```json
{
  "airport": "ADOLFO SU\u00c1REZ MADRID-BARAJAS",
  "period": "2026-01",
  "passengers": 5379569,
  "passengersYoYPct": 3.49,
  "operations": 34658,
  "operationsYoYPct": 2.51,
  "cargoKg": 62538434,
  "cargoYoYPct": 6.27,
  "provisional": true,
  "url": "https://www.aena.es/sites/Satellite?blobcol=urldata&blobkey=id&blobtable=MungoBlobs&blobwhere=1576873282373&ssbinary=true",
  "fileUrl": "https://www.aena.es/sites/Satellite?blobcol=urldata&blobkey=id&blobtable=MungoBlobs&blobwhere=1576873282373&ssbinary=true",
  "scrapedAt": "2026-10-21T03:00:00.000Z"
}
```

- `passengers`, `operations`, `cargoKg` are the monthly totals published by AENA.
- `*YoYPct` is the year-over-year variation in percent, rounded to 2 decimals
  (AENA publishes the fraction; this Actor multiplies by 100).
- `provisional` is `true` when AENA marks the report as provisional data.
- `url` (common Steadfetch envelope) and `fileUrl` point to the same official
  XLSX report; the record is never built from any other page.

### Input

```json
{ "period": "2026-01" }
```

| Field    | Type   | Required | Description |
| -------- | ------ | -------- | ----------- |
| `period` | string | no       | Report month as `YYYY-MM`. Omit it to extract the latest month published in AENA's listing. |

The Actor only requests the reports listing and the selected XLSX file. It
never calls `/consultas-personalizadas` or `infovuelos` (both are behind
reCAPTCHA), and it uses no browser and no proxy.

### Pay-per-event pricing

| Event | When it fires | Price |
| ----- | ------------- | ----- |
| `apify-actor-start` | once per run (synthetic, automatic) | 0.00005 USD |
| `apify-default-dataset-item` | one validated airport/period record saved to the dataset (synthetic, automatic) | 0.001 USD per record = **1.00 USD per 1,000 records** |

Only validated records are pushed to the dataset. Downloads, total rows and
repeat runs of an already delivered month are never charged: there is no
custom per-item event, so a record can never be billed twice.

#### Cost per 1,000 results (measured locally)

| Measurement | Value |
| ----------- | ----- |
| January 2026 XLSX | 55,558 bytes, 117 rows, 66 airport records |
| Parse + join + schema validation | **22.6 ms**, 9 MB JS heap (84 MB process RSS) |
| Network | 1 listing page (~84 KB) + 1 XLSX (~56 KB) |
| Run memory ceiling | 256 MB (Actor minimum) |

At 256 MB and roughly 1-2 s per run (two or three HTTP requests dominate), a run costs
about 0.0001 GB-hour. At 0.30 USD/GB-hour that is **well below 0.01 USD per
1,000 records**. The platform figure has not been billed yet: no platform run
has been spent on this Actor, because the Apify account has a hard 85 USD/month
team credit cap. It will be updated here after the first minimal platform run.

### Dedupe and reruns

Delivered `(airport, period)` keys are stored in the named key-value store
`aena-monthly-state`. Running the same period again emits **0 new records** and
charges nothing. The following month emits the new month's records.

This makes the Actor safe to run on a schedule: a daily run only pays for a new
month once.

### Development

```bash
npm install --include=dev
npm test                     # lint + typecheck + 58 tests (no network, no credit)
npm run demo:fixtures        # offline pipeline on the real Jan-2026 fixture
npm run publish:check
npm run build && npm run verify:build
```

`npm run demo:fixtures` reads `test/fixtures/informes-mensuales-2026.html` and
`test/fixtures/informe-2026-01.xlsx`, runs the same parsing pipeline and prints
the validated records. It needs no `APIFY_TOKEN`.

### Data notes

- AENA publishes these reports as **provisional data** (`DATOS PROVISIONALES`);
  the source can revise them later. The `provisional` flag tells you when the
  snapshot is provisional.
- The January 2026 workbook contains airline-network airports plus
  international subsidiaries (BOAB, ANB, London-Luton) exactly as AENA
  publishes them. Totals (`Total Aeropuertos Espanoles`, `Total BOAB`,
  `Total ANB`, `Total Aeropuertos Brasilenos`, `TOTAL RED AENA`) are excluded
  from v1.
- Values are copied as published. Cargo is in kilograms, exactly as in the
  report.

### FAQ

**How fresh is the data?** Each run downloads the official XLSX of the selected
month from AENA. No cached third-party copy is used.

**Can I get several years?** Yes: run once per `period` (`YYYY-MM`). Every month
from 2004 onward is listed on AENA's site.

**Why is a field `null`?** That airport is not present in the corresponding
table of that report. The Actor never guesses or interpolates.

**Does it need a proxy or a browser?** No. Plain HTTP requests and a local XLSX
reader.

**What happens on a bot challenge or a 403?** The run aborts with a clear error.
Steadfetch never solves CAPTCHAs or tries to bypass access controls.

### Maintenance

This Actor is **tested daily**: automated runs keep the source reachable and the
output schema valid, and failures open an issue in this repository. The daily
QA entry is registered in the Steadfetch QA catalogue and is enabled once the
Actor is published on Apify. Schema changes are announced through repository
releases.

### Legal and attribution

Only public, aggregated data published by AENA is read. The Actor honours
`robots.txt`, sends an honest User-Agent with contact information and makes two
HTTP requests per run when `period` is set (three at most in default mode, when a
second-year listing is needed). See `docs/legal-gate.md` for the reviewed evidence.
Source: AENA (public statistics). Steadfetch is not affiliated with AENA.

# Actor input Schema

## `period` (type: `string`):

Report month in YYYY-MM format (e.g. "2026-01"). Leave empty to use the latest month published in AENA's reports listing.

## Actor input object example

```json
{
  "period": "2026-09"
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "period": "2026-01"
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/aena-estadisticas-extractor").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 = { "period": "2026-01" }

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/aena-estadisticas-extractor").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 '{
  "period": "2026-01"
}' |
apify call steadfetch/aena-estadisticas-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/aena-estadisticas-extractor"
        }
    }
}
```

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/wgvoME4XOGfnaGLbh/builds/FjUGv6NBvPtUwLaii/openapi.json
