Skip to Content
MiddlewareExpress Middleware SDK

Express Middleware SDK

@singleform/express-webhook is the official Express middleware for verifying SingleForm webhook signatures. It handles signature verification, timestamp validation, and provides typed response helpers.

Installation

npm install @singleform/express-webhook

Basic Usage

import express from "express"; import { singleform } from "@singleform/express-webhook"; const app = express(); app.use(express.json()); app.post( "/webhooks/singleform", singleform({ secret: process.env.SINGLEFORM_SECRET }), (req, res) => { const { formId } = req.singleform; const { email, firstName, lastName } = req.body; res.singleformSuccess({ submissionId: "12345", message: "Thank you for your submission!", }); } ); app.listen(3000);

Configuration

singleform({ secret: string, // Required: Your webhook secret timestampTolerance?: number, // Max age in seconds (default: 300) debug?: boolean, // Enable debug logging (default: false) onError?: (error, req, res) => {} // Custom error handler })
OptionTypeDefaultDescription
secretstringRequired. Your webhook secret from the SingleForm dashboard. Starts with sf_secret_.
timestampTolerancenumber300Maximum age of a request in seconds before it’s rejected.
debugbooleanfalseLog validation steps to the console for troubleshooting.
onErrorfunctionCustom handler for validation errors. Receives (error, req, res).

Response Helpers

After verification, the middleware attaches three helper methods to the Express res object.

res.singleformSuccess(data?)

Send a success response.

// Simple success res.singleformSuccess(); // With submission ID (recommended) res.singleformSuccess({ submissionId: "order-789", }); // With custom message shown to the user res.singleformSuccess({ submissionId: "user-456", message: "Welcome! Check your email for confirmation.", }); // With additional data res.singleformSuccess({ submissionId: "ticket-123", message: "Registration confirmed", ticketNumber: "T-2024-123", eventDate: "2024-03-15", });

res.singleformError(type, message, statusCode?)

Send a business logic error (not field-specific).

// Duplicate submission res.singleformError( "DUPLICATE_SUBMISSION", "This email is already registered for this event" ); // Rate limiting res.singleformError( "RATE_LIMITED", "Too many submissions. Please try again in 5 minutes.", 429 ); // Custom business rule res.singleformError( "EVENT_FULL", "Sorry, this event has reached maximum capacity" );

res.singleformValidationError(fields, message?, statusCode?)

Send field-specific validation errors. SingleForm highlights the invalid fields in the mobile app.

// Single field error res.singleformValidationError({ email: "Invalid email format", }); // Multiple field errors res.singleformValidationError({ email: "Email is required", phone: "Phone number must be 10 digits", age: "Must be 18 or older", }); // With custom message res.singleformValidationError( { email: "Invalid format" }, "Please check your email address" );

Request Metadata

After successful verification, the middleware attaches metadata to req.singleform:

{ formId: string; // The form ID from SingleForm timestamp: number; // Unix timestamp of the request nonce: string; // Unique nonce for this request signature: string; // The HMAC signature verified: true; // Always true if middleware passes }

Common Patterns

Basic Validation

app.post( "/webhooks/singleform", singleform({ secret: process.env.SINGLEFORM_SECRET }), async (req, res) => { const { email, firstName, lastName } = req.body; const errors = {}; if (!email || !email.includes("@")) { errors.email = "Valid email is required"; } if (!firstName || firstName.length < 2) { errors.firstName = "First name must be at least 2 characters"; } if (!lastName || lastName.length < 2) { errors.lastName = "Last name must be at least 2 characters"; } if (Object.keys(errors).length > 0) { return res.singleformValidationError(errors); } const submissionId = await saveToDatabase({ email, firstName, lastName }); res.singleformSuccess({ submissionId }); } );

Duplicate Detection

app.post( "/webhooks/singleform", singleform({ secret: process.env.SINGLEFORM_SECRET }), async (req, res) => { const { email } = req.body; const existing = await db.findUserByEmail(email); if (existing) { return res.singleformError( "DUPLICATE_SUBMISSION", "This email is already registered" ); } const user = await db.createUser(req.body); res.singleformSuccess({ submissionId: user.id, message: "Welcome! Check your email for confirmation.", }); } );

Rate Limiting

import rateLimit from "express-rate-limit"; const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, handler: (req, res) => { res.singleformError( "RATE_LIMITED", "Too many submissions. Please try again later.", 429 ); }, }); app.post( "/webhooks/singleform", limiter, singleform({ secret: process.env.SINGLEFORM_SECRET }), async (req, res) => { res.singleformSuccess({ submissionId: "123" }); } );

Error Handling with Try-Catch

app.post( "/webhooks/singleform", singleform({ secret: process.env.SINGLEFORM_SECRET }), async (req, res) => { try { const { email, firstName } = req.body; const result = await processSubmission({ email, firstName }); res.singleformSuccess({ submissionId: result.id, message: "Submission received successfully", }); } catch (error) { console.error("Submission processing error:", error); res.singleformError( "PROCESSING_ERROR", "Unable to process your submission. Please try again.", 500 ); } } );

TypeScript Support

The package exports full TypeScript types:

import { Request, Response } from "express"; import { singleform, SingleFormRequest, SingleFormResponse, } from "@singleform/express-webhook"; app.post( "/webhooks/singleform", singleform({ secret: process.env.SINGLEFORM_SECRET }), (req: Request, res: Response) => { // Access verified metadata const { formId, timestamp } = (req as SingleFormRequest).singleform; // Use response helpers with full IntelliSense (res as SingleFormResponse).singleformSuccess({ submissionId: "123", }); } );

Exported Types

import { singleform, // Middleware factory verifySignature, // Manual signature verification helper SingleFormConfig, // Configuration options SingleFormError, // Error class SingleFormMetadata, // req.singleform type SingleFormRequest, // Extended Express Request SingleFormResponse, // Extended Express Response SingleFormMiddleware, // Middleware function type SingleFormErrorType, // Error type union SingleFormFieldErrors, // Field validation errors SingleFormSuccessData, // Success response data SingleFormSuccessResponse,// Full success response SingleFormErrorResponse, // Full error response } from "@singleform/express-webhook";

Debug Mode

Enable debug logging to troubleshoot webhook issues:

singleform({ secret: process.env.SINGLEFORM_SECRET, debug: true, });

Output:

🔍 [SingleForm] Validating webhook request 📋 [SingleForm] Headers received: { formId, timestamp, nonce, signature } ⏰ [SingleForm] Timestamp valid (age: 2s) 🔐 [SingleForm] Verifying signature... ✅ [SingleForm] Signature verified successfully!

Testing

Use the verifySignature helper for unit tests:

import { verifySignature } from "@singleform/express-webhook"; const isValid = verifySignature( "form-id-123", // formId "1234567890", // timestamp "nonce-abc", // nonce "signature-hex", // signature to verify "your-secret" // webhook secret );

Custom Error Handler

Override the default error response format:

singleform({ secret: process.env.SINGLEFORM_SECRET, onError: (error, req, res) => { // Log to your monitoring service logger.warn("Webhook verification failed", { type: error.type, message: error.message, ip: req.ip, }); // Send custom response res.status(error.statusCode).json({ success: false, error: { type: error.type, message: error.message, }, }); }, });