Skip to main content

Webhooks

Receive real-time HTTP notifications when forms are submitted.

Overview

Webhooks allow your application to receive instant notifications when events occur in FormLeap. When someone submits a form, FormLeap sends an HTTP POST request to your specified URL with the submission data.

Use Cases:

  • Trigger automation workflows
  • Update databases in real-time
  • Send custom notifications (Slack, SMS, email)
  • Process payments
  • Sync with CRM systems
  • Log analytics events

Setting Up Webhooks

Creating a Webhook

  1. Open your form in the builder
  2. Click SettingsIntegrations
  3. Scroll to Webhooks
  4. Click Add Webhook
  5. Enter your endpoint URL
  6. Select events to subscribe to
  7. (Optional) Set a webhook secret
  8. Click Save

Requirements

Your webhook endpoint must:

  • Use HTTPS - Plain HTTP is not supported for security
  • Respond within 10 seconds - Timeouts trigger retries
  • Return 2xx status code - Any other status is considered a failure
  • Be publicly accessible - FormLeap must be able to reach it

Example endpoint:

https://api.yourapp.com/webhooks/formleap

Events

Subscribe to specific events to receive webhooks:

submission.created

Triggered when a new submission is received.

When it fires:

  • User completes and submits the form
  • Submission passes validation
  • Data is saved successfully

Most common use case - Use this for real-time submission notifications.

submission.updated

Triggered when an existing submission is modified.

When it fires:

  • Submission is edited (if editing is enabled)
  • Reference data is updated
  • Status changes

Less common - Only needed if you allow submission editing.

submission.deleted

Triggered when a submission is permanently deleted.

When it fires:

  • Owner deletes a submission
  • Bulk deletion operations

Use case: Sync deletion with your database.


Payload Format

FormLeap sends a JSON payload with submission details:

Request Headers

POST /webhooks/formleap HTTP/1.1
Host: api.yourapp.com
Content-Type: application/json
X-FormLeap-Event: submission.created
X-FormLeap-Signature: sha256=abc123...
X-FormLeap-Delivery-ID: del_xyz789
User-Agent: FormLeap-Webhooks/1.0

Key Headers:

  • X-FormLeap-Event - Event type
  • X-FormLeap-Signature - HMAC signature for verification
  • X-FormLeap-Delivery-ID - Unique delivery identifier (for idempotency)

Request Body

{
  "event": "submission.created",
  "timestamp": "2025-01-20T10:30:00Z",
  "delivery_id": "del_xyz789",
  "form": {
    "id": "form_123abc",
    "name": "Contact Form",
    "workspace_id": "ws_456def"
  },
  "submission": {
    "id": "sub_789ghi",
    "submitted_at": "2025-01-20T10:30:00Z",
    "status": "completed",
    "reference_data": {
      "article_id": "123",
      "source": "blog",
      "campaign": "spring_2025"
    },
    "responses": {
      "name": "John Doe",
      "email": "john@example.com",
      "phone": "+1-555-0123",
      "message": "I love your product!",
      "subscribe_newsletter": true,
      "rating": 5
    },
    "file_uploads": {
      "resume": [
        {
          "filename": "john_doe_resume.pdf",
          "url": "https://files.formleap.app/signed/abc123...",
          "size": 102400,
          "content_type": "application/pdf"
        }
      ]
    }
  }
}

Key Fields:

  • event - Event type (submission.created, etc.)
  • timestamp - ISO 8601 timestamp of event
  • delivery_id - Unique identifier for this webhook delivery
  • form.id - Form identifier
  • submission.id - Submission identifier
  • submission.responses - Key-value pairs of field labels and values
  • submission.reference_data - URL parameters passed to form
  • file_uploads - Uploaded files with signed download URLs

File Upload URLs

File upload URLs are signed and expire after 1 hour. Download files immediately or re-request via API.


Signature Verification

Available on: Pro, Business, and Enterprise plans

Verify webhook authenticity using HMAC-SHA256 signatures.

Why Verify Signatures?

  • Security - Ensure requests come from FormLeap
  • Integrity - Confirm payload hasn't been tampered with
  • Trust - Prevent spoofing and replay attacks

Setting a Webhook Secret

  1. Open Form SettingsIntegrationsWebhooks
  2. Click Generate Secret (or enter your own)
  3. Copy and save the secret securely
  4. Click Save

Important: You won't be able to see the secret again. Store it in your environment variables.

Verification Algorithm

FormLeap computes the signature using:

HMAC-SHA256(webhook_secret, request_body)

The signature is sent in the X-FormLeap-Signature header:

