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

# Seguridad de Webhooks

> Asegura tus endpoints de webhook con verificación de firmas y mejores prácticas — con eventos webhook de gu1 para integración downstream en tiempo real.

## Descripción General

Asegurar tus endpoints de webhook es crítico para garantizar que las solicitudes de webhook provienen de Gu1 y no de actores maliciosos. Esta guía cubre cómo verificar firmas de webhook, implementar mejores prácticas de seguridad y evitar errores comunes de seguridad.

## Verificación de Firmas

Gu1 firma todas las solicitudes de webhook con una firma HMAC SHA-256 usando tu secreto de webhook. La firma se envía en el encabezado `X-Webhook-Signature`, permitiéndote verificar que la solicitud es auténtica.

### Cómo Funciona la Verificación de Firmas

1. **Gu1 genera una firma**: Al enviar un webhook, Gu1 crea un hash HMAC SHA-256 del cuerpo de la solicitud sin procesar usando tu secreto de webhook
2. **La firma se envía en el encabezado**: La firma se incluye en el encabezado `X-Webhook-Signature`
3. **Tu servidor recalcula**: Tu endpoint recalcula la firma usando el mismo secreto y cuerpo sin procesar
4. **Comparar firmas**: Si las firmas coinciden, el webhook es auténtico

<Warning>
  Siempre verifica las firmas de webhook en producción. Sin verificación, cualquiera puede enviar webhooks falsos a tu endpoint y potencialmente comprometer tu sistema.
</Warning>

## Ejemplos de Verificación de Firmas

### Node.js (Express)

```javascript Node.js theme={null}
const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANTE: Almacenar cuerpo sin procesar para verificación de firmas
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/gu1', async (req, res) => {
  try {
    // Obtener firma del encabezado
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    // Verificar firma
    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Procesar webhook
    const { event, timestamp, payload } = req.body;
    await handleWebhook(event, payload);

    // Devolver 200 para confirmar recepción
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

/**
 * Verificar firma HMAC SHA-256
 */
function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);
```

<Note>
  **Crítico**: Debes verificar la firma usando el **cuerpo de solicitud sin procesar** antes de que se analice como JSON. Si verificas contra el cuerpo JSON analizado (ej., `JSON.stringify(req.body)`), la firma no coincidirá porque el formato JSON puede diferir.
</Note>

### Python (Flask)

```python Python theme={null}
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import logging

app = Flask(__name__)

@app.route('/webhooks/gu1', methods=['POST'])
def gu1_webhook():
    try:
        # Obtener firma del encabezado
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Obtener cuerpo sin procesar para verificación de firmas
        raw_body = request.get_data(as_text=True)

        # Verificar firma
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Analizar payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received webhook: {event}')

        # Procesar el webhook
        handle_webhook(event, data)

        return jsonify({
            'success': True,
            'message': 'Webhook received'
        }), 200

    except Exception as e:
        logging.error(f'Webhook error: {e}')
        return jsonify({
            'success': False,
            'error': str(e)
        }), 500

def verify_signature(raw_body: str, signature: str, secret: str) -> bool:
    """Verificar firma HMAC SHA-256"""
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        raw_body.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, expected_signature)

def handle_webhook(event: str, data: dict):
    # Procesar webhook basado en tipo de evento
    pass

if __name__ == '__main__':
    app.run(port=3000)
```

<Tip>
  Usa `hmac.compare_digest()` en lugar de `==` para comparar firmas en Python. Esta función realiza una comparación segura contra ataques de tiempo que previene ataques de temporización.
</Tip>

### Go (Gin)

