Connect Framer forms to a webhook

You can connect a form in Framer to a webhook, allowing submitted data to be sent to any URL you specify.

What are webhooks?

Webhooks enable real-time communication between web apps. They automatically send data from one app to another when specific events occur, eliminating the need for manual checks or constant polling. When you set a webhook as the form’s destination, Framer immediately sends the form data as JSON to the specified URL as an HTTP POST request upon submission.

Webhooks are useful for sending form submissions to your backend server, integrating with third-party services, triggering workflows in automation tools like Zapier, and saving form data to your own database.

How to connect a webhook

  1. Select the form on the canvas.

  2. Click “Add…” next to “Send To” in the right sidebar, and then select “Webhook”.

  3. Enter your webhook URL. Please make sure it starts with https://.

Webhook Forms

What should a webhook expect and return?

  • Framer sends form submissions as JSON to the webhook via an HTTP POST request.

  • The JSON uses the names of the inputs as keys, and the input values as the corresponding values.

  • Your webhook must return a 2xx HTTP status code to indicate successful receipt. If it doesn’t, Framer will retry the request up to 5 times.

  • Redirects (3xx responses) are not followed—Framer requires a direct 2xx response.

How to secure webhook destinations

To ensure requests come only from Framer, enable signature verification:

  1. Set a secret in the webhook destination (minimum 32 characters).

  2. Store this secret securely—it can’t be viewed again.

  3. Never expose the secret in client-side code or publicly.

Each request from Framer includes two headers:

  • Framer-Signature: A SHA-256 hash of the form data and submission ID, signed with your secret.

  • Framer-Webhook-Submission-Id: A unique ID for each form submission (e.g., 0fbbb90e-564b-4870-ac8b-78f9bc4e44a6).

A signature, like the ones mentioned above, follows the format shown below.

sha256=32c554c6f72fec5f0231533ba17978e2bbaf7714a4b6d35a7fc913e4891d23fc

This is a SHA-256 hash of the form data and submission ID, signed with your secret.

Signature verification examples

To help you validate webhook requests from Framer, below are code examples in different programming languages. These examples show how to generate and compare signatures using your secret, ensuring the request is authentic.

The Go example reads the request body, generates an HMAC hash using your secret, and compares it with the Framer-Signature header to verify the request.

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"log"
	"net/http"
)

func isWebhookSignatureValid(secret string, submissionId string, payload []byte, signature string) bool {
	if len(signature) != 71 {
		return false
	}
	payloadHmac := hmac.New(sha256.New, []byte(secret))
	payloadHmac.Write(payload)
	payloadHmac.Write([]byte(submissionId))

	expectedSignature := "sha256=" + hex.EncodeToString(payloadHmac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func main() {
	http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
		body, err := io.ReadAll(r.Body)
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		defer r.Body.Close()

		signature := r.Header.Get("Framer-Signature")
		submissionId := r.Header.Get("framer-webhook-submission-id")

		if !isWebhookSignatureValid("your_webhook_secret", submissionId, body, signature) {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusOK)
	})
	log.Fatal(http.ListenAndServe(":3000", nil

In the Node.js version, the server buffers the request body, computes a hash using the same method, and securely checks it against the provided signature.

const http = require('http');
const crypto = require('crypto');

const WEBHOOK_SECRET = 'your_webhook_secret';

function isWebhookSignatureValid(secret, submissionId, payloadBuffer, signature) {
  if (signature.length !== 71 || !signature.startsWith('sha256=')) return false;

  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(payloadBuffer);
  hmac.update(submissionId);

  const expectedSignature = 'sha256=' + hmac.digest('hex');
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}

const server = http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/webhook') {
    const bodyChunks = [];

    req.on('data', chunk => bodyChunks.push(chunk));
    req.on('end', () => {
      const body = Buffer.concat(bodyChunks);
      const signature = req.headers['framer-signature'];
      const submissionId = req.headers['framer-webhook-submission-id'];

      if (!signature || !submissionId || !isWebhookSignatureValid(WEBHOOK_SECRET, submissionId, body, signature)) {
        res.writeHead(500);
        return res.end('Invalid signature');
      }

      res.writeHead(200);
      res.end('Webhook received');
    });

    req.on('error', () => {
      res.writeHead(500);
      res.end('Server error');
    });
  } else {
    res.writeHead(404);
    res.end();
  }
});

server.listen(3000, () => {
  console.log('Server listening on <http://localhost:3000>');
});

If you need assistance with setting up or verifying your webhook, please reach out to our support team.