Skip to Content
MiddlewarePython Middleware

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

OptionDefaultDescription
secretRequired. Your webhook secret from the SingleForm dashboard.
timestamp_tolerance300Maximum age of a request in seconds before it’s rejected.

Error Types

TypeDescription
MISSING_HEADERSOne or more required headers are missing
INVALID_TIMESTAMPTimestamp header is not a valid integer
TIMESTAMP_EXPIREDRequest is too old (replay attack prevention)
SIGNATURE_MISMATCHHMAC signature does not match