TruForms
Integrations

Verifying webhook signatures

How to verify that incoming webhooks actually came from TruForms.

Last updated

Every outbound webhook TruForms sends is signed. You verify the signature using the secret shown in the form's Integrations → Webhook panel. Rotating the secret in the dashboard takes effect on the next delivery.

Why bother? Your webhook URL is just a public address — anyone who learns it could POST fake submissions to your server. The signature is proof a request genuinely came from TruForms (and wasn't tampered with in transit). Verifying it is a few lines of code; the snippets below are copy-paste ready.

Headers we send

Header Value
Content-Type application/json
User-Agent TruForms-Webhook/1.0
X-TruForms-Event Always submission.created for now.
X-TruForms-Submission-Id The submission UUID (or a random UUID for test-fires).
X-TruForms-Timestamp Unix timestamp (seconds) when we signed the request.
X-TruForms-Signature sha256=<hex> where hex is HMAC-SHA256(secret, timestamp + '.' + body).

Payload shape

{
  "event": "submission.created",
  "id": "b3b1f9e3-...",
  "formId": "c9c4f8a2-...",
  "submission": {
    "name": "Alice",
    "email": "[email protected]",
    "message": "Hi"
  },
  "test": false,
  "createdAt": "2026-04-22T10:00:00.000Z"
}

test: true indicates a synthetic payload from the "Send test" button in the dashboard.

Verification — Node.js

import crypto from 'node:crypto';

export function verifyTruFormsWebhook(req, secret) {
  const signature = req.headers['x-truforms-signature'];
  const timestamp = req.headers['x-truforms-timestamp'];
  if (!signature || !timestamp) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${req.rawBody}`)
    .digest('hex');

  const given = signature.replace(/^sha256=/, '');
  return crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Note you need access to the raw request body (pre-JSON-parse). In Express, use express.json({ verify: (req, _res, buf) => (req.rawBody = buf) }).

Verification — Python

import hmac, hashlib

def verify(headers, body_bytes, secret):
    signature = headers.get('x-truforms-signature', '')
    timestamp = headers.get('x-truforms-timestamp', '')
    if not signature or not timestamp:
        return False
    signed = f'{timestamp}.'.encode() + body_bytes
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    given = signature.removeprefix('sha256=')
    return hmac.compare_digest(given, expected)

Verification — Go

package truforms

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "net/http"
    "strings"
)

func VerifyWebhook(r *http.Request, body []byte, secret string) bool {
    sig := strings.TrimPrefix(r.Header.Get("X-TruForms-Signature"), "sha256=")
    ts := r.Header.Get("X-TruForms-Timestamp")
    if sig == "" || ts == "" {
        return false
    }
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(ts + "."))
    mac.Write(body)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(expected))
}

Replay protection

Reject requests where abs(now - timestamp) > 300 seconds. We sign every request, so an attacker can't modify the body without breaking the signature — but they could replay a valid request. The timestamp + 5-minute window defeats that.

if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
  return res.status(400).send('Stale signature');
}

Delivery guarantees

  • Retries — up to 8 attempts with exponential backoff (starts at 60s, caps around 2 hours).
  • Idempotency — include X-TruForms-Submission-Id in your dedup key; we may deliver the same event twice after a server restart.
  • Dead letter — after the final failed attempt, the delivery is marked dead in the TruForms dashboard and stops retrying. Re-enable the integration to resume new deliveries.

What counts as success?

Any HTTP status in the 2xx range. Return 200 with any body (or no body) as soon as you've persisted or queued the event. Don't do long work in the webhook handler — ACK fast, process async.

Further reading

Verifying webhook signatures — TruForms