Webhooks

Webhooks let you receive each form submission on your own server in real time. Use them to sync submissions to a CRM, post to Slack, trigger workflows, or store data in your own database. Configure webhooks in Form → Integrations in the dashboard.

How it works

When a visitor submits your form, W3Forms processes the submission and enqueues a webhook delivery job. The webhook worker sends an HTTP POST request to your configured URL with the submission data as a JSON body.

  1. Visitor submits form → W3Forms stores the submission.
  2. If the submission is not spam, W3Forms enqueues a webhook job.
  3. The webhook worker POSTs the JSON payload to your URL with an X-W3Forms-Signature header.
  4. Your server verifies the signature and processes the payload.

Spam submissions are stored in your dashboard but do not trigger webhook delivery. This prevents your server from receiving junk data.

Webhook payload

The POST body is a JSON object containing all form fields submitted by the visitor. The Content-Type header is application/json. Example payload:

{
  "name": "Ada Lovelace",
  "email": "ada@example.com",
  "message": "Hello from my website!",
  "_w3forms_meta": {
    "form_id": "uuid-here",
    "submitted_at": "2026-04-01T12:00:00Z",
    "ip": "203.0.113.42"
  }
}

The _w3forms_meta object is added by W3Forms and includes the form ID, submission timestamp, and the submitter's IP address.

Signature verification

Every webhook request is signed with HMAC-SHA256. We compute HMAC-SHA256(raw_body, webhook_secret) and send the hex digest in the X-W3Forms-Signature header. Your server must verify this signature before processing the payload.

Important: verify against the raw request body (the exact bytes received), not a re-serialized JSON object. JSON serialization can change key order or whitespace, which invalidates the signature.

express.webhook.js
import express from "express";
import crypto from "node:crypto";

const app = express();

// Capture raw body for signature verification.
app.post("/w3forms", express.raw({ type: "*/*" }), (req, res) => {
  const secret = process.env.W3FORMS_WEBHOOK_SECRET;
  const signature = req.header("X-W3Forms-Signature") ?? "";
  const rawBody = req.body.toString("utf8");

  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");

  const ok =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));

  if (!ok) return res.status(401).send("Invalid signature");

  const payload = JSON.parse(rawBody);
  // TODO: handle payload (store, forward, etc.)
  return res.json({ received: true });
});

app.listen(3000);

Use constant-time comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks. Never use === for signature comparison.

Setup steps

  1. Go to Form → Integrations in the dashboard.
  2. Add your webhook URL (must be HTTPS in production).
  3. Copy the webhook secret — store it as an environment variable on your server (e.g., W3FORMS_WEBHOOK_SECRET).
  4. Implement signature verification on your endpoint (see examples above).
  5. Test by submitting a form and checking your server logs.

Troubleshooting webhooks

  • Signature mismatch — Ensure you verify the raw body bytes, not parsed JSON. Check that your secret matches the one in the dashboard.
  • Not receiving requests — Verify your URL is publicly accessible (not behind a firewall or localhost). Check that the form is not being flagged as spam.
  • Timeouts — Your webhook endpoint should respond within 10 seconds. For long-running processing, acknowledge the webhook immediately and process asynchronously.
  • HTTPS required — Webhook URLs must use HTTPS in production for security. HTTP is allowed for localhost during development.

Next steps