# Mexico Day-Ahead & Historical Power Prices (CENACE): Hourly (`plainworktools/mexico-power-prices-cenace`) Actor

Unofficial. Hourly CENACE nodal (PML) and load-zone (PEND) prices for SIN, BCA and BCS: tomorrow's day-ahead (MDA) or historical backfill since 2016, plus real-time (MTR) about 7 days later. Energy, loss and congestion components. Not affiliated with or endorsed by CENACE.

- **URL**: https://apify.com/plainworktools/mexico-power-prices-cenace.md
- **Developed by:** [Plainwork Tools](https://apify.com/plainworktools) (community)
- **Categories:** Other
- **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

## Mexico Day-Ahead & Historical Power Prices (CENACE): Hourly PML & Load-Zone Prices

Hourly wholesale electricity prices from Mexico's Wholesale Electricity Market (MEM), published by CENACE, delivered
as one clean row per location, operating day and hour:

- **Load-zone prices (PEND)** for every zone, e.g. MEXICALI, TIJUANA, LA PAZ, CULIACAN, MONTERREY, VDM CENTRO.
- **Nodal prices (PML)** for any NodoP code, e.g. `01PLO-115`.
- **Day-ahead market (MDA):** tomorrow's prices as soon as CENACE publishes them, plus history back to the market start
  in 2016.
- **Real-time market results (MTR):** CENACE publishes these about **7 days after** the operating day. They are
  historical results, never live prices.
- All three systems: **SIN** (national interconnected), **BCA** (Baja California) and **BCS** (Baja California Sur).
- Each row includes the price and its energy, loss and congestion components in MXN/MWh, plus local and UTC interval times.

You don't need to work around CENACE's 7-day and 20-node (10-zone) limits per call or read nested Spanish JSON. The Actor splits
your request into calls within those limits, makes them one at a time, checks every value and gives you a flat table.

**Unofficial.** Not affiliated with or endorsed by CENACE.

### Input examples

**1. Tomorrow's day-ahead prices for 10 load zones** (run it daily on a schedule; about 240 rows ≈ USD 0.25):

```json
{
  "priceType": "zone",
  "market": "MDA",
  "loadZones": ["MEXICALI", "TIJUANA", "ENSENADA", "LA PAZ", "LOS CABOS", "CULIACAN", "MONTERREY", "GUADALAJARA",
                "VDM CENTRO", "HERMOSILLO"],
  "dateMode": "tomorrow"
}
```

**2. One-year backfill of one node** (8,760 rows ≈ USD 8.77):

```json
{
  "priceType": "nodal",
  "market": "MDA",
  "nodes": ["01PLO-115"],
  "dateMode": "range",
  "startDate": "2025-01-01",
  "endDate": "2025-12-31"
}
```

**3. Real-time market (MTR) results for the last 14 published days:**

```json
{
  "priceType": "zone",
  "market": "MTR",
  "loadZones": ["MEXICALI", "CULIACAN"],
  "dateMode": "lastNDays",
  "lastNDays": 14
}
```

| Field | Meaning |
| :--- | :--- |
| `priceType` | `zone` (PEND, default) or `nodal` (PML) |
| `market` | `MDA` (day-ahead, default) or `MTR` (real-time results, about D−7) |
| `loadZones` / `nodes` | 1–200 names or codes; zone spaces or hyphens both work (`LA PAZ`, `la-paz`) |
| `system` | only for codes that the bundled NodosP catalogue doesn't list: `SIN`, `BCA` or `BCS` |
| `dateMode` | `tomorrow` (MDA only), `lastNDays`, or `range` with `startDate`/`endDate` (≤ 366 days, from 2016-01-01) |
| `maxRows` | spend cap in rows (default 50,000); only whole location-days are delivered, so the cap is never exceeded |
| `useApifyProxy` | off by default; when on, the Actor uses Apify's datacenter proxy (never residential) |

The Actor rejects an input with an impossible date, an inverted or too-long range, an empty list, or MTR dates that cannot be
published yet before it makes any request. You pay only the start event for it.

### Output

One dataset row per location, operating day and hour:

```json
{
  "system": "BCS", "market": "MDA", "priceType": "PEND", "location": "LA PAZ", "locationType": "loadZone",
  "loadZone": "LA PAZ", "state": null, "municipality": null, "operatingDate": "2026-10-10", "hourEnding": 1,
  "timezone": "America/Mazatlan", "intervalStartLocal": "2026-10-10T00:00:00-07:00",
  "intervalStartUtc": "2026-10-10T07:00:00Z", "intervalEndUtc": "2026-10-10T08:00:00Z",
  "priceMxnPerMwh": 5412.03, "energyComponent": 5362.29, "lossComponent": 49.75, "congestionComponent": 0,
  "currency": "MXN", "unit": "MWh", "source": "CENACE SW-PEND",
  "sourceUrl": "https://ws01.cenace.gob.mx:8082/SWPEND/SIM/BCS/MDA/LA-PAZ/2026/10/10/2026/10/10/JSON",
  "retrievedAt": "2026-10-19T01:00:00Z",
  "attribution": "Fuente: CENACE (Centro Nacional de Control de Energía), datos abiertos CC BY 4.0; not affiliated with or endorsed by CENACE."
}
```

Node rows also carry `loadZone`, `state` and `municipality` from CENACE's NodosP catalogue (bundled version
v2026-09-23). A code that isn't in the catalogue is still queried when you set `system`. Its enrichment fields are
`null`, and the run logs a warning.

