Saltar a contenido

Factura CFDI 4.0 con n8n

Esta receta conecta una hoja de cálculo de ventas con Cefidia usando n8n. Cada fila nueva se convierte en un CFDI: si la venta trae el correo del cliente, él captura sus datos fiscales desde una liga de autofacturación; si no, se timbra a público en general. El resultado (estado, UUID, liga o error) se escribe de vuelta en la misma fila.

Note

Es una receta de ejemplo, no una integración oficial. Ajusta los datos fiscales, el IVA y los textos a tu operación y revisa los CFDI de prueba antes de activarla.

Qué necesitas

  • Tu cuenta de Cefidia con la razón social registrada, el CSD cargado y crédito disponible. Ver Crear tu cuenta.
  • Una instancia de n8n (la nube de n8n o la tuya) con un nodo MCP Client.
  • Una cuenta de Google con acceso a la hoja.
  • Una cuenta de correo con SMTP para enviar la liga a tus clientes. Cefidia no envía correos por ti.

Experimenta con una razón social de prueba

Antes de facturar con tu razón social real, puedes ensayar todo el flujo con una razón social de pruebas: son RFC de prueba del SAT que Cefidia ya tiene registrados.

  1. Entra a la aplicación web y ve a Registrar una razón social.
  2. Elige Usar un RFC de pruebas del SAT y selecciona uno de la lista (personas morales o físicas). Se agrega a tu cuenta con su CSD ya cargado y la insignia «Pruebas».
  3. Pídele a tu agente «lista mis razones sociales» (list_issuers) y copia el id de la de pruebas. Úsalo como issuer en los nodos MCP y usa su RFC, nombre, régimen y código postal en el nodo Preparar CFDI.

Las facturas de una razón social de pruebas se timbran en el ambiente de pruebas del SAT: no tienen valor fiscal ni consumen crédito, así que puedes repetir el flujo las veces que necesites. Estos CFDI aparecen con la insignia «Prueba» en tu lista. Cuando todo funcione, cambia el issuer y los datos del emisor por los de tu razón social real.

La hoja

Crea una hoja con estas columnas en la fila 1:

Columna La llenas tú Contenido
Folio Sí Número de la fila (por ejemplo, 2 para la primera venta).
Fecha Sí Fecha de la venta.
Concepto Sí Descripción que irá en el CFDI.
Monto Sí Total cobrado, con IVA incluido.
Correo Opcional Correo del cliente. Vacío = público en general.
Estado No Lo escribe el flujo.
UUID No Lo escribe el flujo al timbrar a público en general.
Enlace autofactura No Lo escribe el flujo.
Detalle No Mensaje de apoyo o error.

Cómo funciona

  1. Nueva fila. n8n revisa la hoja cada minuto y detecta las filas agregadas.
  2. Preparar CFDI. Un nodo de código separa el Monto en subtotal e IVA (16 %) y arma el CFDI 4.0 completo.
  3. Crear borrador. Se llama a la herramienta create_invoice_draft del servidor MCP.
  4. ¿Tiene correo?
    • Sí: se llama a request_receiver_data, se envía la liga por correo y la fila queda como «Autofactura enviada». El timbrado ocurre cuando el cliente captura sus datos.
    • No: se llama a sign_cfdi y la fila queda como «Timbrada (Público en General)» con su UUID.
  5. Errores. Si falla cualquier llamada a Cefidia, la fila queda con Estado = Error y el detalle en Detalle.

Paso a paso

1. Conecta n8n con Cefidia

Crea una credencial MCP OAuth2 en n8n:

  • URL del servidor: https://cefidia.com/mcp
  • Cefidia usa OAuth con registro dinámico de cliente: no hay client ID ni secreto que capturar. Al autorizar, inicia sesión con tu cuenta de Cefidia.
  • Los alcances necesarios son issuers:read e invoices:write (más invoices:read si quieres consultar facturas desde n8n). Ver Permisos.

Si tu n8n no completa el registro automático, revisa Si construyes tu propio cliente.

2. Obtén el identificador de tu razón social

Las herramientas piden el argumento issuer. Pídele a tu agente de IA «lista mis razones sociales» (herramienta list_issuers) y copia el id. Úsalo en los tres nodos MCP.