```go Go theme={null}
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

type WebhookPayload struct {
    Event     string                 `json:"event"`
    Timestamp string                 `json:"timestamp"`
    Payload   map[string]interface{} `json:"payload"`
}

func main() {
    r := gin.Default()
    r.POST("/webhooks/gu1", handleWebhook)
    r.Run(":3000")
}

func handleWebhook(c *gin.Context) {
    // Leer cuerpo sin procesar para verificación de firmas
    rawBody, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to read body"})
        return
    }

    // Verificar firma
    signature := c.GetHeader("X-Webhook-Signature")
    webhookSecret := os.Getenv("GU1_WEBHOOK_SECRET")

    if !verifySignature(rawBody, signature, webhookSecret) {
        log.Println("Invalid webhook signature")
        c.JSON(401, gin.H{"error": "Invalid signature"})
        return
    }

    // Analizar payload
    var payload WebhookPayload
    if err := json.Unmarshal(rawBody, &payload); err != nil {
        c.JSON(400, gin.H{"error": "Invalid JSON"})
        return
    }

    log.Printf("Received webhook: %s", payload.Event)

    // Procesar webhook
    handleWebhookEvent(payload)

    c.JSON(200, gin.H{
        "success": true,
        "message": "Webhook received",
    })
}

func verifySignature(rawBody []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func handleWebhookEvent(payload WebhookPayload) {
    // Procesar webhook basado en tipo de evento
}
```

## Cuerpo Sin Procesar vs JSON Analizado

Un error común es verificar la firma usando el objeto JSON analizado en lugar del cuerpo de solicitud sin procesar. Esto siempre fallará porque el formato JSON puede diferir.

<CodeGroup>
  ```javascript Incorrecto - Fallará theme={null}
  // INCORRECTO: Verificando cuerpo analizado
  app.post('/webhooks', express.json(), (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // Esto NO funcionará - JSON.stringify puede formatear diferente
    const body = JSON.stringify(req.body);
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(body)
      .digest('hex');

    if (signature === expectedSignature) {
      // Siempre fallará
    }
  });
  ```

  ```javascript Correcto - Funcionará theme={null}
  // CORRECTO: Verificando cuerpo sin procesar
  app.use(express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf.toString('utf8');
    }
  }));

  app.post('/webhooks', (req, res) => {
    const signature = req.headers['x-webhook-signature'];

    // Esto funciona - usando cuerpo sin procesar
    const expectedSignature = crypto
      .createHmac('sha256', secret)
      .update(req.rawBody)
      .digest('hex');

    if (signature === expectedSignature) {
      // Firma verificada exitosamente
    }
  });
  ```
</CodeGroup>

### Qué firma Gu1 (y qué no)

Gu1 calcula `HMAC-SHA256(secret, raw_request_body)` y envía el digest hexadecimal en `X-Webhook-Signature`.

| Entra en la firma                                        | No entra                                                                       |
| -------------------------------------------------------- | ------------------------------------------------------------------------------ |
| Bytes exactos del body del POST HTTP (string JSON UTF-8) | `X-Webhook-Timestamp`, `X-Webhook-ID`, `X-Webhook-Event` ni ningún otro header |
|                                                          | Query params o path de la URL                                                  |
|                                                          | Campos que tu middleware agrega o quita después de leer el body                |

<Note>
  Usá una comparación **timing-safe** al validar la firma (por ejemplo `crypto.timingSafeEqual` en Node.js). Ver la sección de buenas prácticas más abajo.
</Note>

### Historial de webhooks en el dashboard

El **monitor de webhooks** muestra el payload para debugging y soporte. Esa vista **no** es el string byte-a-byte que se firmó al momento del envío. Pretty-print, copiar desde la UI o hacer `JSON.stringify()` sobre un objeto parseado puede generar un string distinto y hacer que un envío correcto parezca inválido.

Para depurar un fallo, compará el header `X-Webhook-Signature` que recibió tu endpoint con el HMAC calculado sobre el **raw body de ese mismo request HTTP**. No re-verifiques solo con el JSON del dashboard.

### Fallos intermitentes de firma

Si la verificación funciona en algunos eventos pero en otros devuelve `401 Invalid signature` **con el mismo secret y endpoint**, las causas habituales son:

1. **JSON parseado y re-serializado** — `JSON.stringify(req.body)` después de `express.json()` (o equivalente) no reproduce de forma confiable los bytes del body de Gu1. Payloads distintos (orden de keys, forma anidada, formato numérico) pueden fallar solo a veces.
2. **Middleware que muta el body** — deduplicar arrays, quitar campos `null`, ordenar keys o normalizar strings antes de verificar cambia el input firmado.
3. **Secret incorrecto** — secret regenerado en el dashboard mientras en tu entorno quedó el valor viejo.
4. **Header de firma ausente** — si el webhook no tiene secret configurado, Gu1 puede omitir `X-Webhook-Signature`; tratar un header faltante como firma inválida es esperable.

