Integration
Verify Maxforms webhook signatures before trusting events
Verify a Maxforms form webhook with its raw body, Webhook Secret, and three signature headers before processing the submission.
Which webhook does this example verify?
This example applies to the Webhook integration on a form’s Integrate tab. It follows the Standard Webhooks signature format.
API webhook subscriptions use X-Maxforms-Signature and a different signing string. Use the separate API webhook verification guide for those deliveries.
Headers required for verification
| Header | Meaning | How the receiver uses it |
|---|---|---|
| webhook-id | Message identifier | Include it in the signed content |
| webhook-timestamp | Unix timestamp in seconds | Reject stale or future requests outside your tolerance |
| webhook-signature | v1, followed by a base64 HMAC-SHA256 signature | Compare against the locally computed signature |
Build the signature from the original bytes
The signed input is {webhook-id}.{webhook-timestamp}.{raw request body}. Do not parse and re-serialize JSON before checking it.
When the secret starts with whsec_, decode the remaining base64 string into key bytes. A custom secret without that prefix uses its literal bytes.
The example below rejects requests more than five minutes from the server clock. Keep the clock synchronized and reject invalid signatures before applying any business change.
Working Node.js verification example
This function uses Node.js built-in crypto. Pass the raw body as a Buffer and lowercase header names from your HTTP server.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyMaxformsWebhook(secret, rawBody, headers) {
const id = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (typeof secret !== 'string' || !secret || !Buffer.isBuffer(rawBody)) {
return false;
}
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
typeof signatures !== 'string' || !/^\d+$/.test(timestamp)) {
return false;
}
const sentAt = Number(timestamp);
if (!Number.isSafeInteger(sentAt) ||
Math.abs(Math.floor(Date.now() / 1000) - sentAt) > 300) {
return false;
}
let key = Buffer.from(secret, 'utf8');
if (secret.startsWith('whsec_')) {
const encoded = secret.slice(6);
key = Buffer.from(encoded, 'base64');
if (!key.length || key.toString('base64') !== encoded) {
return false;
}
}
const expected = createHmac('sha256', key)
.update(`${id}.${timestamp}.`)
.update(rawBody)
.digest();
return signatures.split(/\s+/).some((signature) => {
const match = /^v1,([A-Za-z0-9+/]+={0,2})$/.exec(signature);
if (!match) {
return false;
}
const received = Buffer.from(match[1], 'base64');
return received.toString('base64') === match[1] &&
received.length === expected.length &&
timingSafeEqual(received, expected);
});
}
Call verifyMaxformsWebhook(process.env.MAXFORMS_WEBHOOK_SECRET, rawBody, request.headers) before parsing the body. On false, reject the request. Never hardcode the real secret into source control.
A valid signature does not prevent duplicate work
After verification, accept the event safely and return a successful response promptly. Queue longer work if the receiving system needs it.
Deduplicate the intended business action using the submission identifier and event type. Keep a durable record so a retry cannot create a second order or notification.
The timestamp check limits old signed requests. It does not make requests unique within that time window.
Test the receiver before relying on it
- 01
Send a valid test
Use Send Test on the form’s Webhook integration. Verify that your receiver accepts the signed payload.
- 02
Alter the body
Change one byte in a captured test body without changing its headers. The receiver must reject it.
- 03
Try stale and missing headers
Use an expired timestamp or remove a signature header. Both requests must fail verification.
- 04
Deliver the same event twice
Confirm your application accepts a retry safely without repeating the business action.
Follow the current setup contract
form webhook setup guide includes the captured payload and delivery behavior. The Standard Webhooks specification documents the signing format.
Questions
Frequently asked questions
01Can I verify the request after parsing and rebuilding the JSON?
No. Verify the original raw body bytes. Parsing and re-serializing JSON can change whitespace or field order and invalidate the signature.
02Is the Webhook Secret the same as an API token?
No. Use the secret configured under Sign Requests in the form’s Webhook integration. Keep it on your receiving server.
03Does a valid signature mean I can process a delivery twice?
No. Verify the signature, then deduplicate the business action. A legitimate retry can carry valid authentication.
Start free
Ready to build smarter online forms?
Create secure online forms, automate submissions, collect payments, and launch faster with Maxforms, the powerful alternative to Jotform.