X-FormLeap-Signature: sha256=a1b2c3d4e5f6...

Python Implementation

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

WEBHOOK_SECRET = os.environ['FORMLEAP_WEBHOOK_SECRET']

def verify_signature(payload, signature, secret):
    """Verify FormLeap webhook signature"""
    if not signature:
        return False

    # Extract signature value (format: "sha256=...")
    if not signature.startswith('sha256='):
        return False

    received_signature = signature.replace('sha256=', '')

    # Compute expected signature
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Use constant-time comparison to prevent timing attacks
    return hmac.compare_digest(received_signature, expected_signature)

@app.route('/webhooks/formleap', methods=['POST'])
def handle_webhook():
    # Get raw payload and signature
    payload = request.get_data(as_text=True)
    signature = request.headers.get('X-FormLeap-Signature')

    # Verify signature
    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({'error': 'Invalid signature'}), 401

    # Parse and process webhook
    data = request.json
    event = data['event']
    submission = data['submission']

    if event == 'submission.created':
        # Process new submission
        process_submission(submission)

    return jsonify({'status': 'success'}), 200

Node.js Implementation

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

const app = express();
const WEBHOOK_SECRET = process.env.FORMLEAP_WEBHOOK_SECRET;

// Verify signature middleware
function verifySignature(req, res, next) {
  const signature = req.headers['x-formleap-signature'];
  const payload = JSON.stringify(req.body);

  if (!signature) {
    return res.status(401).json({ error: 'Missing signature' });
  }

  // Extract signature value
  const receivedSignature = signature.replace('sha256=', '');

  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');

  // Use constant-time comparison
  const isValid = crypto.timingSafeEqual(
    Buffer.from(receivedSignature),
    Buffer.from(expectedSignature)
  );

  if (!isValid) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  next();
}

// Webhook endpoint
app.post('/webhooks/formleap',
  express.json(),
  verifySignature,
  (req, res) => {
    const { event, submission } = req.body;

    if (event === 'submission.created') {
      // Process new submission
      processSubmission(submission);
    }

    res.status(200).json({ status: 'success' });
  }
);

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Ruby Implementation

require 'sinatra'
require 'json'
require 'openssl'

WEBHOOK_SECRET = ENV['FORMLEAP_WEBHOOK_SECRET']

def verify_signature(payload, signature, secret)
  return false unless signature

  # Extract signature value
  return false unless signature.start_with?('sha256=')
  received_signature = signature.sub('sha256=', '')

  # Compute expected signature
  expected_signature = OpenSSL::HMAC.hexdigest(
    OpenSSL::Digest.new('sha256'),
    secret,
    payload
  )

  # Use constant-time comparison
  Rack::Utils.secure_compare(received_signature, expected_signature)
end

post '/webhooks/formleap' do
  # Get raw payload and signature
  payload = request.body.read
  signature = request.env['HTTP_X_FORMLEAP_SIGNATURE']

  # Verify signature
  unless verify_signature(payload, signature, WEBHOOK_SECRET)
    halt 401, { error: 'Invalid signature' }.to_json
  end

  # Parse and process webhook
  data = JSON.parse(payload)
  event = data['event']
  submission = data['submission']

  if event == 'submission.created'
    # Process new submission
    process_submission(submission)
  end

  status 200
  { status: 'success' }.to_json
end

Handling Webhooks

Responding to Webhooks

Return quickly - Respond with 200 OK as soon as possible:

app.post('/webhooks/formleap', async (req, res) => {
  // Verify signature first
  if (!verifySignature(req)) {
    return res.status(401).send('Invalid signature');
  }

  // Queue for background processing
  await queue.add('process-submission', req.body);

  // Respond immediately
  res.status(200).json({ status: 'queued' });
});

Don't:

  • Perform long-running operations before responding
  • Make external API calls synchronously
  • Process complex business logic inline

Do:

  • Verify signature first
  • Queue work for background processing
  • Return 200 OK immediately
  • Log errors for debugging

Idempotency

Use the delivery_id to prevent duplicate processing:

@app.route('/webhooks/formleap', methods=['POST'])
def handle_webhook():
    data = request.json
    delivery_id = data['delivery_id']

    # Check if we've already processed this delivery
    if redis.exists(f'webhook:{delivery_id}'):
        return jsonify({'status': 'already_processed'}), 200

    # Process webhook
    process_submission(data['submission'])

    # Mark as processed (expire after 7 days)
    redis.setex(f'webhook:{delivery_id}', 604800, '1')

    return jsonify({'status': 'success'}), 200

Error Handling

