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

HeaderMeaningHow the receiver uses it
webhook-idMessage identifierInclude it in the signed content
webhook-timestampUnix timestamp in secondsReject stale or future requests outside your tolerance
webhook-signaturev1, followed by a base64 HMAC-SHA256 signatureCompare 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

  1. 01

    Send a valid test

    Use Send Test on the form’s Webhook integration. Verify that your receiver accepts the signed payload.

  2. 02

    Alter the body

    Change one byte in a captured test body without changing its headers. The receiver must reject it.

  3. 03

    Try stale and missing headers

    Use an expired timestamp or remove a signature header. Both requests must fail verification.

  4. 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

01

Can 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.

02

Is 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.

03

Does 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.