Verifying webhooks
Deftform sends a webhook to your configured endpoint(s) every time one of your forms is submitted. To make sure a request genuinely came from Deftform and wasn't tampered with in transit, each request is signed with your workspace's webhook secret.
Request format
Method:
POSTContent-Type:
application/jsonBody: JSON describing the submission, for example:
{ "id": "9f1c2d3e-4b5a-6789-0abc-def012345678", "number": 123, "number_formatted": "INV-123", "form_id": "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809", "form_name": "Contact form", "referrer": "https://example.com/contact", "data": []}
Headers
Header | Description |
|---|---|
| HMAC-SHA256 signature of the raw request body, in the form |
| Your plaintext webhook secret. Provided for backward compatibility only — prefer verifying the signature above. |
Signature specification
Algorithm: HMAC-SHA256.
Signing key: Your workspace webhook secret, used directly (no decoding or transformation). Find it under Settings → Connections → Webhook.
Signed payload: The exact raw bytes of the HTTP request body — nothing else is prepended or appended, and no timestamp is included.
Encoding: Lowercase hexadecimal, prefixed with
sha256=.
Important: Compute the HMAC over the raw request body before any JSON parsing or re-serialization. Re-encoding the JSON can change bytes (key order, whitespace, unicode escaping) and cause the signature to mismatch.
Verifying the signature
PHP
<?php$payload = file_get_contents('php://input'); // raw body$secret = 'YOUR_WEBHOOK_SECRET';$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);$signature = $_SERVER['HTTP_X_DEFTFORM_SIGNATURE_256'] ?? ''; if (! hash_equals($expected, $signature)) { http_response_code(401); exit;}
Laravel
<?php$expected = 'sha256=' . hash_hmac('sha256', $request->getContent(), config('services.deftform.webhook_secret')); abort_unless( hash_equals($expected, (string) $request->header('X-Deftform-Signature-256')), 401);
Node.js (Express)
const crypto = require('crypto'); // Capture the RAW body, e.g. app.use(express.raw({ type: 'application/json' }))app.post('/webhooks/deftform', (req, res) => { const expected = 'sha256=' + crypto.createHmac('sha256', process.env.DEFTFORM_WEBHOOK_SECRET) .update(req.body) // req.body is a Buffer .digest('hex'); const received = req.get('X-Deftform-Signature-256') || ''; const valid = expected.length === received.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); if (!valid) return res.status(401).end(); const payload = JSON.parse(req.body.toString()); // …process the submission res.sendStatus(200);});
Always compare using a constant-time function (hash_equals, crypto.timingSafeEqual) rather than == to avoid timing attacks.
Replay protection
Signatures do not currently include a timestamp, so there is no built-in expiry window. To protect against replayed requests, de-duplicate on the id field in the payload — each form submission has a unique id. Store processed IDs and ignore any you've already handled.
Security recommendations
Always serve your webhook endpoint over HTTPS.
Treat the webhook secret as a credential — never expose it client-side or commit it to source control.
If you believe your secret has leaked, rotate it in Settings → Connections → Webhook and update your receiver.
Respond with a
2xxstatus once you've accepted the request; Deftform retries on failures.