Si usas Stripe como procesador de pagos y necesitas emitir CFDIs automáticamente en México, esta es la integración que tu stack necesitaba. En este tutorial conectarás Stripe con gigstack usando Node.js para que cada payment_intent.succeeded genere una factura timbrada ante el SAT sin intervención manual.
Stripe maneja el cobro. gigstack maneja el cumplimiento fiscal mexicano: CFDI 4.0, timbrado SAT y complementos de pago. La combinación elimina el loop manual de cobro → crear factura → enviar PDF → registrar en SAT. Con esta integración, ese flujo completo ocurre en segundos, automáticamente.
Requisitos previos:
Instala las dependencias necesarias:
npm init -y
npm install stripe express dotenv axios
Crea tu archivo .env:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
GIGSTACK_API_KEY=your_gigstack_api_key
GIGSTACK_BASE_URL=https://api.gigstack.io/v2
PORT=3000
Antes de generar facturas, el cliente debe existir en gigstack con su información fiscal. Este endpoint valida el RFC contra el SAT (verificación EFOS incluida).
// gigstack-client.js
const axios = require('axios');
require('dotenv').config();
const gigstackApi = axios.create({
baseURL: process.env.GIGSTACK_BASE_URL,
headers: {
'Authorization': `Bearer ${process.env.GIGSTACK_API_KEY}`,
'Content-Type': 'application/json'
}
});
async function createOrGetClient(stripeCustomer) {
try {
// Verificar si ya existe por external_id
const search = await gigstackApi.get(
`/clients?external_id=${stripeCustomer.id}`
);
if (search.data.data.length > 0) {
return search.data.data[0];
}
} catch (err) {
// No encontrado, continuar con creación
}
// Crear nuevo cliente con datos fiscales
const response = await gigstackApi.post('/clients', {
name: stripeCustomer.metadata.razon_social || stripeCustomer.name,
rfc: stripeCustomer.metadata.rfc,
email: stripeCustomer.email,
tax_system: stripeCustomer.metadata.regimen_fiscal || '601',
cfdi_use: stripeCustomer.metadata.uso_cfdi || 'G03',
address: {
zip: stripeCustomer.metadata.codigo_postal,
country: 'MEX'
},
external_id: stripeCustomer.id
});
console.log('Cliente creado en gigstack:', response.data.data.id);
return response.data.data;
}
module.exports = { gigstackApi, createOrGetClient };
Nota 2026: La API v2 de gigstack valida automáticamente el RFC contra el EFOS (lista negra SAT). Si el RFC está en esa lista, el endpoint retorna 400 con RFC_IN_EFOS_LIST.
Este es el corazón de la integración. Cuando Stripe dispara payment_intent.succeeded, llamamos a gigstack para crear y timbrar el CFDI 4.0 automáticamente.
// invoice-generator.js
const { gigstackApi } = require('./gigstack-client');
async function createInvoiceFromStripePayment(paymentIntent, gigstackClientId) {
const amountMXN = paymentIntent.amount / 100; // Stripe usa centavos
// Calcular base sin IVA (16%)
const subtotal = parseFloat((amountMXN / 1.16).toFixed(2));
const invoicePayload = {
client_id: gigstackClientId,
payment_form: '03', // Transferencia electrónica
items: [
{
description: paymentIntent.metadata.description || 'Servicio de software',
product_key: paymentIntent.metadata.sat_product_key || '81112501',
unit_key: 'E48',
quantity: 1,
unit_price: subtotal,
taxes: [
{
type: 'IVA',
rate: 0.16,
factor: 'Tasa',
withholding: false
}
]
}
],
currency: paymentIntent.currency === 'mxn' ? 'MXN' : 'USD',
// exchange_rate es opcional: gigstack usa el tipo de cambio del día
external_id: paymentIntent.id, // Idempotencia: evita duplicados
notes: `Pago procesado vía Stripe. ID: ${paymentIntent.id}`
};
const response = await gigstackApi.post('/invoices/income', invoicePayload);
console.log(`CFDI timbrado - Folio: ${response.data.data.folio_number}`);
console.log(`UUID SAT: ${response.data.data.uuid}`);
return response.data.data;
}
module.exports = { createInvoiceFromStripePayment };
Conecta todo en un servidor Express que escuche los eventos de Stripe y accione gigstack:
// server.js
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const { gigstackApi, createOrGetClient } = require('./gigstack-client');
const { createInvoiceFromStripePayment } = require('./invoice-generator');
require('dotenv').config();
const app = express();
// CRÍTICO: raw body para verificar firma de Stripe
app.post(
'/webhook/stripe',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['stripe-signature'];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error('Firma inválida:', err.message);
return res.status(400).send(`Webhook Error: ${err.message}`);
}
if (event.type === 'payment_intent.succeeded') {
const paymentIntent = event.data.object;
// Solo facturar si el cliente proporcionó su RFC
if (!paymentIntent.metadata?.rfc) {
return res.json({ received: true, invoiced: false });
}
try {
// 1. Recuperar cliente de Stripe
const stripeCustomer = await stripe.customers.retrieve(
paymentIntent.customer
);
stripeCustomer.metadata.rfc = paymentIntent.metadata.rfc;
stripeCustomer.metadata.razon_social =
paymentIntent.metadata.razon_social;
stripeCustomer.metadata.codigo_postal =
paymentIntent.metadata.codigo_postal;
// 2. Crear/obtener cliente en gigstack
const gigstackClient = await createOrGetClient(stripeCustomer);
// 3. Generar CFDI
const invoice = await createInvoiceFromStripePayment(
paymentIntent,
gigstackClient.id
);
// 4. Obtener archivos (PDF + XML)
const files = await gigstackApi.get(
`/invoices/income/${invoice.id}/files`
);
console.log('PDF:', files.data.data.pdf_url);
console.log('XML:', files.data.data.xml_url);
return res.json({
received: true,
invoiced: true,
folio: invoice.folio_number,
uuid: invoice.uuid
});
} catch (error) {
console.error(
'Error al generar CFDI:',
error.response?.data || error.message
);
// Siempre responde 200 a Stripe para evitar reintentos no deseados
// Loggea el error para revisión manual
return res.json({
received: true,
invoiced: false,
error: error.message
});
}
}
res.json({ received: true });
}
);
app.listen(process.env.PORT, () => {
console.log(`Servidor webhook en puerto ${process.env.PORT}`);
});
Los CFDIs pueden fallar por validaciones del PAC después de enviados (RFC inválido, complemento incorrecto, etc.). Suscríbete a los eventos de gigstack para capturar estos casos:
// setup-gigstack-webhook.js
const { gigstackApi } = require('./gigstack-client');
async function registerGigstackWebhook() {
const response = await gigstackApi.post('/webhooks', {
url: 'https://tu-dominio.com/webhook/gigstack',
events: [
'invoice.created',
'invoice.error',
'invoice.cancelled'
],
secret: 'tu_webhook_secret_gigstack'
});
console.log('Webhook registrado:', response.data.data.id);
}
registerGigstackWebhook();
gigstack usa la misma URL base para staging; solo cambia tu API key por la de pruebas. Para Stripe, usa el Stripe CLI para simular eventos localmente sin necesidad de deploy:
# Instalar Stripe CLI
brew install stripe/stripe-cli/stripe
# Autenticar
stripe login
# Escuchar y reenviar a tu servidor local
stripe listen --forward-to localhost:3000/webhook/stripe
# En otra terminal, disparar evento con metadata de RFC
stripe trigger payment_intent.succeeded \
--add payment_intent:metadata.rfc=XAXX010101000 \
--add payment_intent:metadata.razon_social="Empresa Demo SA de CV" \
--add payment_intent:metadata.codigo_postal=06600
Un CFDI exitoso retorna un uuid real timbrado en el ambiente de pruebas del PAC. Verifica el estado en status.gigstack.io.
Idempotencia: El campo external_id en el payload de gigstack evita duplicados si Stripe reintenta el webhook. gigstack retorna 409 si ya existe una factura con ese external_id. Siempre úsalo con el ID del payment_intent.
Rate limiting: La API v2 de gigstack acepta hasta 100 requests/minuto por API key. Para procesar pagos en batch, implementa una cola con BullMQ + Redis en lugar de llamadas síncronas directas.
Pagos en USD: Envía currency: 'USD' en el payload y omite exchange_rate. gigstack obtiene automáticamente el tipo de cambio oficial del Banxico para el CFDI. Solo incluye exchange_rate si tienes un tipo de cambio contractual específico.
El RFC del cliente está en la lista de contribuyentes con operaciones simuladas del SAT. gigstack valida contra el EFOS automáticamente. No es posible emitir CFDI a estos RFC. Soluciones: notifica al cliente que su RFC está bloqueado por el SAT, o usa RFC genérico de público en general (XAXX010101000) si el monto es menor a $2,000 MXN.
Verifica que estés usando express.raw() antes de cualquier middleware express.json() global para esa ruta. Si el body se parsea como JSON antes de la verificación de firma, Stripe lanza WebhookSignatureVerificationError. Segundo, confirma que el STRIPE_WEBHOOK_SECRET corresponda al endpoint específico registrado en el dashboard de Stripe (cada endpoint tiene su propio secret).
El servidor responde 200 a Stripe (como en el código de este tutorial) y loggea el error. Stripe no reintentará si recibe 200. Implementa un worker que revise cada hora los payment_intent de Stripe que no tengan invoice_id en su metadata, y procese los pendientes. Monitorea el estado de la API en status.gigstack.io.
Agrega un campo de metadata en Stripe al crear el PaymentIntent: facturacion_automatica: 'false'. En tu webhook, revisa ese valor antes de llamar a gigstack. Los clientes que prefieran autofacturar por su cuenta pueden indicarlo en el checkout; los demás reciben la factura automáticamente.
