Skip to main content

What are webhooks?

Webhooks are HTTP notifications that Taxo sends to your application when important events occur, such as the completion of an extraction. Instead of constant polling, your application receives updates automatically.

Real-time

Receive instant notifications when documents are ready

Efficiency

Eliminates the need for constant API polling

Reliability

Automatic retry system with exponential backoff

Security

HMAC-SHA256 signature verification to authenticate events

Webhook configuration

1

Access dashboard

Go to Taxo DashboardSettingsWebhooks
2

Create endpoint

Click “Add Webhook” and enter your endpoint URL
3

Select events

Choose the types of events you want to receive:
  • extraction.completed - Extraction completed successfully
  • extraction.failed - Extraction failed
  • document.ready - Individual document ready for download
4

Configure security

Optionally, configure a secret to verify HMAC signatures
5

Test connection

Use the “Test Webhook” button to verify that your endpoint responds correctly

Event types

extraction.completed

Sent when an extraction completes successfully.
{
  "event": {
    "type": "extraction.completed",
    "timestamp": "2025-01-04T13:15:42.123Z",
    "version": "1.0"
  },
  "data": {
    "extractionId": "JOB20250104123456789A",
    "status": "COMPLETED",
    "completedAt": "2025-01-04T13:15:42.123Z",
    "summary": {
      "totalDocuments": 1500,
      "successfulDownloads": 1498,
      "failedDownloads": 2,
      "totalSizeBytes": 15728640
    },
    "subject": {
      "identification": "ABC010101ABC",
      "fullName": "Empresa ABC S.A. de C.V.",
      "personType": "MORAL"
    },
    "options": {
      "informationType": "INVOICE",
      "period": {
        "from": "2024-01-01",
        "to": "2024-12-31"
      },
      "direction": "RECEIVED"
    }
  }
}

extraction.failed

Sent when an extraction fails due to an unrecoverable error.
{
  "event": {
    "type": "extraction.failed",
    "timestamp": "2025-01-04T12:38:12.456Z",
    "version": "1.0"
  },
  "data": {
    "extractionId": "JOB20250104123456789A",
    "status": "FAILED",
    "failedAt": "2025-01-04T12:38:12.456Z",
    "error": {
      "code": "INVALID_CREDENTIALS",
      "message": "Las credenciales CIEC proporcionadas son incorrectas",
      "details": {
        "satResponse": "Usuario o contraseña incorrectos"
      }
    },
    "subject": {
      "identification": "ABC010101ABC"
    },
    "options": {
      "informationType": "INVOICE",
      "period": {
        "from": "2024-01-01",
        "to": "2024-12-31"
      }
    }
  }
}

document.ready

Sent for each document that is processed successfully (optional, may generate many notifications).
{
  "event": {
    "type": "document.ready",
    "timestamp": "2025-01-04T13:10:15.789Z",
    "version": "1.0"
  },
  "data": {
    "extractionId": "JOB20250104123456789A",
    "document": {
      "id": "doc_550e8400-e29b-41d4-a716-446655440000",
      "uuid": "12345678-1234-1234-1234-123456789012",
      "type": "INVOICE",
      "issueDate": "2024-03-15T10:30:00Z",
      "amount": 1160.00,
      "currency": "MXN",
      "emitter": {
        "rfc": "XYZ020202XYZ",
        "name": "Proveedor XYZ S.A. de C.V."
      },
      "receiver": {
        "rfc": "ABC010101ABC",
        "name": "Empresa ABC S.A. de C.V."
      },
      "availableFormats": ["XML", "PDF"]
    }
  }
}

Webhook endpoint implementation

const express = require('express');
const crypto = require('crypto');
const app = express();

// Middleware para capturar el body raw (necesario para verificar firma)
app.use('/webhooks/taxo', express.raw({ type: 'application/json' }));

app.post('/webhooks/taxo', (req, res) => {
  const signature = req.headers['x-taxo-signature'];
  const timestamp = req.headers['x-taxo-timestamp'];
  const payload = req.body;
  
  // Verificar que el webhook no sea muy antiguo (5 minutos)
  const webhookTimestamp = parseInt(timestamp);
  const currentTime = Math.floor(Date.now() / 1000);
  if (currentTime - webhookTimestamp > 300) {
    return res.status(400).json({ error: 'Webhook too old' });
  }
  
  // Verificar firma HMAC (si tienes secreto configurado)
  const webhookSecret = process.env.TAXO_WEBHOOK_SECRET;
  if (webhookSecret && signature) {
    const expectedSignature = crypto
      .createHmac('sha256', webhookSecret)
      .update(timestamp + '.' + payload)
      .digest('hex');
    
    if (signature !== `sha256=${expectedSignature}`) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
  }
  
  // Parsear el payload
  const event = JSON.parse(payload.toString());
  
  // Procesar evento
  try {
    handleWebhookEvent(event);
    
    // Responder con 200 para confirmar recepción
    res.status(200).json({ received: true });
    
  } catch (error) {
    console.error('Error procesando webhook:', error);
    res.status(500).json({ error: 'Processing failed' });
  }
});

