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.
Maintainers
Readme
cfe-api
💸 ¿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.tsincluidos - ✅ 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)
- Ve a https://app.cfe.mx/Aplicaciones/CCFE/MiEspacio/Login.aspx
- Crea una cuenta nueva (o usa una existente)
- 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/apikeyPaso 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 → rpuen 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-apiRequiere 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. Leecontext.operationycontext.reason, y abre un issue. La librería necesita actualización del parser.InvalidCredentialsErrordespués de cambiar password: actualiza las variables de entorno con la nueva.loginexitoso perolistServicesdevuelve[]: tu cuenta no tiene servicios vinculados. UsalinkService()con los datos del recibo, olinkServiceFromBillImage()con una foto.
Limitaciones conocidas
getConsumptionHistorysiempre 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.downloadCFDIXML: 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
lookupByXmethods (RPU + 7 honest stubs for identifiers CFE doesn't support + 1 universal via OCR) - ✅ Native TypeScript with
.d.tsincluded - ✅ Works in Node.js ≥ 20 — no browser automation, no Playwright
Installation
npm install cfe-apiRequires 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. Readcontext.operationandcontext.reasonand file an issue.InvalidCredentialsErrorafter a portal password change: update your env vars with the new password.loginsucceeds butlistServicesreturns[]: your account has no linked services. UselinkService()with bill data, orlinkServiceFromBillImage()with a photo.
Known limitations
getConsumptionHistoryalways 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.downloadCFDIXML: 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 testIntegration 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:integrationDevelopment
git clone https://github.com/0xJesus/cfe-api.git
cd cfe-api
npm install
npm run typecheck
npm run build
npm testSee 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
