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
- Open your form in the builder
- Click Settings → Integrations
- Scroll to Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select events to subscribe to
- (Optional) Set a webhook secret
- 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
- Open Form Settings → Integrations → Webhooks
- Click Generate Secret (or enter your own)
- Copy and save the secret securely
- 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:
- Go to Form Settings → Integrations → Webhooks
- Click View Logs next to your webhook
-
See delivery history:
- Timestamp
- Status (Success, Failed, Retrying)
- Response time
- Attempt count
Manual Retry
Retry a failed delivery manually:
- View webhook logs
- Find the failed delivery
- Click Retry
- 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:
- Visit https://webhook.site
- Copy your unique URL
- Add it as a webhook in FormLeap
- Submit a test form
- 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:
- Open your form in preview mode
- Fill out with test data
-
Mark email as test:
test+webhook@example.com - Submit
- 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
- Verify signatures - Always validate HMAC signatures
- Respond quickly - Return 200 OK within 10 seconds
- Process async - Queue long-running tasks
-
Be idempotent - Use
delivery_idto prevent duplicates - Handle errors gracefully - Return appropriate status codes
- Log everything - Track successes and failures
- Monitor webhooks - Set up alerts for high failure rates
- Test thoroughly - Use ngrok or webhook.site for development
- Secure secrets - Use environment variables
- Validate input - Don't trust webhook data blindly
What's Next?
- API Reference - Complete REST API documentation
- MCP Integration - AI-powered form creation
- Integrations Guide - Embedding and reference fields
Need help? Contact support@formleap.app