Node.js es una de las tecnologías más populares para construir aplicaciones web y APIs. Si estás desarrollando un SaaS, e-commerce o cualquier sistema que necesite facturación electrónica en México, este tutorial te muestra cómo integrar CFDI 4.0 usando JavaScript.
Cubriremos desde la configuración inicial hasta casos de uso avanzados, con código que puedes copiar y adaptar a tu proyecto.
Creamos un proyecto nuevo con las dependencias necesarias:
mkdir facturacion-cfdi
cd facturacion-cfdi
npm init -y
npm install dotenv node-fetch
Crea un archivo .env con tus credenciales:
GIGSTACK_API_KEY=tu_jwt_token_aqui
GIGSTACK_API_URL=https://api.gigstack.io/v2
Creamos una clase reutilizable para interactuar con la API:
// gigstack-client.js
import fetch from 'node-fetch';
import 'dotenv/config';
class GigstackClient {
constructor() {
this.baseUrl = process.env.GIGSTACK_API_URL;
this.apiKey = process.env.GIGSTACK_API_KEY;
}
async request(endpoint, options = {}) {
const url = this.baseUrl + endpoint;
const response = await fetch(url, {
...options,
headers: {
'Authorization': 'Bearer ' + this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Error en la API');
}
return data;
}
async get(endpoint) {
return this.request(endpoint, { method: 'GET' });
}
async post(endpoint, body) {
return this.request(endpoint, {
method: 'POST',
body: JSON.stringify(body)
});
}
async delete(endpoint, body = {}) {
return this.request(endpoint, {
method: 'DELETE',
body: JSON.stringify(body)
});
}
}
export default new GigstackClient();
Antes de facturar, necesitas crear clientes con sus datos fiscales:
// clients.js
import client from './gigstack-client.js';
// Crear un nuevo cliente
async function createClient(clientData) {
const response = await client.post('/clients', {
name: clientData.name, // Razón social exacta
email: clientData.email,
tax_id: clientData.rfc, // RFC
tax_system: clientData.regimen, // Código de régimen fiscal
zip_code: clientData.codigoPostal
});
console.log('Cliente creado:', response.data.id);
return response.data;
}
// Obtener cliente por RFC
async function getClientByRFC(rfc) {
const response = await client.get('/clients?tax_id=' + rfc);
return response.data[0] || null;
}
// Validar datos fiscales
async function validateClientData(rfc, name) {
const response = await client.post('/clients/validate', {
tax_id: rfc,
name: name
});
return response.data.valid;
}
// Ejemplo de uso
async function main() {
// Primero validamos
const isValid = await validateClientData(
'XAXX010101000',
'Publico En General'
);
if (isValid) {
const newClient = await createClient({
name: 'Publico En General',
email: 'cliente@example.com',
rfc: 'XAXX010101000',
regimen: '616',
codigoPostal: '06600'
});
}
}
El endpoint principal para emitir facturas:
// invoices.js
import client from './gigstack-client.js';
async function createInvoice(invoiceData) {
const response = await client.post('/invoices/income', {
client: invoiceData.clientId,
items: invoiceData.items.map(item => ({
description: item.description,
quantity: item.quantity,
unit_price: item.unitPrice,
product_key: item.claveSAT, // Clave producto SAT
unit_key: item.claveUnidad, // Clave unidad SAT
taxes: [{
type: 'IVA',
rate: 0.16,
inclusive: false
}]
})),
payment_form: invoiceData.formaPago, // 01=Efectivo, 03=Transferencia, etc.
payment_method: invoiceData.metodoPago, // PUE o PPD
use: invoiceData.usoCFDI, // G01, G03, etc.
currency: invoiceData.currency || 'MXN',
notes: invoiceData.notas || ''
});
return response.data;
}
// Ejemplo: Factura de servicio de software
async function facturarServicioSoftware() {
const invoice = await createInvoice({
clientId: 'cli_abc123',
items: [{
description: 'Licencia mensual de software SaaS',
quantity: 1,
unitPrice: 999.00,
claveSAT: '43232408', // Software como servicio
claveUnidad: 'E48' // Unidad de servicio
}],
formaPago: '03', // Transferencia electrónica
metodoPago: 'PUE', // Pago en una sola exhibición
usoCFDI: 'G03' // Gastos en general
});
console.log('Factura creada:');
console.log(' UUID:', invoice.uuid);
console.log(' Total:', invoice.total);
console.log(' PDF:', invoice.pdf_url);
console.log(' XML:', invoice.xml_url);
return invoice;
}
// Ejemplo: Factura con múltiples conceptos
async function facturarMultiplesProductos() {
const invoice = await createInvoice({
clientId: 'cli_xyz789',
items: [
{
description: 'Desarrollo de aplicación móvil',
quantity: 1,
unitPrice: 50000.00,
claveSAT: '43232301',
claveUnidad: 'E48'
},
{
description: 'Hosting anual',
quantity: 12,
unitPrice: 500.00,
claveSAT: '43233501',
claveUnidad: 'E48'
},
{
description: 'Dominio .com.mx',
quantity: 1,
unitPrice: 350.00,
claveSAT: '43233502',
claveUnidad: 'E48'
}
],
formaPago: '03',
metodoPago: 'PUE',
usoCFDI: 'G03'
});
return invoice;
}
Para clientes que pagan en USD u otra moneda:
async function facturarEnDolares() {
const invoice = await createInvoice({
clientId: 'cli_internacional',
items: [{
description: 'Consultoría técnica internacional',
quantity: 40,
unitPrice: 150.00, // USD
claveSAT: '80111501',
claveUnidad: 'E48'
}],
formaPago: '03',
metodoPago: 'PUE',
usoCFDI: 'G03',
currency: 'USD'
// gigstack obtiene el tipo de cambio automáticamente
});
console.log('Total USD:', invoice.total);
console.log('Tipo de cambio:', invoice.exchange_rate);
return invoice;
}
Para cancelar una factura emitida:
async function cancelInvoice(uuid, reason, substituteUuid = null) {
// Motivos de cancelación:
// 01 = Comprobante emitido con errores con relación
// 02 = Comprobante emitido con errores sin relación
// 03 = No se llevó a cabo la operación
// 04 = Operación nominativa relacionada en factura global
const body = {
uuid: uuid,
cancellation_reason: reason
};
// Si el motivo es 01, debe incluir UUID de sustitución
if (reason === '01' && substituteUuid) {
body.substitute_uuid = substituteUuid;
}
const response = await client.delete('/invoices/' + uuid, body);
console.log('Cancelación solicitada:', response.data.status);
// Puede ser: 'cancelled', 'pending_acceptance', 'rejected'
return response.data;
}
// Ejemplo: Cancelar por error y reexpedir
async function cancelarYReexpedir(uuidOriginal, clientId, items) {
// 1. Crear la factura correcta primero
const newInvoice = await createInvoice({
clientId,
items,
formaPago: '03',
metodoPago: 'PUE',
usoCFDI: 'G03'
});
// 2. Cancelar la original con relación a la nueva
await cancelInvoice(uuidOriginal, '01', newInvoice.uuid);
return newInvoice;
}
Para facturas con método de pago PPD (Pago en Parcialidades o Diferido):
async function registerPayment(paymentData) {
const response = await client.post('/payments/register', {
invoice_uuid: paymentData.invoiceUuid,
amount: paymentData.amount,
payment_date: paymentData.date, // Fecha del pago real
payment_form: paymentData.forma // 03=Transferencia, etc.
});
console.log('Complemento generado:', response.data.complement_uuid);
return response.data;
}
// Ejemplo: Factura pagada en 2 parcialidades
async function pagarEnParcialidades() {
// Primero crear factura PPD
const invoice = await createInvoice({
clientId: 'cli_abc123',
items: [{
description: 'Proyecto de desarrollo',
quantity: 1,
unitPrice: 100000.00,
claveSAT: '43232301',
claveUnidad: 'E48'
}],
formaPago: '99', // Por definir (PPD)
metodoPago: 'PPD', // Pago en parcialidades
usoCFDI: 'G03'
});
// Registrar primer pago (50%)
const payment1 = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 58000.00, // 50% con IVA
date: '2026-02-15',
forma: '03'
});
// Registrar segundo pago (50%)
const payment2 = await registerPayment({
invoiceUuid: invoice.uuid,
amount: 58000.00,
date: '2026-03-15',
forma: '03'
});
return { invoice, payments: [payment1, payment2] };
}
Implementación robusta de manejo de errores:
class GigstackError extends Error {
constructor(message, code, details) {
super(message);
this.code = code;
this.details = details;
}
}
async function safeCreateInvoice(invoiceData) {
try {
return await createInvoice(invoiceData);
} catch (error) {
// Errores comunes y cómo manejarlos
if (error.message.includes('RFC')) {
throw new GigstackError(
'RFC inválido o no registrado en SAT',
'INVALID_RFC',
{ rfc: invoiceData.clientId }
);
}
if (error.message.includes('razón social')) {
throw new GigstackError(
'La razón social no coincide con el RFC',
'NAME_MISMATCH',
{ suggestion: 'Verifica el nombre exacto en constancia SAT' }
);
}
if (error.message.includes('régimen fiscal')) {
throw new GigstackError(
'Régimen fiscal inválido para este RFC',
'INVALID_TAX_SYSTEM',
{ suggestion: 'Verifica el régimen en la constancia de situación fiscal' }
);
}
if (error.message.includes('código postal')) {
throw new GigstackError(
'Código postal no corresponde al domicilio fiscal',
'INVALID_ZIP_CODE',
{ suggestion: 'Usa el CP del domicilio fiscal registrado en SAT' }
);
}
// Error genérico
throw new GigstackError(
'Error al crear factura: ' + error.message,
'INVOICE_ERROR',
{ original: error }
);
}
}
// Uso con try/catch
async function handleInvoiceRequest(req, res) {
try {
const invoice = await safeCreateInvoice(req.body);
res.json({ success: true, invoice });
} catch (error) {
if (error instanceof GigstackError) {
res.status(400).json({
success: false,
error: error.code,
message: error.message,
details: error.details
});
} else {
res.status(500).json({
success: false,
error: 'INTERNAL_ERROR',
message: 'Error interno del servidor'
});
}
}
}
Ejemplo completo de API REST para facturación:
import express from 'express';
import client from './gigstack-client.js';
const app = express();
app.use(express.json());
// Crear factura
app.post('/api/invoices', async (req, res) => {
try {
const invoice = await createInvoice(req.body);
res.json({ success: true, data: invoice });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
});
// Obtener factura por UUID
app.get('/api/invoices/:uuid', async (req, res) => {
try {
const response = await client.get('/invoices/' + req.params.uuid);
res.json({ success: true, data: response.data });
} catch (error) {
res.status(404).json({ success: false, error: 'Factura no encontrada' });
}
});
// Cancelar factura
app.delete('/api/invoices/:uuid', async (req, res) => {
try {
const result = await cancelInvoice(
req.params.uuid,
req.body.reason,
req.body.substituteUuid
);
res.json({ success: true, data: result });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
});
// Registrar pago
app.post('/api/payments', async (req, res) => {
try {
const payment = await registerPayment(req.body);
res.json({ success: true, data: payment });
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
});
app.listen(3000, () => {
console.log('API de facturación corriendo en puerto 3000');
});
Usa el ambiente de staging para pruebas:
// test-invoices.js
import { describe, it, expect } from 'vitest';
import client from './gigstack-client.js';
describe('Facturación CFDI', () => {
it('debe crear una factura válida', async () => {
const invoice = await createInvoice({
clientId: 'cli_test_123',
items: [{
description: 'Producto de prueba',
quantity: 1,
unitPrice: 100.00,
claveSAT: '43232408',
claveUnidad: 'E48'
}],
formaPago: '03',
metodoPago: 'PUE',
usoCFDI: 'G03'
});
expect(invoice.uuid).toBeDefined();
expect(invoice.total).toBe(116.00);
expect(invoice.xml_url).toContain('http');
});
it('debe rechazar RFC inválido', async () => {
await expect(createInvoice({
clientId: 'cli_rfc_invalido',
// ...
})).rejects.toThrow('RFC');
});
});
Con estos ejemplos tienes todo lo necesario para integrar facturación CFDI 4.0 en tus aplicaciones Node.js. El código es modular y fácil de adaptar a tu arquitectura específica.