# Verificar firmas (https://docs.pulsarml.com/es/webhooks/signatures)

Comprueba que cada solicitud de webhook realmente viene de Pulsar y no se alteró en el camino.



Tu endpoint es una URL pública, así que cualquiera puede enviarle una solicitud. Cada notificación de Pulsar incluye un encabezado `X-Pulsar-Signature`, un hash del cuerpo de la solicitud calculado con tu **secreto de firma**. Solo Pulsar y tú conocen el secreto, así que una firma que coincide demuestra que la solicitud viene de Pulsar y que el cuerpo no se modificó.

La firma es el **HMAC-SHA256** del **cuerpo sin procesar de la solicitud**, con tu secreto de firma como clave, escrito en **hexadecimal en minúsculas**.

<Callout type="warn" title="Usa el secreto exactamente como te lo da Pulsar">
  El secreto de firma es una cadena de 32 caracteres. Usa su texto (bytes UTF-8) como clave del HMAC, tal cual. &#x2A;*No lo decodifiques de Base64 ni de hexadecimal.** La decodificación suele funcionar sin errores pero produce una clave incorrecta, y entonces todas las verificaciones de firma fallan.
</Callout>

## Cómo verificar una solicitud [#how-to-verify-a-request]

<Steps>
  <Step>
    ### Lee el cuerpo sin procesar [#read-the-raw-body]

    Calcula la firma sobre los **bytes exactos** que envió Pulsar, antes de cualquier parseo de JSON. Parsear y volver a serializar el JSON puede cambiar los espacios o el orden de las claves, y eso cambia el hash. La mayoría de los frameworks te dan el cuerpo sin procesar por separado; consulta los ejemplos más abajo.
  </Step>

  <Step>
    ### Calcula la firma esperada [#compute-the-expected-signature]

    Calcula el HMAC-SHA256 del cuerpo sin procesar con tu secreto de firma como clave, y codifica el resultado en hexadecimal.
  </Step>

  <Step>
    ### Compárala con el encabezado [#compare-it-with-the-header]

    Compara tu resultado con el encabezado `X-Pulsar-Signature` usando una comparación de **tiempo constante** (`hmac.compare_digest` en Python, `crypto.timingSafeEqual` en Node.js). Un `==` simple puede revelar, por el tiempo de respuesta, qué tan acertada era una firma adivinada.
  </Step>

  <Step>
    ### Acepta o rechaza [#accept-or-reject]

    Si las firmas coinciden, procesa el evento y devuelve `2XX`. Si falta el encabezado o no coincide, devuelve `401` e ignora la solicitud.
  </Step>
</Steps>

<Callout title="Prueba tu verificación con un ping primero">
  Pulsar no reintenta una respuesta `4XX` (consulta [Reintentos](/es/webhooks#retries)). Así que si tu secreto está mal configurado, los eventos reales se rechazan y quedan sin entregar hasta que Pulsar los vuelva a enviar. Pide un [ping](/es/webhooks#ping-events) y confirma que tu endpoint devuelve `2XX` antes de depender de él.
</Callout>

## Código de ejemplo [#sample-code]

Ambos ejemplos leen el secreto de una variable de entorno `PULSAR_WEBHOOK_SECRET`.

<Tabs items="['Python (Flask)', 'Node.js (Express)']">
  <Tab value="Python (Flask)">
    ```python
    import hashlib
    import hmac
    import os

    from flask import Flask, request

    app = Flask(__name__)
    SECRET = os.environ["PULSAR_WEBHOOK_SECRET"].encode("utf-8")


    @app.post("/webhooks/pulsar")
    def pulsar_webhook():
        signature = request.headers.get("X-Pulsar-Signature", "")

        # Los bytes exactos que envió Pulsar, antes de parsear el JSON.
        body = request.get_data()
        expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()

        if not hmac.compare_digest(expected, signature):
            return "", 401

        notification = request.get_json()
        if notification["eventType"] == "Ping":
            return "", 200

        # Encola el trabajo y responde de inmediato; consulta "Responde con un 2XX, rápido".
        enqueue(notification)
        return "", 202
    ```
  </Tab>

  <Tab value="Node.js (Express)">
    ```js
    const crypto = require('node:crypto');
    const express = require('express');

    const app = express();
    const SECRET = process.env.PULSAR_WEBHOOK_SECRET;

    // express.raw conserva los bytes exactos que envió Pulsar. No uses express.json()
    // en esta ruta: parsea el cuerpo antes de que puedas verificar la firma.
    app.post('/webhooks/pulsar', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = Buffer.from(req.get('X-Pulsar-Signature') ?? '');
      const expected = Buffer.from(
        crypto.createHmac('sha256', SECRET).update(req.body).digest('hex'),
      );

      if (signature.length !== expected.length || !crypto.timingSafeEqual(signature, expected)) {
        return res.sendStatus(401);
      }

      const notification = JSON.parse(req.body.toString('utf8'));
      if (notification.eventType === 'Ping') return res.sendStatus(200);

      // Encola el trabajo y responde de inmediato; consulta "Responde con un 2XX, rápido".
      enqueue(notification);
      res.sendStatus(202);
    });
    ```
  </Tab>
</Tabs>

## Solicitudes repetidas [#replayed-requests]

La firma cubre solo el cuerpo, así que cualquiera que obtenga una copia de una solicitud, por ejemplo de un log, podría enviártela de nuevo sin cambios y seguiría pasando la verificación. Protégete de la misma forma en que [manejas los duplicados](/es/webhooks#duplicates-and-ordering): guarda cada `eventId` que proceses y omite los que ya hayas visto.

No rechaces solicitudes solo porque `eventTimestamp` sea antiguo. Los reintentos y reenvíos vuelven a enviar a propósito el evento original, con su marca de tiempo original, posiblemente mucho después.