3. Crea el flujo

Arma estos nodos en n8n y conéctalos como se indica:

Nodo Tipo Función
Nueva fila en hoja de facturas Google Sheets Trigger (rowAdded, cada minuto) Dispara el flujo.
Preparar CFDI Code (una vez por elemento) Calcula IVA y arma el CFDI.
Crear borrador en Cefidia MCP Client, create_invoice_draft Crea el borrador.
¿Tiene correo? If Elige entre autofactura y público en general.
Generar enlace de autofacturación MCP Client, request_receiver_data Genera la liga.
Enviar enlace por correo Send Email Entrega la liga al cliente.
Marcar autofactura enviada Google Sheets, update Registra estado y liga.
Timbrar factura Público en General MCP Client, sign_cfdi Timbra sin receptor propio.
Marcar factura timbrada Google Sheets, update Registra el UUID.
Guardar error … Google Sheets, update Una rama de error por cada llamada a Cefidia.

En los tres nodos MCP Client activa On Error → Continue (using error output) para poder mandar los fallos a las ramas de error.

Preparar CFDI

El nodo de código convierte la fila en el CFDI. Reemplaza los valores marcados con los de tu razón social:

const row = $json;
const correo = String(row.Correo || '').trim();
const conCorreo = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(correo);
const montoTexto = String(row.Monto ?? '').replace(/[^0-9.\-]/g, '');
const total = Math.round(parseFloat(montoTexto) * 100) / 100;
if (!isFinite(total) || total <= 0) {
  throw new Error('Monto inválido en la fila ' + row.row_number + ': ' + row.Monto);
}
// El monto de la hoja incluye IVA 16%
const subtotal = Math.round((total / 1.16) * 100) / 100;
const iva = Math.round((total - subtotal) * 100) / 100;
const f = (n) => n.toFixed(2);
const ahora = $now.setZone('America/Mexico_City');
const traslado = { Base: f(subtotal), Impuesto: '002', TipoFactor: 'Tasa', TasaOCuota: '0.160000', Importe: f(iva) };
const cfdi = {
  Version: '4.0',
  Fecha: ahora.toFormat("yyyy-MM-dd'T'HH:mm:ss"),
  FormaPago: '03',
  MetodoPago: 'PUE',
  Moneda: 'MXN',
  TipoDeComprobante: 'I',
  Exportacion: '01',
  LugarExpedicion: '00000',                       // tu código postal de expedición
  SubTotal: f(subtotal),
  Total: f(total),
  Emisor: { Rfc: 'TU_RFC', Nombre: 'TU RAZÓN SOCIAL', RegimenFiscal: '601' },
  Receptor: {
    Rfc: 'XAXX010101000',
    Nombre: conCorreo ? 'POR CAPTURAR' : 'PUBLICO EN GENERAL',
    DomicilioFiscalReceptor: '00000',             // mismo código postal de expedición
    RegimenFiscalReceptor: '616',
    UsoCFDI: 'S01',
  },
  Conceptos: [{
    ClaveProdServ: '80101500',                    // clave del SAT de tu producto o servicio
    Cantidad: '1',
    ClaveUnidad: 'E48',
    Descripcion: String(row.Concepto || 'Servicios de consultoría'),
    ValorUnitario: f(subtotal),
    Importe: f(subtotal),
    ObjetoImp: '02',
    Impuestos: { Traslados: [traslado] },
  }],
  Impuestos: { TotalImpuestosTrasladados: f(iva), Traslados: [traslado] },
};
if (!conCorreo) {
  // Sin correo se factura a público en general: requiere información global
  cfdi.InformacionGlobal = { Periodicidad: '01', Meses: ahora.toFormat('MM'), 'Año': ahora.toFormat('yyyy') };
}
return { json: { row_number: row.row_number, correo, conCorreo, concepto: row.Concepto, total: f(total), cfdi } };

Qué ajustar:

  • Emisor: tu RFC, razón social y régimen fiscal, tal como están en tu CSD.
  • LugarExpedicion: el código postal de tu domicilio fiscal.
  • ClaveProdServ, ClaveUnidad, FormaPago, MetodoPago: los de tu operación.
  • Si tus ventas no llevan IVA al 16 %, cambia el cálculo de subtotal, iva y traslado.

