Developers
Webhooks
Each new response posted to your server, signed and retried.
A webhook posts each new response to a web address of yours as it arrives — to a CRM, a Slack bot, a spreadsheet script or your own backend.
Adding a webhook
- Open the form and go to its Share tab.
- Choose Add a webhook: a name, the address to post to (
https://recommended), and a secret of your own, used to sign every delivery. - Choose the events and save. Each webhook belongs to the one form it was added on.
Webhooks need the Admin or Owner role.
Events
| Event | When |
|---|---|
submission.created | A new response is saved. Sent straight away. |
submission.updated | A response is changed. Queued, and sent with the next round of deliveries (see retries). |
The request
Each delivery is a POST with a JSON body:
POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: datamold/1.0
X-Form-Event: submission.created
X-Form-Delivery: 01J…
X-Form-Signature: sha256=5d41402abc4b2a76b9719d911017c592…{
"event": "submission.created",
"at": 1791965028499,
"form": { "id": "01J…", "name": "Event registration", "slug": "k7q2m9xa3b" },
"submission": {
"id": "01J…",
"code": "K7Q2M",
"status": "complete",
"stage": "New",
"tags": [],
"source": "web",
"createdAt": 1791965028499,
"data": { "full_name": "Asha Rao", "city": "Pune", "seats": 2 }
}
}dataholds the answers by field key.X-Form-Deliveryis unique per delivery — use it to ignore a repeat.- Times are epoch milliseconds.
Checking the signature
When the webhook has a secret, every delivery carries X-Form-Signature: sha256= followed by the hex HMAC-SHA256 of the raw request body,
keyed with your secret. Compute the same and compare before trusting the request.
import { createHmac, timingSafeEqual } from 'node:crypto';
// Express: read the body raw, before any JSON parser touches it.
app.post('/datamold', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + createHmac('sha256', process.env.DATAMOLD_SECRET)
.update(req.body) // the raw bytes
.digest('hex');
const given = req.get('X-Form-Signature') ?? '';
const ok = given.length === expected.length &&
timingSafeEqual(Buffer.from(given), Buffer.from(expected));
if (!ok) return res.status(401).end();
const { event, submission } = JSON.parse(req.body.toString('utf8'));
// …your work here…
res.status(200).end();
});import hmac, hashlib
def is_from_datamold(raw_body: bytes, header: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or '')Answering and retries
- Answer with any
2xxstatus within 10 seconds and the delivery is done. - Anything else — another status, a timeout, no connection — is retried, up to 6 attempts in all. Each retry waits longer than the last: at least 1 minute, then 5 minutes, 25 minutes, about 2 hours and about 10 hours.
- Waiting retries are sent when the organization's next response arrives, and in any case during the nightly run — so a retry can come later than those minimums, never sooner.
- Because a delivery can arrive more than once, make your handler safe to repeat: key on
X-Form-Deliveryor the submissionid.
The Share tab shows each webhook's last attempt: when it was, and the status your server answered or the error.