Gareth Wilson Gareth Wilson

Guide to OpnForm Webhooks: Features and Best Practices

Published


OpnForm is an open-source form builder, used on its hosted service or self-hosted, for surveys, sign-up forms, contact forms and the like. Its webhook integration sends each new submission to a URL you choose, which is how teams feed responses into a CRM, a ticketing tool or their own database without exporting spreadsheets.

This guide covers adding the webhook integration, the submission payload with its two views of the same answers, enabling and verifying X-Webhook-Signature, and what OpnForm does when a delivery fails.

What are OpnForm webhooks?

Webhooks in OpnForm are an integration on an individual form, added in the form's Integrations settings or through the API. Each integration has an HTTPS URL, an optional signing secret, up to 10 custom headers and, optionally, conditional logic so it only fires for certain answers.

Every submission produces one POST. There are no event types to choose from. OpnForm logs each delivery attempt against the integration, and the latest eligible failed attempt can be retried through the API.

OpnForm webhook features

FeatureDetails
Signaturehex HMAC-SHA256
Signature headerx-webhook-signature
BodyJSON

From OpnForm's webhook integration reference:

  • webhook_url must be HTTPS and resolve only to public IP addresses. Private, loopback, link-local and cloud metadata addresses are refused.
  • Signing happens only when webhook_secret is set. It must be at least 12 characters.
  • Custom headers can't override Authorization, X-Webhook-Signature, Content-Type, Host, Cookie and some others.
  • Automatic retries, a timeout and expected status codes aren't documented.

Events and payloads

FieldDescription
form_id, form_title, form_slugThe form that received the submission
submission_idSubmission id; use it for idempotency
edit_linkLink to edit the submission
submissionField name to submitted value
dataField id to { value, name, type }
metaExtra context such as attribution UTM values

submission and data describe the same answers in two ways: submission is keyed by the field's label, which is easy to read but changes if someone renames a question, while data is keyed by field ID and includes each field's type. Use data for anything that must survive edits to the form.

This is the submission sample from the webhook samples library:

{
  "form_id": 123,
  "form_title": "Contact Form",
  "form_slug": "contact-form",
  "submission_id": 456,
  "edit_link": "https://opnform.com/forms/contact-form?submission_id=example",
  "submission": {
    "Name": "Jane Doe"
  },
  "data": {
    "field-id": {
      "value": "Jane Doe",
      "name": "Name",
      "type": "text"
    }
  },
  "meta": {
    "attribution": {
      "utm_source": "newsletter",
      "utm_campaign": "summer-launch"
    }
  }
}

meta.attribution carries UTM values captured with the submission, which is useful for lead attribution. Every value in submission and data was typed by the person filling in the form, so validate types and lengths before use.

Setting up OpnForm webhooks

  1. Open the form, go to Integrations and add a Webhook integration.
  2. Enter your HTTPS endpoint, for example https://example.com/webhooks/opnform.
  3. Enter a webhook secret. OpnForm doesn't generate one, so create a random string of at least 12 characters and store it as OPNFORM_WEBHOOK_SECRET.
  4. Optionally add custom headers or conditional logic, then save.

Set a webhook secret on the OpnForm webhook integration. Without one, deliveries are unsigned and fail verification.

The API equivalent:

curl -X POST 'https://api.opnform.com/open/forms/123/integrations' \
  -H 'Authorization: Bearer YOUR_PAT' \
  -H 'Content-Type: application/json' \
  -d '{
    "integration_id": "webhook",
    "status": "active",
    "data": {
      "webhook_url": "https://example.com/webhooks/opnform",
      "webhook_secret": "a-random-secret-of-12-plus-characters"
    }
  }'

To use Hookdeck, create an OpnForm source with the same secret and use the source URL as webhook_url. If an integration has no secret, leave verification off on the source, since OpnForm sends no signature. Because private addresses are rejected, local testing needs a public tunnel:

npx hookdeck-cli listen 3000 opnform --path /webhooks/opnform

Securing OpnForm webhooks

