Webhooks for form submissions

Pipe every submission into your own system — CRM, database, queue, anything with an HTTPS endpoint. We POST signed JSON, retry on failure, and log every delivery.

Endpoint requirements

  • A public HTTPS URL that accepts POST with a JSON body.
  • Respond 2xx within 8 seconds (we time out after 8s; 4s to connect). Do slow work after responding — queue it.
  • Any non-2xx response or timeout counts as a failure and triggers a retry.

Setup

  1. In Static Contact, open your Form and go to the Webhooks tab.
  2. Click Add webhook, keep the type as Generic (signed JSON), and enter your endpoint URL.
  3. Leave the signing secret blank to have one generated, or paste your own. Save, then copy the secret into your endpoint's configuration.

Webhooks are available on paid plans; a form can deliver to several destinations at once.

The payload

{
    "form": {
        "id": "019ea10b-...",
        "name": "example.com",
        "email": "you@example.com"
    },
    "submission": {
        "id": 123,
        "data": { "name": "Jane", "email": "jane@acme.com", "message": "Hi!" },
        "received_at": "2026-06-08T12:00:00+00:00"
    }
}

submission.data contains exactly the fields your form posted — you control the shape by naming your form fields.

Verifying the signature

Every request carries a signature header so you can prove it came from us and not someone who found your URL:

X-StaticContact-Signature: sha256=<HMAC-SHA256(raw_request_body, your_secret)>

Compute the HMAC over the raw request body (before any JSON parsing) and compare with a constant-time function. PHP:

<?php
$secret   = getenv('STATIC_CONTACT_SECRET');
$body     = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_STATICCONTACT_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit('invalid signature');
}

$payload = json_decode($body, true);
// handle the submission, respond fast
http_response_code(200);

Node (Express):

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

const app = express();

app.post('/hook', express.raw({ type: 'application/json' }), (req, res) => {
    const expected = 'sha256=' + crypto
        .createHmac('sha256', process.env.STATIC_CONTACT_SECRET)
        .update(req.body)
        .digest('hex');
    const given = req.headers['x-staticcontact-signature'] ?? '';

    const valid = given.length === expected.length
        && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
    if (!valid) return res.status(401).end();

    const payload = JSON.parse(req.body);
    // handle the submission, respond fast
    res.status(200).end();
});

Retries & delivery logs

  • On failure we retry up to 3 attempts with increasing waits: 1 minute, 5 minutes, 15 minutes.
  • Plans with delivery logs show every attempt (status code, response excerpt, attempt number) on the Webhooks tab, with a manual Resend per destination.
  • Deliveries are queued — a slow endpoint never delays your email notification or other webhooks.

Troubleshooting

  • Signature mismatch — you're verifying a parsed-then-re-encoded body instead of the raw bytes, or the secret differs from the one saved on the webhook. Use the raw request body.
  • Saving with a blank secret never breaks verification — blank keeps the existing secret; a new secret is only set when you type one.
  • Timeouts — respond first, process after. We allow 8 seconds total.
  • Nothing arrives — check the webhook is Enabled and the delivery log for the exact response.

Just want chat notifications?

No code needed for Slack, Discord or Telegram — those use each platform's own webhook URLs and aren't signed (the secret lives in the URL).

© 2026 Static Contact. All rights reserved.