#### Times (the UTC fields are derived)

`hourEnding` is CENACE's hour number, kept as published. It runs 1–24, 1–23 on a spring-forward day and 1–25 on a
fall-back day. The Actor derives `intervalStartLocal`, `intervalStartUtc` and `intervalEndUtc` as local midnight
plus (hour − 1) elapsed hours, in these time zones:

| System | Time zone | Evidence |
| :--- | :--- | :--- |
| BCA | America/Tijuana | follows US daylight saving time: 23 hours on 2026-03-08, 25 on 2025-11-02 |
| SIN | America/Mexico_City | 23 hours on 2021-04-04, 25 on 2021-10-31, 24 on 2025-11-02 (Mexico ended DST in 2022) |
| BCS | America/Mazatlan | 23 hours on 2021-04-04, 25 on 2021-10-31 |

These zones are inferred from CENACE's own hour counts on those days, not from a CENACE rule text. That is why the
UTC fields are labelled *derived*. With this rule the UTC intervals are gap-free and strictly increasing across every DST change.

### Coverage: what you get and what you pay for

The Actor delivers and charges a location-day only when it is **complete**: every hour from 1 to N is present
exactly once, and every value is numeric. Everything else appears in the `OUTPUT` record of the run's key-value store
and is **not charged**:

| `missing[].reason` | Meaning |
| :--- | :--- |
| `notYetPublished` | CENACE has no data yet: MDA for tomorrow, MTR for the last ~7 days |
| `noDataReturned` | CENACE returned no values for an earlier date, e.g. an inactive node |
| `beforeMarketStart` | before the first market day: MDA 2016-01-29 (SIN), 2016-01-27 (BCA), 2016-03-23 (BCS); MTR 2017-01-27 |
| `incompleteDay` | some hours were missing or extra; the whole day is withheld |
| `parseError` | a non-numeric value or two different values for the same hour |
| `sourceError` | CENACE answered with an error for that call |
| `maxRowsReached` / `chargeLimitReached` | your row cap or the run's maximum charge was reached |
| `runFailed` | the run stopped early (see `OUTPUT.error`); rows delivered before the stop stay delivered |

`OUTPUT` also lists each location's requested and returned dates, `errors[]`, `rowsCharged`, the catalogue version
and the request count. Exact duplicate values are delivered once (`duplicatesDropped`).

**MTR lag:** CENACE publishes real-time market results about 7 days after the operating day. On 2026-10-18, the latest
MTR day was 2026-10-11. An MTR request for later dates is rejected before any call is made.

### Pricing (pay per event)

| Event | Price |
| :--- | :--- |
| Actor start | USD 0.005 per run |
| Price row | USD 1.00 per 1,000 delivered rows |

The Actor charges only for rows it actually delivers. Missing dates, empty results, errors and duplicates are free. Set
`maxRows` or the run's maximum total charge to cap spend.

### How it accesses CENACE

