> For the complete documentation index, see [llms.txt](https://docs.dapta.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.dapta.ai/dapta-docs-es/dforms/connect/webhooks/verify-signature.md).

# Verificar la firma

Demuestra que una petición de webhook vino de verdad de dForms: recalcula la firma HMAC-SHA256 sobre el cuerpo crudo con tu secreto de firma y compárala con la cabecera X-Forms-Signature en tiempo con

Cuando defines un **Secreto de firma** en un webhook, cada petición que envía dForms lleva una cabecera `X-Forms-Signature`. Tu endpoint puede recalcular esa firma con el mismo secreto y rechazar todo lo que no coincida. Esta página muestra exactamente cómo se construye la firma y te da código listo para copiar en Node.js y Python.

Aquí no hay capturas: todo ocurre en tu servidor.

***

## Cómo se construye la firma

| Elemento  | Valor                                                                    |
| --------- | ------------------------------------------------------------------------ |
| Algoritmo | HMAC-SHA256                                                              |
| Clave     | el **Secreto de firma** que escribiste en la tarjeta del webhook         |
| Mensaje   | el cuerpo crudo de la petición, byte a byte, exactamente como se recibió |
| Cabecera  | `X-Forms-Signature`                                                      |
| Formato   | `sha256=` seguido del digest hex en minúsculas                           |

Cabecera de ejemplo:

```
X-Forms-Signature: sha256=b5b23906929f0809297e68964ed4b86c2baec69003c36585c389b65ea9474ea1
```

Dos cosas importan al verificar:

* **Usa el cuerpo crudo.** Calcula el HMAC sobre los bytes que recibiste, antes de cualquier parseo o reserialización de JSON. Los frameworks que parsean JSON solos suelen cambiar los espacios o el orden de las claves, y el digest deja de coincidir. Lee primero el cuerpo crudo, verifica, y luego parsea.
* **Compara en tiempo constante.** Usa la comparación segura frente a temporización de tu lenguaje (`crypto.timingSafeEqual` en Node, `hmac.compare_digest` en Python) en lugar de `==`, para que un atacante no pueda averiguar la firma byte a byte.

Si el webhook no tiene secreto de firma, la cabecera simplemente no está y no hay nada que verificar. Puedes añadir un secreto cuando quieras desde la pestaña **Conectar** del formulario; las entregas nuevas se firman desde ese momento.

***

## Node.js (Express)

```js
const crypto = require("node:crypto");
const express = require("express");

const app = express();
const SECRET = process.env.FORMS_WEBHOOK_SECRET; // el Secreto de firma que pusiste en dForms

// Conserva el cuerpo crudo: verifica ANTES de parsear el JSON.
app.post("/webhooks/forms", express.raw({ type: "application/json" }), (req, res) => {
  const received = req.get("X-Forms-Signature") || "";
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  if (!valid) return res.status(401).send("invalid signature");

  const event = JSON.parse(req.body.toString("utf8"));
  // event.phase es "partial" o "complete"; event.data lleva las respuestas
  console.log("submission", event.submission.id, event.phase, event.data);

  res.status(200).send("ok"); // cualquier 2xx le dice a dForms que la entrega llegó
});

app.listen(3000);
```

***

## Python (Flask)

```python
import hmac, hashlib, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["FORMS_WEBHOOK_SECRET"].encode()  # el Secreto de firma que pusiste en dForms

@app.post("/webhooks/forms")
def forms_webhook():
    raw = request.get_data()  # bytes crudos, antes de parsear JSON
    received = request.headers.get("X-Forms-Signature", "")
    expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(received, expected):
        abort(401)

    event = request.get_json(force=True)
    # event["phase"] es "partial" o "complete"; event["data"] lleva las respuestas
    print("submission", event["submission"]["id"], event["phase"], event["data"])

    return "ok", 200  # cualquier 2xx le dice a dForms que la entrega llegó
```

***

## Compruébalo con Enviar prueba

No necesitas una persona real para probar tu código. En la pestaña **Conectar** del formulario, haz clic en **Enviar prueba** en la tarjeta del webhook: la petición de prueba se firma con el mismo secreto y el mismo esquema que una entrega real, así que un verificador que acepte la prueba aceptará también las respuestas de verdad. El cuerpo de prueba lleva `"test": true` dentro de `data`, así que puedes saltártelo en tu lógica de negocio si quieres.

Cuando revises una firma que no coincide, verifica contra el cuerpo crudo que recibió tu endpoint, no contra una copia del **Historial del webhook**. El historial muestra la cookie de HubSpot de la visita como `[hidden]`, así que un cuerpo copiado que la llevaba ya no coincide con su firma.

> **💡 Tip:** Las otras cabeceras te dejan ir más lejos. `x-forms-delivery` es una clave de idempotencia que puedes guardar para ignorar duplicados, y `x-forms-timestamp` (segundos Unix) te deja rechazar peticiones demasiado viejas para tu gusto. Mira [Referencia del payload y las cabeceras](/dapta-docs-es/dforms/connect/webhooks/payload-reference.md).

> **⚠️ Nota:** Un `401` o cualquier otra respuesta que no sea 2xx desde tu endpoint se trata como intento fallido y se reintenta con backoff. Si rotas el secreto, actualízalo en dForms y en tu servidor a la vez; si no, los reintentos en vuelo se rechazarán hasta agotarse.

## Qué sigue

* [Referencia del payload y las cabeceras](/dapta-docs-es/dforms/connect/webhooks/payload-reference.md): todos los campos y cabeceras de la petición.
* [Probar y ver el historial de entregas](/dapta-docs-es/dforms/connect/webhooks/test-and-history.md): mira qué se envió y qué contestó tu endpoint.
* [Entrega, reintentos e historial](/dapta-docs-es/dforms/connect/delivery-and-retries.md): cómo funcionan los reintentos y los fallos.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.dapta.ai/dapta-docs-es/dforms/connect/webhooks/verify-signature.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
