Webhooks

Verify Your SimpleForm Webhook Signature Correctly

SimpleForm signs every webhook POST. Here is what that header protects against, and the five-step check that turns an unverified webhook into one you can trust.

· The SimpleForm Team

A webhook that fires from an unverified source is a webhook you cannot trust. If your Slack channel, Zapier automation, or internal ticketing system reacts to every POST that hits its URL, anyone who finds that URL can forge a submission and trigger the same action a real one would — a fake lead in your CRM, a bogus support ticket, a spam alert dropped into your team's inbox. SimpleForm is a hosted form backend for static websites: you point a plain HTML form at a SimpleForm endpoint, and it handles the email delivery, storage, spam filtering, and optional webhook calls, so you never write server code. Webhook signature verification is the one step that turns "a request arrived" into "a request I can trust," and it takes about ten lines of code to add.

What Is a Webhook Signature and Why Does It Matter?

A webhook signature is a small hash value that proves a request came from the sender it claims to come from, and that the body was not altered after the sender computed the hash. Without one, a webhook URL is just a bare, guessable string — whoever has it can pretend to be the real service.

This matters more than it looks like it should, because most teams do not treat webhook endpoints as a security boundary. A webhook that posts a message to Slack feels harmless. A webhook that creates a customer record, kicks off a paid Zapier chain, or opens a support ticket with a priority flag is not harmless at all, and that is exactly the kind of endpoint that ends up wired to a form submission event.

How Does SimpleForm Sign Its Webhook Requests?

SimpleForm computes the signature as an HMAC-SHA256 hex digest of the raw request body, using the signing secret you set when you attach a webhook to a form, and sends the result in an X-SimpleForm-Signature header on every delivery. The hashing method follows the HMAC specification, RFC 2104, so any standard HMAC-SHA256 implementation in your language works without a custom library.

The payload itself is plain JSON — an event field such as submission.created, a form object with the form's id and name, and a submission object holding the submission's id, created_at timestamp, and the raw form data. Webhooks are available starting on the Pro plan, which costs $9 a month for 5,000 submissions and up to five email recipients — see the full plan details if you're deciding which tier covers your traffic. The signing secret is optional on every webhook you create, which means an unsigned webhook is a choice you made, not a default you're stuck with.

The full payload shape, every response code your endpoint might return, and the rest of the webhook reference live in the SimpleForm docs — worth a read once, so your handler doesn't guess at field names.

How Do You Verify the Signature on Your Server?

Verification is five steps, and every step matters — skip the raw-body step and the rest fails silently.

  1. Read the raw request body as a string before any JSON parsing runs — hashing a re-serialized object almost never matches the exact bytes SimpleForm signed.
  2. Compute an HMAC-SHA256 digest of that raw body using your webhook's signing secret, and hex-encode the result.
  3. Read the X-SimpleForm-Signature header from the incoming request.
  4. Compare the two hex strings using a constant-time comparison function — hash_equals() in PHP, crypto.timingSafeEqual() in Node — never a plain ==, which leaks timing information an attacker can exploit.
  5. Reject the request with a non-200 status if the values don't match, and only parse and act on the body once they do.

None of this requires a webhook library. A signing secret, a hashing function that ships with your language, and a string comparison are the whole toolchain.

What Does a Verification Handler Look Like in Practice?

Here is the same five steps as a small Node.js endpoint. The part people get wrong is reading the body as raw bytes before any framework middleware parses it as JSON — parse first and the hash you compute will never match the one SimpleForm sent, because whitespace and key order can shift during re-serialization.

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signature)
  );
}

app.post('/webhooks/simpleform', express.raw({ type: '*/*' }), (req, res) => {
  const signature = req.get('X-SimpleForm-Signature') || '';
  if (!verifySignature(req.body, signature, process.env.SIMPLEFORM_SECRET)) {
    return res.status(400).send('bad signature');
  }
  const payload = JSON.parse(req.body);
  res.sendStatus(200);
});

The route uses express.raw() instead of express.json() so req.body arrives as an untouched buffer — exactly what the signature was computed against. Only after verifySignature() returns true do you call JSON.parse(req.body) and act on the submission data. A PHP handler follows the identical shape: read file_get_contents('php://input') before touching $_POST, hash it with hash_hmac('sha256', $rawBody, $secret), and compare with hash_equals() rather than ===.

What Happens If You Skip Verification?

Nothing happens, until it does. An unsigned webhook endpoint accepts any POST shaped like the real thing, and most attackers testing for open endpoints do not announce themselves — they just probe and move on to whatever accepts the payload.

Without signature verificationWith signature verification
Anyone who finds or guesses the webhook URL can trigger the same downstream action as a real submission.A forged POST fails the HMAC check and never reaches your automation logic.
A tampered payload — an edited email address, injected text — is processed as-is.Any change to the body after signing invalidates the hash, so tampered payloads are rejected.
You have no way to prove, after the fact, that a request actually came from SimpleForm.The signature is evidence you can point to when debugging or auditing a delivery.

SimpleForm's own spam layer catches a lot before a webhook ever fires — IP-based rate limiting caps submissions at 10 per IP per endpoint per hour and returns a 429 once that limit is hit, and the honeypot field filters most bots before they reach the dashboard at all. Signature verification is not a replacement for that layer; it is what protects the step after it, once a payload leaves SimpleForm and lands on code you wrote.

Isn't This Overkill for a Small Contact Form?

For a webhook that only pings a Slack channel with a copy of the message, honestly, no one will lose sleep over a forged entry. But the moment a webhook does something — creates a customer, updates a spreadsheet with real numbers attached, opens a ticket that pages a person — the ten extra lines of verification code cost you far less time than explaining an incident after the fact. Write the check once, on the endpoint that matters, and stop thinking about it.

Verify Your First Signed Webhook

Create a free SimpleForm account, add a webhook to a form, set a signing secret, and send yourself a test submission. Your endpoint sees the X-SimpleForm-Signature header on that very first delivery, so you can wire up verification once, confirm it rejects a mismatched signature, and stop worrying about what's actually hitting that URL.

Frequently asked questions

It is an HMAC-SHA256 hex digest of the raw webhook request body, computed using the signing secret you set on that webhook, and sent as a header on every delivery. Comparing it against a hash you compute yourself with the same secret confirms the request came from SimpleForm and the body was not changed in transit.

Verify any webhook that triggers an action beyond a simple notification, such as creating a record, updating a spreadsheet, or opening a ticket. A webhook that only posts a copy of the message to a Slack channel carries far less risk if left unverified, but the check is cheap enough to add everywhere.

Reject the request with a non-200 status before parsing or acting on the body. A mismatch means the payload either did not come from SimpleForm or was altered after SimpleForm sent it, so nothing downstream should treat it as a real submission.

Yes. Webhooks are available starting on the Pro plan, which also raises your submission ceiling to 5,000 a month and adds file uploads and an auto-responder. The Free plan delivers submissions by email and dashboard only, with no webhook option.

Yes. SimpleForm fires a standard POST with a JSON body and the signature header to any URL you configure, including a Zapier catch hook or a Discord webhook URL. Verification still applies if the receiving tool exposes a way to check the header; otherwise treat the convenience integration as unverified by design.

Ship a working form in five minutes. Point your form's action at a SimpleForm endpoint and submissions land in your inbox and dashboard straight away. Start free or read the docs.

More from the blog