# Webhooks

> Formspree Docs · Plugins · Updated February 28, 2026

#### Available on: Professional, Business plans

Formspree allows you to configure a webhook that will be triggered each time a form receives a new submission. Webhooks can be used with serverless functions, custom server code, or other 3rd party services, to perform custom handling of form submissions.

Serverless functions provide a light weight way to process form submissions. Rather than creating a full web server just to process form submissions, you can create a single serverless function and deploy it to a serverless platform. There are several platforms for hosting functions, such as [Cloudflare Workers](https://developers.cloudflare.com/workers/), [AWS Lambda](https://aws.amazon.com/lambda/), and [Google Cloud Functions](https://cloud.google.com/functions/). The example below uses Cloudflare Workers, which offers a generous free tier and a quick path from code to a deployed endpoint.

When enabling your Webhook Plugin you'll be prompted to select a webhook type, either **Simple Webhook**, **Build Hook**, or **REST Hook**.

## Simple Webhooks

These are normal webhooks. We'll just send the payload to the desired URL as POST requests without asking any questions.

## Build Hooks

These are hooks that can be used to trigger new builds at third-party services like [Netlify](https://www.netlify.com/docs/webhooks/#incoming-webhooks). This is basically a webhook with an empty body.

## REST Hooks

<div class="callout callout-warning">
  <div><strong>Heads up:</strong> The REST Hooks protocol has been officially sunset and is no longer an actively maintained specification. Formspree still supports it for backwards compatibility with existing integrations, but new integrations should prefer <strong>Simple Webhooks</strong> above.</div>
</div>

In case you need it, we also send webhooks that adhere to the [REST Hooks](http://resthooks.org/) protocol. That is supported by some services and involves a handshake at the moment the webhook is being created.

### Handling the Resthook handshake

When you choose the Resthook protocol, before Formspree begins sending webhook requests, it will send a confirmation request with an `X-Hook-Secret` header. See [the resthooks docs](http://resthooks.org/docs/security/) on security for more information about the resthook handshake.

Here's an example of how to handle the resthook handshake in a [Cloudflare Worker](https://developers.cloudflare.com/workers/get-started/guide/), though the same concepts apply if you're implementing in your own web server.

```javascript
export default {
  async fetch(request) {
    const url = new URL(request.url);
    if (url.pathname !== '/hook') {
      return new Response(url.pathname + '\n');
    }

    const hookSecret = request.headers.get('x-hook-secret');
    if (hookSecret) {
      // Step 1: Confirm the subscription. This request includes an
      // X-Hook-Secret header, and we echo it back to complete the
      // handshake.
      return new Response(null, {
        headers: { 'x-hook-secret': hookSecret },
      });
    }

    // Step 2: Receive webhook requests — do something with the payload!
    const payload = await request.json();
    console.log('Received webhook!', payload);
    return new Response(null, { status: 200 });
  },
};
```

In Step 1 above, the Worker responds to the initial resthook handshake with a response that mirrors back the `x-hook-secret` header from the request. The body is empty.

Step 2 is where your custom functionality goes. Cloudflare Workers run on a V8 isolate with the standard `fetch` API plus bindings for KV, R2, D1, and queues — you can persist submissions, forward them to another service, or trigger any downstream action from here. See the [Cloudflare Workers docs](https://developers.cloudflare.com/workers/) for deployment and binding details.

## Webhooks Payload

A webhook is sent on every new submission received.

Except for Buildhooks (which are empty), the payload of the webhooks has the following structure:

```json
{
  "form": "",
  "keys": ["keys", "in", "the", "order", "they", "were", "submitted"],
  "submission": {
    "_date": "2029-12-31T00:00:00",
    "field": "value",
    "other-fields": "other value"
  }
}
```
