ToolzYard Blog

Guías y tutoriales para desarrolladores

Tendencia en México · XML y facturación

Cómo leer un CFDI 4.0: del XML a JSON paso a paso (Guía 2026)

Publicado: 18 de septiembre de 2026 • Por el

Respuesta rápida

Un CFDI 4.0 es un XML cuyo nodo raíz es cfdi:Comprobante y casi todos sus datos están en atributos. Al convertirlo a JSON, los atributos quedan en @attributes y el UUID (folio fiscal) está en cfdi:Complemento → tfd:TimbreFiscalDigital. Normaliza los conceptos a arreglo, maneja los montos en centavos y busca los nodos por namespace.

Cualquier sistema que opere en México termina recibiendo facturas electrónicas: cuentas por pagar, conciliación bancaria, reembolsos de gastos o un ERP que necesita registrar compras. El CFDI 4.0 es obligatorio desde abril de 2023, y en 2026 el SAT ha actualizado sus catálogos y guías de llenado varias veces. Aun así, la estructura del XML no cambia, y es lo primero que un desarrollador necesita dominar. En esta guía leemos un CFDI real (con datos ficticios), lo convertimos a JSON y resolvemos los errores que aparecen en casi todas las integraciones.

La estructura de un CFDI 4.0

Un CFDI es un documento XML cuyo nodo raíz es cfdi:Comprobante, dentro del namespace http://www.sat.gob.mx/cfd/4. Casi toda la información está en atributos, no en texto dentro de las etiquetas. Este es un ejemplo mínimo de una factura de ingreso:

<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4"
    Version="4.0" Serie="A" Folio="1024" Fecha="2026-09-15T10:30:00"
    FormaPago="03" MetodoPago="PUE" Moneda="MXN"
    SubTotal="3000.00" Total="3480.00"
    TipoDeComprobante="I" Exportacion="01" LugarExpedicion="64000" ...>

  <cfdi:Emisor Rfc="AAA010101AAA" Nombre="EMPRESA DE EJEMPLO" RegimenFiscal="601"/>

  <cfdi:Receptor Rfc="MOPJ850312AB1" Nombre="JUAN MORALES PEREZ"
      DomicilioFiscalReceptor="06600" RegimenFiscalReceptor="612" UsoCFDI="G03"/>

  <cfdi:Conceptos>
    <cfdi:Concepto ClaveProdServ="81111500" Cantidad="2" ClaveUnidad="E48"
        Descripcion="Desarrollo de sitio web" ValorUnitario="1500.00"
        Importe="3000.00" ObjetoImp="02">
      <cfdi:Impuestos>
        <cfdi:Traslados>
          <cfdi:Traslado Base="3000.00" Impuesto="002" TipoFactor="Tasa"
              TasaOCuota="0.160000" Importe="480.00"/>
        </cfdi:Traslados>
      </cfdi:Impuestos>
    </cfdi:Concepto>
  </cfdi:Conceptos>

  <cfdi:Impuestos TotalImpuestosTrasladados="480.00"> ... </cfdi:Impuestos>

  <cfdi:Complemento>
    <tfd:TimbreFiscalDigital xmlns:tfd="http://www.sat.gob.mx/TimbreFiscalDigital"
        Version="1.1" UUID="5FB2822E-396D-4725-8521-CDC4BDD20CCF"
        FechaTimbrado="2026-09-15T10:31:12" RfcProvCertif="AAA010101AAA" .../>
  </cfdi:Complemento>
</cfdi:Comprobante>

Los nodos que más vas a consultar:

  • Comprobante: totales, moneda, fecha, tipo (I ingreso, E egreso, P pago, T traslado, N nómina), forma y método de pago.
  • Emisor / Receptor: RFC, nombre y régimen fiscal. En 4.0 el receptor también incluye su código postal fiscal y su régimen.
  • Conceptos: cada línea de la factura, con sus propios impuestos.
  • Complemento → TimbreFiscalDigital: lo agrega el PAC al timbrar. Aquí vive el UUID, el folio fiscal que identifica la factura ante el SAT.

