npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

cfe-api

v0.3.0

Published

Unofficial TypeScript client for the Mexican CFE (Comisión Federal de Electricidad) portal. Fetch balance, bills, consumption, and download receipts programmatically.

Readme

cfe-api

npm version npm downloads license GitHub Sponsors GitHub stars

💸 ¿Te ahorra horas de trabajo? / Saving you hours? Considera patrocinarme en GitHub Sponsors o invitarme un café en Ko-fi. // Consider sponsoring me on GitHub or buying me a coffee.

Idiomas / Languages: 🇲🇽 Español · 🇺🇸 English


🇲🇽 Documentación en Español

Cliente TypeScript no oficial para el portal Mi Espacio de la Comisión Federal de Electricidad (CFE) (app.cfe.mx). Consulta tu saldo, lista tus servicios, descarga recibos y CFDIs en PDF, y vincula servicios automáticamente desde una foto del recibo — todo desde Node.js con una API tipada limpia.

⚠️ No afiliada, respaldada ni soportada por CFE. Esta librería accede al portal público Mi Espacio en tu nombre usando las credenciales que tú proveas. Úsala con responsabilidad, asume tu propio riesgo, y respeta límites razonables de peticiones.

¿Por qué existe?

CFE no publica un API para desarrolladores. Cualquiera que quiera automatizar tareas como "descargar todos mis CFDIs" o "alertarme cuando mi saldo pase de $X" tiene que scrapear a mano el portal ASP.NET WebForms, lo cual implica lidiar con __VIEWSTATE, cookies del WAF Incapsula, parseo de fechas en español, y selectores HTML frágiles.

cfe-api envuelve todo eso en una librería pequeña, tipada y probada para que tú te enfoques en lo que quieres construir.

Funcionalidades

  • Login al portal Mi Espacio con usuario + contraseña
  • Listar servicios vinculados (RPUs)
  • Vincular nuevo servicio con datos del recibo físico O con foto del recibo (OCR vía Gemini Vision)
  • Saldo actual (adeudo, fecha límite, estado)
  • Historial de recibos (períodos bimestrales)
  • Descargar PDF del recibo
  • Descargar CFDI PDF (XML pendiente para v0.3)
  • Persistencia de sesión a disco (no re-loggeas en cada request)
  • Auto re-login cuando expira la sesión
  • Retry con backoff ante WAF/timeouts/5xx
  • Mutex FIFO para llamadas concurrentes seguras
  • 9 métodos lookupByX (RPU + 7 stubs honestos para identificadores que CFE no soporta + 1 universal vía OCR)
  • TypeScript nativo con .d.ts incluidos
  • ✅ Funciona en Node.js ≥ 20 — sin browser automation, sin Playwright

Setup para bots (workaround recomendado)

CFE no tiene API pública para consultar saldo/recibos por RPU directamente. El portal público crashea con 500 en el paso de descarga. El workaround es usar UNA cuenta Mi Espacio del operador del bot como "cuenta maestra" que vincula y consulta recibos de todos los usuarios del bot.

Paso 1: Crear cuenta Mi Espacio (1 vez, 2 min)

  1. Ve a https://app.cfe.mx/Aplicaciones/CCFE/MiEspacio/Login.aspx
  2. Crea una cuenta nueva (o usa una existente)
  3. No necesitas vincular ningún servicio manualmente — la librería lo hace automáticamente

Paso 2: Variables de entorno

# .env de tu server/bot
CFE_USER=tu_usuario_mi_espacio
CFE_PASSWORD=tu_password_mi_espacio
GEMINI_API_KEY=tu_api_key_de_gemini   # gratis en aistudio.google.com/apikey

Paso 3: Tu bot recibe una foto y hace todo automático

import { CfeClient, GeminiVisionAdapter } from 'cfe-api'

// Arranca UNA VEZ al iniciar tu server
const client = await CfeClient.fromFile('./.cfe-session.json', {
  logger: console,
  autoReLogin: true,
})
if (!client.isAuthenticated()) {
  await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)
}

