Devs
December 12, 2025

Webhooks Event-Driven: Tutorial Paso a Paso con Código Real | gigstack API 2025

TL;DR

  • Qué vas a aprender: Implementar webhooks event-driven para automatizar facturación en tiempo real cuando ocurren eventos en tu plataforma (pagos, suscripciones, reembolsos)
  • Tiempo estimado: 30-40 minutos
  • Stack técnico: Node.js + Express + gigstack API + webhooks bidireccionales

El problema técnico: sincronización asíncrona y consistencia de datos

La mayoría de developers empiezan integrando facturación con polling o consultas programadas:

// ❌ Mal approach: polling cada 5 minutos
setInterval(async () => {
  const payments = await getNewPayments();
  for (const payment of payments) {
    await createInvoice(payment);
  }
}, 300000);

Problemas con este approach:

  • Latencia de hasta 5 minutos entre pago y factura
  • Consume recursos innecesariamente
  • Difícil manejar retries y errores
  • No escala bien con volumen

Antes intenté conectar una API de facturación tradicional. Tenías que implementar toda la lógica de sincronización, retry, idempotencia, y además aprender toda la parte fiscal. No eres contador, es frustrante como dev. gigstack te quita esa carga fiscal y te da webhooks nativos para event-driven architecture.

¿Qué hace gigstack diferente?

gigstack usa webhooks bidireccionales:

  1. Tú envías webhooks a gigstack: Cuando ocurre un evento en tu plataforma (pago exitoso), notificas a gigstack
  2. gigstack te envía webhooks: Cuando se completa la factura (timbrado exitoso, error, cancelación), gigstack te notifica

Esto crea un flujo completamente asíncrono y resiliente:

Tu app → [pago exitoso] → gigstack → [factura timbrada] → Tu app

Pre-requisitos

  • Cuenta en gigstack (Regístrate aquí)
  • API key (obtenerla en app.gigstack.pro/settings?subtab=api)
  • Node.js 16+ y npm instalados
  • Conocimientos: JavaScript básico, REST APIs, Express.js
  • Endpoint público HTTPS para recibir webhooks (puedes usar ngrok para testing local)

Tutorial paso a paso con código

Paso 1: Setup inicial del proyecto

Instala las dependencias necesarias:

mkdir gigstack-webhooks-demo
cd gigstack-webhooks-demo
npm init -y
npm install express axios dotenv crypto

Crea tu archivo .env:

GIGSTACK_API_KEY=tu_api_key_aqui
WEBHOOK_SECRET=tu_secret_para_verificar_webhooks
PORT=3000

Estructura básica del servidor (index.js):

require('dotenv').config();
const express = require('express');
const crypto = require('crypto');
const axios = require('axios');

const app = express();
app.use(express.json());

const GIGSTACK_API_URL = 'https://api.gigstack.pro/v1';
const GIGSTACK_API_KEY = process.env.GIGSTACK_API_KEY;

// Configuración axios para gigstack
const gigstackClient = axios.create({
  baseURL: GIGSTACK_API_URL,
  headers: {
    'Authorization': `Bearer ${GIGSTACK_API_KEY}`,
    'Content-Type': 'application/json'
  }
});

app.listen(process.env.PORT, () => {
  console.log(`Servidor corriendo en puerto ${process.env.PORT}`);
});

Paso 2: Implementar endpoint para recibir webhooks de gigstack

Cuando gigstack completa una factura, te envía un webhook. Necesitas un endpoint para recibirlo:

// Middleware para verificar firma del webhook
function verifyWebhookSignature(req, res, next) {
  const signature = req.headers['x-gigstack-signature'];
  const timestamp = req.headers['x-gigstack-timestamp'];
  
  if (!signature || !timestamp) {
    return res.status(401).json({ error: 'Missing signature headers' });
  }
  
  // Verificar que el webhook no sea muy antiguo (prevenir replay attacks)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - timestamp) > 300) { // 5 minutos tolerancia
    return res.status(401).json({ error: 'Webhook timestamp too old' });
  }
  
  // Crear firma esperada
  const payload = JSON.stringify(req.body);
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(`${timestamp}.${payload}`)
    .digest('hex');
  
  // Comparación segura para prevenir timing attacks
  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  next();
}

// Endpoint para recibir webhooks de gigstack
app.post('/webhooks/gigstack', verifyWebhookSignature, async (req, res) => {
  const { event_type, data } = req.body;
  
  console.log(`📨 Webhook recibido: ${event_type}`);
  
  try {
    switch (event_type) {
      case 'invoice.issued':
        await handleInvoiceIssued(data);
        break;
      
      case 'invoice.failed':
        await handleInvoiceFailed(data);
        break;
      
      case 'invoice.cancelled':
        await handleInvoiceCancelled(data);
        break;
      
      default:
        console.log(`⚠️ Evento no manejado: ${event_type}`);
    }
    
    // Siempre responder 200 rápidamente
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Error procesando webhook:', error);
    // Aún así devolver 200 para evitar reintentos innecesarios
    res.status(200).json({ received: true, error: error.message });
  }
});

