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

# Webhooks

> Send form data to any URL

Webhooks let you send form submissions to your own server or third-party services.

## Setup

1. Go to your form settings
2. Enable webhooks
3. Enter your webhook URL
4. Optionally add a webhook secret for signature verification

## Payload Format

FormBnB sends a POST request with JSON body:

```json theme={null}
{
  "event": "submission.created",
  "timestamp": "2026-01-01T00:00:00.000Z",
  "form": {
    "id": "form_123",
    "name": "Contact Form",
    "slug": "contact"
  },
  "submission": {
    "id": "sub_xyz789",
    "data": {
      "email": "user@example.com",
      "name": "John Doe",
      "message": "Hello, I have a question..."
    },
    "createdAt": "2026-01-01T00:00:00.000Z"
  }
}
```

## Signature Verification

If you set a webhook secret, FormBnB includes a signature header for verification:

```
X-FormBnB-Timestamp: 1767225600000
X-FormBnB-Signature: abc123...
```

Verify the signature in your webhook handler:

```javascript theme={null}
import crypto from 'crypto';

function verifySignature(payload, timestamp, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${payload}`)
    .digest('hex');
  
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-formbnb-signature'];
  const timestamp = req.headers['x-formbnb-timestamp'];
  const isValid = verifySignature(req.body.toString(), timestamp, signature, process.env.WEBHOOK_SECRET);
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  const data = JSON.parse(req.body);
  // Process the webhook...
  
  res.status(200).send('OK');
});
```

## Retries

FormBnB automatically retries failed webhooks with exponential backoff:

| Attempt | Delay      |
| ------- | ---------- |
| 1       | Immediate  |
| 2       | 1 minute   |
| 3       | 5 minutes  |
| 4       | 30 minutes |

A webhook is considered failed if:

* Response status is not 2xx
* Request times out (30 seconds)
* Connection error

## Response

Your webhook should return a 2xx status code to acknowledge receipt. The response body is ignored.

## Testing

Test your webhook locally using [ngrok](https://ngrok.com):

```bash theme={null}
ngrok http 3000
```

Then use the ngrok URL as your webhook URL during development.

## Example Handlers

### Node.js / Express

```javascript theme={null}
const express = require('express');
const app = express();

app.post('/webhook', express.json(), (req, res) => {
  const { submission } = req.body;
  
  console.log('New submission:', submission.data.email);
  
  // Process the submission...
  
  res.status(200).send('OK');
});
```

### Python / Flask

```python theme={null}
from flask import Flask, request

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    
    print(f"New submission: {data['submission']['data']['email']}")
    
    # Process the submission...
    
    return 'OK', 200
```

### Next.js API Route

```typescript theme={null}
// app/api/webhook/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get('x-formbnb-signature');
  const timestamp = request.headers.get('x-formbnb-timestamp');
  
  // Verify signature
  const expected = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET!)
    .update(`${timestamp}.${body}`)
    .digest('hex');
  
  if (expected !== signature) {
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
  }
  
  const data = JSON.parse(body);
  
  // Process the submission...
  console.log('New submission:', data.submission.data.email);
  
  return NextResponse.json({ received: true });
}
```