const ocr = new GeminiVisionAdapter() // usa GEMINI_API_KEY del env

// Cuando un usuario manda foto de su recibo:
bot.on('photo', async (chatId, photoBuffer) => {
  const { service, billData } = await client.linkServiceFromBillImage(
    photoBuffer,
    { ocr, alias: `user_${chatId}` },
  )
  await db.save(chatId, {
    rpu: service.rpu,
    nombre: billData.nombre,
    direccion: billData.direccion,
  })
  bot.send(chatId,
    `✓ Servicio vinculado: ${billData.nombre}\n` +
    `Dirección: ${billData.direccion}\n` +
    `RPU: ${service.rpu}\n\n` +
    `Escribe "saldo" para consultar tu adeudo.`
  )
})

// Cuando un usuario pide su saldo:
bot.on('saldo', async (chatId) => {
  const { rpu } = await db.get(chatId)
  const balance = await client.getBalance(rpu)
  bot.send(chatId,
    `💡 Saldo: $${balance.adeudoTotal} MXN\n` +
    `📅 Fecha límite: ${balance.fechaLimitePago?.toLocaleDateString('es-MX')}\n` +
    `📊 Periodo: ${balance.periodoVigente}\n` +
    `${balance.cortado ? '⚠️ SERVICIO CORTADO' : '✅ Servicio activo'}`
  )
})

// Cuando un usuario pide su recibo PDF:
bot.on('recibo', async (chatId) => {
  const { rpu } = await db.get(chatId)
  const history = await client.getBillingHistory(rpu)
  const pdf = await client.downloadReceipt(rpu, history[0].periodo)
  bot.sendDocument(chatId, pdf, `recibo-${history[0].periodo}.pdf`)
})

¿Es seguro?

  • Los usuarios NUNCA necesitan cuenta Mi Espacio — solo mandan una foto
  • Cada usuario solo puede ver SU RPU — tu bot mapea userId → rpu en tu DB
  • linkService requiere datos del recibo físico — nadie puede vincular un RPU ajeno sin tener el recibo
  • Las requests concurrentes son thread-safe — el mutex FIFO serializa operaciones
  • La sesión se persiste a disco — no re-logueas en cada request

Limitaciones

  • ~50-100 RPUs por cuenta Mi Espacio (estimado). Para más, usa un pool de cuentas.
  • La consulta pública por RPU + nombre (sin cuenta Mi Espacio) solo permite validar que el servicio existe y listar períodos — el paso de descarga crashea el server de CFE con error 500 (bug de CFE, no nuestro).

Instalación

npm install cfe-api

Requiere Node.js 20+. Incluye Playwright Chromium para la consulta pública (~112 MB adicionales al primer npm install).

Inicio rápido

import { CfeClient } from 'cfe-api'

const client = new CfeClient()
await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)

const servicios = await client.listServices()
for (const s of servicios) {
  const saldo = await client.getBalance(s.rpu)
  console.log(`${s.alias} (${s.rpu}): $${saldo.adeudoTotal}`)
}

Uso en producción (recomendado para bots)

Para bots y servicios long-running, usa el patrón de sesión persistente para no re-autenticarte en cada request (CFE rate-limita logins agresivos):

import { CfeClient } from 'cfe-api'

// Auto-carga sesión cacheada si existe .cfe-session.json
const client = await CfeClient.fromFile('./.cfe-session.json', {
  logger: console,           // opcional pero útil en producción
  maxRetries: 3,             // retry en WAF/429/5xx transientes
  retryBaseDelayMs: 500,
  autoReLogin: true,         // re-login transparente si expira
})

if (!client.isAuthenticated()) {
  await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)
  // Auto-guardado en ./.cfe-session.json después del login
}

// Sesiones persisten entre reinicios
const saldo = await client.getBalance(rpu)

El cliente incluye un mutex FIFO que serializa operaciones para evitar race conditions cuando varios usuarios del bot disparan llamadas concurrentes:

