'Solicitud Aceptada' pero no descarga: la referencia de CodEstatus y EstadoSolicitud del SAT (5000, 5002, 5005) — Cesar Ayala
← Todos los artículos

'Solicitud Aceptada' pero no descarga: la referencia de CodEstatus y EstadoSolicitud del SAT (5000, 5002, 5005)

'Solicitud Aceptada' (CodEstatus 5000) es solo el acuse de SolicitaDescarga: significa 'la recibí', no 'ya está lista'. Para saber cuándo bajar los paquetes, polleas VerificaSolicitud con el IdSolicitud y lees EstadoSolicitud (1 Aceptada, 2 EnProceso, 3 Terminada, 4 Error, 5 Rechazada, 6 Vencida); solo el 3, Terminada, te entrega los IdsPaquetes.

La respuesta corta: “Solicitud Aceptada” (5000) es el acuse, no el paquete

Solicitud AceptadaCodEstatus 5000 — es únicamente el acuse de SolicitaDescarga: significa “la recibí”, no “ya está lista”. Nada se puede descargar todavía, y reenviar SolicitaDescarga no la hace estar lista más rápido. Para saber cuándo bajar los paquetes tienes que pollear VerificaSolicitud con el IdSolicitud que te devolvió el paso anterior y leer el campo EstadoSolicitud; solo el 3 (Terminada) te entrega los IdsPaquetes con los que después llamas Descarga.

Los seis valores del ciclo son 1 Aceptada, 2 EnProceso, 3 Terminada, 4 Error, 5 Rechazada, 6 Vencida. Si te quedaste esperando a que SolicitaDescarga “cambie” de respuesta y te avise que ya está, te vas a quedar esperando para siempre: ese endpoint siempre contesta lo mismo. Soy ingeniero, no tu contador — esto sí lo he puesto en producción, pero las reglas vigentes del SAT las confirmas contra el SAT. Abajo va la referencia completa de códigos.

SolicitaDescarga
5000 Aceptada (acuse)
VerificaSolicitud (poll)
3 Terminada
Descarga por paquete

La trampa ‘aceptada != lista’: por qué tu solicitud lleva horas parada

El escenario clásico: mandas SolicitaDescarga, recibes 5000 “Solicitud Aceptada”, y como nunca ves nada “listo”, vuelves a mandar la misma solicitud. Y otra vez. Y otra. Horas después sigues sin XML y ya generaste un problema nuevo. El SAT procesa de forma asíncrona: 5000 solo confirma que tu petición entró a la cola. El único lugar donde el resultado madura es VerificaSolicitud, no SolicitaDescarga.

Reenviar la misma consulta no la acelera — la empeora. Como los parámetros (FechaInicial, FechaFinal, RfcEmisor, RfcReceptor, TipoSolicitud) son idénticos, el SAT primero te empieza a responder 5005 (solicitud duplicada) y, si insistes, 5002 (se agotaron las solicitudes de por vida para esa consulta exacta). Es decir: el patrón de “reintentar” que funciona con cualquier otra API aquí te quema cupos.

El patrón correcto es emitir SolicitaDescarga una sola vez, persistir el IdSolicitud, y a partir de ahí pollear VerificaSolicitud con espera creciente. Es el mismo tradeoff que desgloso en webhooks vs polling vs API: el SAT no te da webhook, así que el polling disciplinado es todo lo que tienes. Y si tu solicitud lleva horas en EnProceso, no es un error — es un rango muy amplio sobre un RFC de alto volumen; eso lo trato en el post hermano de EnProceso y los límites reales.

EstadoSolicitud: los 6 estados del ciclo de vida, y por qué solo Terminada (3) te devuelve los IdsPaquetes

EstadoSolicitud es el campo que devuelve VerificaSolicitud y es tu única fuente de verdad sobre el avance. Son seis valores. Los dos “vivos” son 1 Aceptada y 2 EnProceso: sigue trabajando, espera y reintenta. 3 Terminada es el único estado que trae los IdsPaquetes. Y 4 Error, 5 Rechazada, 6 Vencida son terminales — el 6 significa que el resultado ya caducó, porque los paquetes viven solo unas 72 horas. VerificaSolicitud también te regresa un CodigoEstadoSolicitud y el NumeroCFDIs, dato que vale oro para depurar.

  1. 1 Aceptada / 2 EnProcesoEl SAT sigue armando los paquetes. Espera y reintenta con backoff.
  2. 3 TerminadaÚnico estado que te entrega los IdsPaquetes para bajar.
  3. 4 Error / 5 RechazadaTerminales. Lee el CodEstatus para el motivo y no reintentes a ciegas.
  4. 6 VencidaEl resultado caducó (los paquetes viven ~72 h). Vuelve a solicitar.

La consecuencia práctica: no intentes bajar nada hasta ver EstadoSolicitud == "3". Cualquier otro valor no tiene IdsPaquetes, y tratar de descargar con las manos vacías es el segundo error más común después de reenviar la solicitud.