// Handlers para cada tipo de evento
async function handleInvoiceIssued(invoiceData) {
  console.log(`✅ Factura emitida: ${invoiceData.invoice_id}`);
  console.log(`UUID: ${invoiceData.uuid}`);
  console.log(`PDF: ${invoiceData.pdf_url}`);
  
  // Aquí actualizas tu base de datos
  await updatePaymentInDatabase({
    payment_id: invoiceData.metadata?.payment_id,
    invoice_id: invoiceData.invoice_id,
    invoice_uuid: invoiceData.uuid,
    invoice_pdf: invoiceData.pdf_url,
    invoice_xml: invoiceData.xml_url,
    status: 'invoiced'
  });
  
  // Opcional: notificar al cliente
  await sendInvoiceEmailToCustomer(invoiceData);
}

async function handleInvoiceFailed(invoiceData) {
  console.error(`❌ Error en factura: ${invoiceData.invoice_id}`);
  console.error(`Razón: ${invoiceData.error_message}`);
  
  // Log del error para debugging
  await logInvoiceError({
    payment_id: invoiceData.metadata?.payment_id,
    error: invoiceData.error_message,
    error_code: invoiceData.error_code
  });
  
  // Notificar al equipo interno
  await notifyInvoiceError(invoiceData);
}

async function handleInvoiceCancelled(invoiceData) {
  console.log(`🚫 Factura cancelada: ${invoiceData.invoice_id}`);
  
  await updatePaymentInDatabase({
    payment_id: invoiceData.metadata?.payment_id,
    status: 'invoice_cancelled'
  });
}

Paso 3: Enviar eventos a gigstack desde tu app

Cuando ocurre un pago en tu plataforma, envías un evento a gigstack para que genere la factura:

// Endpoint que maneja pagos exitosos en tu app
app.post('/payments/webhook', async (req, res) => {
  const payment = req.body; // Webhook de Stripe, Conekta, etc.
  
  if (payment.status === 'succeeded') {
    try {
      // Crear factura en gigstack
      const invoice = await createInvoiceInGigstack(payment);
      
      console.log(`📄 Factura creada: ${invoice.data.invoice_id}`);
      
      // Guardar referencia en tu DB
      await saveInvoiceReference(payment.id, invoice.data.invoice_id);
      
      res.status(200).json({ success: true });
      
    } catch (error) {
      console.error('Error creando factura:', error.response?.data || error.message);
      
      // Log error pero no fallar el pago
      await logError('invoice_creation_failed', {
        payment_id: payment.id,
        error: error.message
      });
      
      res.status(200).json({ success: true, invoice_error: true });
    }
  }
});

async function createInvoiceInGigstack(payment) {
  return await gigstackClient.post('/invoices', {
    // Información del cliente
    customer: {
      email: payment.customer.email,
      name: payment.customer.name,
      rfc: payment.customer.rfc || 'XAXX010101000', // RFC genérico si no lo tienes
      tax_system: payment.customer.tax_system || '616' // Régimen por defecto
    },
    
    // Items de la factura
    items: payment.line_items.map(item => ({
      description: item.description,
      quantity: item.quantity || 1,
      unit_price: item.amount / 100, // Convertir de centavos a pesos
      // gigstack calcula automáticamente impuestos
    })),
    
    // Metadata para tracking
    metadata: {
      payment_id: payment.id,
      payment_method: payment.payment_method,
      source: 'webhook_automation'
    },
    
    // URL del webhook donde gigstack te notificará
    webhook_url: 'https://tu-dominio.com/webhooks/gigstack'
  });
}

Paso 4: Testing con ngrok

Para probar webhooks localmente, usa ngrok:

# Instalar ngrok
npm install -g ngrok

# Exponer tu servidor local
ngrok http 3000

Ngrok te dará una URL pública como https://abc123.ngrok.io. Úsala como webhook_url en tus requests a gigstack.

Prueba el flujo completo:

# Simular un pago exitoso
curl -X POST http://localhost:3000/payments/webhook \
  -H "Content-Type: application/json" \
  -d '{
    "id": "pay_test_123",
    "status": "succeeded",
    "customer": {
      "email": "test@example.com",
      "name": "Juan Pérez",
      "rfc": "XAXX010101000"
    },
    "line_items": [{
      "description": "Suscripción Premium",
      "amount": 49900
    }]
  }'

Deberías ver:

  1. Log en tu servidor: "📄 Factura creada: inv_xxx"
  2. Después de ~5-10 segundos: "📨 Webhook recibido: invoice.issued"
  3. Log final: "✅ Factura emitida: inv_xxx"

Errores comunes y soluciones

❌ Error 1: "Invalid webhook signature"

Causa: La firma del webhook no coincide o el timestamp es muy antiguo.

Solución:

// Verificar que estás usando el secret correcto
console.log('Webhook secret:', process.env.WEBHOOK_SECRET);