<Warning>
  La verificación de firma es **opcional** pero recomendada. Gu1 igual entrega webhooks si no verificás; el `401` lo devuelve **tu** servidor cuando tu lógica de verificación rechaza el request.
</Warning>

## Patrones de Idempotencia

Los webhooks pueden entregarse más de una vez debido a problemas de red, timeouts o reintentos. Implementa idempotencia para asegurar que procesas cada webhook solo una vez.

### Idempotencia Basada en Base de Datos

Almacena IDs de webhook procesados en tu base de datos:

```javascript Node.js theme={null}
async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Verificar si ya fue procesado
  const alreadyProcessed = await db.webhookLog.findUnique({
    where: { webhookId }
  });

  if (alreadyProcessed) {
    console.log('Webhook already processed, skipping');
    return; // Saltar duplicado
  }

  // Procesar webhook
  await processWebhookEvent(webhook);

  // Marcar como procesado
  await db.webhookLog.create({
    data: {
      webhookId,
      event: webhook.event,
      processedAt: new Date(),
      payload: webhook
    }
  });
}
```

### Idempotencia Basada en Caché

Para webhooks de alto volumen, usa un caché como Redis:

```javascript Node.js con Redis theme={null}
const Redis = require('ioredis');
const redis = new Redis();

async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Intentar establecer la clave con NX (solo si no existe)
  const result = await redis.set(
    `webhook:${webhookId}`,
    '1',
    'EX', 86400, // Expira después de 24 horas
    'NX'
  );

  if (!result) {
    console.log('Webhook already processed, skipping');
    return; // Ya procesado
  }

  // Procesar webhook
  await processWebhookEvent(webhook);
}
```

```python Python con Redis theme={null}
import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def handle_webhook(webhook):
    webhook_id = f"{webhook['payload']['entityId']}_{webhook['event']}_{webhook['timestamp']}"

    # Intentar establecer la clave con NX (solo si no existe)
    result = redis_client.set(
        f"webhook:{webhook_id}",
        "1",
        ex=86400,  # Expira después de 24 horas
        nx=True
    )

    if not result:
        print('Webhook already processed, skipping')
        return  # Ya procesado

    # Procesar webhook
    process_webhook_event(webhook)
```

## Mejores Prácticas de Seguridad

