# Verificar la firma (/docs/webhook-signatures)

Cada webhook trae `Psp-Signature`:

```
Psp-Signature: t=1790769600,v1=c0c9eaafdea23b0f8c992b380b286917c6459b0729b2496b8452fa695215e6b8
```

- `t` es el momento de la firma, en segundos Unix.
- `v1` es `HMAC-SHA256` en hexadecimal, con tu secreto como clave, sobre `t` + `.` + el cuerpo **exacto** que recibiste.
- La clave es el secreto **completo**, incluido el prefijo `whsec_`.
- Puede venir más de un `v1` (durante una rotación). Basta con que uno coincida.

Para aceptar un webhook:

1. Calcula el HMAC con el cuerpo sin tocar: no lo parsees ni lo vuelvas a serializar antes.
2. Compara con cada `v1` en **tiempo constante**.
3. Rechaza si `t` difiere de tu hora en más de **5 minutos**: así nadie puede reenviarte un aviso viejo.

<Tabs items={["Node.js", "Python", "Go"]}>
<Tab value="Node.js">

```js
const crypto = require("crypto");

// rawBody: the exact bytes received (a Buffer).
function verifyCaiboSignature(secret, header, rawBody, toleranceSeconds = 300) {
  let t = null;
  const signatures = [];
  for (const part of String(header || "").split(",")) {
    const [key, value] = part.trim().split("=", 2);
    if (key === "t") {
      if (t !== null || !/^\d+$/.test(value)) return false;
      t = value;
    } else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) {
      signatures.push(Buffer.from(value, "hex"));
    }
  }
  if (t === null || signatures.length === 0) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  if (!signatures.some((s) => crypto.timingSafeEqual(s, expected))) return false;
  return Math.abs(Date.now() / 1000 - Number(t)) <= toleranceSeconds;
}

// Express: read the body raw, verify, then parse.
app.post("/webhooks/caibo", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyCaiboSignature(process.env.CAIBO_WEBHOOK_SECRET, req.get("Psp-Signature"), req.body)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  queue.push(event); // process later; dedupe on event.id
  res.sendStatus(200);
});
```

</Tab>
<Tab value="Python">

```python
import hashlib
import hmac
import re
import time

_HEX64 = re.compile(r"^[0-9a-fA-F]{64}$")

def verify_caibo_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    t, signatures = None, []
    for part in (header or "").split(","):
        key, _, value = part.strip().partition("=")
        if key == "t":
            if t is not None or not value.isdigit():
                return False
            t = value
        elif key == "v1" and _HEX64.match(value):
            signatures.append(bytes.fromhex(value))
    if t is None or not signatures:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).digest()
    if not any(hmac.compare_digest(s, expected) for s in signatures):
        return False
    return abs(time.time() - int(t)) <= tolerance

# Flask: request.get_data() is the raw body.
@app.post("/webhooks/caibo")
def caibo_webhook():
    if not verify_caibo_signature(os.environ["CAIBO_WEBHOOK_SECRET"], request.headers.get("Psp-Signature"), request.get_data()):
        return "", 400
    event = request.get_json()
    enqueue(event)  # process later; dedupe on event["id"]
    return "", 200
```

</Tab>
<Tab value="Go">

```go
package caibo

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
)

// VerifySignature reports whether header signs body with secret, within tolerance.
func VerifySignature(secret, header string, body []byte, tolerance time.Duration) bool {
	var ts string
	var signatures [][]byte
	for _, part := range strings.Split(header, ",") {
		key, value, _ := strings.Cut(strings.TrimSpace(part), "=")
		switch key {
		case "t":
			if ts != "" {
				return false
			}
			ts = value
		case "v1":
			if s, err := hex.DecodeString(value); err == nil && len(s) == sha256.Size {
				signatures = append(signatures, s)
			}
		}
	}
	t, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || len(signatures) == 0 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(ts + "."))
	mac.Write(body)
	expected := mac.Sum(nil)
	ok := false
	for _, s := range signatures {
		ok = ok || hmac.Equal(s, expected)
	}
	age := time.Since(time.Unix(t, 0))
	return ok && age <= tolerance && age >= -tolerance
}
```

</Tab>
</Tabs>

## Prueba tu código [#prueba-tu-código]

Con estos valores tu función debe aceptar la firma (ignorando la tolerancia, porque `t` es una fecha pasada):

| | |
| --- | --- |
| Secreto | `whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef` |
| Cuerpo (exacto, sin salto de línea final) | `{"id":"evt_1","type":"payout.completed","data":{"amount":"10.00"}}` |
| `t` | `1790769600` |
| Firma esperada | `t=1790769600,v1=c0c9eaafdea23b0f8c992b380b286917c6459b0729b2496b8452fa695215e6b8` |

Para probar contra tu servidor real, usa <Endpoint method="POST" path="/v1/webhook-endpoints/{id}/ping" href="/docs/api/webhooks/ping-webhook-endpoint" />.

<Callout type="warn" title="El error más común">
Muchos frameworks parsean el JSON antes de que lo veas. Si firmas `JSON.stringify(req.body)` en lugar del cuerpo original, la firma no coincide. Lee el cuerpo **crudo**.
</Callout>
