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

# Headers y firma

> Verifica que cada entrega viene de Pruff y no fue modificada.

## Headers

Cada entrega incluye estos headers:

| Header | Descripción | Ejemplo |
| - | - | - |
| `Content-Type` | Siempre JSON. | `application/json` |
| `X-Webhook-Event` | Nombre del evento. | `request.updateStatus` |
| `X-Webhook-Id` | Identificador del tipo de evento. Es el mismo en todas las entregas de ese evento. | `1` |
| `X-Webhook-Timestamp` | Momento en que Pruff generó la entrega, en segundos Unix. | `1769547728` |
| `X-Webhook-Signature` | Timestamp y firma HMAC de la entrega. | `t=1769547728,v1=2789cdd9...e734d` |
| `X-Webhook-Attempt` | Número de intento, de `1` a `4`. | `1` |

Si tu suscripción los tiene configurados, también se envían:

| Header | Descripción | Ejemplo |
| - | - | - |
| Header de autenticación | Nombre y valor que definiste para que tu sistema autentique a Pruff. El valor es el mismo en todas las entregas. | `Authorization: Bearer <tu-token>` |
| Header de idempotencia | Nombre que definiste. El valor es único por entrega y se repite en sus reintentos. | `Idempotency-Key: b4311a65-dd61-4cf9-953e-fad778502f12` |

## Cómo se genera la firma

Pruff firma cada entrega con el secret de tu suscripción usando HMAC-SHA256. El mensaje firmado es el timestamp, un punto y el body exacto de la request:

```text theme={null}
firma = hex( HMAC_SHA256( secret, "{timestamp}.{body}" ) )
```

El resultado se envía en `X-Webhook-Signature` con el formato `t={timestamp},v1={firma}`. El timestamp de `t` es el mismo que el de `X-Webhook-Timestamp`.

<Warning>
  Los reintentos reenvían la entrega original sin volver a firmarla: el timestamp y la firma no cambian entre intentos. El cuarto intento llega unos 21 minutos después del timestamp. Si rechazas entregas antiguas, acepta al menos 30 minutos de antigüedad o perderás los reintentos.
</Warning>

## Cómo validar una entrega

<Steps>
  <Step title="Lee el body sin parsear">
    Calcula la firma sobre los bytes que recibiste. Si parseas el JSON y lo vuelves a serializar, el resultado puede cambiar y la firma no coincidirá.
  </Step>

  <Step title="Extrae t y v1 de X-Webhook-Signature">
    Separa el header por comas y cada parte por el primer `=`.
  </Step>

  <Step title="Calcula la firma esperada">
    HMAC-SHA256 con tu secret sobre `{t}.{body}`, en hexadecimal.
  </Step>

  <Step title="Compara en tiempo constante">
    Usa una comparación de tiempo constante para no filtrar información por tiempos de respuesta. Si no coinciden, rechaza la entrega.
  </Step>

  <Step title="Opcional: valida la antigüedad">
    Rechaza entregas cuyo `t` tenga más de 30 minutos, para acotar la reutilización de entregas interceptadas.
  </Step>
</Steps>

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  import crypto from 'node:crypto';
  import express from 'express';

  const SECRET = process.env.PRUFF_WEBHOOK_SECRET;
  const MAX_AGE_SECONDS = 30 * 60;

  function isValidSignature(rawBody, signatureHeader) {
    const parts = {};
    for (const part of (signatureHeader ?? '').split(',')) {
      const separator = part.indexOf('=');
      if (separator > 0) {
        parts[part.slice(0, separator)] = part.slice(separator + 1);
      }
    }
    const timestamp = Number(parts.t);
    if (!Number.isInteger(timestamp) || !parts.v1) return false;
    if (Math.abs(Date.now() / 1000 - timestamp) > MAX_AGE_SECONDS) return false;

    const expected = crypto
      .createHmac('sha256', SECRET)
      .update(`${parts.t}.`)
      .update(rawBody)
      .digest('hex');
    const received = parts.v1;
    return (
      expected.length === received.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))
    );
  }

  const app = express();

  app.post(
    '/pruff/invoices/new',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      if (!isValidSignature(req.body, req.get('X-Webhook-Signature'))) {
        return res.sendStatus(401);
      }
      const event = JSON.parse(req.body.toString('utf8'));
      // Encola el evento para procesarlo fuera del handler.
      res.sendStatus(204);
    },
  );
  ```

  ```python Python (Flask) theme={null}
  import hashlib
  import hmac
  import os
  import time

  from flask import Flask, abort, request

  SECRET = os.environ["PRUFF_WEBHOOK_SECRET"].encode()
  MAX_AGE_SECONDS = 30 * 60


  def is_valid_signature(raw_body: bytes, signature_header: str) -> bool:
      parts = {}
      for part in signature_header.split(","):
          key, separator, value = part.partition("=")
          if separator:
              parts[key] = value
      timestamp = parts.get("t", "")
      received = parts.get("v1", "")
      if not timestamp.isdigit() or not received:
          return False
      if abs(time.time() - int(timestamp)) > MAX_AGE_SECONDS:
          return False

      expected = hmac.new(
          SECRET, timestamp.encode() + b"." + raw_body, hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(expected, received)


  app = Flask(__name__)


  @app.post("/pruff/invoices/new")
  def new_invoices():
      if not is_valid_signature(
          request.get_data(), request.headers.get("X-Webhook-Signature", "")
      ):
          abort(401)
      event = request.get_json()
      # Encola el evento para procesarlo fuera del handler.
      return "", 204
  ```
</CodeGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.