// Las 5 llamadas se ejecutan en orden, sin pisarse
const resultados = await Promise.all([
  client.getBalance(rpu1),
  client.getBalance(rpu2),
  client.getBalance(rpu3),
  client.getBillingHistory(rpu1),
  client.listServices(),
])

Bot de atención al cliente con OCR

Si quieres un bot donde el usuario sólo manda una foto de su recibo:

import { CfeClient, GeminiVisionAdapter } from 'cfe-api'
import { readFileSync } from 'node:fs'

const client = await CfeClient.fromFile('./.cfe-session.json')
if (!client.isAuthenticated()) {
  await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)
}

const ocr = new GeminiVisionAdapter()  // GEMINI_API_KEY del entorno

// Onboarding del usuario del bot — UNA SOLA VEZ
const fotoRecibo = readFileSync('recibo-del-usuario.jpg')
const { service, billData } = await client.linkServiceFromBillImage(fotoRecibo, {
  ocr,
  alias: 'CasaJuan',
})
console.log(`✓ ${billData.nombre}, debe $${billData.total}`)
// Guarda { rpu: service.rpu } asociado al userId del bot

// Después de eso, consultas instantáneas:
const saldo = await client.getBalance(service.rpu)
const pdf = await client.downloadReceipt(service.rpu, '2026-03')

API

Constructor

new CfeClient(opciones?)
interface CfeClientOptions {
  userAgent?: string         // Override del User-Agent default (Chrome desktop)
  timeout?: number           // Timeout por request en ms (default 30000)
  autoReLogin?: boolean      // Default true. Re-login automático al expirar.
  sessionFile?: string       // Archivo donde persistir/cargar la sesión
  maxRetries?: number        // Reintentos por error transitorio (default 3)
  retryBaseDelayMs?: number  // Delay base del exponential backoff (default 500)
  logger?: CfeLogger         // Logger compatible con console/pino/winston
}
// Constructor estático que auto-carga la sesión persistida
await CfeClient.fromFile('./.cfe-session.json', { logger: console })

Autenticación

| Método | Descripción | |---|---| | login(usuario, password): Promise<void> | Autentica contra Mi Espacio. Lanza InvalidCredentialsError si falla. | | logout(): Promise<void> | Limpia el estado en memoria. | | isAuthenticated(): boolean | True si hay sesión cargada. | | hasServerSession(): Promise<boolean> | Verifica que hay cookie ASP.NET_SessionId. | | saveSession(path): Promise<void> | Persiste la sesión a un archivo (chmod 600). | | loadSession(path): Promise<boolean> | Carga sesión desde archivo. False si no existe. | | getSessionAge(): number \| null | Edad de la sesión en ms (útil para refresh proactivo). |

Servicios

| Método | Descripción | |---|---| | listServices(): Promise<Service[]> | Lista todos los RPUs vinculados. [] si no hay ninguno. | | linkService({ rpu, nombre, total, alias }): Promise<Service> | Vincula un nuevo servicio (requiere los 3 datos del recibo). |

interface Service {
  rpu: string           // 12 dígitos
  alias: string         // alias definido por el usuario
  nombre: string        // nombre del titular como aparece impreso
  tarifa?: string
  direccion?: string
}

Saldo y facturación

| Método | Descripción | |---|---| | getBalance(rpu): Promise<BalanceInfo> | Saldo actual + fecha límite + estado. | | getBillingHistory(rpu): Promise<BillingPeriod[]> | Historial de períodos bimestrales. | | downloadReceipt(rpu, periodo): Promise<Buffer> | PDF del recibo (binario). | | downloadCFDI(rpu, periodo): Promise<{ xml: Buffer; pdf?: Buffer }> | CFDI fiscal (PDF y/o XML). |

interface BalanceInfo {
  rpu: string
  adeudoTotal: number          // MXN
  fechaLimitePago?: Date
  periodoVigente?: string
  cortado: boolean             // true si el servicio está suspendido
}

interface BillingPeriod {
  periodo: string              // 'YYYY-MM'
  fechaEmision: Date
  fechaLimite: Date
  total: number
  pagado: boolean
  referenciasPago?: string[]
}

Lookups (todos los identificadores imaginables)