function handleWebhookEvent(event) {
  console.log(`Event received: ${event.event.type}`);
  
  switch (event.event.type) {
    case 'extraction.completed':
      handleExtractionCompleted(event.data);
      break;
      
    case 'extraction.failed':
      handleExtractionFailed(event.data);
      break;
      
    case 'document.ready':
      handleDocumentReady(event.data);
      break;
      
    default:
      console.log(`Unhandled event type: ${event.event.type}`);
  }
}

function handleExtractionCompleted(data) {
  console.log(`✅ Extraction ${data.extractionId} completed:`);
  console.log(`   - ${data.summary.successfulDownloads} documents downloaded`);
  console.log(`   - ${data.summary.failedDownloads} failures`);
  
  // Here you can:
  // - Update status in your database
  // - Send email notification
  // - Start automatic document download
  // - Process documents with your business logic
}

function handleExtractionFailed(data) {
  console.error(`❌ Extraction ${data.extractionId} failed:`);
  console.error(`   Error: ${data.error.message}`);
  
  // Here you can:
  // - Mark extraction as failed in your DB
  // - Send alert to support team
  // - Attempt reprocessing if error is recoverable
}

function handleDocumentReady(data) {
  console.log(`📄 Document ready: ${data.document.uuid}`);
  
  // Process individual document
  // - Download automatically
  // - Extract specific data
  // - Notify downstream systems
}

app.listen(3000, () => {
  console.log('Webhook server running on port 3000');
});

Signature verification

Webhooks include an HMAC-SHA256 signature to verify their authenticity:
1

Get data

  • timestamp: Header X-Taxo-Timestamp
  • signature: Header X-Taxo-Signature
  • payload: Body raw del webhook
2

Build string to sign

Concatenate: timestamp + "." + payload
3

Calculate HMAC

Use your webhook secret to calculate HMAC-SHA256 of the previous string
4

Compare signatures

The received signature should equal sha256={your_calculated_hmac}

Best practices

Los webhooks pueden enviarse múltiples veces. Usa el campo event.timestamp como clave de idempotencia para evitar procesamiento duplicado.
const processedEvents = new Set();

function handleWebhookEvent(event) {
  const eventKey = `${event.event.type}_${event.event.timestamp}`;
  
  if (processedEvents.has(eventKey)) {
    console.log('Evento ya procesado, ignorando');
    return;
  }
  
  // Procesar evento...
  processedEvents.add(eventKey);
}
Configura timeouts apropiados en tu endpoint:
  • Timeout de respuesta: 10 segundos máximo
  • Procesamiento asíncrono: Para tareas largas, responde 200 inmediatamente y procesa en background
  • Reintentos automáticos: Taxo reintentará hasta 5 veces con backoff exponencial
Implementa logging detallado para debugging:
function handleWebhookEvent(event) {
  const correlationId = crypto.randomUUID();
  
  console.log(`[${correlationId}] Webhook recibido:`, {
    type: event.event.type,
    timestamp: event.event.timestamp,
    extractionId: event.data.extractionId
  });
  
  try {
    // Procesar evento...
    console.log(`[${correlationId}] Evento procesado exitosamente`);
  } catch (error) {
    console.error(`[${correlationId}] Error procesando webhook:`, error);
    throw error;
  }
}
  • HTTPS obligatorio: Los webhooks solo se envían a URLs HTTPS
  • Verificar firma: Siempre valida la firma HMAC en producción
  • Validar timestamp: Rechaza webhooks muy antiguos (más de 5 minutos)
  • Rate limiting: Implementa protección contra ataques de denegación de servicio

Testing and debugging

Test webhooks locally

To test webhooks in local development, use tools like ngrok:
# Install ngrok
npm install -g ngrok

# Expose local port
ngrok http 3000

# Use the public URL in webhook configuration
# Ejemplo: https://abc123.ngrok.io/webhooks/taxo

Test webhook

You can test your endpoint with this example payload:
curl -X POST "https://tu-dominio.com/webhooks/taxo" \
  -H "Content-Type: application/json" \
  -H "X-Taxo-Signature: sha256=test" \
  -H "X-Taxo-Timestamp: $(date +%s)" \
  -d '{
    "event": {
      "type": "extraction.completed",
      "timestamp": "2025-01-04T13:15:42.123Z",
      "version": "1.0"
    },
    "data": {
      "extractionId": "TEST123456789",
      "status": "COMPLETED",
      "summary": {
        "totalDocuments": 100,
        "successfulDownloads": 98,
        "failedDownloads": 2
      }
    }
  }'

Troubleshooting

  1. Verifica que la URL esté configurada correctamente
  2. Asegúrate de que tu endpoint responda con status 2xx
  3. Revisa que no haya firewalls bloqueando las IPs de Taxo
  4. Confirma que el certificado SSL sea válido
  1. Verifica que el secreto webhook esté configurado correctamente
  2. Asegúrate de usar el body raw (no parseado) para calcular la firma
  3. Confirma que estás concatenando timestamp + ”.” + payload
  4. Verifica que el algoritmo sea HMAC-SHA256
  1. Implementa idempotencia usando el timestamp del evento
  2. Almacena IDs de eventos procesados en cache/base de datos
  3. Responde siempre con 200 even si el evento ya fue procesado