Si el XML llega en una sola línea, pégalo primero en el XML Formatter para verlo con sangrías. El formateador y el convertidor XML se ejecutan por completo en tu navegador, así que el RFC y los datos de la factura no se envían a ningún servidor.

Convertir el CFDI a JSON

Al pegar el XML en el convertidor XML a JSON, cada etiqueta se convierte en una llave (con todo y su prefijo cfdi:) y los atributos quedan agrupados en @attributes. Este es un extracto de la salida real:

{
  "cfdi:Comprobante": {
    "@attributes": {
      "Version": "4.0",
      "Fecha": "2026-09-15T10:30:00",
      "SubTotal": "3000.00",
      "Moneda": "MXN",
      "Total": "3480.00",
      "TipoDeComprobante": "I",
      ...
    },
    "cfdi:Emisor": {
      "@attributes": { "Rfc": "AAA010101AAA", "Nombre": "EMPRESA DE EJEMPLO", "RegimenFiscal": "601" }
    },
    "cfdi:Conceptos": {
      "cfdi:Concepto": {
        "@attributes": { "Cantidad": "2", "Descripcion": "Desarrollo de sitio web", "Importe": "3000.00", ... },
        "cfdi:Impuestos": { ... }
      }
    },
    "cfdi:Complemento": {
      "tfd:TimbreFiscalDigital": {
        "@attributes": { "UUID": "5FB2822E-396D-4725-8521-CDC4BDD20CCF", "FechaTimbrado": "2026-09-15T10:31:12", ... }
      }
    }
  }
}

Para extraer un valor sin recorrer todo el árbol, usa el JSONPath Tester con una ruta separada por puntos:

cfdi:Comprobante.cfdi:Complemento.tfd:TimbreFiscalDigital.@attributes.UUID
→ "5FB2822E-396D-4725-8521-CDC4BDD20CCF"

Error 1: un concepto es objeto, dos conceptos son arreglo

Este es el error más común al convertir un CFDI a JSON. Con un solo cfdi:Concepto, el convertidor genera un objeto. Con dos o más, genera un arreglo. El código que funcionó con la factura de prueba falla con la primera factura de varias partidas:

const conceptos = json['cfdi:Comprobante']['cfdi:Conceptos']['cfdi:Concepto'];

conceptos.map(c => c['@attributes'].Descripcion);
// TypeError: conceptos.map is not a function   (cuando hay un solo concepto)

// Solución: normaliza siempre a arreglo
const lista = [].concat(conceptos ?? []);
lista.map(c => c['@attributes'].Descripcion);  // ✓ funciona con 1 o con N

Lo mismo aplica a cfdi:Traslado, cfdi:Retencion y a los documentos relacionados en los complementos de pago. Normaliza a arreglo en todo nodo que el SAT permita repetir.

Error 2: sumar montos con decimales flotantes

Los importes vienen como texto ("3480.00"), y conviene que sigan así hasta el último momento. Si los conviertes con parseFloat y los sumas, aparecen errores de redondeo: en JavaScript, 0.1 + 0.2 da 0.30000000000000004. Al conciliar miles de facturas contra un estado de cuenta, esas diferencias de centavos se acumulan. Opera en centavos enteros (Math.round(parseFloat(total) * 100)) o usa una librería de decimales, y compara siempre con el Total declarado en el XML.

Error 3: buscar etiquetas sin namespace

Si lees el XML directamente en lugar de convertirlo, recuerda que las etiquetas tienen namespace. Una búsqueda por "Emisor" no encuentra nada. Busca por namespace y nombre local:

// JavaScript (navegador)
const doc = new DOMParser().parseFromString(xml, 'application/xml');
const CFDI = 'http://www.sat.gob.mx/cfd/4';
const TFD  = 'http://www.sat.gob.mx/TimbreFiscalDigital';

const emisorRfc = doc.getElementsByTagNameNS(CFDI, 'Emisor')[0].getAttribute('Rfc');
const uuid = doc.getElementsByTagNameNS(TFD, 'TimbreFiscalDigital')[0].getAttribute('UUID');

# Python
import xml.etree.ElementTree as ET
ns = {'cfdi': 'http://www.sat.gob.mx/cfd/4',
      'tfd': 'http://www.sat.gob.mx/TimbreFiscalDigital'}
