Verify signed webhooks
Verify Qubeforms webhook signatures with timestamped HMAC-SHA256 checks. Protect signing secrets, authenticate request bodies, and handle duplicate events and retries.
1. Connect a webhook receiver
Starter, Pro, and Business can send submission notifications to your server. As the form owner, open Settings → Delivery & spam, enter a Webhook URL and New webhook signing secret, then select Save delivery settings. The endpoint must be a public HTTPS URL without embedded credentials. Localhost, private-network destinations, and redirects are not supported. Use the final destination URL directly. For development, use a public HTTPS test receiver that you control.
2. Set and protect the signing secret
Choose a separate random secret of 32–256 characters and configure the same value on your receiver. This secret signs webhooks; it is not the public form key, the read key, or the management API key. Store it only in private server configuration. Leaving the secret field blank when saving retains the existing secret. To rotate it, update both systems together; deliveries use the secret configured when they are sent. Clear the Webhook URL and save to disable future webhooks.
3. Read the event
Qubeforms sends a JSON POST with event type submission.created. The id is the submission ID, form identifies the form, and data contains the submitted fields. The payload does not contain attachment download links; retrieve those through the form read API if needed. Notifications are asynchronous, and delivery can be retried. Use the submission ID to recognize duplicates instead of assuming each incoming request is a new submission.
{
"id": "EXAMPLE_SUBMISSION_ID",
"type": "submission.created",
"created_at": "2026-09-10 12:00:00",
"form": {"id": "EXAMPLE_FORM_ID", "name": "Contact"},
"data": {"email": "alex@example.com", "message": "Please contact me."}
}4. Verify before parsing or processing
Read X-Form-Timestamp and X-Form-Signature from the request headers. The signature is v1= followed by the hexadecimal HMAC-SHA256 of the timestamp, a period, and the exact raw body bytes. Preserve the raw body before your framework’s JSON parser runs. Do not parse and re-encode it for verification. The Node.js helper below rejects malformed signatures, uses a constant-time comparison, and applies a five-minute clock tolerance as a receiver-side policy. Keep your server clock accurate.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyQubeformsWebhook(rawBody, timestamp, signature, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || secret.length < 32) {
return false;
}
if (typeof timestamp !== 'string' || !/^\d{10}$/.test(timestamp)) return false;
if (typeof signature !== 'string' || !/^v1=[a-f0-9]{64}$/.test(signature)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (age > 300) return false;
const expected = createHmac('sha256', secret)
.update(timestamp + '.')
.update(rawBody)
.digest();
const provided = Buffer.from(signature.slice(3), 'hex');
return provided.length === expected.length && timingSafeEqual(provided, expected);
}5. Acknowledge safely and handle duplicates
After verification, parse the JSON, check the expected form and event type, and durably record or enqueue the work. Return a 2xx response promptly. Qubeforms waits up to 8 seconds for a webhook request and treats non-2xx responses as failures. If you already accepted an event ID, acknowledge the duplicate without repeating its business action. Make deduplication atomic in your database or queue. Signature validation establishes authenticity; it does not prevent the same valid event from being delivered twice.
6. Troubleshoot and retry
Inspect the submission’s delivery status and last error. Signature mismatches usually mean the wrong secret, altered raw bytes, a missing header, or an inaccurate server clock. Check public DNS, HTTPS, firewalls, and the receiver’s response time for connection failures. Webhook redirects are not followed. Failed sends are retried with increasing delays, up to 6 attempts by default. A retry gets a new signing timestamp. If the form is paused, the plan loses webhook access, or the configured endpoint changes before a queued send, that delivery can be cancelled. See Delivery status and retries for the status meanings.
7. Test the complete flow
Submit a real test form and compare the parsed event with the inbox record. Test a changed body, a wrong signature, and an old timestamp; your receiver should reject all three. Test an intentional receiver failure and verify a retry reaches you without creating duplicate downstream work. Never send your webhook secret in the response, browser JavaScript, a query string, or logs.
Public form keys can appear in browser code. Read keys, management credentials, and webhook signing secrets must stay on the server.