Webhook Security
Every webhook request from SingleForm is cryptographically signed so your server can verify that it came from SingleForm and isn’t a replay of an earlier request.
The signature authenticates the sender, not the body. It is computed over
formId.timestamp.nonce, so verifying it proves the caller holds your webhook secret and that the request isn’t a replay. It is not a checksum of the payload — don’t treat a valid signature as proof the body is unmodified. HTTPS is what protects the payload in transit.
Webhook Headers
Each request includes 4 headers:
| Header | Description | Example |
|---|---|---|
X-SingleForm-Signature | HMAC-SHA256 hex digest | a3f2e1b9c8d7... |
X-SingleForm-Timestamp | Unix timestamp in seconds | 1706400000 |
X-SingleForm-Nonce | Unique request identifier (32-character hex string) | a1b2c3d4e5f6a7b8... |
X-SingleForm-Form-Id | The form’s unique ID | d4e5f6a7-b8c9-... |
How Signature Verification Works
The signature is computed as follows:
- Construct the payload string:
{formId}.{timestamp}.{nonce} - Compute
HMAC-SHA256(payload, your_webhook_secret) - The result is a hex-encoded digest
To verify an incoming request:
- Extract all 4 headers
- Check the timestamp is within 5 minutes of the current time (prevents replay attacks)
- Reconstruct the payload:
{formId}.{timestamp}.{nonce} - Compute the HMAC-SHA256 of the payload using your webhook secret
- Compare the computed signature with
X-SingleForm-Signatureusing a timing-safe comparison
Manual Verification Example (Node.js)
If you’re not using the Express middleware, here’s how to verify signatures manually:
import crypto from "crypto";
function verifyWebhook(req) {
const signature = req.headers["x-singleform-signature"];
const timestamp = req.headers["x-singleform-timestamp"];
const nonce = req.headers["x-singleform-nonce"];
const formId = req.headers["x-singleform-form-id"];
// Check all headers are present
if (!signature || !timestamp || !nonce || !formId) {
return false;
}
// Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false;
}
// Compute expected signature
const payload = `${formId}.${timestamp}.${nonce}`;
const expected = crypto
.createHmac("sha256", process.env.SINGLEFORM_SECRET)
.update(payload)
.digest("hex");
// Timing-safe comparison
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
} catch {
return false;
}
}Timestamp Tolerance
By default, requests older than 300 seconds (5 minutes) are rejected. This prevents replay attacks where an attacker captures a valid request and resends it later.
If you’re using the Express middleware, you can configure the tolerance:
singleform({
secret: process.env.SINGLEFORM_SECRET,
timestampTolerance: 600, // Allow 10 minutes instead of 5
});Security Error Types
When signature verification fails, the middleware returns one of these errors:
| Error Type | Status | When It Occurs |
|---|---|---|
MISSING_HEADERS | 401 | One or more required headers are absent |
INVALID_TIMESTAMP | 401 | Timestamp is not a valid number |
TIMESTAMP_EXPIRED | 401 | Request is older than the tolerance window |
INVALID_SIGNATURE | 401 | Signature format is invalid (wrong length) |
SIGNATURE_MISMATCH | 401 | Computed signature doesn’t match the header |
All security errors follow this format:
{
"success": false,
"error": {
"type": "SIGNATURE_MISMATCH",
"message": "Signature verification failed. Check your webhook secret."
}
}Webhook Secret Format
Your webhook secret:
- Starts with the prefix
sf_secret_ - Followed by a 64-character hex string
- Example:
sf_secret_a1b2c3d4e5f6...
Generate or regenerate your secret in the SingleForm dashboard under Settings → Webhook Security.
Store your webhook secret in an environment variable. Never commit it to source code or expose it in client-side code.
Best Practices
- Always use HTTPS for your webhook endpoint
- Store secrets in environment variables, never in source code
- Use the Express SDK when possible — it handles all verification automatically
- Monitor for
SIGNATURE_MISMATCHerrors — frequent mismatches may indicate your secret needs to be rotated - Don’t disable timestamp validation — it’s your primary defense against replay attacks