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 Settings → Integrations
  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 Settings → Integrations → Webhooks
  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 Settings → Integrations → Webhooks
  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