> ## Documentation Index
> Fetch the complete documentation index at: https://docs-mx.taxo.ws/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when your extractions are ready

## 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.

<CardGroup cols={2}>
  <Card title="Real-time" icon="bolt">
    Receive instant notifications when documents are ready
  </Card>

  <Card title="Efficiency" icon="gauge-high">
    Eliminates the need for constant API polling
  </Card>

  <Card title="Reliability" icon="shield-check">
    Automatic retry system with exponential backoff
  </Card>

  <Card title="Security" icon="lock">
    HMAC-SHA256 signature verification to authenticate events
  </Card>
</CardGroup>

## Webhook configuration

<Steps>
  <Step title="Access dashboard">
    Go to [Taxo Dashboard](https://dashboard.taxo.co) → **Settings** → **Webhooks**
  </Step>

  <Step title="Create endpoint">
    Click **"Add Webhook"** and enter your endpoint URL
  </Step>

  <Step title="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
  </Step>

  <Step title="Configure security">
    Optionally, configure a secret to verify HMAC signatures
  </Step>

  <Step title="Test connection">
    Use the **"Test Webhook"** button to verify that your endpoint responds correctly
  </Step>
</Steps>

## Event types

### extraction.completed

Sent when an extraction completes successfully.

```json theme={null}
{
  "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.

```json theme={null}
{
  "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).

```json theme={null}
{
  "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

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  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');
  });
  ```

  ```python Python (FastAPI) theme={null}
  from fastapi import FastAPI, Request, HTTPException, Header
  import hmac
  import hashlib
  import json
  import time
  from typing import Optional

  app = FastAPI()

  WEBHOOK_SECRET = os.environ.get('TAXO_WEBHOOK_SECRET')

  @app.post("/webhooks/taxo")
  async def webhook_handler(
      request: Request,
      x_taxo_signature: Optional[str] = Header(None),
      x_taxo_timestamp: Optional[str] = Header(None)
  ):
      # Obtener body raw
      body = await request.body()
      
      # Verificar timestamp (no más de 5 minutos de antigüedad)
      if x_taxo_timestamp:
          webhook_timestamp = int(x_taxo_timestamp)
          current_time = int(time.time())
          if current_time - webhook_timestamp > 300:
              raise HTTPException(status_code=400, detail="Webhook too old")
      
      # Verificar firma HMAC
      if WEBHOOK_SECRET and x_taxo_signature:
          expected_signature = hmac.new(
              WEBHOOK_SECRET.encode(),
              f"{x_taxo_timestamp}.{body.decode()}".encode(),
              hashlib.sha256
          ).hexdigest()
          
          if x_taxo_signature != f"sha256={expected_signature}":
              raise HTTPException(status_code=401, detail="Invalid signature")
      
      # Parsear evento
      try:
          event = json.loads(body.decode())
      except json.JSONDecodeError:
          raise HTTPException(status_code=400, detail="Invalid JSON")
      
      # Procesar evento
      try:
          await handle_webhook_event(event)
          return {"received": True}
      except Exception as e:
          print(f"Error procesando webhook: {e}")
          raise HTTPException(status_code=500, detail="Processing failed")

  async def handle_webhook_event(event: dict):
      event_type = event["event"]["type"]
      data = event["data"]
      
      print(f"Evento recibido: {event_type}")
      
      if event_type == "extraction.completed":
          await handle_extraction_completed(data)
      elif event_type == "extraction.failed":
          await handle_extraction_failed(data)
      elif event_type == "document.ready":
          await handle_document_ready(data)
      else:
          print(f"Tipo de evento no manejado: {event_type}")

  async def handle_extraction_completed(data: dict):
      extraction_id = data["extractionId"]
      summary = data["summary"]
      
      print(f"✅ Extracción {extraction_id} completada:")
      print(f"   - {summary['successfulDownloads']} documentos descargados")
      print(f"   - {summary['failedDownloads']} fallos")
      
      # Implementar lógica de negocio aquí
      # - Actualizar base de datos
      # - Enviar notificaciones
      # - Iniciar procesamientos adicionales

  async def handle_extraction_failed(data: dict):
      extraction_id = data["extractionId"]
      error = data["error"]
      
      print(f"❌ Extracción {extraction_id} falló:")
      print(f"   Error: {error['message']}")
      
      # Implementar manejo de errores
      # - Logging detallado
      # - Alertas al equipo
      # - Lógica de retry si aplica

  async def handle_document_ready(data: dict):
      document = data["document"]
      print(f"📄 Documento listo: {document['uuid']}")
      
      # Procesar documento individual
      # - Descarga automática
      # - Extracción de datos
      # - Integración con otros sistemas

  if __name__ == "__main__":
      import uvicorn
      uvicorn.run(app, host="0.0.0.0", port=8000)
  ```

  ```java Java (Spring Boot) theme={null}
  @RestController
  @RequestMapping("/webhooks")
  public class TaxoWebhookController {
      
      @Value("${taxo.webhook.secret:}")
      private String webhookSecret;
      
      @PostMapping(value = "/taxo", consumes = "application/json")
      public ResponseEntity<?> handleWebhook(
              @RequestBody String payload,
              @RequestHeader(value = "X-Taxo-Signature", required = false) String signature,
              @RequestHeader(value = "X-Taxo-Timestamp", required = false) String timestamp) {
          
          try {
              // Verificar timestamp
              if (timestamp != null) {
                  long webhookTimestamp = Long.parseLong(timestamp);
                  long currentTime = System.currentTimeMillis() / 1000;
                  if (currentTime - webhookTimestamp > 300) {
                      return ResponseEntity.badRequest().body("Webhook too old");
                  }
              }
              
              // Verificar firma HMAC
              if (!webhookSecret.isEmpty() && signature != null) {
                  String expectedSignature = calculateHmacSha256(timestamp + "." + payload, webhookSecret);
                  if (!signature.equals("sha256=" + expectedSignature)) {
                      return ResponseEntity.status(401).body("Invalid signature");
                  }
              }
              
              // Parsear evento
              ObjectMapper mapper = new ObjectMapper();
              JsonNode event = mapper.readTree(payload);
              
              // Procesar evento
              handleWebhookEvent(event);
              
              return ResponseEntity.ok(Map.of("received", true));
              
          } catch (Exception e) {
              logger.error("Error procesando webhook", e);
              return ResponseEntity.status(500).body("Processing failed");
          }
      }
      
      private void handleWebhookEvent(JsonNode event) {
          String eventType = event.get("event").get("type").asText();
          JsonNode data = event.get("data");
          
          logger.info("Evento recibido: {}", eventType);
          
          switch (eventType) {
              case "extraction.completed":
                  handleExtractionCompleted(data);
                  break;
              case "extraction.failed":
                  handleExtractionFailed(data);
                  break;
              case "document.ready":
                  handleDocumentReady(data);
                  break;
              default:
                  logger.warn("Tipo de evento no manejado: {}", eventType);
          }
      }
      
      private void handleExtractionCompleted(JsonNode data) {
          String extractionId = data.get("extractionId").asText();
          JsonNode summary = data.get("summary");
          
          logger.info("✅ Extracción {} completada:", extractionId);
          logger.info("   - {} documentos descargados", 
                     summary.get("successfulDownloads").asInt());
          logger.info("   - {} fallos", summary.get("failedDownloads").asInt());
          
          // Implementar lógica de negocio
      }
      
      private void handleExtractionFailed(JsonNode data) {
          String extractionId = data.get("extractionId").asText();
          String errorMessage = data.get("error").get("message").asText();
          
          logger.error("❌ Extracción {} falló: {}", extractionId, errorMessage);
          
          // Implementar manejo de errores
      }
      
      private void handleDocumentReady(JsonNode data) {
          String documentUuid = data.get("document").get("uuid").asText();
          logger.info("📄 Documento listo: {}", documentUuid);
          
          // Procesar documento individual
      }
      
      private String calculateHmacSha256(String data, String secret) {
          try {
              Mac mac = Mac.getInstance("HmacSHA256");
              SecretKeySpec secretKeySpec = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
              mac.init(secretKeySpec);
              byte[] hash = mac.doFinal(data.getBytes());
              return bytesToHex(hash);
          } catch (Exception e) {
              throw new RuntimeException("Error calculating HMAC", e);
          }
      }
      
      private String bytesToHex(byte[] bytes) {
          StringBuilder result = new StringBuilder();
          for (byte b : bytes) {
              result.append(String.format("%02x", b));
          }
          return result.toString();
      }
  }
  ```
</CodeGroup>

## Signature verification

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

<Steps>
  <Step title="Get data">
    * `timestamp`: Header `X-Taxo-Timestamp`
    * `signature`: Header `X-Taxo-Signature`
    * `payload`: Body raw del webhook
  </Step>

  <Step title="Build string to sign">
    Concatenate: `timestamp + "." + payload`
  </Step>

  <Step title="Calculate HMAC">
    Use your webhook secret to calculate HMAC-SHA256 of the previous string
  </Step>

  <Step title="Compare signatures">
    The received signature should equal `sha256={your_calculated_hmac}`
  </Step>
</Steps>

## Best practices

<AccordionGroup>
  <Accordion title="Idempotencia" icon="arrows-rotate">
    Los webhooks pueden enviarse múltiples veces. Usa el campo `event.timestamp` como clave de idempotencia para evitar procesamiento duplicado.

    ```javascript theme={null}
    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);
    }
    ```
  </Accordion>

  <Accordion title="Timeouts y reintentos" icon="clock-rotate-left">
    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
  </Accordion>

  <Accordion title="Logging y monitoreo" icon="chart-line">
    Implementa logging detallado para debugging:

    ```javascript theme={null}
    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;
      }
    }
    ```
  </Accordion>

  <Accordion title="Seguridad" icon="shield-check">
    * **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
  </Accordion>
</AccordionGroup>

## Testing and debugging

### Test webhooks locally

To test webhooks in local development, use tools like ngrok:

```bash theme={null}
# 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:

```bash theme={null}
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

<AccordionGroup>
  <Accordion title="Webhook no se recibe" icon="exclamation-triangle">
    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
  </Accordion>

  <Accordion title="Error de firma inválida" icon="key">
    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
  </Accordion>

  <Accordion title="Webhooks duplicados" icon="copy">
    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
  </Accordion>
</AccordionGroup>
