Docs

Webhooks

Set a webhook URL for a form and CalcForms sends an HTTPS POST for every accepted submission, with the inputs and every calculated result.

Setting one up

Open the form in your dashboard and choose Webhook. Enter the URL and, if you want to verify deliveries, a secret. The URL must be public and use HTTPS; addresses on private or internal networks are refused. Test it by submitting the form once. You can pause a webhook without deleting it.

The request

One request per accepted submission, as JSON:

{
  "event": "submission.created",
  "form_id": 105,
  "form_title": "Fence quote",
  "form_slug": "fence-quote",
  "submission_id": 4821,
  "submitted_at": "2026-09-08T14:02:11.000Z",
  "submitter_email": "customer@example.com",
  "source": "self_serve",
  "inputs": { "yard_size": "Average yard", "fence_type": "Wood privacy", "gates": 1 },
  "calculated": { "total": 4800 }
}
FieldMeaning
eventAlways submission.created today.
sourceself_serve for the public link, invite for a private invite, internal for the owner's internal view.
submitter_emailThe email the form collected, or the invited customer's email. Null or absent when the form did not collect one.
inputsEvery answer, keyed by field id.
calculatedEvery calculated field, keyed by field id, including the ones marked private. The webhook is for systems you control; it is not filtered the way the customer's page is.

Headers on every request:

HeaderValue
Content-Typeapplication/json
User-AgentCalcForms-Webhook/1.0
X-CalcForms-EventThe event name, submission.created.
X-CalcForms-SignaturePresent when you set a secret. sha256=<hex>: an HMAC-SHA256 of the raw request body, keyed with your secret.

Delivery

Delivery is a single attempt with a five-second timeout. There are no retries, so return a 2xx as soon as you have stored the body, and do your processing afterwards. A slow or failing endpoint never blocks or fails the submission itself; the submission is already saved in your dashboard before the webhook is sent.

Verifying the signature

Compute the HMAC over the exact bytes you received, before any parsing or re-serialisation, and compare it to the header in constant time. Reject the request if they differ.

Node (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.CALCFORMS_WEBHOOK_SECRET;

// express.raw keeps the body as the bytes CalcForms sent, which is what the signature covers.
app.post('/calcforms', express.raw({ type: 'application/json' }), (req, res) => {
  const given = req.get('X-CalcForms-Signature') || '';
  const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
  const ok = given.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
  if (!ok) return res.status(401).end();

  res.status(200).end(); // answer first: one attempt, five-second timeout
  const submission = JSON.parse(req.body);
  // your processing here
});

Python (Flask)

import hashlib, hmac, os
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["CALCFORMS_WEBHOOK_SECRET"].encode()

@app.post("/calcforms")
def calcforms():
    given = request.headers.get("X-CalcForms-Signature", "")
    expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(given, expected):
        abort(401)
    submission = request.get_json(force=True)
    # your processing here; keep it short or hand off to a queue
    return "", 200

Sending a test request yourself

To exercise your endpoint without submitting a form, sign a body with the same secret and send it:

SECRET='your-webhook-secret'
BODY='{"event":"submission.created","form_id":105,"submission_id":1,"inputs":{},"calculated":{}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -X POST https://example.com/calcforms \
  -H "Content-Type: application/json" \
  -H "User-Agent: CalcForms-Webhook/1.0" \
  -H "X-CalcForms-Event: submission.created" \
  -H "X-CalcForms-Signature: sha256=$SIG" \
  --data-binary "$BODY"

Using Zapier, Make or n8n? Point the webhook at a catch-hook URL from that tool and it will receive the same payload. Note that catch hooks are a paid feature on Zapier.