Python Middleware
singleform-webhook verifies SingleForm webhook signatures in Python. Works with Flask, Django, FastAPI, or any framework. The core verification uses only the Python standard library and has zero dependencies.
Installation
# Core only (no framework dependencies)
pip install singleform-webhook
# With framework extras
pip install singleform-webhook[flask]
pip install singleform-webhook[django]
pip install singleform-webhook[fastapi]Flask
import os
from flask import Flask, request, jsonify, g
from singleform_webhook.flask import (
singleform_webhook,
singleform_success,
singleform_validation_error,
)
app = Flask(__name__)
@app.route("/webhooks/singleform", methods=["POST"])
@singleform_webhook(secret=os.environ["SINGLEFORM_SECRET"])
def handle_webhook():
form_id = g.singleform.form_id
body = request.get_json()
email = body.get("email")
if not email:
return jsonify(singleform_validation_error({"email": "Email is required"})), 400
# Your business logic here
return jsonify(singleform_success({"submissionId": "12345"}))Django
# settings.py
SINGLEFORM_SECRET = os.environ["SINGLEFORM_SECRET"]
SINGLEFORM_TIMESTAMP_TOLERANCE = 300 # optional, default 300s# views.py
import json
from django.http import JsonResponse
from singleform_webhook.django import (
singleform_webhook_view,
singleform_success,
singleform_validation_error,
)
@singleform_webhook_view
def handle_webhook(request):
form_id = request.singleform.form_id
body = json.loads(request.body)
email = body.get("email")
if not email:
return JsonResponse(
singleform_validation_error({"email": "Email is required"}),
status=400,
)
# Your business logic here
return JsonResponse(singleform_success({"submissionId": "12345"}))# urls.py
from django.urls import path
from . import views
urlpatterns = [
path("webhooks/singleform", views.handle_webhook),
]FastAPI
import os
from fastapi import Depends, FastAPI, Body
from singleform_webhook.fastapi import (
SingleFormWebhook,
SingleFormMetadata,
singleform_success,
singleform_validation_error,
)
app = FastAPI()
verify = SingleFormWebhook(secret=os.environ["SINGLEFORM_SECRET"])
@app.post("/webhooks/singleform")
async def handle_webhook(
sf: SingleFormMetadata = Depends(verify),
body: dict = Body(...),
):
email = body.get("email")
if not email:
return singleform_validation_error({"email": "Email is required"})
# Your business logic here
return singleform_success({"submissionId": "12345"})Manual Verification
Use the core functions directly with any framework:
from singleform_webhook import verify_webhook, verify_signature, SingleFormError
# Verify from a dict of headers
try:
metadata = verify_webhook(
headers={"X-SingleForm-Signature": "...", ...},
secret="sf_secret_...",
timestamp_tolerance=300,
)
print(f"Verified request from form {metadata.form_id}")
except SingleFormError as e:
print(f"Verification failed: {e.type} - {e.message}")
# Or verify a single signature
is_valid = verify_signature(
form_id="form_abc123",
timestamp="1700000000",
nonce="random-nonce",
signature="computed-hmac-hex",
secret="sf_secret_...",
)Response Helpers
All framework integrations export three response helpers:
from singleform_webhook import (
singleform_success,
singleform_error,
singleform_validation_error,
)
# Success
singleform_success({"submissionId": "123"})
# => {"success": True, "data": {"submissionId": "123"}}
# Business logic error
singleform_error("DUPLICATE_SUBMISSION", "This email is already registered")
# => {"success": False, "error": {"type": "DUPLICATE_SUBMISSION", "message": "..."}}
# Field validation errors
singleform_validation_error({"email": "Invalid format", "phone": "Required"})
# => {"success": False, "error": {"type": "VALIDATION_FAILED", "message": "...", "fields": {...}}}Configuration Options
| Option | Default | Description |
|---|---|---|
secret | — | Required. Your webhook secret from the SingleForm dashboard. |
timestamp_tolerance | 300 | Maximum age of a request in seconds before it’s rejected. |
Error Types
| Type | Description |
|---|---|
MISSING_HEADERS | One or more required headers are missing |
INVALID_TIMESTAMP | Timestamp header is not a valid integer |
TIMESTAMP_EXPIRED | Request is too old (replay attack prevention) |
SIGNATURE_MISMATCH | HMAC signature does not match |