---
title: "Firma de webhooks"
description: "Verificar HMAC-SHA256 sobre timestamp y body crudo antes de procesar un evento."
---

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

# Firma de webhooks

# Firma de webhooks

Cada delivery incluye una firma HMAC-SHA256. Verificarla evita aceptar requests falsificados o alterados.

```http
OrderFast-Event-Id: evt_019f7067f8b47b4e
OrderFast-Timestamp: 1784294108
OrderFast-Signature: v1=0c240b7f...
```

## Contenido firmado

El mensaje se construye sin espacios adicionales:

```text
{timestamp}.{raw_body}
```

`raw_body` son exactamente los bytes recibidos. Serializar otra vez el JSON puede cambiar espacios, escapes u orden de claves y producir una firma distinta.

## Algoritmo

1. Leer `OrderFast-Timestamp` y `OrderFast-Signature`.
2. Rechazar headers faltantes o mal formados.
3. Convertir el timestamp Unix y comprobar que está dentro de la tolerancia acordada.
4. Concatenar el timestamp, un punto y el body crudo.
5. Calcular HMAC-SHA256 con el secreto del endpoint.
6. Codificar el digest en hexadecimal y anteponer `v1=`.
7. Comparar los bytes en tiempo constante.
8. Solo entonces parsear y procesar el JSON.

## Node.js

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyWebhook({ rawBody, timestamp, signature, secret }) {
  const signed = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]);
  const expected = `v1=${createHmac("sha256", secret)
.update(signed)
.digest("hex")}`;

  const receivedBytes = Buffer.from(signature, "utf8");
  const expectedBytes = Buffer.from(expected, "utf8");
  return (
receivedBytes.length === expectedBytes.length &&
timingSafeEqual(receivedBytes, expectedBytes)
  );
}
```

Configurá el framework para conservar `rawBody` como `Buffer`. No pases primero por `JSON.parse()`.

## Python

```python
import hashlib
import hmac

def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
signed = timestamp.encode("utf-8") + b"." + raw_body
digest = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
expected = f"v1={digest}"
return hmac.compare_digest(expected, signature)
```

## Tolerancia temporal

La comprobación del timestamp limita ataques de replay. Usá la tolerancia publicada por el ambiente; si todavía no está definida en el contrato, acordala antes de producción y mantené los relojes sincronizados. No aceptes timestamps arbitrariamente antiguos solo porque la firma es válida.

## Rotación de secretos

Durante una rotación pueden coexistir temporalmente dos secretos:

1. verificar primero con el secreto nuevo;
2. si falla, verificar con el anterior mientras dure la ventana;
3. registrar qué versión validó, sin registrar el secreto ni la firma completa;
4. retirar el secreto anterior al cerrar la rotación.

## Fallas comunes

| Problema                                     | Resultado                                                        |
| -------------------------------------------- | ---------------------------------------------------------------- |
| Parsear y serializar JSON antes de verificar | Cambian los bytes y falla la firma.                              |
| Proxy que transforma el body                 | La aplicación ya no recibe el contenido firmado.                 |
| Comparación con `==`                         | Puede filtrar información temporal.                              |
| Reloj desincronizado                         | Se rechazan deliveries válidas o se amplía la ventana de replay. |
| Secreto de `test` usado en `live`            | Todas las firmas fallan.                                         |

Respondé `401` ante una firma inválida y no modifiques ningún recurso de negocio. Nunca registres el secreto ni el header completo de firma.

Source: https://docs.pagofast.com/webhooks/signatures/index.mdx