estado = extract_attr(resp.text, "VerificaSolicitudDescargaResult", "EstadoSolicitud")

if estado == "3":                 # Terminada -> only here you get packages
    paquetes = extract_all(resp.text, "IdsPaquetes")
elif estado in ("1", "2"):        # Aceptada / EnProceso -> keep polling
    schedule_retry_with_backoff()
elif estado in ("4", "5", "6"):   # Error / Rechazada / Vencida -> terminal
    handle_terminal(estado, cod_estatus)

El flujo completo de los 4 pasos SOAP (Autenticacion, SolicitaDescarga, VerificaSolicitud, Descarga) firmados con tu e.firma está en la guía de Descarga Masiva con e.firma en Python; este post se concentra en qué significa cada código cuando algo se atora.

Referencia de CodEstatus: códigos de autenticación y validación (300–305)

Estos códigos aparecen cuando el mensaje ni siquiera pasa el filtro de identidad o de forma. No dependen de tus datos de negocio, sino de la firma y del XML:

  • 300 Usuario no válido — el RFC solicitante no está habilitado para el servicio, o el Authorization está mal. Fix: revisa que autenticaste bien y que el RFC tiene acceso a Descarga Masiva.
  • 301 XML mal formado — típicamente un RFC receptor inválido dentro de la consulta, o un sobre mal armado. Fix: valida los RFC y la estructura del XML antes de mandarlo.
  • 302 Sello mal formado — la firma WS-Security no está bien construida. Fix: revisa la canonicalización y el digest SHA1.
  • 303 Sello no corresponde con RfcSolicitante — firmaste con un certificado que no es el del RFC que dices ser. Fix: que el .cer/.key correspondan al RfcSolicitante.
  • 304 Certificado revocado o caduco — tu e.firma ya no sirve. Fix: renueva la e.firma en el SAT.
  • 305 Certificado inválido — el certificado no es una e.firma válida (por ejemplo, mandaste un CSD de sellado). Fix: usa la e.firma (FIEL), no el certificado de sellado.

La mayoría de estos se evitan de raíz dejando que una implementación de referencia arme el XML-DSig — lo veremos al final. Antes de facturar contra un RFC dudoso, además conviene validar el RFC sin la Constancia de Situación Fiscal, que es una causa frecuente de 301.

Referencia de CodEstatus: códigos de solicitud y descarga (5000, 5002–5011, 404)

Estos ya son del negocio de la descarga. Aquí es donde vive el famoso “aceptada”:

  • 5000 Solicitud recibida con éxito (Aceptada) — el acuse. No es “listo”: ve a pollear VerificaSolicitud.
  • 5002 Se agotaron las solicitudes de por vida — mandaste la MISMA consulta demasiadas veces; es un tope de por vida para esa petición exacta. Fix: cambia parámetros o parte el rango.
  • 5003 Tope máximo — excediste el máximo de CFDI/Metadata por solicitud. Fix: parte el rango en ventanas más chicas.
  • 5004 No se encontró la información — no hay CFDI en ese rango, así que no se generó paquete. Fix: ajusta fechas/RFC; no es un bug.
  • 5005 Solicitud duplicada — ya existe una solicitud con los mismos parámetros. Fix: reúsa su IdSolicitud, no crees una nueva.
  • 5008 Máximo de descargas permitidas — un paquete se puede bajar máximo 2 veces. Fix: persiste el ZIP la primera vez.
  • 5011 Límite de descargas por folio por día — pegaste al tope diario por folio. Fix: espera al siguiente día o reagrupa.
  • 404 Error no controlado — falla del lado del SAT. Fix: reintenta con backoff más tarde.
5000Aceptada — acuse, ve a VerificaSolicitud
5002Se agotaron las de por vida — cambia parámetros
5004No se encontró info — ajusta el rango
5005Duplicada — reúsa el IdSolicitud
5008Máx 2 descargas por paquete
404Error no controlado — reintenta con backoff

Las dos trampas que más pegan: 5005 duplicada y 5002 de por vida

Estas dos son casi siempre el mismo error contado en dos actos, y son la causa raíz de la solicitud parada horas.

5005 Solicitud duplicada. El SAT ya tiene una solicitud con tu misma combinación de FechaInicial, FechaFinal, RfcEmisor, RfcReceptor y TipoSolicitud. No te está regañando: te está diciendo “ya existe, no la dupliques”. El fix no es cambiar parámetros para engañarlo — es recuperar el IdSolicitud que ya tenías (por eso lo persistes) y pollear VerificaSolicitud sobre ese. Crear una nueva con datos ligeramente distintos solo te fragmenta el trabajo.

