05 — Integrations · Webhooks
Forward submissions to your own backend.
Every submission can be POSTed to a URL you control, signed with HMAC-SHA256 so you can trust it came from FormPing.
Setup
- Open the dashboard and pick the token you want to forward.
- Open Webhooks in the token's settings.
- Paste your endpoint URL (HTTPS only) and a webhook secret — generate a long random string.
- Save. The next submission to this token will hit your endpoint.
Treat the webhook secret like a password. Never commit it to a repo. Store it as an environment variable on your server.
The payload
Webhooks are sent as POST requests with JSON bodies. Sample payload:
{
"id": "msg_01HX2K9Z0M8YQ5FN7GA6V3T1WB",
"token_id": "tok_01HX0001",
"received_at": "2026-04-08T10:14:32.117Z",
"fields": {
"Name": "Ada Lovelace",
"Email": "[email protected]",
"Message": "Hello from the docs!"
},
"score": null,
"meta": {
"ip_country": "CH",
"user_agent": "Mozilla/5.0 ..."
}
}
Headers
Content-Type: application/jsonX-FormPing-Signature: sha256=<hex>— HMAC-SHA256 of the raw request body, signed with your webhook secretX-FormPing-Event: submission.createdX-FormPing-Delivery: <uuid>— unique per delivery attempt; safe to use as an idempotency key
Verifying the signature
Always verify the signature before trusting the payload. Compute HMAC-SHA256 of the
raw request body (before any JSON parsing) using your webhook secret, and
compare it to the value in X-FormPing-Signature.
// Express + Node 18+
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const SECRET = process.env.FORMPING_WEBHOOK_SECRET;
// IMPORTANT: capture the raw body before json parsing
app.post(
'/webhooks/formping',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.header('x-formping-signature') || '';
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const a = Buffer.from(sig);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('invalid signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
console.log('new submission:', payload.id);
res.status(200).send('ok');
},
);
app.listen(3000);
<?php
// PHP 8+
$secret = getenv('FORMPING_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_FORMPING_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
echo 'invalid signature';
exit;
}
$payload = json_decode($body, true);
error_log('new submission: ' . $payload['id']);
http_response_code(200);
echo 'ok';
# Flask
import hmac
import hashlib
import os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["FORMPING_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/formping")
def formping_webhook():
body = request.get_data() # raw bytes — do not use request.json
sig = request.headers.get("X-FormPing-Signature", "")
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401, "invalid signature")
payload = request.get_json()
print("new submission:", payload["id"])
return "ok", 200
Always use a constant-time comparison (timingSafeEqual, hash_equals, hmac.compare_digest). A naive == check is vulnerable to timing attacks.
Retries
If your endpoint returns anything other than a 2xx status, FormPing retries
with exponential backoff: roughly 30s, 2min, 10min. After three failures the delivery is
dropped — but the submission itself is still safe in your inbox.
Your endpoint should respond as fast as possible (under a few seconds) and do any heavy work asynchronously. Long-running handlers will time out and trigger unnecessary retries.
Idempotency
Use the X-FormPing-Delivery header (a UUID) as an idempotency key in your
database. If the same delivery hits you twice — for instance because your 200
response got lost in transit — you can safely ignore the duplicate.
Best practices
- Always verify the signature.
- Always use HTTPS.
- Respond with
200 OKas fast as possible. Queue any slow work. - Handle duplicates using
X-FormPing-Delivery. - Rotate the webhook secret if you ever suspect it's been exposed.