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-webhookBasic 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
})| Option | Type | Default | Description |
|---|---|---|---|
secret | string | — | Required. Your webhook secret from the SingleForm dashboard. Starts with sf_secret_. |
timestampTolerance | number | 300 | Maximum age of a request in seconds before it’s rejected. |
debug | boolean | false | Log validation steps to the console for troubleshooting. |
onError | function | — | Custom 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,
},
});
},
});