FormDexSecure form operations
Try FormDex
FormDexSecure form operations

A product by MeshDex

FormDex is a secure form operations platform for collecting, validating, and routing website submissions. Manage protected endpoints, email notifications, webhook delivery, and submission history from one reliable workspace.

Product

PlatformPricingDocumentation

Access

LoginRegisterReset password

Legal

Privacy PolicyTerms of ServiceRefund PolicyContact Us
  1. Home
  2. Documentation
  3. Webhooks
Docs home

Webhook delivery

Receive FormDex events safely.

Forward accepted submissions to your backend, verify every request with HMAC, and design the receiver for retries and duplicate delivery.

When to use it

Server-to-server delivery

Use a webhook when a successful FormDex submission must create a CRM lead, start an internal workflow, update your own database, notify a helpdesk, or trigger another backend service. Email notifications do not require a webhook.

The signing secret belongs only on the receiving server. Never put it in browser JavaScript, React client components, HTML, a public repository, or a mobile application.

Platforms

Where a webhook receiver can run

PlatformRecommended approach
Node.jsExpress, Fastify, NestJS, or another backend route.
Next.jsAn App Router route handler or other server-only endpoint.
WordPressA small plugin with a WordPress REST API endpoint; verify in PHP.
PHPA server endpoint that reads the exact raw request body.
ReactNot in browser code. Use a server route, serverless function, or backend.
Zapier / MakeWebhook triggers work, but signature verification depends on the platform. Use a code step or secure proxy when possible.

Configure FormDex

URL, delivery toggle, and secret

  1. 1. Create an HTTPS receiver that accepts POST.
  2. 2. In Form Settings › Protection, enable Webhook delivery.
  3. 3. Paste the receiver URL revealed directly below the toggle and save.
  4. 4. Copy the newly generated one-time signing secret and store it in the receiver’s server environment as FORMDEX_WEBHOOK_SECRET.
  5. 5. Submit a real test, verify it in Submissions, and confirm the webhook delivery status.
FormDex generates a secret only when webhook delivery is enabled. Turning delivery off preserves the URL and an existing secret so delivery can be safely resumed later; a form that has never enabled webhooks has no webhook secret.

Authentication

Verify the exact raw body

FormDex signs timestamp + "." + exactRawRequestBody with HMAC-SHA256. Verify before parsing or changing the JSON. Reject timestamps older than five minutes and compare signatures in constant time.

X-FormDex-Event: submission.created
X-FormDex-Delivery-Id: delivery-id
X-FormDex-Timestamp: 1791624000
X-FormDex-Signature: sha256=<hex-hmac>
Do not verify re-serialized JSON. Whitespace and key ordering can change the bytes and produce a different signature.

Event contract

Submission payload

The payload object contains the accepted submitted fields, including enabled dynamic fields. When files were accepted, attachments contains safe metadata only; bytes and public download links are never sent. Treat new keys as forward-compatible additions and use event plus version when routing events.

{
  "event": "submission.created",
  "version": 1,
  "formId": "your-form-id",
  "submissionId": "submission-id",
  "createdAt": "2026-10-10T09:19:00.706Z",
  "payload": {
    "fullName": "Aarav Sharma",
    "email": "[email protected]",
    "message": "Please contact me",
    "customField": "Any enabled dynamic field is included here"
  },
  "attachments": [
    { "fileName": "brief.pdf", "mimeType": "application/pdf", "sizeBytes": 245760 }
  ]
}

Node.js / Express

Use a raw-body route

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/formdex", express.raw({ type: "application/json" }), (req, res) => {
  const rawBody = req.body.toString("utf8");
  const timestamp = req.get("x-formdex-timestamp") || "";
  const received = req.get("x-formdex-signature") || "";
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));

  if (!timestamp || !Number.isFinite(age) || age > 300) {
    return res.status(401).send("Stale webhook");
  }

  const digest = crypto
    .createHmac("sha256", process.env.FORMDEX_WEBHOOK_SECRET)
    .update(timestamp + "." + rawBody)
    .digest("hex");
  const expected = "sha256=" + digest;
  const valid = received.length === expected.length && crypto.timingSafeEqual(
    Buffer.from(received), Buffer.from(expected)
  );

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

  const event = JSON.parse(rawBody);
  // Deduplicate with x-formdex-delivery-id, persist work, then return quickly.
  res.status(204).end();
});

Next.js / React frameworks

Verify in server code

A React browser component is not a webhook receiver and cannot protect the signing secret. In Next.js, use a route handler; in other React stacks use the framework’s server or serverless-function layer.

import crypto from "node:crypto";

export async function POST(request) {
  const rawBody = await request.text();
  const timestamp = request.headers.get("x-formdex-timestamp") || "";
  const received = request.headers.get("x-formdex-signature") || "";
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!timestamp || !Number.isFinite(age) || age > 300) {
    return new Response("Stale webhook", { status: 401 });
  }

  const expected = "sha256=" + crypto
    .createHmac("sha256", process.env.FORMDEX_WEBHOOK_SECRET)
    .update(timestamp + "." + rawBody)
    .digest("hex");
  const valid = received.length === expected.length && crypto.timingSafeEqual(
    Buffer.from(received), Buffer.from(expected)
  );
  if (!valid) return new Response("Invalid signature", { status: 401 });

  const event = JSON.parse(rawBody);
  return new Response(null, { status: 204 });
}

PHP / WordPress

Register a REST endpoint in a plugin

Put this in a small custom plugin, not a theme file. Store the secret in server configuration rather than WordPress content or frontend settings.

add_action('rest_api_init', function () {
  register_rest_route('my-site/v1', '/formdex', [
    'methods' => 'POST',
    'permission_callback' => '__return_true',
    'callback' => function (WP_REST_Request $request) {
      $raw_body = $request->get_body();
      $timestamp = $request->get_header('x-formdex-timestamp');
      $received = $request->get_header('x-formdex-signature');

      if (!$timestamp || abs(time() - intval($timestamp)) > 300) {
        return new WP_REST_Response(['error' => 'Stale webhook'], 401);
      }

      $digest = hash_hmac(
        'sha256',
        $timestamp . '.' . $raw_body,
        getenv('FORMDEX_WEBHOOK_SECRET')
      );
      $expected = 'sha256=' . $digest;
      if (!hash_equals($expected, $received ?: '')) {
        return new WP_REST_Response(['error' => 'Invalid signature'], 401);
      }

      $event = json_decode($raw_body, true, 512, JSON_THROW_ON_ERROR);
      // Deduplicate by X-FormDex-Delivery-Id and enqueue durable local work.
      return new WP_REST_Response(null, 204);
    },
  ]);
});

Reliability

Acknowledge quickly and expect retries

  • • Return a 2xx response only after the event is durably accepted.
  • • Deduplicate using X-FormDex-Delivery-Id; the same event can be retried.
  • • Queue slow CRM/API work locally and respond quickly.
  • • Treat 4xx configuration errors as permanent; temporary failures and timeouts may be retried.
  • • Log delivery IDs and status, but never log the signing secret or unnecessary customer payloads.

Production checklist

  • • HTTPS endpoint
  • • Exact raw-body HMAC verification
  • • Five-minute timestamp tolerance
  • • Constant-time comparison
  • • Delivery-ID deduplication
  • • Fast 2xx acknowledgement

Secret handling

Keep the signing secret in a server-side secret manager or environment variable. If it is exposed, replace it in Form Settings and update the receiver together.

Submitting from WordPress?

The outbound FormDex connector and an inbound webhook receiver are separate integrations.

Open WordPress submission guide