Return appropriate status codes:

  • 200-299 - Success, no retry
  • 400-499 - Client error, no retry (except 408, 429)
  • 500-599 - Server error, will retry
app.post('/webhooks/formleap', async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).json({ status: 'success' });
  } catch (error) {
    if (error.name === 'ValidationError') {
      // Client error - don't retry
      res.status(400).json({ error: error.message });
    } else {
      // Server error - FormLeap will retry
      console.error('Webhook processing failed:', error);
      res.status(500).json({ error: 'Internal server error' });
    }
  }
});

Retries

If your endpoint returns an error, FormLeap automatically retries:

Retry Schedule:

Attempt Delay After Failure
1 1 minute
2 5 minutes
3 15 minutes
4 1 hour
5 3 hours

After 5 failed attempts, FormLeap stops retrying and marks the delivery as failed.

Viewing Retry Status

Check webhook delivery status:

  1. Go to Form SettingsIntegrationsWebhooks
  2. Click View Logs next to your webhook
  3. See delivery history:
    • Timestamp
    • Status (Success, Failed, Retrying)
    • Response time
    • Attempt count

Manual Retry

Retry a failed delivery manually:

  1. View webhook logs
  2. Find the failed delivery
  3. Click Retry
  4. FormLeap resends immediately

Testing Webhooks

Local Development with ngrok

Test webhooks on your local machine:

# Start your local server
npm start  # Listening on localhost:3000

# In another terminal, start ngrok
ngrok http 3000

# Output:
# Forwarding  https://abc123.ngrok.io -> localhost:3000

# Use the ngrok URL in FormLeap:
# https://abc123.ngrok.io/webhooks/formleap

Testing Services

Use these services to inspect webhook payloads:

webhook.site:

  1. Visit https://webhook.site
  2. Copy your unique URL
  3. Add it as a webhook in FormLeap
  4. Submit a test form
  5. View the request in webhook.site

RequestBin (requestbin.com):

  • Similar to webhook.site
  • Inspect headers, body, and response

Test Submissions

Submit test data to trigger webhooks:

  1. Open your form in preview mode
  2. Fill out with test data
  3. Mark email as test: test+webhook@example.com
  4. Submit
  5. Check webhook logs

Security Best Practices

Always Verify Signatures

Never skip signature verification in production:

// ❌ NEVER DO THIS
app.post('/webhooks/formleap', (req, res) => {
  // No signature verification - anyone can POST here!
  processSubmission(req.body);
  res.send('OK');
});

// ✅ ALWAYS DO THIS
app.post('/webhooks/formleap', (req, res) => {
  if (!verifySignature(req)) {
    return res.status(401).send('Unauthorized');
  }

  processSubmission(req.body);
  res.send('OK');
});

Use HTTPS

FormLeap only sends webhooks to HTTPS endpoints:

  • https://api.yourapp.com/webhooks
  • http://api.yourapp.com/webhooks (rejected)

Secure Your Secrets

Store webhook secrets securely:

# Environment variables (recommended)
export FORMLEAP_WEBHOOK_SECRET="whsec_abc123..."

# Secrets manager (AWS, GCP, Azure)
# Never hardcode or commit to version control

Rate Limiting

Protect your endpoint from abuse:

const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000,  // 1 minute
  max: 100,                  // 100 requests per minute
  message: 'Too many requests'
});

app.post('/webhooks/formleap', webhookLimiter, handleWebhook);

Validate Payload

Don't trust webhook data blindly:

function validateSubmission(submission) {
  // Check required fields
  if (!submission.id || !submission.responses) {
    throw new Error('Invalid submission format');
  }

  // Validate email format
  const email = submission.responses.email;
  if (email && !isValidEmail(email)) {
    throw new Error('Invalid email format');
  }

  // Sanitize text inputs
  for (const [key, value] of Object.entries(submission.responses)) {
    if (typeof value === 'string') {
      submission.responses[key] = sanitize(value);
    }
  }

  return submission;
}

Best Practices Summary

  1. Verify signatures - Always validate HMAC signatures
  2. Respond quickly - Return 200 OK within 10 seconds
  3. Process async - Queue long-running tasks
  4. Be idempotent - Use delivery_id to prevent duplicates
  5. Handle errors gracefully - Return appropriate status codes
  6. Log everything - Track successes and failures
  7. Monitor webhooks - Set up alerts for high failure rates
  8. Test thoroughly - Use ngrok or webhook.site for development
  9. Secure secrets - Use environment variables
  10. Validate input - Don't trust webhook data blindly

What's Next?


Need help? Contact support@formleap.app