5002 Se agotaron las solicitudes de por vida. Este es el 5005 que ignoraste demasiadas veces. Cada consulta idéntica tiene un cupo de vida, y reenviar la misma petición una y otra vez lo consume hasta cero. Cuando llegas a 5002, esa consulta exacta ya no vuelve a correr. El fix es cambiar los parámetros de forma real (parte el rango de fechas en ventanas por semana o por día, o pide Metadata en vez de CFDI completo), de modo que sea una consulta distinta, no la misma disfrazada.

# WRONG: same query re-sent on a loop -> 5005, then 5002 (burned for life)
while not done:
    resp = solicita_descarga(same_envelope, token)   # identical params every time

# RIGHT: solicit once, persist, then poll the stored IdSolicitud
id_solicitud = db.get(query_key) or solicita_descarga(envelope, token)
db.save(query_key, id_solicitud)
estado = verifica_solicitud(id_solicitud, token)     # poll THIS id, don't re-solicit

Si un rango sigue tronando en 5003 (tope máximo), pártelo también: pedir Metadata primero es mucho más ligero que el CFDI completo y evita chocar con los topes.

El checklist de depuración de 30 segundos

Casi todo el “no sé por qué se atoró” se resuelve logueando cuatro campos en cada respuesta de VerificaSolicitud: EstadoSolicitud, CodEstatus, IdSolicitud y NumeroCFDIs. Con esos cuatro números, cualquier caso pasa de misterio a diagnóstico en medio minuto.

import logging
log = logging.getLogger("descarga-masiva")

log.info(
    "verifica id=%s EstadoSolicitud=%s CodEstatus=%s NumeroCFDIs=%s",
    id_solicitud,           # which request am I even polling?
    estado_solicitud,       # 1..6 -> where in the lifecycle
    cod_estatus,            # 5000/5002/5004/5005... -> the reason
    numero_cfdis,           # 0 here usually means 5004 (nothing in range)
)

Cómo se lee: EstadoSolicitud te dice dónde está (¿sigue en 2 EnProceso o ya cayó a 5 Rechazada?); CodEstatus te dice por qué; IdSolicitud te confirma que estás polleando la solicitud correcta y no una vieja; y NumeroCFDIs en 0 casi siempre significa 5004 (no hay nada en ese rango) y no un bug de tu código.

EstadoSolicitud1..6 — dónde está en el ciclo
CodEstatus5000/5002/5004/5005 — por qué
IdSolicitudque polleas la solicitud correcta
NumeroCFDIs0 ≈ 5004 (rango vacío), no un bug

Deja de parsear a mano: usa los helpers de estado (phpcfdi, python-satcfdi)

Mapear estos números en tu propio código con if/elif funciona hasta que el SAT ajusta un código y tu parser se rompe en silencio. En PHP, phpcfdi/sat-ws-descarga-masiva ya trae los estados como métodos: getStatus()->isAccepted(), isInProgress(), isFinished(), isFailure(), isRejected(), isExpired() y getCodeRequest(). Ahí ramificas por intención y no por números mágicos:

// phpcfdi/sat-ws-descarga-masiva (PHP) — estados tipados
$status = $verify->getStatus();          // mapea los códigos crudos a un estado

if ($status->isInProgress()) {
    scheduleRetryWithBackoff();
} elseif ($status->isFinished()) {       // Terminada — ya hay paquetes
    downloadAllPackages($verify->getPackagesIds());
} elseif ($status->isRejected() || $status->isFailure()) {
    alertEngineer($verify->getCodeRequest());  // revisa el CodEstatus
} elseif ($status->isExpired()) {
    reissueRequest();                    // Vencida — el resultado ya no existe
}

En Python, los clientes mantenidos — python-satcfdi (SAT-CFDI) y cfdiclient — no traen esos helpers booleanos; te devuelven los campos crudos, así que ramificas sobre el valor directo:

# python-satcfdi / cfdiclient: lee EstadoSolicitud directo (no hay is_finished())
if status["EstadoSolicitud"] == "3":       # Terminada -> ya hay paquetes
    for paquete in status["IdsPaquetes"]:
        sat.recover_comprobante_download(paquete)
elif status["EstadoSolicitud"] == "6":     # Vencida -> vuelve a solicitar
    resubmit()

Sea con helpers tipados o leyendo el campo crudo, mantén la semántica (Terminada == ya hay paquetes) en un solo lugar auditado, y tú te quedas con lo que sí es tuyo: persistir el IdSolicitud, el backoff, y decidir qué hacer con cada terminal. Cuando corres esto para muchos RFC a la vez — un despacho, por ejemplo — la referencia de códigos se vuelve el corazón del orquestador; ese pipeline de producción lo detallo en el post hermano del pipeline para múltiples RFCs, y todo el ecosistema arranca en el hub de facturación CFDI.

Ingeniero, no contador. Yo escribo el código y sí lo pongo en producción contra el SAT — no soy tu contador ni tu abogado. Los códigos, estados y topes de este post están verificados hoy, pero cambian: confírmalos siempre contra las fuentes oficiales vigentes, nunca contra tu memoria (ni contra la mía).