Webhooks

Integrate form submissions with webhooks

Form Webhooks

Webhooks allow you to receive real-time notifications when form events occur, enabling integrations with external services.

Setting Up Webhooks

Via Dashboard

  1. Navigate to Forms > Select a form > Settings
  2. Scroll to Webhooks
  3. Enter your webhook URL
  4. Save changes

Via API

const response = await fetch('/api/forms/{formId}', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key',
  },
  body: JSON.stringify({
    webhookUrl: 'https://your-server.com/webhook/forms',
  }),
});

Multiple Webhooks

For advanced integrations, configure multiple webhooks:

const response = await fetch('/api/forms/{formId}/webhooks', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-API-Key': 'your-api-key',
  },
  body: JSON.stringify({
    url: 'https://your-server.com/webhook/forms',
    events: ['submission.created', 'submission.updated'],
    secret: 'your-webhook-secret',
  }),
});

Webhook Events

submission.created

Triggered when a new form submission is completed.

{
  "event": "submission.created",
  "timestamp": "2024-01-15T10:30:00Z",
  "form": {
    "id": "form-uuid",
    "title": "Contact Form",
    "slug": "contact-form"
  },
  "submission": {
    "id": "submission-uuid",
    "data": {
      "name": "John Doe",
      "email": "john@example.com",
      "message": "Hello from the form!"
    },
    "metadata": {
      "sessionId": "session-uuid",
      "ipAddress": "192.168.1.1",
      "userAgent": "Mozilla/5.0...",
      "referrer": "https://example.com/page",
      "duration": 45,
      "submittedAt": "2024-01-15T10:30:00Z"
    }
  }
}

submission.updated

Triggered when a partial submission is updated.

{
  "event": "submission.updated",
  "timestamp": "2024-01-15T10:28:00Z",
  "form": {
    "id": "form-uuid",
    "title": "Contact Form"
  },
  "submission": {
    "id": "submission-uuid",
    "data": {
      "name": "John Doe",
      "email": "john@example.com"
    },
    "status": "partial",
    "currentField": 2
  }
}

form.view

Triggered when a form is viewed.

{
  "event": "form.view",
  "timestamp": "2024-01-15T10:25:00Z",
  "form": {
    "id": "form-uuid",
    "title": "Contact Form"
  },
  "metadata": {
    "sessionId": "session-uuid",
    "referrer": "https://example.com/page"
  }
}

Webhook Security

Signature Verification

All webhooks are signed with HMAC-SHA256. Verify the signature to ensure authenticity:

import crypto from 'crypto';
 
function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
 
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`)
  );
}
 
// In your webhook handler
app.post('/webhook/forms', (req, res) => {
  const signature = req.headers['x-transactional-signature'];
  const payload = JSON.stringify(req.body);
 
  if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
 
  // Process webhook...
  res.status(200).json({ received: true });
});

IP Allowlist

Webhook requests come from these IPs:

52.xxx.xxx.xxx
52.xxx.xxx.xxx

You can allowlist these IPs for additional security.

Webhook Headers

Every webhook request includes these headers:

HeaderDescription
Content-Typeapplication/json
X-Transactional-EventEvent type (e.g., submission.created)
X-Transactional-SignatureHMAC signature
X-Transactional-TimestampRequest timestamp
X-Transactional-Delivery-IdUnique delivery ID

Retry Policy

Failed webhooks are retried with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
612 hours

After 6 failed attempts, the webhook is marked as failed and no further retries occur.

Success Criteria

A webhook is considered successful if:

  • HTTP status code is 2xx
  • Response is received within 30 seconds

Webhook Logs

View webhook delivery history in the dashboard:

  1. Navigate to Forms > Select a form > Webhooks
  2. Click on a webhook to view logs

Each log entry shows:

  • Delivery timestamp
  • Event type
  • HTTP status code
  • Response body
  • Retry count

API Access

// Get webhook delivery logs
const response = await fetch('/api/forms/{formId}/webhooks/{webhookId}/logs', {
  headers: {
    'X-API-Key': 'your-api-key',
  },
});
 
const { data } = await response.json();
// Array of delivery logs

Integration Examples

Slack Notification

app.post('/webhook/forms', async (req, res) => {
  const { event, form, submission } = req.body;
 
  if (event === 'submission.created') {
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `New submission on ${form.title}`,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `*New Form Submission*\n*Form:* ${form.title}`,
            },
          },
          {
            type: 'section',
            fields: Object.entries(submission.data).map(([key, value]) => ({
              type: 'mrkdwn',
              text: `*${key}:*\n${value}`,
            })),
          },
        ],
      }),
    });
  }
 
  res.status(200).json({ received: true });
});

Google Sheets

import { google } from 'googleapis';
 
app.post('/webhook/forms', async (req, res) => {
  const { event, submission } = req.body;
 
  if (event === 'submission.created') {
    const sheets = google.sheets({ version: 'v4', auth: authClient });
 
    await sheets.spreadsheets.values.append({
      spreadsheetId: process.env.SHEET_ID,
      range: 'Sheet1!A:Z',
      valueInputOption: 'RAW',
      requestBody: {
        values: [[
          new Date().toISOString(),
          submission.data.name,
          submission.data.email,
          submission.data.message,
        ]],
      },
    });
  }
 
  res.status(200).json({ received: true });
});

Zapier

Create a Zapier webhook and use it as your form's webhook URL:

  1. Create a new Zap
  2. Choose "Webhooks by Zapier" as trigger
  3. Select "Catch Hook"
  4. Copy the webhook URL
  5. Add it to your form settings

HubSpot CRM

app.post('/webhook/forms', async (req, res) => {
  const { event, submission } = req.body;
 
  if (event === 'submission.created') {
    await fetch('https://api.hubspot.com/crm/v3/objects/contacts', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${process.env.HUBSPOT_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        properties: {
          email: submission.data.email,
          firstname: submission.data.name?.split(' ')[0],
          lastname: submission.data.name?.split(' ').slice(1).join(' '),
          phone: submission.data.phone,
          company: submission.data.company,
        },
      }),
    });
  }
 
  res.status(200).json({ received: true });
});

Custom CRM

app.post('/webhook/forms', async (req, res) => {
  const { event, form, submission } = req.body;
 
  if (event === 'submission.created') {
    // Map form fields to your CRM fields
    const lead = {
      source: 'Website Form',
      formName: form.title,
      name: submission.data.name,
      email: submission.data.email,
      phone: submission.data.phone,
      message: submission.data.message,
      createdAt: submission.metadata.submittedAt,
    };
 
    await yourCRM.leads.create(lead);
  }
 
  res.status(200).json({ received: true });
});

Testing Webhooks

Test Endpoint

Send a test webhook to verify your endpoint:

const response = await fetch('/api/forms/{formId}/webhooks/{webhookId}/test', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your-api-key',
  },
});

Local Development

Use ngrok or similar to test webhooks locally:

ngrok http 3000

Then use the ngrok URL as your webhook URL.

Troubleshooting

Webhook Not Triggering

  1. Check form is published
  2. Verify webhook URL is correct
  3. Check webhook logs for errors
  4. Ensure server returns 2xx status

Signature Verification Failing

  1. Use the raw request body for verification
  2. Check secret matches
  3. Ensure no body parsing middleware modifies the payload

Timeouts

  1. Webhooks timeout after 30 seconds
  2. Return 200 immediately, process async
  3. Use a queue for long-running tasks
app.post('/webhook/forms', async (req, res) => {
  // Acknowledge immediately
  res.status(200).json({ received: true });
 
  // Process in background
  processSubmission(req.body).catch(console.error);
});

Next Steps