<AccordionGroup>
  <Accordion title="1. Siempre Verificar Firmas">
    Nunca omitas la verificación de firmas en ambientes de producción. Esta es tu defensa principal contra webhooks falsos.

    ```javascript theme={null}
    // Siempre verificar la firma
    if (!verifySignature(req.rawBody, signature, secret)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }
    ```
  </Accordion>

  <Accordion title="2. Usar Solo HTTPS">
    Configura tus endpoints de webhook para usar solo HTTPS. Rechaza solicitudes HTTP:

    ```javascript theme={null}
    app.post('/webhooks', (req, res) => {
      // Verificar si la solicitud es HTTPS
      if (req.protocol !== 'https') {
        return res.status(403).json({ error: 'HTTPS required' });
      }

      // Procesar webhook...
    });
    ```
  </Accordion>

  <Accordion title="3. Validar Tipos de Eventos">
    Solo procesa tipos de eventos que esperas:

    ```javascript theme={null}
    const ALLOWED_EVENTS = [
      'entity.created',
      'entity.updated',
      'entity.status_changed',
      'kyc.validation_approved'
    ];

    if (!ALLOWED_EVENTS.includes(webhook.event)) {
      console.warn('Unknown event type:', webhook.event);
      return res.status(400).json({ error: 'Unknown event type' });
    }
    ```
  </Accordion>

  <Accordion title="4. Implementar Límite de Tasa">
    Protege tu endpoint del abuso con limitación de tasa:

    ```javascript theme={null}
    const rateLimit = require('express-rate-limit');

    const webhookLimiter = rateLimit({
      windowMs: 1 * 60 * 1000, // 1 minuto
      max: 100, // Limitar cada IP a 100 solicitudes por minuto
      message: 'Too many webhook requests'
    });

    app.post('/webhooks', webhookLimiter, handleWebhook);
    ```
  </Accordion>

  <Accordion title="5. Almacenar Secretos de Webhook de Forma Segura">
    Nunca hardcodees secretos de webhook. Usa variables de entorno o gestión de secretos:

    ```javascript theme={null}
    // Bueno - variable de entorno
    const secret = process.env.GU1_WEBHOOK_SECRET;

    // Malo - hardcodeado
    const secret = 'abc123'; // ¡Nunca hagas esto!
    ```

    Para producción, usa un administrador de secretos:

    * AWS Secrets Manager
    * HashiCorp Vault
    * Azure Key Vault
    * Google Secret Manager
  </Accordion>

  <Accordion title="6. Validar Estructura del Payload">
    Valida la estructura del payload del webhook antes de procesar:

    ```javascript theme={null}
    function isValidEntityPayload(payload) {
      return (
        payload &&
        payload.entity &&
        payload.entity.id &&
        payload.entity.type
      );
    }

    if (!isValidEntityPayload(webhook.payload)) {
      return res.status(400).json({ error: 'Invalid payload structure' });
    }
    ```
  </Accordion>

  <Accordion title="7. Usar Comparaciones Seguras contra Tiempo">
    Al comparar firmas, usa funciones de comparación seguras contra tiempo para prevenir ataques de temporización:

    ```javascript theme={null}
    const crypto = require('crypto');

    function verifySignature(rawBody, signature, secret) {
      const expectedSignature = crypto
        .createHmac('sha256', secret)
        .update(rawBody)
        .digest('hex');

      // Comparación segura contra tiempo
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }
    ```
  </Accordion>

  <Accordion title="8. Implementar Logs de Webhook">
    Registra todos los intentos de webhook para auditoría y depuración:

    ```javascript theme={null}
    async function logWebhook(webhook, success, error = null) {
      await db.webhookLog.create({
        data: {
          event: webhook.event,
          timestamp: webhook.timestamp,
          success,
          error: error?.message,
          payload: webhook,
          processedAt: new Date()
        }
      });
    }
    ```
  </Accordion>

  <Accordion title="9. Devolver 200 Rápidamente">
    Responde con código de estado 200 rápidamente para prevenir reintentos. Procesa trabajo pesado de forma asíncrona:

    ```javascript theme={null}
    app.post('/webhooks', async (req, res) => {
      // Verificar firma primero
      if (!verifySignature(req.rawBody, signature, secret)) {
        return res.status(401).json({ error: 'Invalid signature' });
      }

      // Confirmar inmediatamente
      res.status(200).json({ received: true });

      // Procesar de forma asíncrona
      processWebhook(req.body).catch(console.error);
    });
    ```
  </Accordion>

  <Accordion title="10. Implementar Lógica de Reintentos">
    Si falla el procesamiento del webhook, almacénalo para reintento:

    ```javascript theme={null}
    try {
      await processWebhook(webhook);
    } catch (error) {
      // Almacenar webhook fallido para reintento
      await db.failedWebhook.create({
        data: {
          webhook,
          error: error.message,
          retryCount: 0,
          nextRetryAt: new Date(Date.now() + 5 * 60 * 1000) // Reintentar en 5 minutos
        }
      });

      // Aún devolver 200 para prevenir que Gu1 reintente
      res.status(200).json({ success: false });
    }
    ```
  </Accordion>
</AccordionGroup>

## Errores Comunes de Seguridad a Evitar

<Warning>
  Evita estos errores comunes de seguridad que pueden comprometer tus endpoints de webhook:
</Warning>

### 1. Omitir Verificación de Firmas

```javascript theme={null}
// NO HAGAS ESTO - Sin verificación de firmas
app.post('/webhooks', (req, res) => {
  // Procesando webhook sin verificación
  handleWebhook(req.body);
  res.status(200).send('OK');
});
```

**Riesgo**: Cualquiera puede enviar webhooks falsos a tu endpoint.

### 2. Verificar JSON Analizado en Lugar de Cuerpo Sin Procesar

```javascript theme={null}
// NO HAGAS ESTO - Verificando cuerpo analizado
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const body = JSON.stringify(req.body); // ¡Incorrecto!

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  // Siempre fallará
});
```