cfe-api expone un método lookupByX por cada identificador concebible. CFE solo soporta lookup por RPU. Los demás métodos lanzan NotSupportedByCfeError con explicación de POR QUÉ no funcionan (privacidad, ley LFPDPPP, identificador interno, etc).

| Método | Funciona | Razón | |---|---|---| | lookupByRpu(rpu) | ✅ | Identificador principal de CFE | | lookupByAccountNumber(num) | ❌ | API privado bancario B2B (sólo bancos) | | lookupByPhone(tel) | ❌ | CFE no indexa por teléfono | | lookupByName(nombre) | ❌ | Privacidad + ley LFPDPPP | | lookupByEmail(email) | ❌ | Sólo para login del portal | | lookupByAddress(dir) | ❌ | No hay resolver público | | lookupByRfc(rfc) | ❌ | Sólo para CFDI dentro de sesión | | lookupByRmu(rmu) | ❌ | Identificador interno de CFE | | linkServiceFromBillImage(foto, { ocr }) | ✅ | Universal — OCR del recibo | | parseBillImage(foto, { ocr }) | ✅ | Sólo extrae datos sin vincular |

OCR para automatizar onboarding

import { GeminiVisionAdapter } from 'cfe-api'

const ocr = new GeminiVisionAdapter({
  apiKey: process.env.GEMINI_API_KEY,  // o GOOGLE_API_KEY
  model: 'gemini-2.5-flash',           // default
})

const datos = await client.parseBillImage(fotoBuffer, { ocr })
// → { rpu, nombre, total, numeroCuenta, rmu, direccion }

Free API key en https://aistudio.google.com/apikey.

Errores tipados

Todos extienden CfeError:

| Clase | Cuándo se lanza | |---|---| | CfeError | Clase base de cualquier error de la librería | | NotAuthenticatedError | Método protegido llamado antes de login() | | InvalidCredentialsError | CFE rechazó las credenciales | | ServiceNotFoundError | RPU no vinculado / datos del recibo no coinciden | | WafBlockedError | WAF Incapsula devolvió 403 | | RateLimitError | CFE devolvió HTTP 429 | | SessionExpiredError | Sesión inválida (cookie expirada) | | PortalChangedError | El parser no encontró el HTML esperado — CFE cambió su markup. El campo context tiene detalles diagnósticos. | | NotSupportedByCfeError | Lookup por un identificador que CFE no expone. Tiene un campo reason explicando por qué. |

Manejo de errores

import { CfeError, InvalidCredentialsError, WafBlockedError } from 'cfe-api'

try {
  await client.login(user, pass)
} catch (e) {
  if (e instanceof InvalidCredentialsError) { /* credenciales malas */ }
  if (e instanceof WafBlockedError) { /* esperar y reintentar */ }
  if (e instanceof CfeError) { /* cualquier error de cfe-api */ }
}

Troubleshooting

  • WafBlockedError: el WAF Incapsula te está bloqueando. Suele ser temporal. Espera unos minutos, baja la frecuencia de requests, o usa otra IP.
  • PortalChangedError: CFE cambió su HTML. Lee context.operation y context.reason, y abre un issue. La librería necesita actualización del parser.
  • InvalidCredentialsError después de cambiar password: actualiza las variables de entorno con la nueva.
  • login exitoso pero listServices devuelve []: tu cuenta no tiene servicios vinculados. Usa linkService() con los datos del recibo, o linkServiceFromBillImage() con una foto.

Limitaciones conocidas

  • getConsumptionHistory siempre devuelve [] por ahora. CFE no expone el histórico de kWh en el portal web — sólo está dentro de cada PDF del recibo. v0.3 lo extraerá parseando los PDFs descargados.
  • downloadCFDI XML: los CFDIs tipo VT/OCR (facturas de terceros) devuelven HTML re-render en lugar del XML. La librería detecta esto y devuelve el PDF (que sí funciona). El XML estará disponible en v0.3 con captura de request del browser real.
  • No hay lookup por número de cuenta / teléfono / nombre: CFE no expone esos lookups públicamente, por diseño anti-abuso. Cada usuario debe vincular su servicio una vez con los datos de su recibo.