root = ET.parse('factura.xml').getroot()
total = root.get('Total')
uuid = root.find('.//tfd:TimbreFiscalDigital', ns).get('UUID')

Usa el namespace completo, no el prefijo. El prefijo cfdi: es una convención y el XML podría usar otro. También revisa Version: si llegan facturas antiguas 3.3, su namespace es http://www.sat.gob.mx/cfd/3 y no aparecerán con el de 4.0.

Validar el RFC y el UUID con expresiones regulares

Antes de guardar datos de un CFDI, conviene validar el formato. Un RFC tiene 3 letras (persona moral) o 4 (persona física), seguidas de la fecha en formato AAMMDD y una homoclave de 3 caracteres, cuyo último carácter es un dígito o la letra A:

// RFC (persona física o moral)
/^[A-ZÑ&]{3,4}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])[A-Z\d]{2}[A\d]$/

AAA010101AAA   ✓ (moral)       MOPJ850312AB1  ✓ (física)
XAXX010101000  ✓ (público en general)
ABC011301AAA   ✗ (mes 13)

// UUID del timbre fiscal
/^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i

Prueba ambos patrones en el Regex Tester. Toma en cuenta que el regex solo valida el formato: no comprueba el dígito verificador ni que el RFC esté registrado, y tampoco que la factura siga vigente. Para eso consulta los servicios del SAT, como la verificación de CFDI por UUID. El UUID es un identificador estándar de 128 bits; en ¿Qué es un UUID? explicamos su formato (en inglés).

Checklist para tu integración

  • Formatea el XML antes de depurarlo y confirma que Version="4.0".
  • Usa el UUID como llave única para evitar registrar la misma factura dos veces.
  • Normaliza a arreglo todo nodo repetible (conceptos, traslados, retenciones, pagos).
  • Maneja los importes como texto o centavos, nunca como flotantes.
  • Busca los nodos por namespace, no por prefijo.
  • Valida formato de RFC y UUID localmente y la vigencia contra el SAT.
  • Mantén tus catálogos (régimen fiscal, uso de CFDI, claves de producto) al día: el SAT los actualizó varias veces en 2026.

Herramientas útiles de ToolzYard

Conclusión

Leer un CFDI 4.0 es sencillo una vez que entiendes tres cosas: casi todo está en atributos, las etiquetas tienen namespace y los nodos repetibles cambian de forma según cuántos haya. Convierte el XML a JSON para explorarlo, usa el UUID como identificador único, maneja los montos sin flotantes y valida los datos antes de guardarlos. Con eso, tu integración funcionará igual con una factura de una partida que con una de cien.

Preguntas frecuentes

¿Dónde está el UUID o folio fiscal en el XML de un CFDI?

En el nodo tfd:TimbreFiscalDigital, dentro de cfdi:Complemento, como atributo UUID. Lo agrega el PAC cuando timbra la factura y es el identificador único del comprobante ante el SAT.

¿Por qué mi código falla cuando la factura tiene un solo concepto?

Al convertir XML a JSON, un nodo que aparece una vez se convierte en objeto y uno que aparece varias veces se convierte en arreglo. Normaliza siempre con [].concat(valor) antes de recorrer conceptos, traslados o retenciones.

¿Cuál es la diferencia entre el namespace de CFDI 3.3 y 4.0?

CFDI 4.0 usa http://www.sat.gob.mx/cfd/4 y CFDI 3.3 usaba http://www.sat.gob.mx/cfd/3. Si buscas nodos por namespace, las facturas de una versión no aparecerán con el namespace de la otra.

¿Un regex basta para validar un RFC?

No. El regex valida el formato (letras, fecha y homoclave), pero no comprueba el dígito verificador ni que el RFC esté inscrito ante el SAT. Úsalo como primer filtro y valida contra el SAT cuando lo necesites.

¿Es seguro pegar mis facturas en una herramienta en línea?

El XML Formatter y el convertidor XML a JSON de ToolzYard procesan el XML en tu navegador y no lo envían a un servidor. Aun así, evita compartir facturas con datos reales en herramientas que no sepas cómo manejan la información.