**Riesgo**: La verificación de firmas siempre fallará.

### 3. Usar HTTP en Lugar de HTTPS

```javascript theme={null}
// NO HAGAS ESTO - Aceptando HTTP
http://tuapp.com/webhooks
```

**Riesgo**: Los payloads de webhook pueden ser interceptados en tránsito.

### 4. Hardcodear Secretos

```javascript theme={null}
// NO HAGAS ESTO - Secreto hardcodeado
const secret = 'my-webhook-secret-123';
```

**Riesgo**: Secretos expuestos en control de versiones o logs.

### 5. No Implementar Idempotencia

```javascript theme={null}
// NO HAGAS ESTO - Procesando duplicados
app.post('/webhooks', (req, res) => {
  // Sin verificación de duplicados
  await createUser(req.body.entity);
  res.status(200).send('OK');
});
```

**Riesgo**: Webhooks duplicados crearán registros duplicados.

### 6. Exponer Errores a Clientes

```javascript theme={null}
// NO HAGAS ESTO - Exponiendo errores internos
app.post('/webhooks', async (req, res) => {
  try {
    await processWebhook(req.body);
  } catch (error) {
    res.status(500).json({
      error: error.stack, // Exponiendo stack trace
      query: error.query  // Exponiendo consulta de base de datos
    });
  }
});
```

**Riesgo**: Fuga de información interna a atacantes.

### 7. No Validar Tipos de Eventos

```javascript theme={null}
// NO HAGAS ESTO - Procesando eventos desconocidos
app.post('/webhooks', (req, res) => {
  // Aceptando cualquier tipo de evento
  handleAnyEvent(req.body.event, req.body.payload);
});
```

**Riesgo**: Atacantes pueden enviar tipos de eventos arbitrarios.

### 8. Usar Secretos Débiles

```javascript theme={null}
// NO HAGAS ESTO - Secreto débil
const secret = '123456';
```

**Riesgo**: Los secretos pueden ser forzados por fuerza bruta.

**Solución**: Usa secretos fuertes, generados aleatoriamente (al menos 32 caracteres).

## Probar Seguridad de Webhook

### Probar Firmas Inválidas

```bash theme={null}
# Probar con firma inválida
curl -X POST https://tuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: invalid-signature" \
  -d '{"event":"entity.created","payload":{}}'

# Debería devolver 401 Unauthorized
```

### Probar Ataques de Repetición

Envía el mismo webhook dos veces y verifica idempotencia:

```bash theme={null}
# Enviar webhook una vez
curl -X POST https://tuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Enviar mismo webhook de nuevo
curl -X POST https://tuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Segunda solicitud debería ser ignorada (idempotencia)
```

## Monitoreo y Alertas

Configura monitoreo para seguridad de webhook:

```javascript theme={null}
// Rastrear métricas de webhook
const metrics = {
  totalReceived: 0,
  signatureVerified: 0,
  signatureFailed: 0,
  processed: 0,
  failed: 0
};

app.post('/webhooks', (req, res) => {
  metrics.totalReceived++;

  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.rawBody, signature, secret)) {
    metrics.signatureFailed++;

    // Alertar si hay demasiados fallos de firma
    if (metrics.signatureFailed > 10) {
      alertSecurityTeam('High webhook signature failure rate');
    }

    return res.status(401).json({ error: 'Invalid signature' });
  }

  metrics.signatureVerified++;
  // Procesar webhook...
});
```

## Próximos Pasos

<CardGroup cols={2}>
  <Card title="Configuración de Webhook" icon="gear" href="/es/webhooks/configuration">
    Aprende cómo configurar webhooks
  </Card>

  <Card title="Eventos de Entidades" icon="user" href="/es/webhooks/events/entity-events">
    Maneja eventos de ciclo de vida de entidades
  </Card>

  <Card title="Eventos de KYC" icon="id-card" href="/es/webhooks/events/kyc-events">
    Procesa eventos de verificación de KYC
  </Card>

  <Card title="Eventos de Reglas" icon="gavel" href="/es/webhooks/events/rule-events">
    Responde a reglas de cumplimiento
  </Card>
</CardGroup>