Soporta este proyecto

Mantener un scraper de CFE significa perseguir constantemente cambios en su HTML, pelear con el WAF y reverse-engineeriar nuevos endpoints. Si cfe-api te ahorró días de trabajo:

  • Dale star al repo en GitHub — la visibilidad ayuda a que otros devs mexicanos lo encuentren
  • 💖 Patrocíname vía GitHub Sponsors — el apoyo recurrente mantiene el proyecto vivo
  • Cómprame un café en Ko-fi o Buy Me a Coffee
  • 🐛 Reporta bugs y manda PRs — la mejor contribución

🇺🇸 English documentation

Unofficial TypeScript client for the Mexican Comisión Federal de Electricidad (CFE) portal (app.cfe.mx). Fetch your electricity balance, list linked services, read billing history, download receipts and CFDIs, and link services from a bill photo via OCR — all from Node.js with a clean typed API.

⚠️ Not affiliated with, endorsed by, or supported by CFE. This library accesses the public Mi Espacio portal on your behalf using credentials you provide. Use it responsibly and at your own risk, and be mindful of request rates.

Why

CFE does not publish a developer API for its customer portal. Anyone who wants to automate things like "download all my CFDIs to the local filesystem" or "alert me when the balance exceeds X" has to scrape the ASP.NET WebForms portal by hand, which involves dealing with __VIEWSTATE, Incapsula WAF cookies, Spanish-locale date parsing, and brittle HTML selectors.

cfe-api wraps all of that into a small, typed, tested library so you can focus on what you want to build.

Features

  • Login to CFE Mi Espacio with username + password
  • List linked services (RPUs)
  • Link a new service via bill data OR via bill image OCR (Gemini Vision)
  • Get current balance (adeudo, due date, status)
  • Get billing history (bimonthly periods)
  • Download PDF receipts
  • Download CFDI PDF (XML pending v0.3)
  • Session persistence to disk (no re-login per request)
  • Auto re-login on session expiry
  • Retry with exponential backoff on WAF / 429 / 5xx
  • Per-instance FIFO mutex for safe concurrent calls
  • 9 lookupByX methods (RPU + 7 honest stubs for identifiers CFE doesn't support + 1 universal via OCR)
  • Native TypeScript with .d.ts included
  • ✅ Works in Node.js ≥ 20 — no browser automation, no Playwright

Installation

npm install cfe-api

Requires Node.js 20+.

Quick start

import { CfeClient } from 'cfe-api'

const client = new CfeClient()
await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)

const services = await client.listServices()
for (const svc of services) {
  const balance = await client.getBalance(svc.rpu)
  console.log(`${svc.alias} (${svc.rpu}): $${balance.adeudoTotal}`)
}

Production usage (recommended for bots)

For bots and long-running services, use the persistent session pattern so you don't re-authenticate on every request (CFE rate-limits aggressive logins):

import { CfeClient } from 'cfe-api'

// Auto-loads cached session if .cfe-session.json exists
const client = await CfeClient.fromFile('./.cfe-session.json', {
  logger: console,           // optional but useful in prod
  maxRetries: 3,             // retry on transient WAF/429/5xx
  retryBaseDelayMs: 500,
  autoReLogin: true,         // re-login transparently if session expires
})

if (!client.isAuthenticated()) {
  await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)
  // Auto-saved to ./.cfe-session.json after successful login
}

// Sessions persist across restarts
const balance = await client.getBalance(rpu)

The client also includes a per-instance FIFO mutex so concurrent calls from multiple bot users don't race each other:

const results = await Promise.all([
  client.getBalance(rpu1),
  client.getBalance(rpu2),
  client.getBalance(rpu3),
  client.getBillingHistory(rpu1),
  client.listServices(),
])

Customer service bot with OCR

For a bot where users only send a photo of their bill:

import { CfeClient, GeminiVisionAdapter } from 'cfe-api'
import { readFileSync } from 'node:fs'

