Descargar CFDIs del portal del SAT manualmente es una tarea repetitiva que consume horas de trabajo contable cada mes. Con Python y la API de Ocilar puedes automatizar todo el proceso: desde resolver el captcha hasta descargar y parsear el XML de cada factura. Esta guía te muestra cómo hacerlo paso a paso.
¿Qué necesitas para automatizar el SAT?
- RFC — Registro Federal de Contribuyentes
- CIEC — Contraseña del portal SAT (Clave de Identificación Electrónica Confidencial)
- Python 3.9+ con Playwright instalado
- API key de Ocilar para resolver el captcha automáticamente
Instalación de dependencias
pip install playwright ocilar requests playwright install chromium
Flujo completo: login + descarga de CFDIs
Paso 1 — Resolver el captcha del SAT
import asyncio
import base64
from playwright.async_api import async_playwright
from ocilar import OcilarClient
client = OcilarClient(api_key="sk-your_key")
async def get_captcha_solution(page) -> str:
"""Extrae y resuelve el captcha de la página actual del SAT."""
# Esperar a que cargue la imagen del captcha
await page.wait_for_selector("img[src*='captcha']", timeout=15000)
# Extraer como base64
captcha_b64 = await page.$eval("img[src*='captcha']", """img => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
canvas.getContext('2d').drawImage(img, 0, 0);
return canvas.toDataURL('image/png').split(',')[1];
}""")
# Resolver con Ocilar (modelo dedicado para SAT)
result = client.solve_sat(image_base64=captcha_b64)
print(f"Captcha resuelto: {result.text} (confianza: {result.confidence:.0%})")
return result.text Paso 2 — Login al portal SAT
async def login_sat(page, rfc: str, ciec: str) -> bool:
"""Inicia sesión en el portal del SAT con RFC y CIEC."""
await page.goto("https://portalcfdi.facturaelectronica.sat.gob.mx/")
# Esperar formulario de login
await page.wait_for_selector("#rfc", timeout=10000)
# Llenar RFC y CIEC
await page.fill("#rfc", rfc.upper())
await page.fill("#ciec", ciec)
# Resolver captcha
captcha_text = await get_captcha_solution(page)
await page.fill("#captchaImg", captcha_text)
# Hacer click en "Entrar"
await page.click("#submit")
# Verificar que el login fue exitoso
try:
await page.wait_for_selector(".menu-principal", timeout=10000)
print("Login exitoso")
return True
except Exception:
print("Login fallido — verifica RFC, CIEC o captcha")
return False Paso 3 — Buscar y descargar CFDIs
async def descargar_cfdi_xml(page, uuid: str, destino: str) -> bool:
"""Descarga el XML de un CFDI por su UUID."""
try:
# Navegar a la consulta de CFDIs
await page.goto("https://portalcfdi.facturaelectronica.sat.gob.mx/consulta.aspx")
await page.wait_for_load_state("networkidle")
# Buscar por UUID
await page.fill("#ctl00_MainContent_TxtUUID", uuid)
await page.click("#ctl00_MainContent_BtnBusqueda")
await page.wait_for_load_state("networkidle")
# Click en "Descargar XML"
download_btn = await page.query_selector("a[title='Descarga XML']")
if not download_btn:
print(f"UUID {uuid} no encontrado")
return False
async with page.expect_download() as download_info:
await download_btn.click()
download = await download_info.value
await download.save_as(destino)
print(f"Descargado: {destino}")
return True
except Exception as e:
print(f"Error descargando {uuid}: {e}")
return False Paso 4 — Flujo completo
async def descargar_todos_cfdi(rfc: str, ciec: str, uuids: list, carpeta: str = "./cfdi"):
"""Descarga múltiples CFDIs del portal SAT."""
import os
os.makedirs(carpeta, exist_ok=True)
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
# Login
logged_in = await login_sat(page, rfc, ciec)
if not logged_in:
await browser.close()
raise RuntimeError("No se pudo iniciar sesión en el SAT")
# Descargar cada CFDI
resultados = {}
for uuid in uuids:
destino = os.path.join(carpeta, f"{uuid}.xml")
ok = await descargar_cfdi_xml(page, uuid, destino)
resultados[uuid] = "descargado" if ok else "error"
await browser.close()
return resultados
# Uso
uuids_a_descargar = [
"6128a3d4-1234-5678-abcd-ef0123456789",
"7239b4e5-5678-9012-bcde-f01234567890",
]
resultados = asyncio.run(descargar_todos_cfdi(
rfc="TU_RFC",
ciec="TU_CIEC",
uuids=uuids_a_descargar,
carpeta="./facturas_descargadas"
))
print(resultados) Parsear los CFDIs descargados
Una vez descargados los XMLs, puedes extraer los datos estructurados con la API de Ocilar:
from ocilar import OcilarClient
import os
client = OcilarClient(api_key="sk-your_key")
def parsear_cfdi_folder(carpeta: str) -> list:
"""Parsea todos los XMLs de CFDI en una carpeta."""
resultados = []
for archivo in os.listdir(carpeta):
if not archivo.endswith(".xml"):
continue
ruta = os.path.join(carpeta, archivo)
result = client.extract_cfdi(file_path=ruta)
resultados.append({
"uuid": result.uuid,
"emisor": result.emisor_nombre,
"rfc_emisor": result.emisor_rfc,
"receptor": result.receptor_nombre,
"total": result.total,
"iva": result.iva,
"fecha": result.fecha,
"conceptos": [
{"desc": c.descripcion, "importe": c.importe}
for c in result.conceptos
]
})
print(f"Parseado: {result.uuid} | {result.emisor_rfc} | ${result.total} MXN")
return resultados
facturas = parsear_cfdi_folder("./facturas_descargadas")
print(f"Total facturas procesadas: {len(facturas)}")
print(f"Total a pagar: ${sum(f['total'] for f in facturas):,.2f} MXN") Manejo de errores comunes
Captcha incorrecto — reintentar
async def login_sat_con_reintentos(page, rfc: str, ciec: str, max_intentos: int = 3) -> bool:
for intento in range(max_intentos):
# Recargar captcha si no es el primer intento
if intento > 0:
reload_btn = await page.query_selector("#imgCaptcha")
if reload_btn:
await reload_btn.click()
await page.wait_for_timeout(1000)
captcha_text = await get_captcha_solution(page)
await page.fill("#captchaImg", captcha_text)
await page.click("#submit")
try:
await page.wait_for_selector(".menu-principal", timeout=8000)
return True
except Exception:
print(f"Intento {intento + 1} fallido, reintentando...")
return False Sesión expirada
async def verificar_sesion(page) -> bool:
"""Verifica si la sesión del SAT sigue activa."""
try:
elemento = await page.query_selector(".menu-principal")
return elemento is not None
except Exception:
return False Casos de uso
Software contable
Importa automáticamente las facturas de proveedores cada semana. Elimina la captura manual en tu sistema contable (CONTPAQi, Aspel, SAP, etc.).
Conciliación mensual de IVA
Descarga todos los CFDIs del mes y cruza los datos con tu registro interno para detectar diferencias antes de presentar tu declaración.
Auditorías y revisiones fiscales
Cuando el SAT requiere documentación, puedes descargar en minutos años de CFDIs en lugar de hacerlo manualmente uno por uno.
Precios
Resolver el captcha del SAT cuesta $0.10 por cada 1,000 resoluciones. Parsear cada CFDI XML cuesta $0.05 por documento.
Preguntas frecuentes
¿Es legal automatizar el portal del SAT?
Acceder programáticamente a tu propia información fiscal es práctica estándar para software contable y ERPs. Ocilar resuelve el captcha técnico — el uso es tu responsabilidad. Ocilar no tiene afiliación con el SAT.
¿Funciona con e.firma además de CIEC?
Este ejemplo usa CIEC. La autenticación con e.firma requiere manejo de certificados .cer y .key — contáctanos si necesitas ese flujo específico.
¿Cuántos CFDIs puedo descargar por sesión?
El portal del SAT no tiene un límite documentado, pero recomendamos no exceder 500 descargas por sesión para evitar bloqueos temporales. Agrega pausas entre descargas en volúmenes altos.
¿Soporta la descarga masiva desde el buzón tributario?
Sí, el mismo patrón de login + captcha aplica al buzón tributario. El selector específico puede variar — consulta nuestra documentación para ejemplos actualizados.