Crear el borrador

En el nodo create_invoice_draft, modo JSON:

={{ JSON.stringify({ issuer: 'ID_DE_TU_RAZON_SOCIAL', cfdi: $json.cfdi }) }}

Generar la liga (rama con correo)

En el nodo request_receiver_data:

={{ JSON.stringify({
  issuer: 'ID_DE_TU_RAZON_SOCIAL',
  invoice_id: $json.structuredContent.invoice.id,
  expected_version: $json.structuredContent.invoice.version
}) }}

La respuesta trae structuredContent.url y structuredContent.expires_at. La liga se devuelve una sola vez: por eso el flujo la envía y la guarda en la hoja en el mismo recorrido.

Enviar la liga

En Send Email, usa tu SMTP, pon como destinatario el correo de la fila y en el cuerpo la liga {{ $json.structuredContent.url }} junto con su vencimiento (expires_at). Redacta el mensaje con tu propia voz y tu propio remitente.

Timbrar a público en general (rama sin correo)

En el nodo sign_cfdi:

={{ JSON.stringify({
  issuer: 'ID_DE_TU_RAZON_SOCIAL',
  invoice_id: $json.structuredContent.invoice.id,
  expected_version: $json.structuredContent.invoice.version
}) }}

Actualizar la hoja

Los nodos de Google Sheets usan la operación Update y se emparejan por row_number. En la rama de éxito con correo, el flujo escribe Estado = Autofactura enviada, la liga en Enlace autofactura y el vencimiento en Detalle. En la rama de público en general escribe Estado = Timbrada (Público en General) y el UUID. En las ramas de error escribe Estado = Error y los primeros 500 caracteres de la respuesta en Detalle.

4. Pruébalo

  1. Usa una razón social de pruebas. Con el flujo inactivo, agrega una fila de prueba con tu propio correo y ejecuta el flujo manualmente.
  2. Revisa el borrador y el correo recibido. Abre la liga, captura datos de prueba y confirma que se timbra.
  3. Agrega otra fila sin correo para probar la rama de público en general.
  4. Cuando todo coincida con lo esperado, activa el flujo.

Lo que debes saber

  • Cada timbrado con tu razón social real consume crédito (con una razón social de pruebas no) ($2 MXN por CFDI timbrado, IVA incluido). Revisa tu saldo en Créditos y recargas. Si el timbrado falla, el crédito se devuelve.
  • Las ligas de autofactura duran 30 días para capturar datos y 30 días tras el timbrado para descargar. Mientras la liga esté abierta, quien la tenga puede capturar datos y disparar el timbrado a tu cargo; envíala solo a tu cliente. Detalle en Autofacturación.
  • El borrador queda bloqueado mientras la liga está abierta: no se puede modificar ni eliminar. Para cambiarlo, revoca la liga con revoke_receiver_request.
  • Una liga abierta por factura. Si el flujo se vuelve a ejecutar sobre la misma venta, la herramienta responde already_requested; ese caso cae en la rama de error.
  • Evita duplicados. El disparador procesa cada fila nueva una vez. Si editas una fila ya procesada, el flujo no vuelve a correr; si borras y vuelves a pegar la fila, se genera otro CFDI.
  • Público en general se usa cuando no hay correo para pedir los datos. Confirma con tu contador que esa forma de facturar aplica a tus ventas.
  • No pongas credenciales en la hoja ni en los nodos de código. Usa las credenciales de n8n.

Problemas frecuentes

Síntoma Qué revisar
Estado = Error y insufficient_scope La credencial MCP no tiene invoices:write. Vuelve a autorizar.
needs_csd La razón social aún no tiene CSD cargado. Súbelo en la aplicación web.
Error de crédito insuficiente Recarga tu crédito. Ver Créditos y recargas.
Monto inválido La columna Monto está vacía o no es un número.
already_requested Esa factura ya tiene una liga abierta; revócala si necesitas otra.
Errores de validación del CFDI Revisa Emisor, el código postal y las claves del SAT en el nodo Preparar CFDI.
El flujo no detecta filas Verifica que esté activo y que el disparador apunte a la hoja correcta.

Referencia