const client = await CfeClient.fromFile('./.cfe-session.json')
if (!client.isAuthenticated()) {
  await client.login(process.env.CFE_USER!, process.env.CFE_PASSWORD!)
}

const ocr = new GeminiVisionAdapter()  // GEMINI_API_KEY from env

// User onboarding — ONCE per user
const billPhoto = readFileSync('user-bill.jpg')
const { service, billData } = await client.linkServiceFromBillImage(billPhoto, {
  ocr,
  alias: 'JohnsHouse',
})
console.log(`✓ ${billData.nombre}, owes $${billData.total}`)
// Save { rpu: service.rpu } associated with the bot's userId

// Then, instant queries forever:
const balance = await client.getBalance(service.rpu)
const pdf = await client.downloadReceipt(service.rpu, '2026-03')

API

Constructor

new CfeClient(options?)
interface CfeClientOptions {
  userAgent?: string         // Override the default Chrome desktop UA
  timeout?: number           // Per-request timeout in ms (default 30000)
  autoReLogin?: boolean      // Default true. Re-login on session expiry.
  sessionFile?: string       // Path to a JSON file for session persistence
  maxRetries?: number        // Retries for transient errors (default 3)
  retryBaseDelayMs?: number  // Exponential backoff base delay (default 500)
  logger?: CfeLogger         // Logger compatible with console/pino/winston
}
// Static constructor that auto-loads a persisted session
await CfeClient.fromFile('./.cfe-session.json', { logger: console })

Authentication

| Method | Description | |---|---| | login(user, password): Promise<void> | Authenticate against Mi Espacio. Throws InvalidCredentialsError on failure. | | logout(): Promise<void> | Clears in-memory state. | | isAuthenticated(): boolean | True if a session is loaded. | | hasServerSession(): Promise<boolean> | Verifies the ASP.NET_SessionId cookie is present. | | saveSession(path): Promise<void> | Persists the session to a file (chmod 600). | | loadSession(path): Promise<boolean> | Loads a session from a file. False if not found. | | getSessionAge(): number \| null | Session age in ms (for proactive refresh). |

Services

| Method | Description | |---|---| | listServices(): Promise<Service[]> | Lists all linked RPUs. [] if none. | | linkService({ rpu, nombre, total, alias }): Promise<Service> | Links a new service (requires the 3 bill values). |

interface Service {
  rpu: string           // 12-digit service number
  alias: string         // user-defined nickname
  nombre: string        // service holder name on the bill
  tarifa?: string
  direccion?: string
}

Balance and billing

| Method | Description | |---|---| | getBalance(rpu): Promise<BalanceInfo> | Current balance + due date + status. | | getBillingHistory(rpu): Promise<BillingPeriod[]> | Bimonthly billing history. | | downloadReceipt(rpu, period): Promise<Buffer> | Bill PDF (binary). | | downloadCFDI(rpu, period): Promise<{ xml: Buffer; pdf?: Buffer }> | Tax CFDI invoice (PDF and/or XML). |

interface BalanceInfo {
  rpu: string
  adeudoTotal: number          // MXN
  fechaLimitePago?: Date
  periodoVigente?: string
  cortado: boolean             // true if service is disconnected
}

interface BillingPeriod {
  periodo: string              // 'YYYY-MM'
  fechaEmision: Date
  fechaLimite: Date
  total: number
  pagado: boolean
  referenciasPago?: string[]
}

Lookups (every imaginable identifier)

cfe-api exposes a lookupByX method for every conceivable identifier. CFE only supports lookup by RPU. All other methods throw NotSupportedByCfeError with an explanation of why they don't work (privacy, LFPDPPP law, internal identifier, etc).

