> 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-forms/connect/webhooks/verify-signature.md).

# Verify the signature

Prove that a webhook request really came from Dapta Forms: recompute the HMAC-SHA256 signature over the raw body with your signing secret and compare it to the X-Forms-Signature header in constant tim

When you set a **Signing secret** on a webhook, every request Dapta Forms sends carries an `X-Forms-Signature` header. Your endpoint can recompute that signature with the same secret and reject anything that does not match. This page shows exactly how the signature is built and gives you copy-paste code for Node.js and Python.

No screenshots here: everything happens on your server.

***

## How the signature is built

| Item      | Value                                                    |
| --------- | -------------------------------------------------------- |
| Algorithm | HMAC-SHA256                                              |
| Key       | the **Signing secret** you typed in the webhook card     |
| Message   | the raw request body, byte for byte, exactly as received |
| Header    | `X-Forms-Signature`                                      |
| Format    | `sha256=` followed by the lowercase hex digest           |

Example header:

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

Two things matter when you verify:

* **Use the raw body.** Compute the HMAC over the bytes you received, before any JSON parsing or re-serialising. Frameworks that parse JSON automatically often change whitespace or key order, and the digest will no longer match. Read the raw body first, verify, then parse.
* **Compare in constant time.** Use your language's timing-safe comparison (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python) instead of `==`, so an attacker cannot learn the signature one byte at a time.

If the webhook has no signing secret, the header is simply absent and there is nothing to verify. You can add a secret at any time from the form's **Connect** tab; new deliveries are signed from that moment on.

***

## Node.js (Express)

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

const app = express();
const SECRET = process.env.FORMS_WEBHOOK_SECRET; // the Signing secret you set in Dapta Forms

// Keep the body raw: verify BEFORE parsing 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 is "partial" or "complete"; event.data holds the answers
  console.log("submission", event.submission.id, event.phase, event.data);

  res.status(200).send("ok"); // any 2xx tells Dapta Forms the delivery landed
});

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()  # the Signing secret you set in Dapta Forms

@app.post("/webhooks/forms")
def forms_webhook():
    raw = request.get_data()  # raw bytes, before any JSON parsing
    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"] is "partial" or "complete"; event["data"] holds the answers
    print("submission", event["submission"]["id"], event["phase"], event["data"])

    return "ok", 200  # any 2xx tells Dapta Forms the delivery landed
```

***

## Check it with Send test

You do not need a real respondent to try your code. In the form's **Connect** tab, click **Send test** on the webhook card: the test request is signed with the same secret and the same scheme as a real delivery, so a verifier that accepts the test will accept real submissions too. The test body carries `"test": true` inside `data`, so you can skip it in your business logic if you want.

> **💡 Tip:** The other headers help you go further. `x-forms-delivery` is an idempotency key you can store to ignore duplicates, and `x-forms-timestamp` (Unix seconds) lets you reject requests that are too old for your taste. See [Payload & headers reference](/dapta-forms/connect/webhooks/payload-reference.md).

> **⚠️ Note:** A `401` or any other non-2xx answer from your endpoint is treated as a failed attempt and is retried with backoff. If you roll a new secret, update it in Dapta Forms and on your server at the same time, otherwise in-flight retries will be rejected until they run out.

## What's next

* [Payload & headers reference](/dapta-forms/connect/webhooks/payload-reference.md): every field and header in the request.
* [Test & delivery history](/dapta-forms/connect/webhooks/test-and-history.md): see what was sent and what your endpoint answered.
* [Delivery, retries & history](/dapta-forms/connect/delivery-and-retries.md): how retries and failures work.


---

# 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 dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