The Actor calls only CENACE's public SW-PML and SW-PEND web services at `ws01.cenace.gob.mx:8082`, using a fixed URL
template. It doesn't read web pages, follow links, log in or use cookies. It makes one request at a time, waits at
least 1 second between requests, retries up to 3 times on timeouts and server errors, and stops if CENACE refuses
requests (HTTP 403/429). Each run first checks that the service host still has no `robots.txt`. If one appears, the
run stops before any data request and asks for owner review.

### Attribution and reuse

Fuente: CENACE (Centro Nacional de Control de Energía), datos abiertos CC BY 4.0. CENACE publishes the same price
series as open data on datos.gob.mx under Creative Commons Attribution 4.0. Keep the `attribution` field, or an
equivalent credit, when you republish the data. This Actor is not affiliated with or endorsed by CENACE. Rows contain
only codes, dates, times and prices, so they hold no personal data.

# Actor input Schema

## `priceType` (type: `string`):

Load-zone prices (PEND, CENACE SW-PEND) or nodal prices (PML, CENACE SW-PML).

## `market` (type: `string`):

MDA = day-ahead market, published for tomorrow (D+1). MTR = real-time market results, which CENACE publishes about 7 days after the operating day (D-7); MTR is never live.

## `loadZones` (type: `array`):

Load zone names, used when Price type is Load zone. Spaces or hyphens both work ("LA PAZ" or "LA-PAZ"). The system (SIN, BCA, BCS) comes from the bundled CENACE NodosP catalogue. 1-200 per run.

## `nodes` (type: `array`):

CENACE NodoP codes such as 01PLO-115, used when Price type is Node. The system comes from the bundled NodosP catalogue, or from System for codes the catalogue does not list. 1-200 per run.

## `system` (type: `string`):

SIN (national interconnected), BCA (Baja California) or BCS (Baja California Sur). Needed only for nodes or zones that are not in the bundled catalogue; catalogue codes always use their own system.

## `dateMode` (type: `string`):

tomorrow = the next operating day (D+1 in the system's time zone, MDA only); lastNDays = the N most recent operating days that can be published (MDA up to tomorrow, MTR up to 7 days ago); range = Start date to End date.

## `startDate` (type: `string`):

First operating day (YYYY-MM-DD), used only when Dates is range. On or after 2016-01-01; days before the market's first day are reported as beforeMarketStart and not charged.

## `endDate` (type: `string`):

Last operating day (YYYY-MM-DD), used only when Dates is range. At most 366 days after Start date, and no later than tomorrow (MDA) or 7 days ago (MTR).

## `lastNDays` (type: `integer`):

How many operating days to return when Dates is lastNDays (1-366).

## `maxRows` (type: `integer`):

Stop after this many price rows. Whole location-days are delivered, so a run never returns more rows than this. The run's maximum total charge is honoured too.

## `useApifyProxy` (type: `boolean`):

Off by default: the Actor calls CENACE directly. Turn on only if direct access fails from the Apify cloud; it uses Apify's datacenter proxy, never residential.

## Actor input object example

```json
{
  "priceType": "zone",
  "market": "MDA",
  "loadZones": [
    "MEXICALI",
    "CULIACAN"
  ],
  "nodes": [
    "01PLO-115"
  ],
  "system": "SIN",
  "dateMode": "tomorrow",
  "startDate": "2025-10-01",
  "endDate": "2025-12-31",
  "lastNDays": 7,
  "maxRows": 50000,
  "useApifyProxy": false
}
```

# Actor output Schema

## `prices` (type: `string`):

One row per location, operating day and hour, with energy, loss and congestion components.

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

Requested and returned coverage per location, missing days with reasons, and charged rows.

# 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 = {
    "loadZones": [
        "MEXICALI",
        "CULIACAN"
    ],
    "nodes": [
        "01PLO-115"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("plainworktools/mexico-power-prices-cenace").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 = {
    "loadZones": [
        "MEXICALI",
        "CULIACAN",
    ],
    "nodes": ["01PLO-115"],
}

# Run the Actor and wait for it to finish
run = client.actor("plainworktools/mexico-power-prices-cenace").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 '{
  "loadZones": [
    "MEXICALI",
    "CULIACAN"
  ],
  "nodes": [
    "01PLO-115"
  ]
}' |
apify call plainworktools/mexico-power-prices-cenace --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,plainworktools/mexico-power-prices-cenace"
        }
    }
}
```

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/IecqKHIN45DSXnkmr/builds/SdMUOgBxTIydTmYwk/openapi.json