// Verificar formato del payload
console.log('Payload recibido:', JSON.stringify(req.body));

// Verificar timestamp
const timestamp = req.headers['x-gigstack-timestamp'];
const age = Math.floor(Date.now() / 1000) - timestamp;
console.log(`Webhook age: ${age} seconds`);

❌ Error 2: Webhooks duplicados procesándose múltiples veces

Causa: gigstack reintenta webhooks si no recibe respuesta 200.

Solución: Implementar idempotencia:

const processedWebhooks = new Set(); // En producción usar Redis/DB

app.post('/webhooks/gigstack', verifyWebhookSignature, async (req, res) => {
  const webhookId = req.headers['x-gigstack-webhook-id'];
  
  // Verificar si ya procesamos este webhook
  if (processedWebhooks.has(webhookId)) {
    console.log('⏭️ Webhook duplicado, ignorando');
    return res.status(200).json({ received: true, duplicate: true });
  }
  
  processedWebhooks.add(webhookId);
  
  // Procesar webhook...
  // Responder rápido
  res.status(200).json({ received: true });
});

❌ Error 3: "RFC inválido" al crear factura

Causa: RFC no cumple formato del SAT o no existe en padrón.

Solución: Validar RFC antes de enviar:

function validateRFC(rfc) {
  // Persona física: 13 caracteres
  // Persona moral: 12 caracteres
  // RFC genérico: XAXX010101000
  const rfcRegex = /^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/;
  
  if (!rfcRegex.test(rfc)) {
    return 'XAXX010101000'; // RFC genérico público
  }
  
  return rfc.toUpperCase();
}

// Usar al crear factura
customer: {
  rfc: validateRFC(payment.customer.rfc)
}

❌ Error 4: Webhook timeout (gigstack no recibe respuesta)

Causa: Procesamiento muy lento en el webhook handler.

Solución: Procesar asíncronamente:

app.post('/webhooks/gigstack', verifyWebhookSignature, async (req, res) => {
  const { event_type, data } = req.body;
  
  // ✅ Responder inmediatamente
  res.status(200).json({ received: true });
  
  // Procesar después (no bloquear respuesta)
  setImmediate(async () => {
    try {
      await processWebhookEvent(event_type, data);
    } catch (error) {
      console.error('Error procesando webhook:', error);
      // Loggear para retry manual si es necesario
    }
  });
});

Best practices

1. Seguridad

  • Siempre verificar firmas: No confíes en webhooks sin verificar la firma HMAC
  • HTTPS obligatorio: gigstack solo envía webhooks a URLs HTTPS en producción
  • Rotate secrets: Cambia tu webhook secret periódicamente

2. Performance

  • Responder rápido: Webhook handlers deben responder 200 en <5 segundos
  • Procesamiento async: Usa queues (Bull, BeeQueue) para procesar eventos pesados
  • Batch updates: Agrupa actualizaciones a DB en lugar de hacer N queries individuales

3. Reliability

  • Idempotencia: Usar IDs únicos para prevenir procesamiento duplicado
  • Retry logic: gigstack reintenta webhooks fallidos (429, 5xx), pero debes manejar edge cases
  • Dead letter queue: Guardar webhooks que fallan después de N reintentos para revisión manual

4. Monitoring

// Logging estructurado para debugging
const logger = {
  webhookReceived: (eventType, webhookId) => {
    console.log(JSON.stringify({
      event: 'webhook_received',
      type: eventType,
      webhook_id: webhookId,
      timestamp: new Date().toISOString()
    }));
  },
  
  webhookProcessed: (eventType, duration) => {
    console.log(JSON.stringify({
      event: 'webhook_processed',
      type: eventType,
      duration_ms: duration,
      timestamp: new Date().toISOString()
    }));
  }
};

5. Testing

  • Usa sandbox de gigstack para testing sin consumir folios reales
  • Implementa test mode flag en webhooks
  • Simula reintentos y fallos en tus tests

Próximos pasos

Una vez que dominas webhooks básicos con gigstack, explora:

  • Complementos de pago: Facturar en parcialidades con complementos automáticos
  • Webhooks bidireccionales avanzados: Sincronización de estado en tiempo real
  • Multi-tenant con gigstack Connect: Facturación para marketplaces
  • Retry policies personalizadas: Configurar comportamiento de reintentos
  • Event sourcing: Usar webhooks como source of truth para event store

Recursos

Prueba webhooks en sandbox ahora

Crea tu cuenta gratuita en gigstack.pro y obtén acceso inmediato a:

  • Ambiente de pruebas completo (sandbox)
  • Webhooks de testing con simulator
  • Logs de webhooks en tiempo real en el dashboard
  • Ejemplos de código en Node.js, Python, PHP
  • Documentación interactiva de API

Event-driven facturación en 30 minutos. Sin overhead fiscal. Con webhooks nativos que simplemente funcionan.

Porque la arquitectura asíncrona no debería ser la parte difícil de tu integración.

Blogs que te pueden gustar