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

# Webhooks

> Send every new response to your own server as signed JSON.

A webhook is the plain option underneath every other integration. When someone submits, SurveyOwl sends one HTTP `POST` with a JSON body to a URL you choose. It suits developers, and any tool that gives you an incoming webhook URL.

## Set it up

Open the form, go to **Integrations**, choose **Webhook**, and fill in:

* **Endpoint URL**, a full URL such as `https://your-api.com/webhook`.
* **Signing secret**, optional but recommended. Enter your own or generate one. It usually looks like `whsec_...`.
* **Custom headers**, if your server expects any, such as an `Authorization` header.

Save, then submit a test response. Each delivery is logged, so you can open **View logs** to see the status your server returned.

## What you receive

The body is a flat JSON object with one key per answer. Keys are the field's ID. On a Hosted form each answer also appears under its question text, so the same answer shows up twice. On an Endpoint form the keys are just the field names you sent.

```json theme={null}
{
  "email": "ada@example.com",
  "message": "Please contact me next week."
}
```

Every request carries these headers:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `User-Agent` | `SurveyOwl-Webhook-Client/1.0` |
| `X-Submission-ID` | The ID of the submission |
| `X-SurveyOwl-Signature` | Hex HMAC-SHA256 of the raw body. Sent only if you set a signing secret. |
| `X-Signature-256` | The same signature as `sha256=<hex>`. Sent only if you set a signing secret. |

Any 2xx status counts as delivered. Anything else counts as a failed delivery.

## Verify a request

Compute the HMAC over the **raw request body**, before you parse it, and compare it with the signature header. Parsing and re-serializing the JSON changes the bytes and the check fails.

```js theme={null}
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.SURVEYOWL_WEBHOOK_SECRET;

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
  const received = req.get('X-SurveyOwl-Signature') ?? '';

  const valid =
    received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  if (!valid) return res.sendStatus(401);

  const answers = JSON.parse(req.body.toString('utf8'));
  // hand `answers` to a queue here, then answer quickly
  res.sendStatus(200);
});

app.listen(3000);
```

Reply quickly with a 2xx and do slow work in a background job.

<Warning>
  **Never put the signing secret in browser code.** Verification belongs on your server. If the secret shows up in logs, screenshots or a public repository, generate a new one and save it in the integration.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.