| Method | Works | Reason | |---|---|---| | lookupByRpu(rpu) | ✅ | CFE's primary identifier | | lookupByAccountNumber(num) | ❌ | Private B2B bank API only | | lookupByPhone(phone) | ❌ | CFE doesn't index by phone | | lookupByName(name) | ❌ | Privacy + LFPDPPP law | | lookupByEmail(email) | ❌ | Portal login only | | lookupByAddress(addr) | ❌ | No public resolver | | lookupByRfc(rfc) | ❌ | CFDI-only inside session | | lookupByRmu(rmu) | ❌ | Internal CFE identifier | | linkServiceFromBillImage(image, { ocr }) | ✅ | Universal — OCR a bill | | parseBillImage(image, { ocr }) | ✅ | Extract data without linking |

OCR for friction-free onboarding

import { GeminiVisionAdapter } from 'cfe-api'

const ocr = new GeminiVisionAdapter({
  apiKey: process.env.GEMINI_API_KEY,  // or GOOGLE_API_KEY
  model: 'gemini-2.5-flash',           // default
})

const data = await client.parseBillImage(imageBuffer, { ocr })
// → { rpu, nombre, total, numeroCuenta, rmu, direccion }

Free API key at https://aistudio.google.com/apikey.

Typed errors

All extend CfeError:

| Class | When it fires | |---|---| | CfeError | Base class for every library error | | NotAuthenticatedError | A protected method was called before login() | | InvalidCredentialsError | login() was rejected by the portal | | ServiceNotFoundError | RPU not linked / linkService bill data mismatch | | WafBlockedError | Incapsula WAF returned 403 | | RateLimitError | Portal returned HTTP 429 | | SessionExpiredError | Session cookie is no longer valid | | PortalChangedError | A parser couldn't find the expected HTML — CFE updated their markup. The context field has diagnostic details. | | NotSupportedByCfeError | Lookup by an identifier CFE doesn't expose. Has a reason field explaining why. |

Error handling

import { CfeError, InvalidCredentialsError, WafBlockedError } from 'cfe-api'

try {
  await client.login(user, pass)
} catch (e) {
  if (e instanceof InvalidCredentialsError) { /* bad credentials */ }
  if (e instanceof WafBlockedError) { /* wait and retry */ }
  if (e instanceof CfeError) { /* any cfe-api error */ }
}

Troubleshooting

  • WafBlockedError: Incapsula is blocking you. Usually transient. Wait a few minutes, lower request rate, or use a different IP.
  • PortalChangedError: CFE changed their HTML. Read context.operation and context.reason and file an issue.
  • InvalidCredentialsError after a portal password change: update your env vars with the new password.
  • login succeeds but listServices returns []: your account has no linked services. Use linkService() with bill data, or linkServiceFromBillImage() with a photo.

Known limitations

  • getConsumptionHistory always returns [] for now. CFE doesn't expose the historical kWh series on the web portal — that data lives only inside each receipt PDF. v0.3 will extract it by parsing the downloaded PDFs.
  • downloadCFDI XML: VT/OCR-type CFDI rows (third-party invoices) return an HTML re-render instead of the XML. The library detects this and returns the PDF (which works). XML support will arrive in v0.3 via real-browser request capture.
  • No public lookup by account number / phone / name / address: CFE intentionally does not expose these lookups for privacy reasons. Each user must link their service once with bill data.

Testing

Unit tests run offline against captured HTML fixtures:

npm test

Integration tests hit the real CFE portal with your credentials (skipped by default):

CFE_INTEGRATION=1 \
CFE_USER=your_user \
CFE_PASSWORD=your_pass \
CFE_RPU=your_rpu \
npm run test:integration

Development

git clone https://github.com/0xJesus/cfe-api.git
cd cfe-api
npm install
npm run typecheck
npm run build
npm test

See docs/reverse-engineering.md for technical notes about the CFE portal's internals.

Support this project

Maintaining a CFE scraper means constantly chasing changes in their portal HTML, dealing with WAF updates, and reverse-engineering new endpoints. If cfe-api saved you days of work, please consider:

  • Star the repo on GitHub — visibility helps other Mexican devs find it
  • 💖 Sponsor via GitHub Sponsors — recurring support keeps the lights on
  • Buy me a coffee at Ko-fi or Buy Me a Coffee
  • 🐛 Report bugs and submit PRs — the best kind of contribution

License

MIT © 2026 0xJesus