X-Webhook-Signature is sha256= followed by a hex HMAC-SHA256 of the raw body, keyed with your secret. Strip the prefix, compute the HMAC over the received bytes and compare in constant time. OpnForm's signature guide passes two strings to crypto.timingSafeEqual, which throws in Node.js because it needs buffers. The function below compares decoded bytes instead.

Node.js verification

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-webhook-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature.startsWith('sha256=') ? signature.slice(7) : signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler stores the submission by submission_id, using the field IDs from data:

const express = require('express');

const app = express();

app.post('/webhooks/opnform', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.OPNFORM_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return res.sendStatus(400);
  try {
    const answers = Object.entries(payload.data ?? {}).map(([fieldId, field]) => ({
      fieldId,
      label: field?.name,
      type: field?.type,
      value: field?.value,
    }));

    await saveFormResponse(payload.form_id, payload.submission_id, answers, payload.meta ?? {});
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

saveFormResponse ignores a submission_id it already has for that form, which covers a manual retry of the same delivery.

No timestamp is signed. Hookdeck's OpnForm source checks the same sha256= HMAC, responds 200 and records a mismatch as failed verification.

Make OpnForm webhooks production-ready. Hookdeck Event Gateway verifies each OpnForm submission, stores it, and retries delivery to your CRM automatically instead of waiting for someone to retry a failed send by hand.

OpnForm webhook limitations and pain points

Retries are manual

The Problem: A submission sent while your endpoint was down may never arrive unless someone notices.

Why It Happens: OpnForm documents logging every attempt and retrying the latest eligible failed attempt through the API. It doesn't describe automatic retries.

Workarounds:

  • Check the integration's event log (List Integration Events) after any outage.
  • Script the retry endpoint, and compare with the form's submissions in OpnForm to find gaps.

How Hookdeck Can Help: Hookdeck accepts the submission and retries your service automatically, with alerts and bulk replay.

Signing is optional

The Problem: An integration created without a secret sends unsigned requests that anyone could imitate.

Why It Happens: webhook_secret is optional, and OpnForm doesn't generate one for you.

Workarounds:

  • Always set a random secret of at least 12 characters.
  • Reject requests without X-Webhook-Signature.

How Hookdeck Can Help: With verification on, a Hookdeck source records unsigned requests as failed verification, so a missing secret shows up immediately.

Labels change

The Problem: Renaming a question breaks code that reads submission by label.

Why It Happens: submission is keyed by field name, which form editors can change at any time.

Workarounds:

  • Map answers by field ID from data.
  • Keep a mapping of field IDs to your own column names.

How Hookdeck Can Help: A Hookdeck transformation can reshape data into the structure each destination expects, in one place.

Only public endpoints

The Problem: You can't point a webhook at an internal service, even from a self-hosted OpnForm.

Why It Happens: OpnForm rejects URLs that resolve to private, loopback, link-local or metadata addresses.

Workarounds:

  • Expose a public HTTPS receiver that forwards internally.
  • Use a tunnel for development.

How Hookdeck Can Help: Hookdeck provides the public URL and delivers onwards to your service, or to localhost through the CLI.

Best practices

Deduplicate on submission_id

Store form_id and submission_id with a unique constraint, following the webhook idempotency guide.

Respond fast, process later

With no documented timeout or retries, a quick 200 is the safest response. The asynchronous processing guide explains the pattern.

Use conditional logic

Fire the webhook only for submissions that need it, such as qualified leads, rather than filtering everything downstream.

Use custom headers for routing, not authentication

A custom header can carry an API key for a gateway, but the signature is what proves OpnForm sent the request.

Conclusion

OpnForm webhooks send one signed POST per form submission, once you set a secret on the integration. Verify X-Webhook-Signature against the raw body, store answers by field ID and submission_id, and plan for failed deliveries, since OpnForm documents only manual retries.

Hookdeck Event Gateway adds the automatic retries and storage that OpnForm leaves to you.

Get started with Hookdeck for free and handle OpnForm webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.