Cómo Automatizar la Descarga de CFDIs del SAT con Python (2026)

Guía paso a paso para descargar CFDIs del portal del SAT automáticamente con Python. Incluye cómo resolver el captcha SAT, autenticación con RFC y CIEC, y extracción de datos XML.

March 17, 2026 · 7 min read

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?

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.

Try Ocilar free

1,000 free solves. No credit card required.

Get API Key